Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

@mtrmac mtrmac Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, right below the added code is this comment and neither of as has noticed :) So much for comments.

I’ll fix that…

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)
Expand Down
5 changes: 5 additions & 0 deletions directory/directory_src.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package directory

import (
"fmt"
"io"
"io/ioutil"
"os"
Expand Down Expand Up @@ -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))
Expand Down
43 changes: 28 additions & 15 deletions docker/docker_image_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
52 changes: 52 additions & 0 deletions image/docker_list.go
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 15 additions & 1 deletion image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,28 @@ 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
// need to happen within the ImageSource.
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:
Expand Down
1 change: 1 addition & 0 deletions manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions openshift/openshift.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions signature/policy_eval_simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
6 changes: 6 additions & 0 deletions signature/policy_reference_match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand Down
5 changes: 5 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down