diff --git a/directory/directory_dest.go b/directory/directory_dest.go index 51079e10a1..9805e557b3 100644 --- a/directory/directory_dest.go +++ b/directory/directory_dest.go @@ -6,20 +6,21 @@ import ( "os" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) type dirImageDestination struct { - dir string + ref dirReference } -// NewImageDestination returns an ImageDestination for writing to an existing directory. -func NewImageDestination(dir string) types.ImageDestination { - return &dirImageDestination{dir} +// newImageDestination returns an ImageDestination for writing to an existing directory. +func newImageDestination(ref dirReference) types.ImageDestination { + return &dirImageDestination{ref} } -func (d *dirImageDestination) CanonicalDockerReference() reference.Named { - return nil +// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, +// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. +func (d *dirImageDestination) Reference() types.ImageReference { + return d.ref } func (d *dirImageDestination) SupportedManifestMIMETypes() []string { @@ -27,11 +28,11 @@ func (d *dirImageDestination) SupportedManifestMIMETypes() []string { } func (d *dirImageDestination) PutManifest(manifest []byte) error { - return ioutil.WriteFile(manifestPath(d.dir), manifest, 0644) + return ioutil.WriteFile(manifestPath(d.ref.path), manifest, 0644) } func (d *dirImageDestination) PutBlob(digest string, stream io.Reader) error { - layerFile, err := os.Create(layerPath(d.dir, digest)) + layerFile, err := os.Create(layerPath(d.ref.path, digest)) if err != nil { return err } @@ -47,7 +48,7 @@ func (d *dirImageDestination) PutBlob(digest string, stream io.Reader) error { func (d *dirImageDestination) PutSignatures(signatures [][]byte) error { for i, sig := range signatures { - if err := ioutil.WriteFile(signaturePath(d.dir, i), sig, 0644); err != nil { + if err := ioutil.WriteFile(signaturePath(d.ref.path, i), sig, 0644); err != nil { return err } } diff --git a/directory/directory_src.go b/directory/directory_src.go index 2bc19c209a..90b84e132b 100644 --- a/directory/directory_src.go +++ b/directory/directory_src.go @@ -7,29 +7,26 @@ import ( "os" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) type dirImageSource struct { - dir string + ref dirReference } -// NewImageSource returns an ImageSource reading from an existing directory. -func NewImageSource(dir string) types.ImageSource { - return &dirImageSource{dir} +// newImageSource returns an ImageSource reading from an existing directory. +func newImageSource(ref dirReference) types.ImageSource { + return &dirImageSource{ref} } -// IntendedDockerReference returns the Docker reference for this image, _as specified by the user_ -// (not as the image itself, or its underlying storage, claims). Should be fully expanded, i.e. !reference.IsNameOnly. -// This can be used e.g. to determine which public keys are trusted for this image. -// May be nil if unknown. -func (s *dirImageSource) IntendedDockerReference() reference.Named { - return nil +// Reference returns the reference used to set up this source, _as specified by the user_ +// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. +func (s *dirImageSource) Reference() types.ImageReference { + return s.ref } // it's up to the caller to determine the MIME type of the returned manifest's bytes func (s *dirImageSource) GetManifest(_ []string) ([]byte, string, error) { - m, err := ioutil.ReadFile(manifestPath(s.dir)) + m, err := ioutil.ReadFile(manifestPath(s.ref.path)) if err != nil { return nil, "", err } @@ -37,11 +34,11 @@ func (s *dirImageSource) GetManifest(_ []string) ([]byte, string, error) { } func (s *dirImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) { - r, err := os.Open(layerPath(s.dir, digest)) + r, err := os.Open(layerPath(s.ref.path, digest)) if err != nil { return nil, 0, nil } - fi, err := os.Stat(layerPath(s.dir, digest)) + fi, err := os.Stat(layerPath(s.ref.path, digest)) if err != nil { return nil, 0, nil } @@ -51,7 +48,7 @@ func (s *dirImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) { func (s *dirImageSource) GetSignatures() ([][]byte, error) { signatures := [][]byte{} for i := 0; ; i++ { - signature, err := ioutil.ReadFile(signaturePath(s.dir, i)) + signature, err := ioutil.ReadFile(signaturePath(s.ref.path, i)) if err != nil { if os.IsNotExist(err) { break diff --git a/directory/directory_test.go b/directory/directory_test.go index 7eeb5724a4..b6644d61aa 100644 --- a/directory/directory_test.go +++ b/directory/directory_test.go @@ -10,23 +10,28 @@ import ( "github.com/stretchr/testify/require" ) -func TestCanonicalDockerReference(t *testing.T) { - dest := NewImageDestination("/path/to/somewhere") - ref := dest.CanonicalDockerReference() - assert.Nil(t, ref) +func TestDestinationReference(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + + dest, err := ref.NewImageDestination("", true) + require.NoError(t, err) + ref2 := dest.Reference() + assert.Equal(t, tmpDir, ref2.StringWithinTransport()) } func TestGetPutManifest(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "put-manifest") - require.NoError(t, err) + ref, tmpDir := refToTempDir(t) defer os.RemoveAll(tmpDir) man := []byte("test-manifest") - dest := NewImageDestination(tmpDir) + dest, err := ref.NewImageDestination("", true) + require.NoError(t, err) err = dest.PutManifest(man) assert.NoError(t, err) - src := NewImageSource(tmpDir) + src, err := ref.NewImageSource("", true) + require.NoError(t, err) m, mt, err := src.GetManifest(nil) assert.NoError(t, err) assert.Equal(t, man, m) @@ -34,17 +39,18 @@ func TestGetPutManifest(t *testing.T) { } func TestGetPutBlob(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "put-blob") - require.NoError(t, err) + ref, tmpDir := refToTempDir(t) defer os.RemoveAll(tmpDir) digest := "digest-test" blob := []byte("test-blob") - dest := NewImageDestination(tmpDir) + dest, err := ref.NewImageDestination("", true) + require.NoError(t, err) err = dest.PutBlob(digest, bytes.NewReader(blob)) assert.NoError(t, err) - src := NewImageSource(tmpDir) + src, err := ref.NewImageSource("", true) + require.NoError(t, err) rc, size, err := src.GetBlob(digest) assert.NoError(t, err) defer rc.Close() @@ -55,11 +61,11 @@ func TestGetPutBlob(t *testing.T) { } func TestGetPutSignatures(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "put-signatures") - require.NoError(t, err) + ref, tmpDir := refToTempDir(t) defer os.RemoveAll(tmpDir) - dest := NewImageDestination(tmpDir) + dest, err := ref.NewImageDestination("", true) + require.NoError(t, err) signatures := [][]byte{ []byte("sig1"), []byte("sig2"), @@ -67,24 +73,29 @@ func TestGetPutSignatures(t *testing.T) { err = dest.PutSignatures(signatures) assert.NoError(t, err) - src := NewImageSource(tmpDir) + src, err := ref.NewImageSource("", true) + require.NoError(t, err) sigs, err := src.GetSignatures() assert.NoError(t, err) assert.Equal(t, signatures, sigs) } func TestDelete(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "delete") - require.NoError(t, err) + ref, tmpDir := refToTempDir(t) defer os.RemoveAll(tmpDir) - src := NewImageSource(tmpDir) + src, err := ref.NewImageSource("", true) + require.NoError(t, err) err = src.Delete() assert.Error(t, err) } -func TestIntendedDockerReference(t *testing.T) { - src := NewImageSource("/path/to/somewhere") - ref := src.IntendedDockerReference() - assert.Nil(t, ref) +func TestSourceReference(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + + src, err := ref.NewImageSource("", true) + require.NoError(t, err) + ref2 := src.Reference() + assert.Equal(t, tmpDir, ref2.StringWithinTransport()) } diff --git a/directory/directory_transport.go b/directory/directory_transport.go new file mode 100644 index 0000000000..821273c1b7 --- /dev/null +++ b/directory/directory_transport.go @@ -0,0 +1,73 @@ +package directory + +import ( + "github.com/containers/image/image" + "github.com/containers/image/types" + "github.com/docker/docker/reference" +) + +// Transport is an ImageTransport for directory paths. +var Transport = dirTransport{} + +type dirTransport struct{} + +func (t dirTransport) Name() string { + return "dir" +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func (t dirTransport) ParseReference(reference string) (types.ImageReference, error) { + return NewReference(reference), nil +} + +// dirReference is an ImageReference for directory paths. +type dirReference struct { + // Note that the interpretation of paths below depends on the underlying filesystem state, which may change under us at any time! + path string // As specified by the user. May be relative, contain symlinks, etc. +} + +// There is no directory.ParseReference because it is rather pointless. +// Callers who need a transport-independent interface will go through +// dirTransport.ParseReference; callers who intentionally deal with directories +// can use directory.NewReference. + +// NewReference returns a directory reference for a specified path. +func NewReference(path string) types.ImageReference { + return dirReference{path: path} +} + +func (ref dirReference) Transport() types.ImageTransport { + return Transport +} + +// StringWithinTransport returns a string representation of the reference, which MUST be such that +// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. +// NOTE: The returned string is not promised to be equal to the original input to ParseReference; +// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. +func (ref dirReference) StringWithinTransport() string { + return ref.path +} + +// DockerReference returns a Docker reference associated with this reference +// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent, +// not e.g. after redirect or alias processing), or nil if unknown/not applicable. +func (ref dirReference) DockerReference() reference.Named { + return nil +} + +// NewImage returns a types.Image for this reference. +func (ref dirReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + src := newImageSource(ref) + return image.FromSource(src, nil), nil +} + +// NewImageSource returns a types.ImageSource for this reference. +func (ref dirReference) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + return newImageSource(ref), nil +} + +// NewImageDestination returns a types.ImageDestination for this reference. +func (ref dirReference) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + return newImageDestination(ref), nil +} diff --git a/directory/directory_transport_test.go b/directory/directory_transport_test.go new file mode 100644 index 0000000000..1589bccca6 --- /dev/null +++ b/directory/directory_transport_test.go @@ -0,0 +1,95 @@ +package directory + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/containers/image/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "dir", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testNewReference(t, Transport.ParseReference) +} + +func TestNewReference(t *testing.T) { + testNewReference(t, func(ref string) (types.ImageReference, error) { + return NewReference(ref), nil + }) +} + +// testNewReference is a test shared for Transport.ParseReference and NewReference. +func testNewReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + tmpDir, err := ioutil.TempDir("", "dir-transport-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + for _, path := range []string{ + "/", + "/etc", + tmpDir, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + ref, err := fn(path) + require.NoError(t, err, path) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + assert.Equal(t, path, dirRef.path, path) + } +} + +// refToTempDir creates a temporary directory and returns a reference to it. +// The caller should +// defer os.RemoveAll(tmpDir) +func refToTempDir(t *testing.T) (ref types.ImageReference, tmpDir string) { + tmpDir, err := ioutil.TempDir("", "dir-transport-test") + require.NoError(t, err) + ref = NewReference(tmpDir) + return ref, tmpDir +} + +func TestReferenceTransport(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + assert.Equal(t, tmpDir, ref.StringWithinTransport()) +} + +func TestReferenceDockerReference(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + assert.Nil(t, ref.DockerReference()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + _, err := ref.NewImage("/this/doesn't/exist", true) + assert.NoError(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + _, err := ref.NewImageSource("/this/doesn't/exist", true) + assert.NoError(t, err) +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, tmpDir := refToTempDir(t) + defer os.RemoveAll(tmpDir) + _, err := ref.NewImageDestination("/this/doesn't/exist", true) + assert.NoError(t, err) +} diff --git a/doc.go b/doc.go index 37888a7f15..870bb408dd 100644 --- a/doc.go +++ b/doc.go @@ -9,11 +9,15 @@ // ) // // func main() { -// img, err := docker.NewImage("fedora", "", false) +// ref, err := docker.ParseReference("fedora") // if err != nil { // panic(err) // } -// b, err := img.Manifest() +// img, err := ref.NewImage("", true) +// if err != nil { +// panic(err) +// } +// b, _, err := img.Manifest() // if err != nil { // panic(err) // } diff --git a/docker/docker_image.go b/docker/docker_image.go index 4163089abe..1a5b39187f 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -16,10 +16,10 @@ type Image struct { src *dockerImageSource } -// NewImage returns a new Image interface type after setting up +// newImage returns a new Image interface type after setting up // a client to the registry hosting the given image. -func NewImage(img, certPath string, tlsVerify bool) (types.Image, error) { - s, err := newDockerImageSource(img, certPath, tlsVerify) +func newImage(ref dockerReference, certPath string, tlsVerify bool) (types.Image, error) { + s, err := newImageSource(ref, certPath, tlsVerify) if err != nil { return nil, err } @@ -28,12 +28,12 @@ func NewImage(img, certPath string, tlsVerify bool) (types.Image, error) { // SourceRefFullName returns a fully expanded name for the repository this image is in. func (i *Image) SourceRefFullName() string { - return i.src.ref.FullName() + return i.src.ref.ref.FullName() } // GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any. func (i *Image) GetRepositoryTags() ([]string, error) { - url := fmt.Sprintf(tagsURL, i.src.ref.RemoteName()) + url := fmt.Sprintf(tagsURL, i.src.ref.ref.RemoteName()) res, err := i.src.c.makeRequest("GET", url, nil, nil) if err != nil { return nil, err diff --git a/docker/docker_image_dest.go b/docker/docker_image_dest.go index 68dd187f47..818c840975 100644 --- a/docker/docker_image_dest.go +++ b/docker/docker_image_dest.go @@ -10,21 +10,16 @@ import ( "github.com/Sirupsen/logrus" "github.com/containers/image/manifest" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) type dockerImageDestination struct { - ref reference.Named + ref dockerReference c *dockerClient } -// NewImageDestination creates a new ImageDestination for the specified image and connection specification. -func NewImageDestination(img, certPath string, tlsVerify bool) (types.ImageDestination, error) { - ref, err := parseImageName(img) - if err != nil { - return nil, err - } - c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify) +// newImageDestination creates a new ImageDestination for the specified image reference and connection specification. +func newImageDestination(ref dockerReference, certPath string, tlsVerify bool) (types.ImageDestination, error) { + c, err := newDockerClient(ref.ref.Hostname(), certPath, tlsVerify) if err != nil { return nil, err } @@ -34,6 +29,12 @@ func NewImageDestination(img, certPath string, tlsVerify bool) (types.ImageDesti }, nil } +// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, +// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. +func (d *dockerImageDestination) Reference() types.ImageReference { + return d.ref +} + func (d *dockerImageDestination) SupportedManifestMIMETypes() []string { return []string{ // TODO(runcom): we'll add OCI as part of another PR here @@ -43,10 +44,6 @@ func (d *dockerImageDestination) SupportedManifestMIMETypes() []string { } } -func (d *dockerImageDestination) CanonicalDockerReference() reference.Named { - return d.ref -} - func (d *dockerImageDestination) PutManifest(m []byte) error { // FIXME: This only allows upload by digest, not creating a tag. See the // corresponding comment in openshift.NewImageDestination. @@ -54,7 +51,7 @@ func (d *dockerImageDestination) PutManifest(m []byte) error { if err != nil { return err } - url := fmt.Sprintf(manifestURL, d.ref.RemoteName(), digest) + url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), digest) headers := map[string][]string{} mimeType := manifest.GuessMIMEType(m) @@ -78,7 +75,7 @@ func (d *dockerImageDestination) PutManifest(m []byte) error { } func (d *dockerImageDestination) PutBlob(digest string, stream io.Reader) error { - checkURL := fmt.Sprintf(blobsURL, d.ref.RemoteName(), digest) + checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), digest) logrus.Debugf("Checking %s", checkURL) res, err := d.c.makeRequest("HEAD", checkURL, nil, nil) @@ -93,7 +90,7 @@ func (d *dockerImageDestination) PutBlob(digest string, stream io.Reader) error logrus.Debugf("... failed, status %d", res.StatusCode) // FIXME? Chunked upload, progress reporting, etc. - uploadURL := fmt.Sprintf(blobUploadURL, d.ref.RemoteName()) + uploadURL := fmt.Sprintf(blobUploadURL, d.ref.ref.RemoteName()) logrus.Debugf("Uploading %s", uploadURL) res, err = d.c.makeRequest("POST", uploadURL, nil, nil) if err != nil { diff --git a/docker/docker_image_src.go b/docker/docker_image_src.go index 7e719552d7..afce80dd26 100644 --- a/docker/docker_image_src.go +++ b/docker/docker_image_src.go @@ -11,7 +11,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/containers/image/manifest" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) type errFetchManifest struct { @@ -24,17 +23,13 @@ func (e errFetchManifest) Error() string { } type dockerImageSource struct { - ref reference.Named + ref dockerReference c *dockerClient } -// newDockerImageSource is the same as NewImageSource, only it returns the more specific *dockerImageSource type. -func newDockerImageSource(img, certPath string, tlsVerify bool) (*dockerImageSource, error) { - ref, err := parseImageName(img) - if err != nil { - return nil, err - } - c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify) +// newImageSource creates a new ImageSource for the specified image reference and connection specification. +func newImageSource(ref dockerReference, certPath string, tlsVerify bool) (*dockerImageSource, error) { + c, err := newDockerClient(ref.ref.Hostname(), certPath, tlsVerify) if err != nil { return nil, err } @@ -44,16 +39,9 @@ func newDockerImageSource(img, certPath string, tlsVerify bool) (*dockerImageSou }, nil } -// NewImageSource creates a new ImageSource for the specified image and connection specification. -func NewImageSource(img, certPath string, tlsVerify bool) (types.ImageSource, error) { - return newDockerImageSource(img, certPath, tlsVerify) -} - -// IntendedDockerReference returns the Docker reference for this image, _as specified by the user_ -// (not as the image itself, or its underlying storage, claims). Should be fully expanded, i.e. !reference.IsNameOnly. -// This can be used e.g. to determine which public keys are trusted for this image. -// May be nil if unknown. -func (s *dockerImageSource) IntendedDockerReference() reference.Named { +// Reference returns the reference used to set up this source, _as specified by the user_ +// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. +func (s *dockerImageSource) Reference() types.ImageReference { return s.ref } @@ -71,11 +59,11 @@ func simplifyContentType(contentType string) string { } func (s *dockerImageSource) GetManifest(mimetypes []string) ([]byte, string, error) { - reference, err := tagOrDigest(s.ref) + reference, err := tagOrDigest(s.ref.ref) if err != nil { return nil, "", err } - url := fmt.Sprintf(manifestURL, s.ref.RemoteName(), reference) + 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) @@ -97,7 +85,7 @@ func (s *dockerImageSource) GetManifest(mimetypes []string) ([]byte, string, err } func (s *dockerImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) { - url := fmt.Sprintf(blobsURL, s.ref.RemoteName(), digest) + url := fmt.Sprintf(blobsURL, s.ref.ref.RemoteName(), digest) logrus.Debugf("Downloading %s", url) res, err := s.c.makeRequest("GET", url, nil, nil) if err != nil { @@ -126,11 +114,11 @@ func (s *dockerImageSource) Delete() error { headers := make(map[string][]string) headers["Accept"] = []string{manifest.DockerV2Schema2MIMEType} - reference, err := tagOrDigest(s.ref) + reference, err := tagOrDigest(s.ref.ref) if err != nil { return err } - getURL := fmt.Sprintf(manifestURL, s.ref.RemoteName(), reference) + getURL := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), reference) get, err := s.c.makeRequest("GET", getURL, headers, nil) if err != nil { return err @@ -143,13 +131,13 @@ func (s *dockerImageSource) Delete() error { switch get.StatusCode { case http.StatusOK: case http.StatusNotFound: - return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry.", s.ref) + return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry.", s.ref.ref) default: - return fmt.Errorf("Failed to delete %v: %v (%v)", s.ref, body, get.Status) + return fmt.Errorf("Failed to delete %v: %v (%v)", s.ref.ref, body, get.Status) } digest := get.Header.Get("Docker-Content-Digest") - deleteURL := fmt.Sprintf(manifestURL, s.ref.RemoteName(), digest) + deleteURL := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), digest) // When retrieving the digest from a registry >= 2.3 use the following header: // "Accept": "application/vnd.docker.distribution.manifest.v2+json" diff --git a/docker/docker_transport.go b/docker/docker_transport.go new file mode 100644 index 0000000000..d20b2a54e2 --- /dev/null +++ b/docker/docker_transport.go @@ -0,0 +1,95 @@ +package docker + +import ( + "fmt" + "strings" + + "github.com/containers/image/types" + "github.com/docker/docker/reference" +) + +// Transport is an ImageTransport for Docker references. +var Transport = dockerTransport{} + +type dockerTransport struct{} + +func (t dockerTransport) Name() string { + return "docker" +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func (t dockerTransport) ParseReference(reference string) (types.ImageReference, error) { + return ParseReference(reference) +} + +// dockerReference is an ImageReference for Docker images. +type dockerReference struct { + ref reference.Named // By construction we know that !reference.IsNameOnly(ref) +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an Docker ImageReference. +func ParseReference(refString string) (types.ImageReference, error) { + if !strings.HasPrefix(refString, "//") { + return nil, fmt.Errorf("docker: image reference %s does not start with //", refString) + } + ref, err := reference.ParseNamed(strings.TrimPrefix(refString, "//")) + if err != nil { + return nil, err + } + ref = reference.WithDefaultTag(ref) + return NewReference(ref) +} + +// NewReference returns a Docker reference for a named reference. The reference must satisfy !reference.IsNameOnly(). +func NewReference(ref reference.Named) (types.ImageReference, error) { + if reference.IsNameOnly(ref) { + return nil, fmt.Errorf("Docker reference %s has neither a tag nor a digest", ref.String()) + } + // A github.com/distribution/reference value can have a tag and a digest at the same time! + // docker/reference does not handle that, so fail. + // (Even if it were supported, the semantics of policy namespaces are unclear - should we drop + // the tag or the digest first?) + _, isTagged := ref.(reference.NamedTagged) + _, isDigested := ref.(reference.Canonical) + if isTagged && isDigested { + return nil, fmt.Errorf("Docker references with both a tag and digest are currently not supported") + } + return dockerReference{ + ref: ref, + }, nil +} + +func (ref dockerReference) Transport() types.ImageTransport { + return Transport +} + +// StringWithinTransport returns a string representation of the reference, which MUST be such that +// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. +// NOTE: The returned string is not promised to be equal to the original input to ParseReference; +// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. +func (ref dockerReference) StringWithinTransport() string { + return "//" + ref.ref.String() +} + +// DockerReference returns a Docker reference associated with this reference +// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent, +// not e.g. after redirect or alias processing), or nil if unknown/not applicable. +func (ref dockerReference) DockerReference() reference.Named { + return ref.ref +} + +// NewImage returns a types.Image for this reference. +func (ref dockerReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + return newImage(ref, certPath, tlsVerify) +} + +// NewImageSource returns a types.ImageSource for this reference. +func (ref dockerReference) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + return newImageSource(ref, certPath, tlsVerify) +} + +// NewImageDestination returns a types.ImageDestination for this reference. +func (ref dockerReference) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + return newImageDestination(ref, certPath, tlsVerify) +} diff --git a/docker/docker_transport_test.go b/docker/docker_transport_test.go new file mode 100644 index 0000000000..fb78f64652 --- /dev/null +++ b/docker/docker_transport_test.go @@ -0,0 +1,148 @@ +package docker + +import ( + "testing" + + "github.com/containers/image/types" + "github.com/docker/docker/reference" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "@sha256:" + sha256digestHex +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "docker", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + for _, c := range []struct{ input, expected string }{ + {"busybox", ""}, // Missing // prefix + {"//busybox:notlatest", "busybox:notlatest"}, // Explicit tag + {"//busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest + {"//busybox", "busybox:latest"}, // Default tag + // A github.com/distribution/reference value can have a tag and a digest at the same time! + // github.com/docker/reference handles that by dropping the tag. That is not obviously the + // right thing to do, but it is at least reasonable, so test that we keep behaving reasonably. + // This test case should not be construed to make this an API promise. + // FIXME? Instead work extra hard to reject such input? + {"//busybox:latest" + sha256digest, "busybox" + sha256digest}, // Both tag and digest + {"//docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified + {"//UPPERCASEISINVALID", ""}, // Invalid input + } { + ref, err := fn(c.input) + if c.expected == "" { + assert.Error(t, err, c.input) + } else { + require.NoError(t, err, c.input) + dockerRef, ok := ref.(dockerReference) + require.True(t, ok, c.input) + assert.Equal(t, c.expected, dockerRef.ref.String(), c.input) + } + } +} + +// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time. +type refWithTagAndDigest struct{ reference.Canonical } + +func (ref refWithTagAndDigest) Tag() string { + return "notLatest" +} + +// A common list of reference formats to test for the various ImageReference methods. +var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ + {"busybox:notlatest", "busybox:notlatest", "//busybox:notlatest"}, // Explicit tag + {"busybox" + sha256digest, "busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest + {"docker.io/library/busybox:latest", "busybox:latest", "//busybox:latest"}, // All implied values explicitly specified + {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified +} + +func TestNewReference(t *testing.T) { + for _, c := range validReferenceTestCases { + parsed, err := reference.ParseNamed(c.input) + require.NoError(t, err) + ref, err := NewReference(parsed) + require.NoError(t, err, c.input) + dockerRef, ok := ref.(dockerReference) + require.True(t, ok, c.input) + assert.Equal(t, c.dockerRef, dockerRef.ref.String(), c.input) + } + + // Neither a tag nor digest + parsed, err := reference.ParseNamed("busybox") + require.NoError(t, err) + _, err = NewReference(parsed) + assert.Error(t, err) + + // A github.com/distribution/reference value can have a tag and a digest at the same time! + parsed, err = reference.ParseNamed("busybox" + sha256digest) + require.NoError(t, err) + refDigested, ok := parsed.(reference.Canonical) + require.True(t, ok) + tagDigestRef := refWithTagAndDigest{refDigested} + _, err = NewReference(tagDigestRef) + assert.Error(t, err) +} + +func TestReferenceTransport(t *testing.T) { + ref, err := ParseReference("//busybox") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference("//" + c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, c.stringWithinTransport, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference("//" + c.input) + require.NoError(t, err, c.input) + dockerRef := ref.DockerReference() + require.NotNil(t, dockerRef, c.input) + assert.Equal(t, c.dockerRef, dockerRef.String(), c.input) + } +} + +func TestReferenceNewImage(t *testing.T) { + ref, err := ParseReference("//busybox") + require.NoError(t, err) + _, err = ref.NewImage("", true) + assert.NoError(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, err := ParseReference("//busybox") + require.NoError(t, err) + _, err = ref.NewImageSource("", true) + assert.NoError(t, err) +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, err := ParseReference("//busybox") + require.NoError(t, err) + _, err = ref.NewImageDestination("", true) + assert.NoError(t, err) +} diff --git a/docker/docker_utils.go b/docker/docker_utils.go index 4a33ca3f8e..b877279ed9 100644 --- a/docker/docker_utils.go +++ b/docker/docker_utils.go @@ -6,20 +6,6 @@ import ( "github.com/docker/docker/reference" ) -// parseImageName converts a string into a reference. -// It is guaranteed that reference.IsNameOnly is false for the returned value. -func parseImageName(img string) (reference.Named, error) { - ref, err := reference.ParseNamed(img) - if err != nil { - return nil, err - } - ref = reference.WithDefaultTag(ref) - if reference.IsNameOnly(ref) { // Sanity check that we are fulfulling our contract - return nil, fmt.Errorf("Internal inconsistency: reference.IsNameOnly for reference %s (parsed from %s)", ref.String(), img) - } - return ref, nil -} - // tagOrDigest returns a tag or digest from a reference for which !reference.IsNameOnly. func tagOrDigest(ref reference.Named) (string, error) { if ref, ok := ref.(reference.Canonical); ok { diff --git a/image/image.go b/image/image.go index d7dc5d44ee..1d97b36303 100644 --- a/image/image.go +++ b/image/image.go @@ -13,7 +13,6 @@ import ( "github.com/containers/image/manifest" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) var ( @@ -51,12 +50,10 @@ func FromSource(src types.ImageSource, requestedManifestMIMETypes []string) type return &genericImage{src: src, requestedManifestMIMETypes: requestedManifestMIMETypes} } -// IntendedDockerReference returns the Docker reference for this image, _as specified by the user_ -// (not as the image itself, or its underlying storage, claims). Should be fully expanded, i.e. !reference.IsNameOnly. -// This can be used e.g. to determine which public keys are trusted for this image. -// May be nil if unknown. -func (i *genericImage) IntendedDockerReference() reference.Named { - return i.src.IntendedDockerReference() +// Reference returns the reference used to set up this source, _as specified by the user_ +// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. +func (i *genericImage) Reference() types.ImageReference { + return i.src.Reference() } // Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need. diff --git a/oci/oci_dest.go b/oci/oci_dest.go index e611d189bd..7cf242c358 100644 --- a/oci/oci_dest.go +++ b/oci/oci_dest.go @@ -7,12 +7,10 @@ import ( "io/ioutil" "os" "path/filepath" - "regexp" "strings" "github.com/containers/image/manifest" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) type ociManifest struct { @@ -30,32 +28,18 @@ type descriptor struct { } type ociImageDestination struct { - dir string - tag string + ref ociReference } -var refRegexp = regexp.MustCompile(`^([A-Za-z0-9._-]+)+$`) - -// NewImageDestination returns an ImageDestination for writing to an existing directory. -func NewImageDestination(dest string) (types.ImageDestination, error) { - dir := dest - sep := strings.LastIndex(dest, ":") - tag := "latest" - if sep != -1 { - dir = dest[:sep] - tag = dest[sep+1:] - if !refRegexp.MatchString(tag) { - return nil, fmt.Errorf("Invalid reference %s", tag) - } - } - return &ociImageDestination{ - dir: dir, - tag: tag, - }, nil +// newImageDestination returns an ImageDestination for writing to an existing directory. +func newImageDestination(ref ociReference) types.ImageDestination { + return &ociImageDestination{ref: ref} } -func (d *ociImageDestination) CanonicalDockerReference() reference.Named { - return nil +// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, +// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. +func (d *ociImageDestination) Reference() types.ImageReference { + return d.ref } func createManifest(m []byte) ([]byte, string, error) { @@ -116,21 +100,21 @@ func (d *ociImageDestination) PutManifest(m []byte) error { return err } - if err := ioutil.WriteFile(blobPath(d.dir, digest), ociMan, 0644); err != nil { + if err := ioutil.WriteFile(blobPath(d.ref.dir, digest), ociMan, 0644); err != nil { return err } // TODO(runcom): ugly here? - if err := ioutil.WriteFile(ociLayoutPath(d.dir), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0644); err != nil { + if err := ioutil.WriteFile(ociLayoutPath(d.ref.dir), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0644); err != nil { return err } - return ioutil.WriteFile(descriptorPath(d.dir, d.tag), data, 0644) + return ioutil.WriteFile(descriptorPath(d.ref.dir, d.ref.tag), data, 0644) } func (d *ociImageDestination) PutBlob(digest string, stream io.Reader) error { if err := d.ensureParentDirectoryExists("blobs"); err != nil { return err } - blob, err := os.Create(blobPath(d.dir, digest)) + blob, err := os.Create(blobPath(d.ref.dir, digest)) if err != nil { return err } @@ -145,7 +129,7 @@ func (d *ociImageDestination) PutBlob(digest string, stream io.Reader) error { } func (d *ociImageDestination) ensureParentDirectoryExists(parent string) error { - path := filepath.Join(d.dir, parent) + path := filepath.Join(d.ref.dir, parent) if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { if err := os.MkdirAll(path, 0755); err != nil { return err diff --git a/oci/oci_transport.go b/oci/oci_transport.go new file mode 100644 index 0000000000..ab069586e0 --- /dev/null +++ b/oci/oci_transport.go @@ -0,0 +1,91 @@ +package oci + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/containers/image/types" + "github.com/docker/docker/reference" +) + +// Transport is an ImageTransport for Docker references. +var Transport = ociTransport{} + +type ociTransport struct{} + +func (t ociTransport) Name() string { + return "oci" +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func (t ociTransport) ParseReference(reference string) (types.ImageReference, error) { + return ParseReference(reference) +} + +// ociReference is an ImageReference for OCI directory paths. +type ociReference struct { + // Note that the interpretation of paths below depends on the underlying filesystem state, which may change under us at any time! + dir string // As specified by the user. May be relative, contain symlinks, etc. + tag string +} + +var refRegexp = regexp.MustCompile(`^([A-Za-z0-9._-]+)+$`) + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OCI ImageReference. +func ParseReference(reference string) (types.ImageReference, error) { + var dir, tag string + sep := strings.LastIndex(reference, ":") + if sep == -1 { + dir = reference + tag = "latest" + } else { + dir = reference[:sep] + tag = reference[sep+1:] + if !refRegexp.MatchString(tag) { + return nil, fmt.Errorf("Invalid tag %s", tag) + } + } + return NewReference(dir, tag), nil +} + +// NewReference returns an OCI reference for a directory and a tag. +func NewReference(dir, tag string) types.ImageReference { + return ociReference{dir: dir, tag: tag} +} + +func (ref ociReference) Transport() types.ImageTransport { + return Transport +} + +// StringWithinTransport returns a string representation of the reference, which MUST be such that +// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. +// NOTE: The returned string is not promised to be equal to the original input to ParseReference; +// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. +func (ref ociReference) StringWithinTransport() string { + return fmt.Sprintf("%s:%s", ref.dir, ref.tag) +} + +// DockerReference returns a Docker reference associated with this reference +// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent, +// not e.g. after redirect or alias processing), or nil if unknown/not applicable. +func (ref ociReference) DockerReference() reference.Named { + return nil +} + +// NewImage returns a types.Image for this reference. +func (ref ociReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + return nil, errors.New("Full Image support not implemented for oci: image names") +} + +// NewImageSource returns a types.ImageSource for this reference. +func (ref ociReference) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + return nil, errors.New("Reading images not implemented for oci: image names") +} + +// NewImageDestination returns a types.ImageDestination for this reference. +func (ref ociReference) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + return newImageDestination(ref), nil +} diff --git a/oci/oci_transport_test.go b/oci/oci_transport_test.go new file mode 100644 index 0000000000..417f308532 --- /dev/null +++ b/oci/oci_transport_test.go @@ -0,0 +1,140 @@ +package oci + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/containers/image/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "oci", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + tmpDir, err := ioutil.TempDir("", "oci-transport-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + for _, path := range []string{ + "/", + "/etc", + tmpDir, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + for _, tag := range []struct{ suffix, tag string }{ + {":notlatest", "notlatest"}, + {"", "latest"}, + } { + input := path + tag.suffix + ref, err := fn(input) + require.NoError(t, err, input) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, path, ociRef.dir, input) + assert.Equal(t, tag.tag, ociRef.tag, input) + } + } + + ref, err := fn(tmpDir + "/with:colons:and:tag") + require.NoError(t, err) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/with:colons:and", ociRef.dir) + assert.Equal(t, "tag", ociRef.tag) + + _, err = fn(tmpDir + ":invalid'tag!value@") + assert.Error(t, err) +} + +func TestNewReference(t *testing.T) { + const tagValue = "tagValue" + + tmpDir, err := ioutil.TempDir("", "oci-transport-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + ref := NewReference(tmpDir, tagValue) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir, ociRef.dir) + assert.Equal(t, tagValue, ociRef.tag) +} + +// refToTempOCI creates a temporary directory and returns an reference to it. +// The caller should +// defer os.RemoveAll(tmpDir) +func refToTempOCI(t *testing.T) (ref types.ImageReference, tmpDir string) { + tmpDir, err := ioutil.TempDir("", "oci-transport-test") + require.NoError(t, err) + ref = NewReference(tmpDir, "tagValue") + return ref, tmpDir +} + +func TestReferenceTransport(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + defer os.RemoveAll(tmpDir) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "oci-transport-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + for _, c := range []struct{ input, result string }{ + {"/dir1:notlatest", "/dir1:notlatest"}, // Explicit tag + {"/dir2", "/dir2:latest"}, // Default tag + {"/dir3:with:colons:and:tag", "/dir3:with:colons:and:tag"}, + } { + ref, err := ParseReference(tmpDir + c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, tmpDir+c.result, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + defer os.RemoveAll(tmpDir) + assert.Nil(t, ref.DockerReference()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + defer os.RemoveAll(tmpDir) + _, err := ref.NewImage("/this/doesn't/exist", true) + assert.Error(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + defer os.RemoveAll(tmpDir) + _, err := ref.NewImageSource("/this/doesn't/exist", true) + assert.Error(t, err) +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + defer os.RemoveAll(tmpDir) + _, err := ref.NewImageDestination("/this/doesn't/exist", true) + assert.NoError(t, err) +} diff --git a/openshift/openshift.go b/openshift/openshift.go index 36958fe41f..60bea41e06 100644 --- a/openshift/openshift.go +++ b/openshift/openshift.go @@ -8,8 +8,6 @@ import ( "io" "io/ioutil" "net/http" - "net/url" - "regexp" "strings" "github.com/Sirupsen/logrus" @@ -17,29 +15,26 @@ import ( "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/containers/image/version" - "github.com/docker/docker/reference" ) // openshiftClient is configuration for dealing with a single image stream, for reading or writing. type openshiftClient struct { + ref openshiftReference // Values from Kubernetes configuration - baseURL *url.URL httpClient *http.Client bearerToken string // "" if not used username string // "" if not used password string // if username != "" - // Values specific to this image - namespace string - stream string - tag string - canonicalDockerReference reference.Named // Computed from the above in advance, so that later references can not fail. } -// FIXME: Is imageName like this a good way to refer to OpenShift images? -var imageNameRegexp = regexp.MustCompile("^([^:/]*)/([^:/]*):([^:/]*)$") +// newOpenshiftClient creates a new openshiftClient for the specified reference. +func newOpenshiftClient(ref openshiftReference) (*openshiftClient, error) { + // We have already done this parsing in ParseReference, but thrown away + // httpClient. So, parse again. + // (We could also rework/split restClientFor to "get base URL" to be done + // in ParseReference, and "get httpClient" to be done here. But until/unless + // we support non-default clusters, this is good enough.) -// newOpenshiftClient creates a new openshiftClient for the specified image. -func newOpenshiftClient(imageName string) (*openshiftClient, error) { // Overall, this is modelled on openshift/origin/pkg/cmd/util/clientcmd.New().ClientConfig() and openshift/origin/pkg/client. cmdConfig := defaultClientConfig() logrus.Debugf("cmdConfig: %#v", cmdConfig) @@ -54,42 +49,22 @@ func newOpenshiftClient(imageName string) (*openshiftClient, error) { return nil, err } logrus.Debugf("URL: %#v", *baseURL) - - m := imageNameRegexp.FindStringSubmatch(imageName) - if m == nil || len(m) != 4 { - return nil, fmt.Errorf("Invalid image reference %s, %#v", imageName, m) + if *baseURL != *ref.baseURL { + return nil, fmt.Errorf("Unexpected baseURL mismatch: default %#v, reference %#v", *baseURL, *ref.baseURL) } - c := &openshiftClient{ - baseURL: baseURL, + return &openshiftClient{ + ref: ref, httpClient: httpClient, bearerToken: restConfig.BearerToken, username: restConfig.Username, password: restConfig.Password, - - namespace: m[1], - stream: m[2], - tag: m[3], - } - - // Precompute also c.canonicalDockerReference so that later references can not fail. - // FIXME: This is, strictly speaking, a namespace conflict with images placed in a Docker registry running on the same host. - // Do we need to do something else, perhaps disambiguate (port number?) or namespace Docker and OpenShift separately? - dockerRef, err := reference.WithName(fmt.Sprintf("%s/%s/%s", c.baseURL.Host, c.namespace, c.stream)) - if err != nil { - return nil, err - } - c.canonicalDockerReference, err = reference.WithTag(dockerRef, c.tag) - if err != nil { - return nil, err - } - - return c, nil + }, nil } // doRequest performs a correctly authenticated request to a specified path, and returns response body or an error object. func (c *openshiftClient) doRequest(method, path string, requestBody []byte) ([]byte, error) { - url := *c.baseURL + url := *c.ref.baseURL url.Path = path var requestBodyReader io.Reader if requestBody != nil { @@ -168,7 +143,7 @@ func (c *openshiftClient) convertDockerImageReference(ref string) (string, error // about how the OpenShift Atomic Registry is configured, per examples/atomic-registry/run.sh: // -p OPENSHIFT_OAUTH_PROVIDER_URL=https://${INSTALL_HOST}:8443,COCKPIT_KUBE_URL=https://${INSTALL_HOST},REGISTRY_HOST=${INSTALL_HOST}:5000 func (c *openshiftClient) dockerRegistryHostPart() string { - return strings.SplitN(c.baseURL.Host, ":", 2)[0] + ":5000" + return strings.SplitN(c.ref.baseURL.Host, ":", 2)[0] + ":5000" } type openshiftImageSource struct { @@ -181,9 +156,9 @@ type openshiftImageSource struct { imageStreamImageName string // Resolved image identifier, or "" if not known yet } -// NewImageSource creates a new ImageSource for the specified image and connection specification. -func NewImageSource(imageName, certPath string, tlsVerify bool) (types.ImageSource, error) { - client, err := newOpenshiftClient(imageName) +// newImageSource creates a new ImageSource for the specified reference and connection specification. +func newImageSource(ref openshiftReference, certPath string, tlsVerify bool) (types.ImageSource, error) { + client, err := newOpenshiftClient(ref) if err != nil { return nil, err } @@ -195,12 +170,10 @@ func NewImageSource(imageName, certPath string, tlsVerify bool) (types.ImageSour }, nil } -// IntendedDockerReference returns the Docker reference for this image, _as specified by the user_ -// (not as the image itself, or its underlying storage, claims). Should be fully expanded, i.e. !reference.IsNameOnly. -// This can be used e.g. to determine which public keys are trusted for this image. -// May be nil if unknown. -func (s *openshiftImageSource) IntendedDockerReference() reference.Named { - return s.client.canonicalDockerReference +// Reference returns the reference used to set up this source, _as specified by the user_ +// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. +func (s *openshiftImageSource) Reference() types.ImageReference { + return s.client.ref } func (s *openshiftImageSource) GetManifest(mimetypes []string) ([]byte, string, error) { @@ -228,7 +201,7 @@ func (s *openshiftImageSource) ensureImageIsResolved() error { } // FIXME: validate components per validation.IsValidPathSegmentName? - path := fmt.Sprintf("/oapi/v1/namespaces/%s/imagestreams/%s", s.client.namespace, s.client.stream) + path := fmt.Sprintf("/oapi/v1/namespaces/%s/imagestreams/%s", s.client.ref.namespace, s.client.ref.stream) body, err := s.client.doRequest("GET", path, nil) if err != nil { return err @@ -240,7 +213,7 @@ func (s *openshiftImageSource) ensureImageIsResolved() error { } var te *tagEvent for _, tag := range is.Status.Tags { - if tag.Tag != s.client.tag { + if tag.Tag != s.client.ref.tag { continue } if len(tag.Items) > 0 { @@ -252,12 +225,16 @@ func (s *openshiftImageSource) ensureImageIsResolved() error { return fmt.Errorf("No matching tag found") } logrus.Debugf("tag event %#v", te) - dockerRef, err := s.client.convertDockerImageReference(te.DockerImageReference) + dockerRefString, err := s.client.convertDockerImageReference(te.DockerImageReference) + if err != nil { + return err + } + logrus.Debugf("Resolved reference %#v", dockerRefString) + dockerRef, err := docker.ParseReference("//" + dockerRefString) if err != nil { return err } - logrus.Debugf("Resolved reference %#v", dockerRef) - d, err := docker.NewImageSource(dockerRef, s.certPath, s.tlsVerify) + d, err := dockerRef.NewImageSource(s.certPath, s.tlsVerify) if err != nil { return err } @@ -271,9 +248,9 @@ type openshiftImageDestination struct { docker types.ImageDestination // The Docker Registry endpoint } -// NewImageDestination creates a new ImageDestination for the specified image and connection specification. -func NewImageDestination(imageName, certPath string, tlsVerify bool) (types.ImageDestination, error) { - client, err := newOpenshiftClient(imageName) +// newImageDestination creates a new ImageDestination for the specified reference and connection specification. +func newImageDestination(ref openshiftReference, certPath string, tlsVerify bool) (types.ImageDestination, error) { + client, err := newOpenshiftClient(ref) if err != nil { return nil, err } @@ -281,8 +258,12 @@ func NewImageDestination(imageName, certPath string, tlsVerify bool) (types.Imag // FIXME: Should this always use a digest, not a tag? Uploading to Docker by tag requires the tag _inside_ the manifest to match, // i.e. a single signed image cannot be available under multiple tags. But with types.ImageDestination, we don't know // the manifest digest at this point. - dockerRef := fmt.Sprintf("%s/%s/%s:%s", client.dockerRegistryHostPart(), client.namespace, client.stream, client.tag) - docker, err := docker.NewImageDestination(dockerRef, certPath, tlsVerify) + dockerRefString := fmt.Sprintf("//%s/%s/%s:%s", client.dockerRegistryHostPart(), client.ref.namespace, client.ref.stream, client.ref.tag) + dockerRef, err := docker.ParseReference(dockerRefString) + if err != nil { + return nil, err + } + docker, err := dockerRef.NewImageDestination(certPath, tlsVerify) if err != nil { return nil, err } @@ -293,6 +274,12 @@ func NewImageDestination(imageName, certPath string, tlsVerify bool) (types.Imag }, nil } +// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, +// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. +func (d *openshiftImageDestination) Reference() types.ImageReference { + return d.client.ref +} + func (d *openshiftImageDestination) SupportedManifestMIMETypes() []string { return []string{ manifest.DockerV2Schema1SignedMIMEType, @@ -300,10 +287,6 @@ func (d *openshiftImageDestination) SupportedManifestMIMETypes() []string { } } -func (d *openshiftImageDestination) CanonicalDockerReference() reference.Named { - return d.client.canonicalDockerReference -} - func (d *openshiftImageDestination) PutManifest(m []byte) error { // Note: This does absolutely no kind/version checking or conversions. manifestDigest, err := manifest.Digest(m) @@ -311,15 +294,15 @@ func (d *openshiftImageDestination) PutManifest(m []byte) error { return err } // FIXME: We can't do what respositorymiddleware.go does because we don't know the internal address. Does any of this matter? - dockerImageReference := fmt.Sprintf("%s/%s/%s@%s", d.client.dockerRegistryHostPart(), d.client.namespace, d.client.stream, manifestDigest) + dockerImageReference := fmt.Sprintf("%s/%s/%s@%s", d.client.dockerRegistryHostPart(), d.client.ref.namespace, d.client.ref.stream, manifestDigest) ism := imageStreamMapping{ typeMeta: typeMeta{ Kind: "ImageStreamMapping", APIVersion: "v1", }, objectMeta: objectMeta{ - Namespace: d.client.namespace, - Name: d.client.stream, + Namespace: d.client.ref.namespace, + Name: d.client.ref.stream, }, Image: image{ objectMeta: objectMeta{ @@ -328,7 +311,7 @@ func (d *openshiftImageDestination) PutManifest(m []byte) error { DockerImageReference: dockerImageReference, DockerImageManifest: string(m), }, - Tag: d.client.tag, + Tag: d.client.ref.tag, } body, err := json.Marshal(ism) if err != nil { @@ -336,7 +319,7 @@ func (d *openshiftImageDestination) PutManifest(m []byte) error { } // FIXME: validate components per validation.IsValidPathSegmentName? - path := fmt.Sprintf("/oapi/v1/namespaces/%s/imagestreammappings", d.client.namespace) + path := fmt.Sprintf("/oapi/v1/namespaces/%s/imagestreammappings", d.client.ref.namespace) body, err = d.client.doRequest("POST", path, body) if err != nil { return err diff --git a/openshift/openshift_transport.go b/openshift/openshift_transport.go new file mode 100644 index 0000000000..c9c92072a7 --- /dev/null +++ b/openshift/openshift_transport.go @@ -0,0 +1,127 @@ +package openshift + +import ( + "errors" + "fmt" + "net/url" + "regexp" + + "github.com/Sirupsen/logrus" + "github.com/containers/image/types" + "github.com/docker/docker/reference" +) + +// Transport is an ImageTransport for directory paths. +var Transport = openshiftTransport{} + +type openshiftTransport struct{} + +func (t openshiftTransport) Name() string { + return "atomic" +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func (t openshiftTransport) ParseReference(reference string) (types.ImageReference, error) { + return ParseReference(reference) +} + +// openshiftReference is an ImageReference for OpenShift images. +type openshiftReference struct { + baseURL *url.URL + namespace string + stream string + tag string + dockerReference reference.Named // Computed from the above in advance, so that later references can not fail. +} + +// FIXME: Is imageName like this a good way to refer to OpenShift images? +var imageNameRegexp = regexp.MustCompile("^([^:/]*)/([^:/]*):([^:/]*)$") + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OpenShift ImageReference. +func ParseReference(reference string) (types.ImageReference, error) { + // Overall, this is modelled on openshift/origin/pkg/cmd/util/clientcmd.New().ClientConfig() and openshift/origin/pkg/client. + cmdConfig := defaultClientConfig() + logrus.Debugf("cmdConfig: %#v", cmdConfig) + restConfig, err := cmdConfig.ClientConfig() + if err != nil { + return nil, err + } + // REMOVED: SetOpenShiftDefaults (values are not overridable in config files, so hard-coded these defaults.) + logrus.Debugf("restConfig: %#v", restConfig) + baseURL, _, err := restClientFor(restConfig) + if err != nil { + return nil, err + } + logrus.Debugf("URL: %#v", *baseURL) + + m := imageNameRegexp.FindStringSubmatch(reference) + if m == nil || len(m) != 4 { + return nil, fmt.Errorf("Invalid image reference %s, %#v", reference, m) + } + + return NewReference(baseURL, m[1], m[2], m[3]) +} + +// NewReference returns an OpenShift reference for a base URL, namespace, stream and tag. +func NewReference(baseURL *url.URL, namespace, stream, tag string) (types.ImageReference, error) { + // Precompute also dockerReference so that later references can not fail. + // + // This discards ref.baseURL.Path, which is unexpected for a “base URL”; + // but openshiftClient.doRequest actually completely overrides url.Path + // (and defaultServerURL rejects non-trivial Path values), so it is OK for + // us to ignore it as well. + // + // FIXME: This is, strictly speaking, a namespace conflict with images placed in a Docker registry running on the same host. + // Do we need to do something else, perhaps disambiguate (port number?) or namespace Docker and OpenShift separately? + dockerRef, err := reference.WithName(fmt.Sprintf("%s/%s/%s", baseURL.Host, namespace, stream)) + if err != nil { + return nil, err + } + dockerRef, err = reference.WithTag(dockerRef, tag) + if err != nil { + return nil, err + } + + return openshiftReference{ + baseURL: baseURL, + namespace: namespace, + stream: stream, + tag: tag, + dockerReference: dockerRef, + }, nil +} + +func (ref openshiftReference) Transport() types.ImageTransport { + return Transport +} + +// StringWithinTransport returns a string representation of the reference, which MUST be such that +// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. +// NOTE: The returned string is not promised to be equal to the original input to ParseReference; +// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. +func (ref openshiftReference) StringWithinTransport() string { + return fmt.Sprintf("%s/%s:%s", ref.namespace, ref.stream, ref.tag) +} + +// DockerReference returns a Docker reference associated with this reference +// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent, +// not e.g. after redirect or alias processing), or nil if unknown/not applicable. +func (ref openshiftReference) DockerReference() reference.Named { + return ref.dockerReference +} + +// NewImage returns a types.Image for this reference. +func (ref openshiftReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + return nil, errors.New("Full Image support not implemented for atomic: image names") +} + +// NewImageSource returns a types.ImageSource for this reference. +func (ref openshiftReference) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + return newImageSource(ref, certPath, tlsVerify) +} + +// NewImageDestination returns a types.ImageDestination for this reference. +func (ref openshiftReference) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + return newImageDestination(ref, certPath, tlsVerify) +} diff --git a/openshift/openshift_transport_test.go b/openshift/openshift_transport_test.go new file mode 100644 index 0000000000..7303119a71 --- /dev/null +++ b/openshift/openshift_transport_test.go @@ -0,0 +1,82 @@ +package openshift + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "@sha256:" + sha256digestHex +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "atomic", Transport.Name()) +} + +// Transport.ParseReference, ParseReference untested because they depend +// on per-user configuration. +var testBaseURL *url.URL + +func init() { + u, err := url.Parse("https://registry.example.com:8443") + if err != nil { + panic("Error initializing testBaseURL") + } + testBaseURL = u +} + +func TestNewReference(t *testing.T) { + // Success + ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest") + require.NoError(t, err) + osRef, ok := ref.(openshiftReference) + require.True(t, ok) + assert.Equal(t, testBaseURL.String(), osRef.baseURL.String()) + assert.Equal(t, "ns", osRef.namespace) + assert.Equal(t, "stream", osRef.stream) + assert.Equal(t, "notlatest", osRef.tag) + assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", osRef.dockerReference.String()) + + // Components creating an invalid Docker Reference name + _, err = NewReference(testBaseURL, "ns", "UPPERCASEISINVALID", "notlatest") + assert.Error(t, err) + + _, err = NewReference(testBaseURL, "ns", "stream", "invalid!tag@value=") + assert.Error(t, err) +} + +func TestReferenceDockerReference(t *testing.T) { + ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest") + require.NoError(t, err) + dockerRef := ref.DockerReference() + require.NotNil(t, dockerRef) + assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", dockerRef.String()) +} + +func TestReferenceTransport(t *testing.T) { + ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest") + require.NoError(t, err) + assert.Equal(t, "ns/stream:notlatest", ref.StringWithinTransport()) + // We should do one more round to verify that the output can be parsed, to an equal value, + // but that is untested because it depends on per-user configuration. +} + +func TestReferenceNewImage(t *testing.T) { + ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest") + require.NoError(t, err) + _, err = ref.NewImage("", true) + assert.Error(t, err) +} + +// openshfitReference.NewImageSource, openshfitReference.NewImageDestination untested because they depend +// on per-user configuration when initializing httpClient. diff --git a/signature/policy_eval.go b/signature/policy_eval.go index cbcf599dca..6594b665df 100644 --- a/signature/policy_eval.go +++ b/signature/policy_eval.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/Sirupsen/logrus" + "github.com/containers/image/transports" "github.com/containers/image/types" "github.com/docker/docker/reference" ) @@ -70,8 +71,8 @@ type PolicyRequirement interface { // The type is public, but its implementation is private. type PolicyReferenceMatch interface { // matchesDockerReference decides whether a specific image identity is accepted for an image - // (or, usually, for the image's IntendedDockerReference()). Note that - // image.IntendedDockerReference() may be nil. + // (or, usually, for the image's Reference().DockerReference()). Note that + // image.Reference().DockerReference() may be nil. matchesDockerReference(image types.Image, signatureDockerReference string) bool } @@ -155,10 +156,9 @@ func fullyExpandedDockerReference(ref reference.Named) (string, error) { // requirementsForImage selects the appropriate requirements for image. func (pc *PolicyContext) requirementsForImage(image types.Image) (PolicyRequirements, error) { - ref := image.IntendedDockerReference() + ref := image.Reference().DockerReference() if ref == nil { - // FIXME: Tell the user which image this is. - return nil, fmt.Errorf("Can not determine policy for an image with no known Docker reference identity") + return nil, fmt.Errorf("Can not determine policy for image %s with no known Docker reference identity", transports.ImageName(image.Reference())) } ref = reference.WithDefaultTag(ref) // This should not be needed, but if we did receive a name-only reference, this is a reasonable thing to do. @@ -222,7 +222,7 @@ func (pc *PolicyContext) GetSignaturesWithAcceptedAuthor(image types.Image) (sig } }() - logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", image.IntendedDockerReference()) + logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", image.Reference().DockerReference()) reqs, err := pc.requirementsForImage(image) if err != nil { @@ -306,7 +306,7 @@ func (pc *PolicyContext) IsRunningImageAllowed(image types.Image) (res bool, fin } }() - logrus.Debugf("IsRunningImageAllowed for image %s", image.IntendedDockerReference()) + logrus.Debugf("IsRunningImageAllowed for image %s", image.Reference().DockerReference()) reqs, err := pc.requirementsForImage(image) if err != nil { diff --git a/signature/policy_eval_signedby_test.go b/signature/policy_eval_signedby_test.go index bcebcbfb79..7dd48233e0 100644 --- a/signature/policy_eval_signedby_test.go +++ b/signature/policy_eval_signedby_test.go @@ -14,24 +14,32 @@ import ( "github.com/stretchr/testify/require" ) -// dirImageMock returns a types.Image for a directory, claiming a specified intendedDockerReference. -func dirImageMock(t *testing.T, dir, intendedDockerReference string) types.Image { - ref, err := reference.ParseNamed(intendedDockerReference) +// dirImageMock returns a types.Image for a directory, claiming a specified dockerReference. +func dirImageMock(t *testing.T, dir, dockerReference string) types.Image { + ref, err := reference.ParseNamed(dockerReference) + require.NoError(t, err) + return dirImageMockWithRef(t, dir, refImageReferenceMock{ref}) +} + +// dirImageMockWithRef returns a types.Image for a directory, claiming a specified ref. +func dirImageMockWithRef(t *testing.T, dir string, ref types.ImageReference) types.Image { + srcRef := directory.NewReference(dir) + src, err := srcRef.NewImageSource("", true) require.NoError(t, err) return image.FromSource(&dirImageSourceMock{ - ImageSource: directory.NewImageSource(dir), - intendedDockerReference: ref, + ImageSource: src, + ref: ref, }, nil) } -// dirImageSourceMock inherits dirImageSource, but overrides its IntendedDockerReference method. +// dirImageSourceMock inherits dirImageSource, but overrides its Reference method. type dirImageSourceMock struct { types.ImageSource - intendedDockerReference reference.Named + ref types.ImageReference } -func (d *dirImageSourceMock) IntendedDockerReference() reference.Named { - return d.intendedDockerReference +func (d *dirImageSourceMock) Reference() types.ImageReference { + return d.ref } func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { diff --git a/signature/policy_eval_simple.go b/signature/policy_eval_simple.go index 0ad2b2e23a..5d479f23bc 100644 --- a/signature/policy_eval_simple.go +++ b/signature/policy_eval_simple.go @@ -2,7 +2,12 @@ package signature -import "github.com/containers/image/types" +import ( + "fmt" + + "github.com/containers/image/transports" + "github.com/containers/image/types" +) func (pr *prInsecureAcceptAnything) isSignatureAuthorAccepted(image types.Image, sig []byte) (signatureAcceptanceResult, *Signature, error) { // prInsecureAcceptAnything semantics: Every image is allowed to run, @@ -15,11 +20,9 @@ func (pr *prInsecureAcceptAnything) isRunningImageAllowed(image types.Image) (bo } func (pr *prReject) isSignatureAuthorAccepted(image types.Image, sig []byte) (signatureAcceptanceResult, *Signature, error) { - // FIXME? Name the image, or better the matched scope in Policy.Specific. - return sarRejected, nil, PolicyRequirementError("Any signatures for these images are rejected by policy.") + return sarRejected, nil, PolicyRequirementError(fmt.Sprintf("Any signatures for image %s are rejected by policy.", transports.ImageName(image.Reference()))) } func (pr *prReject) isRunningImageAllowed(image types.Image) (bool, error) { - // FIXME? Name the image, or better the matched scope in Policy.Specific. - return false, PolicyRequirementError("Running these images is rejected by policy.") + return false, PolicyRequirementError(fmt.Sprintf("Running image %s is rejected by policy.", transports.ImageName(image.Reference()))) } diff --git a/signature/policy_eval_simple_test.go b/signature/policy_eval_simple_test.go index c4434b10b7..f63f941d86 100644 --- a/signature/policy_eval_simple_test.go +++ b/signature/policy_eval_simple_test.go @@ -1,32 +1,66 @@ package signature -import "testing" +import ( + "testing" + + "github.com/containers/image/types" + "github.com/docker/docker/reference" +) + +// nameOnlyImageMock is a mock of types.Image which only allows transports.ImageName to work +type nameOnlyImageMock struct { + forbiddenImageMock +} + +func (nameOnlyImageMock) Reference() types.ImageReference { + return nameOnlyImageReferenceMock("== StringWithinTransport mock") +} + +// nameOnlyImageReferenceMock is a mock of types.ImageReference which only allows transports.ImageName to work, returning self. +type nameOnlyImageReferenceMock string + +func (ref nameOnlyImageReferenceMock) Transport() types.ImageTransport { + return nameImageTransportMock("== Transport mock") +} +func (ref nameOnlyImageReferenceMock) StringWithinTransport() string { + return string(ref) +} +func (ref nameOnlyImageReferenceMock) DockerReference() reference.Named { + panic("unexpected call to a mock function") +} +func (ref nameOnlyImageReferenceMock) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + panic("unexpected call to a mock function") +} +func (ref nameOnlyImageReferenceMock) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + panic("unexpected call to a mock function") +} +func (ref nameOnlyImageReferenceMock) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + panic("unexpected call to a mock function") +} func TestPRInsecureAcceptAnythingIsSignatureAuthorAccepted(t *testing.T) { pr := NewPRInsecureAcceptAnything() - // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. - sar, parsedSig, err := pr.isSignatureAuthorAccepted(nil, nil) + // Pass nil signature to, kind of, test that the return value does not depend on it. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(nameOnlyImageMock{}, nil) assertSARUnknown(t, sar, parsedSig, err) } func TestPRInsecureAcceptAnythingIsRunningImageAllowed(t *testing.T) { pr := NewPRInsecureAcceptAnything() - // Pass a nil pointer to, kind of, test that the return value does not depend on the image. - res, err := pr.isRunningImageAllowed(nil) + res, err := pr.isRunningImageAllowed(nameOnlyImageMock{}) assertRunningAllowed(t, res, err) } func TestPRRejectIsSignatureAuthorAccepted(t *testing.T) { pr := NewPRReject() - // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. - sar, parsedSig, err := pr.isSignatureAuthorAccepted(nil, nil) + // Pass nil signature to, kind of, test that the return value does not depend on it. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(nameOnlyImageMock{}, nil) assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) } func TestPRRejectIsRunningImageAllowed(t *testing.T) { // This will obviously need to change after this is implemented. pr := NewPRReject() - // Pass a nil pointer to, kind of, test that the return value does not depend on the image. - res, err := pr.isRunningImageAllowed(nil) + res, err := pr.isRunningImageAllowed(nameOnlyImageMock{}) assertRunningRejectedPolicyRequirement(t, res, err) } diff --git a/signature/policy_eval_test.go b/signature/policy_eval_test.go index afcb8e0500..f49413eecf 100644 --- a/signature/policy_eval_test.go +++ b/signature/policy_eval_test.go @@ -5,8 +5,7 @@ import ( "os" "testing" - "github.com/containers/image/directory" - "github.com/containers/image/image" + "github.com/containers/image/types" "github.com/docker/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -100,6 +99,30 @@ func TestFullyExpandedDockerReference(t *testing.T) { } } +// pcImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference +type pcImageReferenceMock struct{ ref reference.Named } + +func (ref pcImageReferenceMock) Transport() types.ImageTransport { + // We use this in error messages, so sadly we must return something. + return nameImageTransportMock("== Transport mock") +} +func (ref pcImageReferenceMock) StringWithinTransport() string { + // We use this in error messages, so sadly we must return something. + return "== StringWithinTransport mock" +} +func (ref pcImageReferenceMock) DockerReference() reference.Named { + return ref.ref +} +func (ref pcImageReferenceMock) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + panic("unexpected call to a mock function") +} +func (ref pcImageReferenceMock) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + panic("unexpected call to a mock function") +} +func (ref pcImageReferenceMock) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + panic("unexpected call to a mock function") +} + func TestPolicyContextRequirementsForImage(t *testing.T) { ktGPG := SBKeyTypeGPGKeys prm := NewPRMMatchExact() @@ -217,6 +240,13 @@ func TestPolicyContextRequirementsForImage(t *testing.T) { assert.Error(t, err) } +// pcImageMock returns a types.Image for a directory, claiming a specified dockerReference and implementing PolicyConfigurationIdentity/PolicyConfigurationNamespaces. +func pcImageMock(t *testing.T, dir, dockerReference string) types.Image { + ref, err := reference.ParseNamed(dockerReference) + require.NoError(t, err) + return dirImageMockWithRef(t, dir, pcImageReferenceMock{ref}) +} + func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) { expectedSig := &Signature{ DockerManifestDigest: TestImageManifestDigest, @@ -258,73 +288,73 @@ func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) { defer pc.Destroy() // Success - img := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") sigs, err := pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Equal(t, []*Signature{expectedSig}, sigs) // Two signatures // FIXME? Use really different signatures for this? - img = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Equal(t, []*Signature{expectedSig, expectedSig}, sigs) // No signatures - img = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) // Only invalid signatures - img = dirImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) // 1 invalid, 1 valid signature (in this order) - img = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Equal(t, []*Signature{expectedSig}, sigs) // Two sarAccepted results for one signature - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:twoAccepts") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:twoAccepts") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Equal(t, []*Signature{expectedSig}, sigs) // sarAccepted+sarRejected for a signature - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptReject") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptReject") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) // sarAccepted+sarUnknown for a signature - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptUnknown") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptUnknown") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Equal(t, []*Signature{expectedSig}, sigs) // sarRejected+sarUnknown for a signature - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:rejectUnknown") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:rejectUnknown") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) // sarUnknown only - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown2") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown2") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) // Empty list of requirements (invalid) - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) require.NoError(t, err) assert.Empty(t, sigs) @@ -336,7 +366,7 @@ func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) { require.NoError(t, err) err = destroyedPC.Destroy() require.NoError(t, err) - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") sigs, err = destroyedPC.GetSignaturesWithAcceptedAuthor(img) assert.Error(t, err) assert.Nil(t, sigs) @@ -345,10 +375,7 @@ func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) { // mistakes only, anyway. // Image without a Docker reference identity - img = image.FromSource(&dirImageSourceMock{ - ImageSource: directory.NewImageSource("fixtures/dir-img-valid"), - intendedDockerReference: nil, - }, nil) + img = dirImageMockWithRef(t, "fixtures/dir-img-valid", pcImageReferenceMock{nil}) sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) assert.Error(t, err) assert.Nil(t, sigs) @@ -356,7 +383,7 @@ func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) { // Error reading signatures. invalidSigDir := createInvalidSigDir(t) defer os.RemoveAll(invalidSigDir) - img = dirImageMock(t, invalidSigDir, "testing/manifest:latest") + img = pcImageMock(t, invalidSigDir, "testing/manifest:latest") sigs, err = pc.GetSignaturesWithAcceptedAuthor(img) assert.Error(t, err) assert.Nil(t, sigs) @@ -390,53 +417,53 @@ func TestPolicyContextIsRunningImageAllowed(t *testing.T) { defer pc.Destroy() // Success - img := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") res, err := pc.IsRunningImageAllowed(img) assertRunningAllowed(t, res, err) // Two signatures // FIXME? Use really different signatures for this? - img = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") res, err = pc.IsRunningImageAllowed(img) assertRunningAllowed(t, res, err) // No signatures - img = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") res, err = pc.IsRunningImageAllowed(img) assertRunningRejectedPolicyRequirement(t, res, err) // Only invalid signatures - img = dirImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") res, err = pc.IsRunningImageAllowed(img) assertRunningRejectedPolicyRequirement(t, res, err) // 1 invalid, 1 valid signature (in this order) - img = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") res, err = pc.IsRunningImageAllowed(img) assertRunningAllowed(t, res, err) // Two allowed results - img = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:twoAllows") + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:twoAllows") res, err = pc.IsRunningImageAllowed(img) assertRunningAllowed(t, res, err) // Allow + deny results - img = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:allowDeny") + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:allowDeny") res, err = pc.IsRunningImageAllowed(img) assertRunningRejectedPolicyRequirement(t, res, err) // prReject works - img = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:reject") + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:reject") res, err = pc.IsRunningImageAllowed(img) assertRunningRejectedPolicyRequirement(t, res, err) // prInsecureAcceptAnything works - img = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:acceptAnything") + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:acceptAnything") res, err = pc.IsRunningImageAllowed(img) assertRunningAllowed(t, res, err) // Empty list of requirements (invalid) - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements") res, err = pc.IsRunningImageAllowed(img) assertRunningRejectedPolicyRequirement(t, res, err) @@ -445,7 +472,7 @@ func TestPolicyContextIsRunningImageAllowed(t *testing.T) { require.NoError(t, err) err = destroyedPC.Destroy() require.NoError(t, err) - img = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") res, err = destroyedPC.IsRunningImageAllowed(img) assertRunningRejected(t, res, err) // Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement @@ -453,10 +480,7 @@ func TestPolicyContextIsRunningImageAllowed(t *testing.T) { // mistakes only, anyway. // Image without a Docker reference identity - img = image.FromSource(&dirImageSourceMock{ - ImageSource: directory.NewImageSource("fixtures/dir-img-valid"), - intendedDockerReference: nil, - }, nil) + img = dirImageMockWithRef(t, "fixtures/dir-img-valid", pcImageReferenceMock{nil}) res, err = pc.IsRunningImageAllowed(img) assertRunningRejected(t, res, err) } diff --git a/signature/policy_reference_match.go b/signature/policy_reference_match.go index 1a2be5d3db..2b2d410996 100644 --- a/signature/policy_reference_match.go +++ b/signature/policy_reference_match.go @@ -3,16 +3,19 @@ package signature import ( - "github.com/docker/docker/reference" + "fmt" + + "github.com/containers/image/transports" "github.com/containers/image/types" + "github.com/docker/docker/reference" ) // parseImageAndDockerReference converts an image and a reference string into two parsed entities, failing on any error and handling unidentified images. func parseImageAndDockerReference(image types.Image, s2 string) (reference.Named, reference.Named, error) { - r1 := image.IntendedDockerReference() + r1 := image.Reference().DockerReference() if r1 == nil { - // FIXME: Tell the user which image this is. - return nil, nil, PolicyRequirementError("Docker reference match attempted on an image with no known Docker reference identity") + return nil, nil, PolicyRequirementError(fmt.Sprintf("Docker reference match attempted on image %s with no known Docker reference identity", + transports.ImageName(image.Reference()))) } r2, err := reference.ParseNamed(s2) if err != nil { @@ -26,7 +29,7 @@ func (prm *prmMatchExact) matchesDockerReference(image types.Image, signatureDoc if err != nil { return false } - // Do not add default tags: image.IntendedDockerReference() has it added already per its construction, and signatureDockerReference should be exact; so, verify that now. + // Do not add default tags: image.Reference().DockerReference() should contain it already, and signatureDockerReference should be exact; so, verify that now. if reference.IsNameOnly(intended) || reference.IsNameOnly(signature) { return false } diff --git a/signature/policy_reference_match_test.go b/signature/policy_reference_match_test.go index 4e600e21eb..af4092b19b 100644 --- a/signature/policy_reference_match_test.go +++ b/signature/policy_reference_match_test.go @@ -50,11 +50,11 @@ func TestParseImageAndDockerReference(t *testing.T) { } } -// refImageMock is a mock of types.Image which returns itself in IntendedDockerReference. +// refImageMock is a mock of types.Image which returns itself in Reference().DockerReference. type refImageMock struct{ reference.Named } -func (ref refImageMock) IntendedDockerReference() reference.Named { - return ref.Named +func (ref refImageMock) Reference() types.ImageReference { + return refImageReferenceMock{ref.Named} } func (ref refImageMock) Manifest() ([]byte, string, error) { panic("unexpected call to a mock function") @@ -75,6 +75,46 @@ func (ref refImageMock) GetRepositoryTags() ([]string, error) { panic("unexpected call to a mock function") } +// refImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference. +type refImageReferenceMock struct{ reference.Named } + +func (ref refImageReferenceMock) Transport() types.ImageTransport { + // We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that. + if ref.Named == nil { + return nameImageTransportMock("== Transport mock") + } + panic("unexpected call to a mock function") +} +func (ref refImageReferenceMock) StringWithinTransport() string { + // We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that. + if ref.Named == nil { + return "== StringWithinTransport for an image with no Docker support" + } + panic("unexpected call to a mock function") +} +func (ref refImageReferenceMock) DockerReference() reference.Named { + return ref.Named +} +func (ref refImageReferenceMock) NewImage(certPath string, tlsVerify bool) (types.Image, error) { + panic("unexpected call to a mock function") +} +func (ref refImageReferenceMock) NewImageSource(certPath string, tlsVerify bool) (types.ImageSource, error) { + panic("unexpected call to a mock function") +} +func (ref refImageReferenceMock) NewImageDestination(certPath string, tlsVerify bool) (types.ImageDestination, error) { + panic("unexpected call to a mock function") +} + +// nameImageTransportMock is a mock of types.ImageTransport which returns itself in Name. +type nameImageTransportMock string + +func (name nameImageTransportMock) Name() string { + return string(name) +} +func (name nameImageTransportMock) ParseReference(reference string) (types.ImageReference, error) { + panic("unexpected call to a mock function") +} + type prmTableTest struct { imageRef, sigRef string result bool @@ -203,10 +243,10 @@ func TestParseDockerReferences(t *testing.T) { } } -// forbiddenImageMock is a mock of types.Image which ensures IntendedDockerReference is not called -type forbiddenImageMock string +// forbiddenImageMock is a mock of types.Image which ensures Reference is not called +type forbiddenImageMock struct{} -func (ref forbiddenImageMock) IntendedDockerReference() reference.Named { +func (ref forbiddenImageMock) Reference() types.ImageReference { panic("unexpected call to a mock function") } func (ref forbiddenImageMock) Manifest() ([]byte, string, error) { @@ -233,7 +273,7 @@ func TestPRMExactReferenceMatchesDockerReference(t *testing.T) { // Do not use NewPRMExactReference, we want to also test the case with an invalid DockerReference, // even though NewPRMExactReference should never let it happen. prm := prmExactReference{DockerReference: test.imageRef} - res := prm.matchesDockerReference(forbiddenImageMock(""), test.sigRef) + res := prm.matchesDockerReference(forbiddenImageMock{}, test.sigRef) assert.Equal(t, test.result, res, fmt.Sprintf("%s vs. %s", test.imageRef, test.sigRef)) } } @@ -243,7 +283,7 @@ func TestPRMExactRepositoryMatchesDockerReference(t *testing.T) { // Do not use NewPRMExactRepository, we want to also test the case with an invalid DockerReference, // even though NewPRMExactRepository should never let it happen. prm := prmExactRepository{DockerRepository: test.imageRef} - res := prm.matchesDockerReference(forbiddenImageMock(""), test.sigRef) + res := prm.matchesDockerReference(forbiddenImageMock{}, test.sigRef) assert.Equal(t, test.result, res, fmt.Sprintf("%s vs. %s", test.imageRef, test.sigRef)) } } diff --git a/transports/transports.go b/transports/transports.go new file mode 100644 index 0000000000..b7961484b3 --- /dev/null +++ b/transports/transports.go @@ -0,0 +1,55 @@ +package transports + +import ( + "fmt" + "strings" + + "github.com/containers/image/directory" + "github.com/containers/image/docker" + "github.com/containers/image/oci" + "github.com/containers/image/openshift" + "github.com/containers/image/types" +) + +// KnownTransports is a registry of known ImageTransport instances. +var KnownTransports map[string]types.ImageTransport + +func init() { + KnownTransports = make(map[string]types.ImageTransport) + for _, t := range []types.ImageTransport{ + directory.Transport, + docker.Transport, + oci.Transport, + openshift.Transport, + } { + name := t.Name() + if _, ok := KnownTransports[name]; ok { + panic(fmt.Sprintf("Duplicate image transport name %s", name)) + } + KnownTransports[name] = t + } +} + +// ParseImageName converts a URL-like image name to a types.ImageReference. +func ParseImageName(imgName string) (types.ImageReference, error) { + parts := strings.SplitN(imgName, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, imgName) + } + transport, ok := KnownTransports[parts[0]] + if !ok { + return nil, fmt.Errorf(`Invalid image name "%s", unknown transport "%s"`, imgName, parts[0]) + } + return transport.ParseReference(parts[1]) +} + +// ImageName converts a types.ImageReference into an URL-like image name, which MUST be such that +// ParseImageName(ImageName(reference)) returns an equivalent reference. +// +// This is the generally recommended way to refer to images in the UI. +// +// NOTE: The returned string is not promised to be equal to the original input to ParseImageName; +// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +func ImageName(ref types.ImageReference) string { + return ref.Transport().Name() + ":" + ref.StringWithinTransport() +} diff --git a/transports/transports_test.go b/transports/transports_test.go new file mode 100644 index 0000000000..592eac020a --- /dev/null +++ b/transports/transports_test.go @@ -0,0 +1,44 @@ +package transports + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKnownTransports(t *testing.T) { + assert.NotNil(t, KnownTransports) // Ensure that the initialization has actually been run + assert.True(t, len(KnownTransports) > 1) +} + +func TestParseImageName(t *testing.T) { + // This primarily tests error handling, TestImageNameHandling is a table-driven + // test for the expected values. + for _, name := range []string{ + "", // Empty + "busybox", // No transport name + ":busybox", // Empty transport name + "docker:", // Empty transport reference + } { + _, err := ParseImageName(name) + assert.Error(t, err, name) + } +} + +// A table-driven test summarizing the various transports' behavior. +func TestImageNameHandling(t *testing.T) { + for _, c := range []struct{ transport, input, roundtrip string }{ + {"dir", "/etc", "/etc"}, + {"docker", "//busybox", "//busybox:latest"}, + {"docker", "//busybox:notlatest", "//busybox:notlatest"}, // This also tests handling of multiple ":" characters + {"oci", "/etc:sometag", "/etc:sometag"}, + // "atomic" not tested here because it depends on per-user configuration for the default cluster. + } { + fullInput := c.transport + ":" + c.input + ref, err := ParseImageName(fullInput) + require.NoError(t, err, fullInput) + s := ImageName(ref) + assert.Equal(t, c.transport+":"+c.roundtrip, s, fullInput) + } +} diff --git a/types/types.go b/types/types.go index 7077579d1e..29860f8864 100644 --- a/types/types.go +++ b/types/types.go @@ -7,15 +7,63 @@ import ( "github.com/docker/docker/reference" ) +// ImageTransport is a top-level namespace for ways to to store/load an image. +// It should generally correspond to ImageSource/ImageDestination implementations. +// +// Note that ImageTransport is based on "ways the users refer to image storage", not necessarily on the underlying physical transport. +// For example, all Docker References would be used within a single "docker" transport, regardless of whether the images are pulled over HTTP or HTTPS +// (or, even, IPv4 or IPv6). +// +// OTOH all images using the same transport should (apart from versions of the image format), be interoperable. +// For example, several different ImageTransport implementations may be based on local filesystem paths, +// but using completely different formats for the contents of that path (a single tar file, a directory containing tarballs, a fully expanded container filesystem, ...) +// +// See also transports.KnownTransports. +type ImageTransport interface { + // Name returns the name of the transport, which must be unique among other transports. + Name() string + // ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. + ParseReference(reference string) (ImageReference, error) +} + +// ImageReference is an abstracted way to refer to an image location, namespaced within an ImageTransport. +// +// The object should preferably be immutable after creation, with any parsing/state-dependent resolving happening +// within an ImageTransport.ParseReference() or equivalent API creating the reference object. +// That's also why the various identification/formatting methods of this type do not support returning errors. +// +// WARNING: While this design freezes the content of the reference within this process, it can not freeze the outside +// world: paths may be replaced by symlinks elsewhere, HTTP APIs may start returning different results, and so on. +type ImageReference interface { + Transport() ImageTransport + // StringWithinTransport returns a string representation of the reference, which MUST be such that + // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. + // NOTE: The returned string is not promised to be equal to the original input to ParseReference; + // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. + // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix; + // instead, see transports.ImageName(). + StringWithinTransport() string + + // DockerReference returns a Docker reference associated with this reference + // (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent, + // not e.g. after redirect or alias processing), or nil if unknown/not applicable. + DockerReference() reference.Named + + // NewImage returns a types.Image for this reference. + NewImage(certPath string, tlsVerify bool) (Image, error) + // NewImageSource returns a types.ImageSource for this reference. + NewImageSource(certPath string, tlsVerify bool) (ImageSource, error) + // NewImageDestination returns a types.ImageDestination for this reference. + NewImageDestination(certPath string, tlsVerify bool) (ImageDestination, error) +} + // ImageSource is a service, possibly remote (= slow), to download components of a single image. // This is primarily useful for copying images around; for examining their properties, Image (below) // is usually more useful. type ImageSource interface { - // IntendedDockerReference returns the Docker reference for this image, _as specified by the user_ - // (not as the image itself, or its underlying storage, claims). Should be fully expanded, i.e. !reference.IsNameOnly. - // This can be used e.g. to determine which public keys are trusted for this image. - // May be nil if unknown. - IntendedDockerReference() reference.Named + // Reference returns the reference used to set up this source, _as specified by the user_ + // (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. + Reference() ImageReference // GetManifest returns the image's manifest along with its MIME type. The empty string is returned if the MIME type is unknown. The slice parameter indicates the supported mime types the manifest should be when getting it. // It may use a remote (= slow) service. GetManifest([]string) ([]byte, string, error) @@ -30,9 +78,9 @@ type ImageSource interface { // ImageDestination is a service, possibly remote (= slow), to store components of a single image. type ImageDestination interface { - // CanonicalDockerReference returns the Docker reference for this image (fully expanded, i.e. !reference.IsNameOnly, but - // reflecting user intent, not e.g. after redirect or alias processing), or nil if unknown. - CanonicalDockerReference() reference.Named + // Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, + // e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. + Reference() ImageReference // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. PutManifest([]byte) error // Note: Calling PutBlob() and other methods may have ordering dependencies WRT other methods of this type. FIXME: Figure out and document. @@ -45,12 +93,10 @@ type ImageDestination interface { // Image is the primary API for inspecting properties of images. type Image interface { + // Reference returns the reference used to set up this source, _as specified by the user_ + // (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. + Reference() ImageReference // ref to repository? - // IntendedDockerReference returns the Docker reference for this image, _as specified by the user_ - // (not as the image itself, or its underlying storage, claims). Should be fully expanded, i.e. !reference.IsNameOnly. - // This can be used e.g. to determine which public keys are trusted for this image. - // May be nil if unknown. - IntendedDockerReference() reference.Named // Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need. // NOTE: It is essential for signature verification that Manifest returns the manifest from which BlobDigests is computed. Manifest() ([]byte, string, error)