diff --git a/copy/copy.go b/copy/copy.go index 91d4cef62c..450827cc4d 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -112,6 +112,14 @@ func Image(ctx *types.SystemContext, policyContext *signature.PolicyContext, des src := image.FromSource(rawSource) defer src.Close() + multiImage, err := src.IsMultiImage() + if err != nil { + return err + } + if multiImage { + return fmt.Errorf("can not copy %s: manifest contains multiple images", transports.ImageName(srcRef)) + } + // Please keep this policy check BEFORE reading any other information about the image. if allowed, err := policyContext.IsRunningImageAllowed(src); !allowed || err != nil { // Be paranoid and fail if either return value indicates so. return fmt.Errorf("Source image rejected: %v", err) diff --git a/directory/directory_src.go b/directory/directory_src.go index c87b0a3b0d..16ab50f9bd 100644 --- a/directory/directory_src.go +++ b/directory/directory_src.go @@ -1,6 +1,7 @@ package directory import ( + "fmt" "io" "io/ioutil" "os" @@ -37,6 +38,10 @@ func (s *dirImageSource) GetManifest() ([]byte, string, error) { return m, "", err } +func (s *dirImageSource) GetTargetManifest(digest string) ([]byte, string, error) { + return nil, "", fmt.Errorf("Getting target manifest not supported by dir:") +} + // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). func (s *dirImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) { r, err := os.Open(s.ref.layerPath(digest)) diff --git a/docker/docker_image_src.go b/docker/docker_image_src.go index 279301b79f..627c778d35 100644 --- a/docker/docker_image_src.go +++ b/docker/docker_image_src.go @@ -84,6 +84,31 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) { return s.cachedManifest, s.cachedManifestMIMEType, nil } +func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) { + url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), tagOrDigest) + headers := make(map[string][]string) + headers["Accept"] = s.requestedManifestMIMETypes + res, err := s.c.makeRequest("GET", url, headers, nil) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + manblob, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, "", err + } + if res.StatusCode != http.StatusOK { + return nil, "", ErrFetchManifest{res.StatusCode, manblob} + } + return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil +} + +// GetTargetManifest returns an image's manifest given a digest. +// This is mainly used to retrieve a single image's manifest out of a manifest list. +func (s *dockerImageSource) GetTargetManifest(digest string) ([]byte, string, error) { + return s.fetchManifest(digest) +} + // ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType // // ImageSource implementations are not required or expected to do any caching, @@ -100,26 +125,14 @@ func (s *dockerImageSource) ensureManifestIsLoaded() error { if err != nil { return err } - url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), reference) - // TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1 - // TODO(runcom) NO, switch on the resulter manifest like Docker is doing - headers := make(map[string][]string) - headers["Accept"] = s.requestedManifestMIMETypes - res, err := s.c.makeRequest("GET", url, headers, nil) - if err != nil { - return err - } - defer res.Body.Close() - manblob, err := ioutil.ReadAll(res.Body) + + manblob, mt, err := s.fetchManifest(reference) if err != nil { return err } - if res.StatusCode != http.StatusOK { - return ErrFetchManifest{res.StatusCode, manblob} - } // We might validate manblob against the Docker-Content-Digest header here to protect against transport errors. s.cachedManifest = manblob - s.cachedManifestMIMEType = simplifyContentType(res.Header.Get("Content-Type")) + s.cachedManifestMIMEType = mt return nil } diff --git a/image/docker_list.go b/image/docker_list.go new file mode 100644 index 0000000000..57f1763eb8 --- /dev/null +++ b/image/docker_list.go @@ -0,0 +1,52 @@ +package image + +import ( + "encoding/json" + "errors" + "runtime" + + "github.com/containers/image/types" +) + +type platformSpec struct { + Architecture string `json:"architecture"` + OS string `json:"os"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` + Variant string `json:"variant,omitempty"` + Features []string `json:"features,omitempty"` +} + +// A manifestDescriptor references a platform-specific manifest. +type manifestDescriptor struct { + descriptor + Platform platformSpec `json:"platform"` +} + +type manifestList struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Manifests []manifestDescriptor `json:"manifests"` +} + +func manifestSchema2FromManifestList(src types.ImageSource, manblob []byte) (genericManifest, error) { + list := manifestList{} + if err := json.Unmarshal(manblob, &list); err != nil { + return nil, err + } + var targetManifestDigest string + for _, d := range list.Manifests { + if d.Platform.Architecture == runtime.GOARCH && d.Platform.OS == runtime.GOOS { + targetManifestDigest = d.Digest + break + } + } + if targetManifestDigest == "" { + return nil, errors.New("no supported platform found in manifest list") + } + manblob, mt, err := src.GetTargetManifest(targetManifestDigest) + if err != nil { + return nil, err + } + return manifestInstanceFromBlob(src, manblob, mt) +} diff --git a/image/image.go b/image/image.go index 2dda50a239..23c2a0a2ba 100644 --- a/image/image.go +++ b/image/image.go @@ -120,6 +120,18 @@ func (i *genericImage) getParsedManifest() (genericManifest, error) { if err != nil { return nil, err } + return manifestInstanceFromBlob(i.src, manblob, mt) +} + +func (i *genericImage) IsMultiImage() (bool, error) { + _, mt, err := i.Manifest() + if err != nil { + return false, err + } + return mt == manifest.DockerV2ListMediaType, nil +} + +func manifestInstanceFromBlob(src types.ImageSource, manblob []byte, mt string) (genericManifest, error) { switch mt { // "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md . // This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might @@ -127,7 +139,9 @@ func (i *genericImage) getParsedManifest() (genericManifest, error) { case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType, "application/json": return manifestSchema1FromManifest(manblob) case manifest.DockerV2Schema2MediaType: - return manifestSchema2FromManifest(i.src, manblob) + return manifestSchema2FromManifest(src, manblob) + case manifest.DockerV2ListMediaType: + return manifestSchema2FromManifestList(src, manblob) case "": return nil, errors.New("could not guess manifest media type") default: diff --git a/manifest/manifest.go b/manifest/manifest.go index 80f57083b8..2e4ba20670 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -30,6 +30,7 @@ var DefaultRequestedManifestMIMETypes = []string{ DockerV2Schema2MediaType, DockerV2Schema1SignedMediaType, DockerV2Schema1MediaType, + DockerV2ListMediaType, } // GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. diff --git a/openshift/openshift.go b/openshift/openshift.go index 0451eaeee1..e77a175bb5 100644 --- a/openshift/openshift.go +++ b/openshift/openshift.go @@ -196,6 +196,13 @@ func (s *openshiftImageSource) Close() { } } +func (s *openshiftImageSource) GetTargetManifest(digest string) ([]byte, string, error) { + if err := s.ensureImageIsResolved(); err != nil { + return nil, "", err + } + return s.docker.GetTargetManifest(digest) +} + func (s *openshiftImageSource) GetManifest() ([]byte, string, error) { if err := s.ensureImageIsResolved(); err != nil { return nil, "", err diff --git a/signature/policy_eval_simple_test.go b/signature/policy_eval_simple_test.go index 90c25b8e06..633396ffe5 100644 --- a/signature/policy_eval_simple_test.go +++ b/signature/policy_eval_simple_test.go @@ -12,6 +12,10 @@ type nameOnlyImageMock struct { forbiddenImageMock } +func (nameOnlyImageMock) IsMultiImage() (bool, error) { + panic("unexpected call to a mock function") +} + func (nameOnlyImageMock) Reference() types.ImageReference { return nameOnlyImageReferenceMock("== StringWithinTransport mock") } diff --git a/signature/policy_reference_match_test.go b/signature/policy_reference_match_test.go index 28765e10a8..52893c471a 100644 --- a/signature/policy_reference_match_test.go +++ b/signature/policy_reference_match_test.go @@ -56,6 +56,9 @@ type refImageMock struct{ reference.Named } func (ref refImageMock) Reference() types.ImageReference { return refImageReferenceMock{ref.Named} } +func (ref refImageMock) IsMultiImage() (bool, error) { + panic("unexpected call to a mock function") +} func (ref refImageMock) Close() { panic("unexpected call to a mock function") } @@ -267,6 +270,9 @@ func TestParseDockerReferences(t *testing.T) { // forbiddenImageMock is a mock of types.Image which ensures Reference is not called type forbiddenImageMock struct{} +func (ref forbiddenImageMock) IsMultiImage() (bool, error) { + panic("unexpected call to a mock function") +} func (ref forbiddenImageMock) Reference() types.ImageReference { panic("unexpected call to a mock function") } diff --git a/types/types.go b/types/types.go index c9c296f268..c4a15b3b4c 100644 --- a/types/types.go +++ b/types/types.go @@ -106,6 +106,9 @@ type ImageSource interface { // GetManifest returns the image's manifest along with its MIME type. The empty string is returned if the MIME type is unknown. // It may use a remote (= slow) service. GetManifest() ([]byte, string, error) + // GetTargetManifest returns an image's manifest given a digest. This is mainly used to retrieve a single image's manifest + // out of a manifest list. + GetTargetManifest(digest string) ([]byte, string, error) // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). GetBlob(digest string) (io.ReadCloser, int64, error) // GetSignatures returns the image's signatures. It may use a remote (= slow) service. @@ -180,6 +183,8 @@ type Image interface { // UpdatedManifest returns the image's manifest modified according to options. // This does not change the state of the Image object. UpdatedManifest(options ManifestUpdateOptions) ([]byte, error) + // IsMultiImage returns true if the image's manifest is a list of images, false otherwise. + IsMultiImage() (bool, error) } // ManifestUpdateOptions is a way to pass named optional arguments to Image.UpdatedManifest