From 01739500b737c6b783f47ece1c7fef578205d569 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 28 Aug 2019 09:43:26 +0200 Subject: [PATCH 1/3] Also support manifest lists (used by docker apps) Signed-off-by: Nicolas De Loof --- manifest_test.go | 14 +++++++++++++- registry/manifest.go | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/manifest_test.go b/manifest_test.go index 6212c511a..527df9648 100644 --- a/manifest_test.go +++ b/manifest_test.go @@ -30,6 +30,18 @@ func TestManifestV1(t *testing.T) { } } +func TestManifestList(t *testing.T) { + out, err := run("manifest", "docker.io/ndeloof/hello-app") + if err != nil { + t.Fatalf("output: %s, error: %v", out, err) + } + + expected := `"schemaVersion": 2,` + if !strings.Contains(out, expected) { + t.Fatalf("expected: %s\ngot: %s", expected, out) + } +} + func TestManifestWithHubDomain(t *testing.T) { // Regression test for https://github.com/genuinetools/reg/issues/164 out, err := run("manifest", "busybox") @@ -37,7 +49,7 @@ func TestManifestWithHubDomain(t *testing.T) { t.Fatalf("output: %s, error: %v", out, err) } - expected := `"schemaVersion": 2,` + expected := `"schemaVersion": 2` if !strings.Contains(out, expected) { t.Fatalf("expected: %s\ngot: %s", expected, out) } diff --git a/registry/manifest.go b/registry/manifest.go index c25a1963d..f854f3068 100644 --- a/registry/manifest.go +++ b/registry/manifest.go @@ -29,7 +29,7 @@ func (r *Registry) Manifest(ctx context.Context, repository, ref string) (distri return nil, err } - req.Header.Add("Accept", schema2.MediaTypeManifest) + req.Header.Add("Accept", schema2.MediaTypeManifest+", "+manifestlist.MediaTypeManifestList) resp, err := r.Client.Do(req.WithContext(ctx)) if err != nil { From e8c40f08235a6a84c21a7bb2b3cc8b6b3b300754 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 19 Sep 2019 09:33:03 +0200 Subject: [PATCH 2/3] Added a test case log content-type to help diagnostic on supported manifests' media types Signed-off-by: Nicolas De Loof --- registry/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/manifest.go b/registry/manifest.go index f854f3068..fc63ca9a6 100644 --- a/registry/manifest.go +++ b/registry/manifest.go @@ -41,7 +41,7 @@ func (r *Registry) Manifest(ctx context.Context, repository, ref string) (distri if err != nil { return nil, err } - r.Logf("registry.manifests resp.Status=%s, body=%s", resp.Status, body) + r.Logf("registry.manifests resp.Status=%s, type=%s, body=%s", resp.Status, resp.Header.Get("Content-type"), body) m, _, err := distribution.UnmarshalManifest(resp.Header.Get("Content-Type"), body) if err != nil { From 8997f242c70bd26ad037e9a1708c789ce87031fb Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 20 Sep 2019 08:27:22 +0200 Subject: [PATCH 3/3] Introduce --index and --oci options This let user tune the media type to be used to get manifest, without breaking compatibility. Signed-off-by: Nicolas De Loof --- README.md | 6 ++++++ digest.go | 12 +++++++++--- manifest.go | 29 ++++++++++++++++++++++++++--- manifest_test.go | 4 ++-- registry/digest.go | 15 +++++++++++---- registry/manifest.go | 10 ++++++++-- 6 files changed, 62 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3b1b661f8..2ad6ff668 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,12 @@ $ reg manifest r.j3ss.co/htop } ``` +For multi-architecture docker images, `--index` allow to get the Manifest index (a.k.a Manifest List) with +all supported platforms/architecture listed. + +OCI compatibility can be checked using `--oci` option, which will force use of [OCI media types](https://github.com/opencontainers/image-spec/blob/master/media-types.md). + + ### Get the Digest ```console $ reg digest r.j3ss.co/htop diff --git a/digest.go b/digest.go index d86a8fff6..f71e90fee 100644 --- a/digest.go +++ b/digest.go @@ -16,9 +16,15 @@ func (cmd *digestCommand) ShortHelp() string { return digestHelp } func (cmd *digestCommand) LongHelp() string { return digestHelp } func (cmd *digestCommand) Hidden() bool { return false } -func (cmd *digestCommand) Register(fs *flag.FlagSet) {} +func (cmd *digestCommand) Register(fs *flag.FlagSet) { + fs.BoolVar(&cmd.index, "index", false, "get manifest index (multi-architecture images, docker apps)") + fs.BoolVar(&cmd.oci, "oci", false, "use OCI media type only") +} -type digestCommand struct{} +type digestCommand struct { + index bool + oci bool +} func (cmd *digestCommand) Run(ctx context.Context, args []string) error { if len(args) < 1 { @@ -37,7 +43,7 @@ func (cmd *digestCommand) Run(ctx context.Context, args []string) error { } // Get the digest. - digest, err := r.Digest(ctx, image) + digest, err := r.Digest(ctx, image, mediatypes(cmd.index, cmd.oci)...) if err != nil { return err } diff --git a/manifest.go b/manifest.go index 8a5b830aa..d75e8da21 100644 --- a/manifest.go +++ b/manifest.go @@ -5,8 +5,10 @@ import ( "encoding/json" "flag" "fmt" - + "github.com/docker/distribution/manifest/manifestlist" + "github.com/docker/distribution/manifest/schema2" "github.com/genuinetools/reg/registry" + v1 "github.com/opencontainers/image-spec/specs-go/v1" ) const manifestHelp = `Get the json manifest for a repository.` @@ -19,10 +21,14 @@ func (cmd *manifestCommand) Hidden() bool { return false } func (cmd *manifestCommand) Register(fs *flag.FlagSet) { fs.BoolVar(&cmd.v1, "v1", false, "force the version of the manifest retrieved to v1") + fs.BoolVar(&cmd.index, "index", false, "get manifest index (multi-architecture images, docker apps)") + fs.BoolVar(&cmd.oci, "oci", false, "use OCI media type only") } type manifestCommand struct { - v1 bool + v1 bool + index bool + oci bool } func (cmd *manifestCommand) Run(ctx context.Context, args []string) error { @@ -50,7 +56,7 @@ func (cmd *manifestCommand) Run(ctx context.Context, args []string) error { } } else { // Get the v2 manifest. - manifest, err = r.Manifest(ctx, image.Path, image.Reference()) + manifest, err = r.Manifest(ctx, image.Path, image.Reference(), mediatypes(cmd.index, cmd.oci)...) if err != nil { return err } @@ -64,3 +70,20 @@ func (cmd *manifestCommand) Run(ctx context.Context, args []string) error { fmt.Println(string(b)) return nil } + +func mediatypes(index, oci bool) []string { + mediatypes := []string{} + if oci { + mediatypes = append(mediatypes, v1.MediaTypeImageManifest) + } else { + mediatypes = append(mediatypes, schema2.MediaTypeManifest) + } + if index { + if oci { + mediatypes = append(mediatypes, v1.MediaTypeImageIndex) + } else { + mediatypes = append(mediatypes, manifestlist.MediaTypeManifestList) + } + } + return mediatypes +} diff --git a/manifest_test.go b/manifest_test.go index 527df9648..dbe5ac21b 100644 --- a/manifest_test.go +++ b/manifest_test.go @@ -31,12 +31,12 @@ func TestManifestV1(t *testing.T) { } func TestManifestList(t *testing.T) { - out, err := run("manifest", "docker.io/ndeloof/hello-app") + out, err := run("manifest", "--index", "docker.io/library/busybox") if err != nil { t.Fatalf("output: %s, error: %v", out, err) } - expected := `"schemaVersion": 2,` + expected := `"mediaType": "application\/vnd.docker.distribution.manifest.list.v2+json"` if !strings.Contains(out, expected) { t.Fatalf("expected: %s\ngot: %s", expected, out) } diff --git a/registry/digest.go b/registry/digest.go index 361ae4748..99c287b63 100644 --- a/registry/digest.go +++ b/registry/digest.go @@ -3,14 +3,16 @@ package registry import ( "context" "fmt" + "github.com/docker/distribution/manifest/schema2" + "github.com/sirupsen/logrus" "net/http" + "strings" - "github.com/docker/distribution/manifest/schema2" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // Digest returns the digest for an image. -func (r *Registry) Digest(ctx context.Context, image Image) (digest.Digest, error) { +func (r *Registry) Digest(ctx context.Context, image Image, mediatypes ...string) (digest.Digest, error) { if len(image.Digest) > 1 { // return early if we already have an image digest. return image.Digest, nil @@ -25,7 +27,12 @@ func (r *Registry) Digest(ctx context.Context, image Image) (digest.Digest, erro return "", err } - req.Header.Add("Accept", schema2.MediaTypeManifest) + if mediatypes == nil { + mediatypes = []string{schema2.MediaTypeManifest} + } + logrus.Debugf("Using media types %s", mediatypes) + req.Header.Add("Accept", strings.Join(mediatypes, ", ")) + resp, err := r.Client.Do(req.WithContext(ctx)) if err != nil { return "", err diff --git a/registry/manifest.go b/registry/manifest.go index fc63ca9a6..9e278a08f 100644 --- a/registry/manifest.go +++ b/registry/manifest.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "errors" + "github.com/sirupsen/logrus" "io/ioutil" "net/http" + "strings" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" @@ -20,7 +22,7 @@ var ( ) // Manifest returns the manifest for a specific repository:tag. -func (r *Registry) Manifest(ctx context.Context, repository, ref string) (distribution.Manifest, error) { +func (r *Registry) Manifest(ctx context.Context, repository, ref string, mediatypes ...string) (distribution.Manifest, error) { uri := r.url("/v2/%s/manifests/%s", repository, ref) r.Logf("registry.manifests uri=%s repository=%s ref=%s", uri, repository, ref) @@ -29,7 +31,11 @@ func (r *Registry) Manifest(ctx context.Context, repository, ref string) (distri return nil, err } - req.Header.Add("Accept", schema2.MediaTypeManifest+", "+manifestlist.MediaTypeManifestList) + if mediatypes == nil { + mediatypes = []string{schema2.MediaTypeManifest} + } + logrus.Debugf("Using media types %s", mediatypes) + req.Header.Add("Accept", strings.Join(mediatypes, ", ")) resp, err := r.Client.Do(req.WithContext(ctx)) if err != nil {