diff --git a/e2e/images_test.go b/e2e/images_test.go index ce7cf1c37..6692ec3bd 100644 --- a/e2e/images_test.go +++ b/e2e/images_test.go @@ -1,8 +1,10 @@ package e2e import ( + "bufio" "fmt" "path/filepath" + "strings" "testing" "gotest.tools/assert" @@ -26,6 +28,23 @@ func expectImageListOutput(t *testing.T, cmd icmd.Cmd, output string) { assert.Equal(t, result.Stdout(), output) } +func verifyImageIDListOutput(t *testing.T, cmd icmd.Cmd, count int, distinct int) { + cmd.Command = dockerCli.Command("app", "image", "ls", "-q") + result := icmd.RunCmd(cmd).Assert(t, icmd.Success) + scanner := bufio.NewScanner(strings.NewReader(result.Stdout())) + lines := []string{} + counter := make(map[string]int) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + counter[scanner.Text()]++ + } + if err := scanner.Err(); err != nil { + assert.Error(t, err, "Verification failed") + } + assert.Equal(t, len(lines), count) + assert.Equal(t, len(counter), distinct) +} + func TestImageList(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd @@ -44,6 +63,16 @@ 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 TestImageRm(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd diff --git a/internal/commands/image/list.go b/internal/commands/image/list.go index dd6bc09c3..2fabf72fe 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -1,6 +1,7 @@ package image import ( + "bytes" "fmt" "io" "strings" @@ -11,10 +12,16 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" "github.com/docker/distribution/reference" + "github.com/docker/docker/pkg/stringid" "github.com/spf13/cobra" ) +type imageListOption struct { + quiet bool +} + func listCmd(dockerCli command.Cli) *cobra.Command { + options := imageListOption{} cmd := &cobra.Command{ Short: "List application images", Use: "ls", @@ -30,14 +37,16 @@ func listCmd(dockerCli command.Cli) *cobra.Command { return err } - return runList(dockerCli, bundleStore) + return runList(dockerCli, options, bundleStore) }, } + flags := cmd.Flags() + flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show numeric IDs") return cmd } -func runList(dockerCli command.Cli, bundleStore store.BundleStore) error { +func runList(dockerCli command.Cli, options imageListOption, bundleStore store.BundleStore) error { bundles, err := bundleStore.List() if err != nil { return err @@ -48,6 +57,9 @@ func runList(dockerCli command.Cli, bundleStore store.BundleStore) error { return err } + if options.quiet { + return printImageIDs(dockerCli, pkgs) + } return printImages(dockerCli, pkgs) } @@ -81,6 +93,24 @@ func printImages(dockerCli command.Cli, refs []pkg) error { return w.Flush() } +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 + } + } + fmt.Fprintln(&buf, stringid.TruncateID(id.String())) + } + fmt.Fprintf(dockerCli.Out(), buf.String()) + return nil +} + func printHeaders(w io.Writer) { var headers []string for _, column := range listColumns { diff --git a/internal/commands/image/list_test.go b/internal/commands/image/list_test.go new file mode 100644 index 000000000..86311e7c0 --- /dev/null +++ b/internal/commands/image/list_test.go @@ -0,0 +1,79 @@ +package image + +import ( + "bufio" + "bytes" + "fmt" + "testing" + + "gotest.tools/assert" + + "github.com/deislabs/cnab-go/bundle" + "github.com/docker/app/internal/store" + "github.com/docker/cli/cli/command" + "github.com/docker/distribution/reference" +) + +type mockRef string + +func (ref mockRef) String() string { + return string(ref) +} + +type bundleStoreStubForListCmd struct { + refMap map[reference.Reference]*bundle.Bundle + // in order to keep the reference in the same order between tests + refList []reference.Reference +} + +func (b *bundleStoreStubForListCmd) Store(ref reference.Reference, bndle *bundle.Bundle) (reference.Reference, error) { + b.refMap[ref] = bndle + b.refList = append(b.refList, ref) + return ref, nil +} + +func (b *bundleStoreStubForListCmd) Read(ref reference.Reference) (*bundle.Bundle, error) { + bndl, ok := b.refMap[ref] + if ok { + return bndl, nil + } + return nil, fmt.Errorf("Bundle not found") +} + +func (b *bundleStoreStubForListCmd) List() ([]reference.Reference, error) { + return b.refList, nil +} + +func (b *bundleStoreStubForListCmd) Remove(ref reference.Reference) error { + return nil +} + +func TestListWithQuietFlag(t *testing.T) { + var buf bytes.Buffer + w := bufio.NewWriter(&buf) + dockerCli, err := command.NewDockerCli(command.WithOutputStream(w)) + assert.NilError(t, err) + bundleStore := &bundleStoreStubForListCmd{ + 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) + assert.NilError(t, err) + expectedOutput := `a855ac937f2e +9aae408ee04f +` + w.Flush() + assert.Equal(t, buf.String(), expectedOutput) +} diff --git a/internal/store/digest.go b/internal/store/digest.go index dc31f7806..85f6582e7 100644 --- a/internal/store/digest.go +++ b/internal/store/digest.go @@ -6,6 +6,7 @@ import ( "io" "regexp" + "github.com/deislabs/cnab-go/bundle" "github.com/docker/distribution/reference" "github.com/opencontainers/go-digest" ) @@ -27,6 +28,11 @@ func FromString(s string) (ID, error) { return ID{s}, nil } +func FromBundle(bndle *bundle.Bundle) (ID, error) { + digest, err := ComputeDigest(bndle) + return ID{digest.Encoded()}, err +} + // ID is an unique identifier for docker app image bundle, implementing reference.Reference type ID struct { digest string