diff --git a/cli/command/manifest/cmd.go b/cli/command/manifest/cmd.go index a914ef6dbb..97fbf7e781 100644 --- a/cli/command/manifest/cmd.go +++ b/cli/command/manifest/cmd.go @@ -28,6 +28,7 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command { newAnnotateCommand(dockerCli), newPushListCommand(dockerCli), newRmManifestListCommand(dockerCli), + newListCommand(dockerCli), ) return cmd } diff --git a/cli/command/manifest/formatter.go b/cli/command/manifest/formatter.go new file mode 100644 index 0000000000..35491a41f2 --- /dev/null +++ b/cli/command/manifest/formatter.go @@ -0,0 +1,85 @@ +package manifest + +import ( + "github.com/distribution/reference" + "github.com/docker/cli/cli/command/formatter" +) + +const ( + defaultManifestListQuietFormat = "{{.Name}}" + defaultManifestListTableFormat = "table {{.Repository}}\t{{.Tag}}" + + repositoryHeader = "REPOSITORY" + tagHeader = "TAG" +) + +// NewFormat returns a Format for rendering using a manifest list Context +func NewFormat(source string, quiet bool) formatter.Format { + switch source { + case formatter.TableFormatKey: + if quiet { + return defaultManifestListQuietFormat + } + return defaultManifestListTableFormat + case formatter.RawFormatKey: + if quiet { + return `name: {{.Name}}` + } + return `repo: {{.Repository}}\ntag: {{.Tag}}\n` + } + return formatter.Format(source) +} + +// FormatWrite writes formatted manifestLists using the Context +func FormatWrite(ctx formatter.Context, manifestLists []reference.Reference) error { + render := func(format func(subContext formatter.SubContext) error) error { + for _, manifestList := range manifestLists { + if n, ok := manifestList.(reference.Named); ok { + if nt, ok := n.(reference.NamedTagged); ok { + if err := format(&manifestListContext{ + name: reference.FamiliarString(manifestList), + repo: reference.FamiliarName(nt), + tag: nt.Tag(), + }); err != nil { + return err + } + } + } + } + return nil + } + return ctx.Write(newManifestListContext(), render) +} + +type manifestListContext struct { + formatter.HeaderContext + name string + repo string + tag string +} + +func newManifestListContext() *manifestListContext { + manifestListCtx := manifestListContext{} + manifestListCtx.Header = formatter.SubHeaderContext{ + "Name": formatter.NameHeader, + "Repository": repositoryHeader, + "Tag": tagHeader, + } + return &manifestListCtx +} + +func (c *manifestListContext) MarshalJSON() ([]byte, error) { + return formatter.MarshalJSON(c) +} + +func (c *manifestListContext) Name() string { + return c.name +} + +func (c *manifestListContext) Repository() string { + return c.repo +} + +func (c *manifestListContext) Tag() string { + return c.tag +} diff --git a/cli/command/manifest/list.go b/cli/command/manifest/list.go new file mode 100644 index 0000000000..0305762385 --- /dev/null +++ b/cli/command/manifest/list.go @@ -0,0 +1,69 @@ +package manifest + +import ( + "context" + "sort" + + "github.com/distribution/reference" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" + flagsHelper "github.com/docker/cli/cli/flags" + "github.com/fvbommel/sortorder" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type listOptions struct { + quiet bool + format string +} + +func newListCommand(dockerCli command.Cli) *cobra.Command { + var options listOptions + + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List local manifest lists", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(cmd.Context(), dockerCli, options) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show manifest list NAMEs") + flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) + return cmd +} + +func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { + + manifestStore := dockerCli.ManifestStore() + + var manifestLists []reference.Reference + + manifestLists, searchErr := manifestStore.List() + if searchErr != nil { + return errors.New(searchErr.Error()) + } + + format := options.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().ManifestListsFormat) > 0 && !options.quiet { + format = dockerCli.ConfigFile().ManifestListsFormat + } else { + format = formatter.TableFormatKey + } + } + + manifestListsCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: NewFormat(format, options.quiet), + } + sort.Slice(manifestLists, func(i, j int) bool { + return sortorder.NaturalLess(manifestLists[i].String(), manifestLists[j].String()) + }) + return FormatWrite(manifestListsCtx, manifestLists) +} diff --git a/cli/config/configfile/file.go b/cli/config/configfile/file.go index 442c31110b..254d9fc410 100644 --- a/cli/config/configfile/file.go +++ b/cli/config/configfile/file.go @@ -24,6 +24,7 @@ type ConfigFile struct { PluginsFormat string `json:"pluginsFormat,omitempty"` VolumesFormat string `json:"volumesFormat,omitempty"` StatsFormat string `json:"statsFormat,omitempty"` + ManifestListsFormat string `json:"manifestListsFormat,omitempty"` DetachKeys string `json:"detachKeys,omitempty"` CredentialsStore string `json:"credsStore,omitempty"` CredentialHelpers map[string]string `json:"credHelpers,omitempty"` diff --git a/cli/manifest/store/store.go b/cli/manifest/store/store.go index dbf7730632..e39ac3d4f5 100644 --- a/cli/manifest/store/store.go +++ b/cli/manifest/store/store.go @@ -21,6 +21,7 @@ type Store interface { Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error) GetList(listRef reference.Reference) ([]types.ImageManifest, error) Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error + List() ([]reference.Reference, error) } // fsStore manages manifest files stored on the local filesystem @@ -86,6 +87,26 @@ func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (typ return manifestInfo.ImageManifest, nil } +// List returns the local manifest lists for a transaction +func (s *fsStore) List() ([]reference.Reference, error) { + fileInfos, err := os.ReadDir(s.root) + switch { + case os.IsNotExist(err): + return nil, nil + case err != nil: + return nil, err + } + + listRefs := make([]reference.Reference, 0, len(fileInfos)) + for _, info := range fileInfos { + refString := filenameToRefString(info.Name()) + if listRef, err := reference.Parse(refString); err == nil { + listRefs = append(listRefs, listRef) + } + } + return listRefs, nil +} + // GetList returns all the local manifests for a transaction func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) { filenames, err := s.listManifests(listRef.String()) @@ -149,8 +170,13 @@ func manifestToFilename(root, manifestList, manifest string) string { } func makeFilesafeName(ref string) string { - fileName := strings.ReplaceAll(ref, ":", "-") - return strings.ReplaceAll(fileName, "/", "_") + fileName := strings.ReplaceAll(ref, ":", "--") + return strings.ReplaceAll(fileName, "/", "__") +} + +func filenameToRefString(filename string) string { + refString := strings.ReplaceAll(filename, "--", ":") + return strings.ReplaceAll(refString, "__", "/") } type notFoundError struct {