diff --git a/e2e/images_test.go b/e2e/images_test.go index 179078237..c53ae9209 100644 --- a/e2e/images_test.go +++ b/e2e/images_test.go @@ -4,11 +4,11 @@ import ( "bufio" "fmt" "path/filepath" + "regexp" "strings" "testing" "gotest.tools/assert" - "gotest.tools/fs" "gotest.tools/icmd" ) @@ -22,10 +22,20 @@ func insertBundles(t *testing.T, cmd icmd.Cmd, info dindSwarmAndRegistryInfo) { icmd.RunCmd(cmd).Assert(t, icmd.Success) } +func assertImageListOutput(t *testing.T, cmd icmd.Cmd, expected string) { + result := icmd.RunCmd(cmd).Assert(t, icmd.Success) + match, _ := regexp.MatchString(expected, result.Stdout()) + assert.Assert(t, match) +} + func expectImageListOutput(t *testing.T, cmd icmd.Cmd, output string) { cmd.Command = dockerCli.Command("app", "image", "ls") - result := icmd.RunCmd(cmd).Assert(t, icmd.Success) - assert.Equal(t, result.Stdout(), output) + assertImageListOutput(t, cmd, output) +} + +func expectImageListDigestsOutput(t *testing.T, cmd icmd.Cmd, output string) { + cmd.Command = dockerCli.Command("app", "image", "ls", "--digests") + assertImageListOutput(t, cmd, output) } func verifyImageIDListOutput(t *testing.T, cmd icmd.Cmd, count int, distinct int) { @@ -48,17 +58,15 @@ func verifyImageIDListOutput(t *testing.T, cmd icmd.Cmd, count int, distinct int func TestImageList(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd - dir := fs.NewDir(t, "") - defer dir.Remove() insertBundles(t, cmd, info) - expected := `APP IMAGE APP NAME -%s push-pull -a-simple-app:latest simple -b-simple-app:latest simple + expected := `REPOSITORY TAG APP IMAGE ID APP NAME +%s latest [a-f0-9]{12} push-pull +a-simple-app latest [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple ` - expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp:latest") + expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp") expectImageListOutput(t, cmd, expectedOutput) }) } @@ -66,18 +74,28 @@ b-simple-app:latest simple func TestImageListQuiet(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd - dir := fs.NewDir(t, "") - defer dir.Remove() insertBundles(t, cmd, info) verifyImageIDListOutput(t, cmd, 3, 2) }) } +func TestImageListDigests(t *testing.T) { + runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { + cmd := info.configuredCmd + insertBundles(t, cmd, info) + expected := `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME +%s latest [a-f0-9]{12} push-pull +a-simple-app latest [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple +` + expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp") + expectImageListDigestsOutput(t, cmd, expectedOutput) + }) +} + func TestImageRm(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd - dir := fs.NewDir(t, "") - defer dir.Remove() insertBundles(t, cmd, info) @@ -100,7 +118,7 @@ Deleted: b-simple-app:latest`, Err: `b-simple-app:latest: reference not found`, }) - expectedOutput := "APP IMAGE APP NAME\n" + expectedOutput := "REPOSITORY TAG APP IMAGE ID APP NAME\n" expectImageListOutput(t, cmd, expectedOutput) }) } @@ -118,8 +136,8 @@ func TestImageTag(t *testing.T) { cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-app", filepath.Join("testdata", "simple")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - singleImageExpectation := `APP IMAGE APP NAME -a-simple-app:latest simple + singleImageExpectation := `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app latest [a-f0-9]{12} simple ` expectImageListOutput(t, cmd, singleImageExpectation) @@ -168,63 +186,63 @@ a-simple-app:latest simple // tag image with only names dockerAppImageTag("a-simple-app", "b-simple-app") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `APP IMAGE APP NAME -a-simple-app:latest simple -b-simple-app:latest simple + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app latest [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple `) // target tag dockerAppImageTag("a-simple-app", "a-simple-app:0.1") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `APP IMAGE APP NAME -a-simple-app:0.1 simple -a-simple-app:latest simple -b-simple-app:latest simple + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app 0.1 [a-f0-9]{12} simple +a-simple-app latest [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple `) // source tag dockerAppImageTag("a-simple-app:0.1", "c-simple-app") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `APP IMAGE APP NAME -a-simple-app:0.1 simple -a-simple-app:latest simple -b-simple-app:latest simple -c-simple-app:latest simple + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app 0.1 [a-f0-9]{12} simple +a-simple-app latest [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple +c-simple-app latest [a-f0-9]{12} simple `) // source and target tags dockerAppImageTag("a-simple-app:0.1", "b-simple-app:0.2") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `APP IMAGE APP NAME -a-simple-app:0.1 simple -a-simple-app:latest simple -b-simple-app:0.2 simple -b-simple-app:latest simple -c-simple-app:latest simple + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app 0.1 [a-f0-9]{12} simple +a-simple-app latest [a-f0-9]{12} simple +b-simple-app 0.2 [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple +c-simple-app latest [a-f0-9]{12} simple `) // given a new application cmd.Command = dockerCli.Command("app", "build", "--tag", "push-pull", filepath.Join("testdata", "push-pull")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `APP IMAGE APP NAME -a-simple-app:0.1 simple -a-simple-app:latest simple -b-simple-app:0.2 simple -b-simple-app:latest simple -c-simple-app:latest simple -push-pull:latest push-pull + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app 0.1 [a-f0-9]{12} simple +a-simple-app latest [a-f0-9]{12} simple +b-simple-app 0.2 [a-f0-9]{12} simple +b-simple-app latest [a-f0-9]{12} simple +c-simple-app latest [a-f0-9]{12} simple +push-pull latest [a-f0-9]{12} push-pull `) // can be tagged to an existing tag dockerAppImageTag("push-pull", "b-simple-app:0.2") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `APP IMAGE APP NAME -a-simple-app:0.1 simple -a-simple-app:latest simple -b-simple-app:0.2 push-pull -b-simple-app:latest simple -c-simple-app:latest simple -push-pull:latest push-pull + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME +a-simple-app 0.1 [a-f0-9]{12} simple +a-simple-app latest [a-f0-9]{12} simple +b-simple-app 0.2 [a-f0-9]{12} push-pull +b-simple-app latest [a-f0-9]{12} simple +c-simple-app latest [a-f0-9]{12} simple +push-pull latest [a-f0-9]{12} push-pull `) }) } diff --git a/internal/commands/image/list.go b/internal/commands/image/list.go index e4feb0ef8..0dd71ec25 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -17,7 +17,13 @@ import ( ) type imageListOption struct { - quiet bool + quiet bool + digests bool +} + +type imageListColumn struct { + header string + value func(p pkg) string } func listCmd(dockerCli command.Cli) *cobra.Command { @@ -42,6 +48,7 @@ func listCmd(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show numeric IDs") + flags.BoolVarP(&options.digests, "digests", "", false, "Show image digests") return cmd } @@ -60,7 +67,7 @@ func runList(dockerCli command.Cli, options imageListOption, bundleStore store.B if options.quiet { return printImageIDs(dockerCli, pkgs) } - return printImages(dockerCli, pkgs) + return printImages(dockerCli, pkgs, options) } func getPackages(bundleStore store.BundleStore, references []reference.Reference) ([]pkg, error) { @@ -82,12 +89,12 @@ func getPackages(bundleStore store.BundleStore, references []reference.Reference return packages, nil } -func printImages(dockerCli command.Cli, refs []pkg) error { +func printImages(dockerCli command.Cli, refs []pkg, options imageListOption) error { w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) - - printHeaders(w) + listColumns := getImageListColumns(options) + printHeaders(w, listColumns) for _, ref := range refs { - printValues(w, ref) + printValues(w, ref, listColumns) } return w.Flush() @@ -97,21 +104,29 @@ func printImageIDs(dockerCli command.Cli, refs []pkg) error { var buf bytes.Buffer for _, ref := range refs { - id, ok := ref.ref.(store.ID) - if !ok { - var err error - id, err = store.FromBundle(ref.bundle) - if err != nil { - return err - } + id, err := getImageID(ref) + if err != nil { + return err } - fmt.Fprintln(&buf, stringid.TruncateID(id.String())) + fmt.Fprintln(&buf, id) } fmt.Fprint(dockerCli.Out(), buf.String()) return nil } -func printHeaders(w io.Writer) { +func getImageID(p pkg) (string, error) { + id, ok := p.ref.(store.ID) + if !ok { + var err error + id, err = store.FromBundle(p.bundle) + if err != nil { + return "", err + } + } + return stringid.TruncateID(id.String()), nil +} + +func printHeaders(w io.Writer, listColumns []imageListColumn) { var headers []string for _, column := range listColumns { headers = append(headers, column.header) @@ -119,7 +134,7 @@ func printHeaders(w io.Writer) { fmt.Fprintln(w, strings.Join(headers, "\t")) } -func printValues(w io.Writer, ref pkg) { +func printValues(w io.Writer, ref pkg, listColumns []imageListColumn) { var values []string for _, column := range listColumns { values = append(values, column.value(ref)) @@ -127,19 +142,43 @@ func printValues(w io.Writer, ref pkg) { fmt.Fprintln(w, strings.Join(values, "\t")) } -var ( - listColumns = []struct { - header string - value func(p pkg) string - }{ - {"APP IMAGE", func(p pkg) string { +func getImageListColumns(options imageListOption) []imageListColumn { + columns := []imageListColumn{ + {"REPOSITORY", func(p pkg) string { + if n, ok := p.ref.(reference.Named); ok { + return reference.FamiliarName(n) + } return reference.FamiliarString(p.ref) }}, - {"APP NAME", func(p pkg) string { - return p.bundle.Name + {"TAG", func(p pkg) string { + if t, ok := p.ref.(reference.Tagged); ok { + return t.Tag() + } + return "" }}, } -) + if options.digests { + columns = append(columns, imageListColumn{"DIGEST", func(p pkg) string { + if t, ok := p.ref.(reference.Digested); ok { + return t.Digest().String() + } + return "" + }}) + } + columns = append(columns, + imageListColumn{"APP IMAGE ID", func(p pkg) string { + id, err := getImageID(p) + if err != nil { + return "" + } + return id + }}, + imageListColumn{"APP NAME", func(p pkg) string { + return p.bundle.Name + }}, + ) + return columns +} type pkg struct { ref reference.Reference diff --git a/internal/commands/image/list_test.go b/internal/commands/image/list_test.go index de4996820..a64ee7a27 100644 --- a/internal/commands/image/list_test.go +++ b/internal/commands/image/list_test.go @@ -46,7 +46,75 @@ func (b *bundleStoreStubForListCmd) LookUp(refOrID string) (reference.Reference, return nil, nil } -func TestListWithQuietFlag(t *testing.T) { +func TestListCmd(t *testing.T) { + ref, err := store.FromString("a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087") + assert.NilError(t, err) + refs := []reference.Reference{ + parseReference(t, "foo/bar@sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865"), + parseReference(t, "foo/bar:1.0"), + ref, + } + bundles := []bundle.Bundle{ + { + Name: "Digested App", + }, + { + Version: "1.0.0", + SchemaVersion: "1.0.0", + Name: "Foo App", + }, + { + Name: "Quiet App", + }, + } + + testCases := []struct { + name string + expectedOutput string + options imageListOption + }{ + { + name: "TestList", + expectedOutput: `REPOSITORY TAG APP IMAGE ID APP NAME +foo/bar 3f825b2d0657 Digested App +foo/bar 1.0 9aae408ee04f Foo App +a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 a855ac937f2e Quiet App +`, + options: imageListOption{}, + }, + { + name: "TestListWithDigests", + expectedOutput: `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME +foo/bar sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865 3f825b2d0657 Digested App +foo/bar 1.0 9aae408ee04f Foo App +a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 sha256:a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 a855ac937f2e Quiet App +`, + options: imageListOption{digests: true}, + }, + { + name: "TestListWithQuiet", + expectedOutput: `3f825b2d0657 +9aae408ee04f +a855ac937f2e +`, + options: imageListOption{quiet: true}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testRunList(t, refs, bundles, tc.options, tc.expectedOutput) + }) + } +} + +func parseReference(t *testing.T, s string) reference.Reference { + ref, err := reference.Parse(s) + assert.NilError(t, err) + return ref +} + +func testRunList(t *testing.T, refs []reference.Reference, bundles []bundle.Bundle, options imageListOption, expectedOutput string) { var buf bytes.Buffer w := bufio.NewWriter(&buf) dockerCli, err := command.NewDockerCli(command.WithOutputStream(w)) @@ -55,23 +123,12 @@ func TestListWithQuietFlag(t *testing.T) { refMap: make(map[reference.Reference]*bundle.Bundle), refList: []reference.Reference{}, } - ref1, err := store.FromString("a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087") - assert.NilError(t, err) - ref2, err := reference.Parse("foo/bar:1.0") - assert.NilError(t, err) - _, err = bundleStore.Store(ref1, &bundle.Bundle{}) - assert.NilError(t, err) - _, err = bundleStore.Store(ref2, &bundle.Bundle{ - Version: "1.0.0", - SchemaVersion: "1.0.0", - Name: "Foo App", - }) - assert.NilError(t, err) - err = runList(dockerCli, imageListOption{quiet: true}, bundleStore) + for i, ref := range refs { + _, err = bundleStore.Store(ref, &bundles[i]) + assert.NilError(t, err) + } + err = runList(dockerCli, options, bundleStore) assert.NilError(t, err) - expectedOutput := `a855ac937f2e -9aae408ee04f -` w.Flush() assert.Equal(t, buf.String(), expectedOutput) }