diff --git a/api/client/formatter/formatter.go b/api/client/formatter/formatter.go index a52ec8eb10..1e250a2522 100644 --- a/api/client/formatter/formatter.go +++ b/api/client/formatter/formatter.go @@ -155,6 +155,10 @@ func (ctx ContainerContext) Write() { ctx.postformat(tmpl, &containerContext{}) } +func isDangling(image types.Image) bool { + return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" +} + func (ctx ImageContext) Write() { switch ctx.Format { case tableFormatKey: @@ -200,42 +204,98 @@ virtual_size: {{.Size}} } for _, image := range ctx.Images { + images := []*imageContext{} + if isDangling(image) { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: "", + tag: "", + digest: "", + }) + } else { + repoTags := map[string][]string{} + repoDigests := map[string][]string{} - repoTags := image.RepoTags - repoDigests := image.RepoDigests - - if len(repoTags) == 1 && repoTags[0] == ":" && len(repoDigests) == 1 && repoDigests[0] == "@" { - // dangling image - clear out either repoTags or repoDigests so we only show it once below - repoDigests = []string{} - } - // combine the tags and digests lists - tagsAndDigests := append(repoTags, repoDigests...) - for _, repoAndRef := range tagsAndDigests { - repo := "" - tag := "" - digest := "" - - if !strings.HasPrefix(repoAndRef, "") { - ref, err := reference.ParseNamed(repoAndRef) + for _, refString := range append(image.RepoTags) { + ref, err := reference.ParseNamed(refString) if err != nil { continue } - repo = ref.Name() - - switch x := ref.(type) { - case reference.Canonical: - digest = x.Digest().String() - case reference.NamedTagged: - tag = x.Tag() + if nt, ok := ref.(reference.NamedTagged); ok { + repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) } } - imageCtx := &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: digest, + for _, refString := range append(image.RepoDigests) { + ref, err := reference.ParseNamed(refString) + if err != nil { + continue + } + if c, ok := ref.(reference.Canonical); ok { + repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) + } } + + for repo, tags := range repoTags { + digests := repoDigests[repo] + + // Do not display digests as their own row + delete(repoDigests, repo) + + if !ctx.Digest { + // Ignore digest references, just show tag once + digests = nil + } + + for _, tag := range tags { + if len(digests) == 0 { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: "", + }) + continue + } + // Display the digests for each tag + for _, dgst := range digests { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: dgst, + }) + } + + } + } + + // Show rows for remaining digest only references + for repo, digests := range repoDigests { + // If digests are displayed, show row per digest + if ctx.Digest { + for _, dgst := range digests { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: "", + digest: dgst, + }) + } + } else { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: "", + }) + } + } + } + for _, imageCtx := range images { err = ctx.contextFormat(tmpl, imageCtx) if err != nil { return diff --git a/api/client/formatter/formatter_test.go b/api/client/formatter/formatter_test.go index 7dd5a68fcd..07cde63f95 100644 --- a/api/client/formatter/formatter_test.go +++ b/api/client/formatter/formatter_test.go @@ -301,7 +301,6 @@ func TestImageContextWrite(t *testing.T) { }, `REPOSITORY TAG IMAGE ID CREATED SIZE image tag1 imageID1 24 hours ago 0 B -image imageID1 24 hours ago 0 B image tag2 imageID2 24 hours ago 0 B imageID3 24 hours ago 0 B `, @@ -312,7 +311,7 @@ image tag2 imageID2 24 hours ago Format: "table {{.Repository}}", }, }, - "REPOSITORY\nimage\nimage\nimage\n\n", + "REPOSITORY\nimage\nimage\n\n", }, { ImageContext{ @@ -322,7 +321,6 @@ image tag2 imageID2 24 hours ago Digest: true, }, `REPOSITORY DIGEST -image image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf image @@ -335,7 +333,7 @@ image Quiet: true, }, }, - "REPOSITORY\nimage\nimage\nimage\n\n", + "REPOSITORY\nimage\nimage\n\n", }, { ImageContext{ @@ -344,7 +342,7 @@ image Quiet: true, }, }, - "imageID1\nimageID1\nimageID2\nimageID3\n", + "imageID1\nimageID2\nimageID3\n", }, { ImageContext{ @@ -355,8 +353,7 @@ image Digest: true, }, `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE -image tag1 imageID1 24 hours ago 0 B -image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B +image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B image tag2 imageID2 24 hours ago 0 B imageID3 24 hours ago 0 B `, @@ -369,7 +366,7 @@ image tag2 }, Digest: true, }, - "imageID1\nimageID1\nimageID2\nimageID3\n", + "imageID1\nimageID2\nimageID3\n", }, // Raw Format { @@ -384,12 +381,6 @@ image_id: imageID1 created_at: %s virtual_size: 0 B -repository: image -tag: -image_id: imageID1 -created_at: %s -virtual_size: 0 B - repository: image tag: tag2 image_id: imageID2 @@ -402,7 +393,7 @@ image_id: imageID3 created_at: %s virtual_size: 0 B -`, expectedTime, expectedTime, expectedTime, expectedTime), +`, expectedTime, expectedTime, expectedTime), }, { ImageContext{ @@ -413,13 +404,6 @@ virtual_size: 0 B }, fmt.Sprintf(`repository: image tag: tag1 -digest: -image_id: imageID1 -created_at: %s -virtual_size: 0 B - -repository: image -tag: digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf image_id: imageID1 created_at: %s @@ -439,7 +423,7 @@ image_id: imageID3 created_at: %s virtual_size: 0 B -`, expectedTime, expectedTime, expectedTime, expectedTime), +`, expectedTime, expectedTime, expectedTime), }, { ImageContext{ @@ -449,7 +433,6 @@ virtual_size: 0 B }, }, `image_id: imageID1 -image_id: imageID1 image_id: imageID2 image_id: imageID3 `, @@ -461,7 +444,7 @@ image_id: imageID3 Format: "{{.Repository}}", }, }, - "image\nimage\nimage\n\n", + "image\nimage\n\n", }, { ImageContext{ @@ -470,7 +453,7 @@ image_id: imageID3 }, Digest: true, }, - "image\nimage\nimage\n\n", + "image\nimage\n\n", }, } diff --git a/integration-cli/docker_cli_by_digest_test.go b/integration-cli/docker_cli_by_digest_test.go index 34f1f7ef88..2f71d0f103 100644 --- a/integration-cli/docker_cli_by_digest_test.go +++ b/integration-cli/docker_cli_by_digest_test.go @@ -284,10 +284,8 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) { out, _ = dockerCmd(c, "images", "--digests") // make sure image 1 has repo, tag, AND repo, , digest - reWithTag1 := regexp.MustCompile(`\s*` + repoName + `\s*tag1\s*\s`) - reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*\s*` + digest1.String() + `\s`) + reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*tag1\s*` + digest1.String() + `\s`) c.Assert(reWithDigest1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest1.String(), out)) - c.Assert(reWithTag1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag1.String(), out)) // make sure image 2 has repo, , digest c.Assert(re2.MatchString(out), checker.True, check.Commentf("expected %q: %s", re2.String(), out)) @@ -298,21 +296,19 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) { out, _ = dockerCmd(c, "images", "--digests") // make sure image 1 has repo, tag, digest - c.Assert(reWithTag1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag1.String(), out)) + c.Assert(reWithDigest1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest1.String(), out)) // make sure image 2 has repo, tag, digest - reWithTag2 := regexp.MustCompile(`\s*` + repoName + `\s*tag2\s*\s`) - reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*\s*` + digest2.String() + `\s`) - c.Assert(reWithTag2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag2.String(), out)) + reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*tag2\s*` + digest2.String() + `\s`) c.Assert(reWithDigest2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest2.String(), out)) // list images out, _ = dockerCmd(c, "images", "--digests") // make sure image 1 has repo, tag, digest - c.Assert(reWithTag1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag1.String(), out)) + c.Assert(reWithDigest1.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest1.String(), out)) // make sure image 2 has repo, tag, digest - c.Assert(reWithTag2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithTag2.String(), out)) + c.Assert(reWithDigest2.MatchString(out), checker.True, check.Commentf("expected %q: %s", reWithDigest2.String(), out)) // make sure busybox has tag, but not digest busyboxRe := regexp.MustCompile(`\s*busybox\s*latest\s*\s`) c.Assert(busyboxRe.MatchString(out), checker.True, check.Commentf("expected %q: %s", busyboxRe.String(), out))