From e3d257e7b5af71f6d73067317555fc06851ca14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 16 May 2016 16:52:44 +0200 Subject: [PATCH 1/5] Decouple (skopeo inspect) output formatting from types.Image.Manifest Does not change behavior. This will allow us to move collecting some of the data to the (skopeo inspect) code and to have a more focused types.Image API, where types.Image.Manifest() does not return a grab bag of manifest-unrelated data, eventually. For how it actually makes the coupling more explicit by having types.Image.Manifest() return a types.DockerImageManifest instead of the too generic types.ImageManifest. We will need to think about which parts of DockerImageManifest are truly generic, later. --- cmd/skopeo/inspect.go | 29 ++++++++++++++++++++++++++++- docker/docker_image.go | 4 ++-- types/types.go | 8 +------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 880bda91a8..790a2017dd 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -3,11 +3,26 @@ package main import ( "encoding/json" "fmt" + "time" "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" ) +// inspectOutput is the output format of (skopeo inspect), primarily so that we can format it with a simple json.MarshalIndent. +type inspectOutput struct { + Name string + Tag string + Digest string + RepoTags []string + Created time.Time + DockerVersion string + Labels map[string]string + Architecture string + Os string + Layers []string +} + var inspectCmd = cli.Command{ Name: "inspect", Usage: "inspect images on a registry", @@ -34,7 +49,19 @@ var inspectCmd = cli.Command{ if err != nil { logrus.Fatal(err) } - out, err := json.MarshalIndent(imgInspect, "", " ") + outputData := inspectOutput{ + Name: imgInspect.Name, + Tag: imgInspect.Tag, + Digest: imgInspect.Digest, + RepoTags: imgInspect.RepoTags, + Created: imgInspect.Created, + DockerVersion: imgInspect.DockerVersion, + Labels: imgInspect.Labels, + Architecture: imgInspect.Architecture, + Os: imgInspect.Os, + Layers: imgInspect.Layers, + } + out, err := json.MarshalIndent(outputData, "", " ") if err != nil { logrus.Fatal(err) } diff --git a/docker/docker_image.go b/docker/docker_image.go index a708ddec79..97fad46117 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -62,7 +62,7 @@ func (i *dockerImage) Signatures() ([][]byte, error) { return i.cachedSignatures, nil } -func (i *dockerImage) Inspect() (types.ImageManifest, error) { +func (i *dockerImage) Inspect() (*types.DockerImageManifest, error) { // TODO(runcom): unused version param for now, default to docker v2-1 m, err := i.getSchema1Manifest() if err != nil { @@ -122,7 +122,7 @@ type v1Image struct { OS string `json:"os,omitempty"` } -func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []string) (types.ImageManifest, error) { +func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []string) (*types.DockerImageManifest, error) { v1 := &v1Image{} if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil { return nil, err diff --git a/types/types.go b/types/types.go index 1f1a4626a9..4bc37138b0 100644 --- a/types/types.go +++ b/types/types.go @@ -62,16 +62,10 @@ type Image interface { // Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need. Signatures() ([][]byte, error) Layers(layers ...string) error // configure download directory? Call it DownloadLayers? - Inspect() (ImageManifest, error) + Inspect() (*DockerImageManifest, error) DockerTar() ([]byte, error) // ??? also, configure output directory } -// ImageManifest is the interesting subset of metadata about an Image. -// TODO(runcom) -type ImageManifest interface { - String() string -} - // DockerImageManifest is a set of metadata describing Docker images and their manifest.json files. // Note that this is not exactly manifest.json, e.g. some fields have been added. type DockerImageManifest struct { From 60e8d6371276af717d2c7d36a3c2274c715fa85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 16 May 2016 17:40:16 +0200 Subject: [PATCH 2/5] Compute the digest in (skopeo inspect) instead of trusting the registry Compute the digest ourselves, the registry is in general untrusted and computing it ourserlves is easy enough. The stop passing the unverifiedCanonicalDigest value around, simplifying ImageSource.GetManifest and related code. In particular, remove retrieveRawManifest and have internal users just call Manifest() now that we don't need the digest. --- cmd/skopeo/copy.go | 2 +- cmd/skopeo/inspect.go | 17 +++++++++++------ directory/directory.go | 9 ++------- docker/docker_image.go | 36 +++++++++++++----------------------- docker/docker_image_src.go | 11 ++++++----- openshift/openshift.go | 4 ++-- types/types.go | 3 +-- 7 files changed, 36 insertions(+), 46 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 5fe78ce6aa..ffa2c2e150 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -54,7 +54,7 @@ func copyHandler(context *cli.Context) { } signBy := context.String("sign-by") - manifest, _, err := src.GetManifest() + manifest, err := src.GetManifest() if err != nil { logrus.Fatalf("Error reading manifest: %s", err.Error()) } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 790a2017dd..02e3f2d696 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -7,6 +7,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" + "github.com/projectatomic/skopeo/dockerutils" ) // inspectOutput is the output format of (skopeo inspect), primarily so that we can format it with a simple json.MarshalIndent. @@ -37,22 +38,26 @@ var inspectCmd = cli.Command{ if err != nil { logrus.Fatal(err) } + rawManifest, err := img.Manifest() + if err != nil { + logrus.Fatal(err) + } if c.Bool("raw") { - b, err := img.Manifest() - if err != nil { - logrus.Fatal(err) - } - fmt.Println(string(b)) + fmt.Println(string(rawManifest)) return } imgInspect, err := img.Inspect() if err != nil { logrus.Fatal(err) } + manifestDigest, err := dockerutils.ManifestDigest(rawManifest) + if err != nil { + logrus.Fatalf("Error computing manifest digest: %s", err.Error()) + } outputData := inspectOutput{ Name: imgInspect.Name, Tag: imgInspect.Tag, - Digest: imgInspect.Digest, + Digest: manifestDigest, RepoTags: imgInspect.RepoTags, Created: imgInspect.Created, DockerVersion: imgInspect.DockerVersion, diff --git a/directory/directory.go b/directory/directory.go index e41854ec15..a399ef6f7e 100644 --- a/directory/directory.go +++ b/directory/directory.go @@ -84,13 +84,8 @@ func (s *dirImageSource) IntendedDockerReference() string { return "" } -func (s *dirImageSource) GetManifest() ([]byte, string, error) { - manifest, err := ioutil.ReadFile(manifestPath(s.dir)) - if err != nil { - return nil, "", err - } - - return manifest, "", nil // FIXME? unverifiedCanonicalDigest value - really primarily used by dockerImage +func (s *dirImageSource) GetManifest() ([]byte, error) { + return ioutil.ReadFile(manifestPath(s.dir)) } func (s *dirImageSource) GetLayer(digest string) (io.ReadCloser, error) { diff --git a/docker/docker_image.go b/docker/docker_image.go index 97fad46117..47c1645479 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -20,8 +20,7 @@ var ( type dockerImage struct { src *dockerImageSource - digest string - rawManifest []byte + cachedManifest []byte // Private cache for Manifest(); nil if not yet known. cachedSignatures [][]byte // Private cache for Signatures(); nil if not yet known. } @@ -44,10 +43,14 @@ func (i *dockerImage) IntendedDockerReference() string { // Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need. func (i *dockerImage) Manifest() ([]byte, error) { - if err := i.retrieveRawManifest(); err != nil { - return nil, err + if i.cachedManifest == nil { + m, err := i.src.GetManifest() + if err != nil { + return nil, err + } + i.cachedManifest = m } - return i.rawManifest, nil + return i.cachedManifest, nil } // Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need. @@ -76,7 +79,7 @@ func (i *dockerImage) Inspect() (*types.DockerImageManifest, error) { if err != nil { return nil, err } - imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1, i.digest, tags) + imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1, tags) if err != nil { return nil, err } @@ -122,7 +125,7 @@ type v1Image struct { OS string `json:"os,omitempty"` } -func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []string) (*types.DockerImageManifest, error) { +func makeImageManifest(name string, m *manifestSchema1, tagList []string) (*types.DockerImageManifest, error) { v1 := &v1Image{} if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil { return nil, err @@ -130,7 +133,6 @@ func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []s return &types.DockerImageManifest{ Name: name, Tag: m.Tag, - Digest: dgst, RepoTags: tagList, DockerVersion: v1.DockerVersion, Created: v1.Created, @@ -181,25 +183,13 @@ func sanitize(s string) string { return strings.Replace(s, "/", "-", -1) } -func (i *dockerImage) retrieveRawManifest() error { - if i.rawManifest != nil { - return nil - } - manblob, unverifiedCanonicalDigest, err := i.src.GetManifest() - if err != nil { - return err - } - i.rawManifest = manblob - i.digest = unverifiedCanonicalDigest - return nil -} - func (i *dockerImage) getSchema1Manifest() (manifest, error) { - if err := i.retrieveRawManifest(); err != nil { + manblob, err := i.Manifest() + if err != nil { return nil, err } mschema1 := &manifestSchema1{} - if err := json.Unmarshal(i.rawManifest, mschema1); err != nil { + if err := json.Unmarshal(manblob, mschema1); err != nil { return nil, err } if err := fixManifestLayers(mschema1); err != nil { diff --git a/docker/docker_image_src.go b/docker/docker_image_src.go index 4277be8b19..cfaa81a7ef 100644 --- a/docker/docker_image_src.go +++ b/docker/docker_image_src.go @@ -55,23 +55,24 @@ func (s *dockerImageSource) IntendedDockerReference() string { return fmt.Sprintf("%s:%s", s.ref.Name(), s.tag) } -func (s *dockerImageSource) GetManifest() (manifest []byte, unverifiedCanonicalDigest string, err error) { +func (s *dockerImageSource) GetManifest() ([]byte, error) { url := fmt.Sprintf(manifestURL, s.ref.RemoteName(), s.tag) // 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 res, err := s.c.makeRequest("GET", url, nil, nil) if err != nil { - return nil, "", err + return nil, err } defer res.Body.Close() manblob, err := ioutil.ReadAll(res.Body) if err != nil { - return nil, "", err + return nil, err } if res.StatusCode != http.StatusOK { - return nil, "", errFetchManifest{res.StatusCode, manblob} + return nil, errFetchManifest{res.StatusCode, manblob} } - return manblob, res.Header.Get("Docker-Content-Digest"), nil + // We might validate manblob against the Docker-Content-Digest header here to protect against transport errors. + return manblob, nil } func (s *dockerImageSource) GetLayer(digest string) (io.ReadCloser, error) { diff --git a/openshift/openshift.go b/openshift/openshift.go index 6425a92876..f351bf460c 100644 --- a/openshift/openshift.go +++ b/openshift/openshift.go @@ -193,9 +193,9 @@ func (s *openshiftImageSource) IntendedDockerReference() string { return s.client.canonicalDockerReference() } -func (s *openshiftImageSource) GetManifest() (manifest []byte, unverifiedCanonicalDigest string, err error) { +func (s *openshiftImageSource) GetManifest() ([]byte, error) { if err := s.ensureImageIsResolved(); err != nil { - return nil, "", err + return nil, err } return s.docker.GetManifest() } diff --git a/types/types.go b/types/types.go index 4bc37138b0..a8a87b77e9 100644 --- a/types/types.go +++ b/types/types.go @@ -35,7 +35,7 @@ type ImageSource interface { // May be "" if unknown. IntendedDockerReference() string // GetManifest returns the image's manifest. It may use a remote (= slow) service. - GetManifest() (manifest []byte, unverifiedCanonicalDigest string, err error) + GetManifest() ([]byte, error) GetLayer(digest string) (io.ReadCloser, error) // GetSignatures returns the image's signatures. It may use a remote (= slow) service. GetSignatures() ([][]byte, error) @@ -71,7 +71,6 @@ type Image interface { type DockerImageManifest struct { Name string Tag string - Digest string RepoTags []string Created time.Time DockerVersion string From 9766f727601ed21e8dded806ba81770abce45c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 16 May 2016 17:50:42 +0200 Subject: [PATCH 3/5] Move listing of repository tags from Image.Manifest to a separate Image.GetRepositoryTags This does not change behavior. Splits listing of repository tags, which is not a property of an image, from the image.Manifest gathering of information about an image. --- cmd/skopeo/inspect.go | 6 +++++- docker/docker_image.go | 12 ++++-------- types/types.go | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 02e3f2d696..da2ff08879 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -54,11 +54,15 @@ var inspectCmd = cli.Command{ if err != nil { logrus.Fatalf("Error computing manifest digest: %s", err.Error()) } + repoTags, err := img.GetRepositoryTags() + if err != nil { + logrus.Fatalf("Error determining repository tags: %s", err.Error()) + } outputData := inspectOutput{ Name: imgInspect.Name, Tag: imgInspect.Tag, Digest: manifestDigest, - RepoTags: imgInspect.RepoTags, + RepoTags: repoTags, Created: imgInspect.Created, DockerVersion: imgInspect.DockerVersion, Labels: imgInspect.Labels, diff --git a/docker/docker_image.go b/docker/docker_image.go index 47c1645479..b7c1ce47c1 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -75,18 +75,15 @@ func (i *dockerImage) Inspect() (*types.DockerImageManifest, error) { if !ok { return nil, fmt.Errorf("error retrivieng manifest schema1") } - tags, err := i.getTags() - if err != nil { - return nil, err - } - imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1, tags) + imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1) if err != nil { return nil, err } return imgManifest, nil } -func (i *dockerImage) getTags() ([]string, error) { +// 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 *dockerImage) GetRepositoryTags() ([]string, error) { // FIXME? Breaking the abstraction. url := fmt.Sprintf(tagsURL, i.src.ref.RemoteName()) res, err := i.src.c.makeRequest("GET", url, nil, nil) @@ -125,7 +122,7 @@ type v1Image struct { OS string `json:"os,omitempty"` } -func makeImageManifest(name string, m *manifestSchema1, tagList []string) (*types.DockerImageManifest, error) { +func makeImageManifest(name string, m *manifestSchema1) (*types.DockerImageManifest, error) { v1 := &v1Image{} if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil { return nil, err @@ -133,7 +130,6 @@ func makeImageManifest(name string, m *manifestSchema1, tagList []string) (*type return &types.DockerImageManifest{ Name: name, Tag: m.Tag, - RepoTags: tagList, DockerVersion: v1.DockerVersion, Created: v1.Created, Labels: v1.Config.Labels, diff --git a/types/types.go b/types/types.go index a8a87b77e9..20bd6dce1d 100644 --- a/types/types.go +++ b/types/types.go @@ -64,6 +64,8 @@ type Image interface { Layers(layers ...string) error // configure download directory? Call it DownloadLayers? Inspect() (*DockerImageManifest, error) DockerTar() ([]byte, error) // ??? also, configure output directory + // 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. + GetRepositoryTags() ([]string, error) } // DockerImageManifest is a set of metadata describing Docker images and their manifest.json files. @@ -71,7 +73,6 @@ type Image interface { type DockerImageManifest struct { Name string Tag string - RepoTags []string Created time.Time DockerVersion string Labels map[string]string From 7598aab52114389a464a502744e7b1ab9924fb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 16 May 2016 19:16:52 +0200 Subject: [PATCH 4/5] Clean up Image.Inspect This does not change behavior. Rename types.DockerImageManifest to types.ImageInspectInfo. This naming more accurately reflects what the function does and how it is expected to be used. (The only outstanding non-inspection piece is the Name field, which is kind of a subset of GetIntendedDockerReference() right now. Not sure whether that is intentional.) Also fold makeImageManifest into its only user. --- docker/docker_image.go | 34 +++++++++++++--------------------- types/types.go | 9 ++++----- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docker/docker_image.go b/docker/docker_image.go index b7c1ce47c1..517d6d3d9b 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -65,7 +65,7 @@ func (i *dockerImage) Signatures() ([][]byte, error) { return i.cachedSignatures, nil } -func (i *dockerImage) Inspect() (*types.DockerImageManifest, error) { +func (i *dockerImage) Inspect() (*types.ImageInspectInfo, error) { // TODO(runcom): unused version param for now, default to docker v2-1 m, err := i.getSchema1Manifest() if err != nil { @@ -75,11 +75,20 @@ func (i *dockerImage) Inspect() (*types.DockerImageManifest, error) { if !ok { return nil, fmt.Errorf("error retrivieng manifest schema1") } - imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1) - if err != nil { + v1 := &v1Image{} + if err := json.Unmarshal([]byte(ms1.History[0].V1Compatibility), v1); err != nil { return nil, err } - return imgManifest, nil + return &types.ImageInspectInfo{ + Name: i.src.ref.FullName(), + Tag: ms1.Tag, + DockerVersion: v1.DockerVersion, + Created: v1.Created, + Labels: v1.Config.Labels, + Architecture: v1.Architecture, + Os: v1.OS, + Layers: ms1.GetLayers(), + }, nil } // 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. @@ -122,23 +131,6 @@ type v1Image struct { OS string `json:"os,omitempty"` } -func makeImageManifest(name string, m *manifestSchema1) (*types.DockerImageManifest, error) { - v1 := &v1Image{} - if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil { - return nil, err - } - return &types.DockerImageManifest{ - Name: name, - Tag: m.Tag, - DockerVersion: v1.DockerVersion, - Created: v1.Created, - Labels: v1.Config.Labels, - Architecture: v1.Architecture, - Os: v1.OS, - Layers: m.GetLayers(), - }, nil -} - // TODO(runcom) func (i *dockerImage) DockerTar() ([]byte, error) { return nil, nil diff --git a/types/types.go b/types/types.go index 20bd6dce1d..5273ce7f1e 100644 --- a/types/types.go +++ b/types/types.go @@ -62,15 +62,14 @@ type Image interface { // Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need. Signatures() ([][]byte, error) Layers(layers ...string) error // configure download directory? Call it DownloadLayers? - Inspect() (*DockerImageManifest, error) + Inspect() (*ImageInspectInfo, error) DockerTar() ([]byte, error) // ??? also, configure output directory // 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. GetRepositoryTags() ([]string, error) } -// DockerImageManifest is a set of metadata describing Docker images and their manifest.json files. -// Note that this is not exactly manifest.json, e.g. some fields have been added. -type DockerImageManifest struct { +// ImageInspectInfo is a set of metadata describing Docker images, primarily their manifest and configuration. +type ImageInspectInfo struct { Name string Tag string Created time.Time @@ -81,6 +80,6 @@ type DockerImageManifest struct { Layers []string } -func (m *DockerImageManifest) String() string { +func (m *ImageInspectInfo) String() string { return fmt.Sprintf("%s:%s", m.Name, m.Tag) } From 6a357b6fcc7a07b9c985777dbd6f596f55d314a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 16 May 2016 19:23:50 +0200 Subject: [PATCH 5/5] Add comments on the use of the API and the general direction. --- types/types.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/types/types.go b/types/types.go index 5273ce7f1e..994f6de715 100644 --- a/types/types.go +++ b/types/types.go @@ -29,13 +29,17 @@ type Repository interface { } // 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 full, unambiguous, Docker reference for this image, _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. // May be "" if unknown. IntendedDockerReference() string // GetManifest returns the image's manifest. It may use a remote (= slow) service. + // FIXME? This should also return a MIME type if known, to differentiate between schema versions. GetManifest() ([]byte, error) + // Note: Calling GetLayer() may have ordering dependencies WRT other methods of this type. FIXME: How does this work with (docker save) on stdin? GetLayer(digest string) (io.ReadCloser, error) // GetSignatures returns the image's signatures. It may use a remote (= slow) service. GetSignatures() ([][]byte, error) @@ -45,12 +49,14 @@ type ImageSource interface { type ImageDestination interface { // CanonicalDockerReference returns the full, unambiguous, Docker reference for this image (even if the user referred to the image using some shorthand notation). CanonicalDockerReference() (string, error) + // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. PutManifest([]byte) error + // Note: Calling PutLayer() and other methods may have ordering dependencies WRT other methods of this type. FIXME: Figure out and document. PutLayer(digest string, stream io.Reader) error PutSignatures(signatures [][]byte) error } -// Image is a Docker image in a repository. +// Image is the primary API for inspecting properties of images. type Image interface { // ref to repository? // IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_ @@ -58,6 +64,7 @@ type Image interface { // May be "" if unknown. IntendedDockerReference() string // Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need. + // FIXME? This should also return a MIME type if known, to differentiate between schema versions. Manifest() ([]byte, error) // Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need. Signatures() ([][]byte, error) @@ -65,6 +72,8 @@ type Image interface { Inspect() (*ImageInspectInfo, error) DockerTar() ([]byte, error) // ??? also, configure output directory // 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. + // Eventually we should move this away from the generic Image interface, and move it into a Docker-specific case within the (skopeo inspect) command, + // see https://github.com/projectatomic/skopeo/pull/58#discussion_r63411838 . GetRepositoryTags() ([]string, error) }