mirror of https://github.com/docker/cli.git
Merge b039c0e9af
into 0ab0eca8bd
This commit is contained in:
commit
0afacae2c1
|
@ -28,6 +28,7 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
newAnnotateCommand(dockerCli),
|
newAnnotateCommand(dockerCli),
|
||||||
newPushListCommand(dockerCli),
|
newPushListCommand(dockerCli),
|
||||||
newRmManifestListCommand(dockerCli),
|
newRmManifestListCommand(dockerCli),
|
||||||
|
newListCommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/cli/manifest/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultManifestListQuietFormat = "{{.Name}}"
|
||||||
|
defaultManifestListTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.Platforms}}"
|
||||||
|
|
||||||
|
repositoryHeader = "REPOSITORY"
|
||||||
|
tagHeader = "TAG"
|
||||||
|
platformsHeader = "PLATFORMS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, manifests map[string][]types.ImageManifest) 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(),
|
||||||
|
imageManifests: manifests[manifestList.String()],
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ctx.Write(newManifestListContext(), render)
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifestListContext struct {
|
||||||
|
formatter.HeaderContext
|
||||||
|
name string
|
||||||
|
repo string
|
||||||
|
tag string
|
||||||
|
imageManifests []types.ImageManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func newManifestListContext() *manifestListContext {
|
||||||
|
manifestListCtx := manifestListContext{}
|
||||||
|
manifestListCtx.Header = formatter.SubHeaderContext{
|
||||||
|
"Name": formatter.NameHeader,
|
||||||
|
"Repository": repositoryHeader,
|
||||||
|
"Tag": tagHeader,
|
||||||
|
"Platforms": platformsHeader,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *manifestListContext) Platforms() string {
|
||||||
|
platforms := []string{}
|
||||||
|
for _, manifest := range c.imageManifests {
|
||||||
|
os := manifest.Descriptor.Platform.OS
|
||||||
|
arch := manifest.Descriptor.Platform.Architecture
|
||||||
|
platforms = append(platforms, fmt.Sprintf("%s/%s", os, arch))
|
||||||
|
}
|
||||||
|
return strings.Join(platforms, ", ")
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/docker/cli/cli/manifest/types"
|
||||||
|
"github.com/fvbommel/sortorder"
|
||||||
|
"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(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(dockerCli command.Cli, options listOptions) error {
|
||||||
|
manifestStore := dockerCli.ManifestStore()
|
||||||
|
|
||||||
|
var manifestLists []reference.Reference
|
||||||
|
|
||||||
|
manifestLists, err := manifestStore.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests := map[string][]types.ImageManifest{}
|
||||||
|
for _, manifestList := range manifestLists {
|
||||||
|
if imageManifests, err := manifestStore.GetList(manifestList); err == nil {
|
||||||
|
manifests[manifestList.String()] = imageManifests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, manifests)
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/manifest/store"
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListErrors(t *testing.T) {
|
||||||
|
manifestStore := store.NewStore(t.TempDir())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
description string
|
||||||
|
args []string
|
||||||
|
flags map[string]string
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "too many arguments",
|
||||||
|
args: []string{"foo"},
|
||||||
|
expectedError: "accepts no arguments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "invalid format",
|
||||||
|
args: []string{},
|
||||||
|
flags: map[string]string{
|
||||||
|
"format": "{{invalid format}}",
|
||||||
|
},
|
||||||
|
expectedError: "template parsing error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
cli := test.NewFakeCli(nil)
|
||||||
|
cli.SetManifestStore(manifestStore)
|
||||||
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
for key, value := range tc.flags {
|
||||||
|
cmd.Flags().Set(key, value)
|
||||||
|
}
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
manifestStore := store.NewStore(t.TempDir())
|
||||||
|
|
||||||
|
list1 := ref(t, "first:1")
|
||||||
|
namedRef := ref(t, "alpine:3.0")
|
||||||
|
err := manifestStore.Save(list1, namedRef, fullImageManifest(t, namedRef))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
namedRef = ref(t, "alpine:3.1")
|
||||||
|
imageManifest := fullImageManifest(t, namedRef)
|
||||||
|
imageManifest.Descriptor.Platform.OS = "linux"
|
||||||
|
imageManifest.Descriptor.Platform.Architecture = "arm64"
|
||||||
|
err = manifestStore.Save(list1, namedRef, imageManifest)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
list2 := ref(t, "second:2")
|
||||||
|
namedRef = ref(t, "alpine:3.2")
|
||||||
|
err = manifestStore.Save(list2, namedRef, fullImageManifest(t, namedRef))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
description string
|
||||||
|
args []string
|
||||||
|
flags map[string]string
|
||||||
|
golden string
|
||||||
|
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "list with no additional flags",
|
||||||
|
args: []string{},
|
||||||
|
golden: "manifest-list.golden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "list with quiet option",
|
||||||
|
args: []string{},
|
||||||
|
flags: map[string]string{
|
||||||
|
"quiet": "true",
|
||||||
|
},
|
||||||
|
golden: "manifest-list-with-quiet-option.golden",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
cli := test.NewFakeCli(nil)
|
||||||
|
cli.SetManifestStore(manifestStore)
|
||||||
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
for key, value := range tc.flags {
|
||||||
|
cmd.Flags().Set(key, value)
|
||||||
|
}
|
||||||
|
assert.NilError(t, cmd.Execute())
|
||||||
|
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
example.com/first:1
|
||||||
|
example.com/second:2
|
|
@ -0,0 +1,3 @@
|
||||||
|
REPOSITORY TAG PLATFORMS
|
||||||
|
example.com/first 1 linux/amd64, linux/arm64
|
||||||
|
example.com/second 2 linux/amd64
|
|
@ -24,6 +24,7 @@ type ConfigFile struct {
|
||||||
PluginsFormat string `json:"pluginsFormat,omitempty"`
|
PluginsFormat string `json:"pluginsFormat,omitempty"`
|
||||||
VolumesFormat string `json:"volumesFormat,omitempty"`
|
VolumesFormat string `json:"volumesFormat,omitempty"`
|
||||||
StatsFormat string `json:"statsFormat,omitempty"`
|
StatsFormat string `json:"statsFormat,omitempty"`
|
||||||
|
ManifestListsFormat string `json:"manifestListsFormat,omitempty"`
|
||||||
DetachKeys string `json:"detachKeys,omitempty"`
|
DetachKeys string `json:"detachKeys,omitempty"`
|
||||||
CredentialsStore string `json:"credsStore,omitempty"`
|
CredentialsStore string `json:"credsStore,omitempty"`
|
||||||
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
|
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Store interface {
|
||||||
Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error)
|
Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error)
|
||||||
GetList(listRef reference.Reference) ([]types.ImageManifest, error)
|
GetList(listRef reference.Reference) ([]types.ImageManifest, error)
|
||||||
Save(listRef reference.Reference, manifest reference.Reference, image 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
|
// fsStore manages manifest files stored on the local filesystem
|
||||||
|
@ -85,6 +86,26 @@ func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (typ
|
||||||
return manifestInfo.ImageManifest, nil
|
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
|
// GetList returns all the local manifests for a transaction
|
||||||
func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) {
|
func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) {
|
||||||
filenames, err := s.listManifests(listRef.String())
|
filenames, err := s.listManifests(listRef.String())
|
||||||
|
@ -148,8 +169,13 @@ func manifestToFilename(root, manifestList, manifest string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeFilesafeName(ref string) string {
|
func makeFilesafeName(ref string) string {
|
||||||
fileName := strings.ReplaceAll(ref, ":", "-")
|
fileName := strings.ReplaceAll(ref, ":", "%")
|
||||||
return strings.ReplaceAll(fileName, "/", "_")
|
return strings.ReplaceAll(fileName, "/", "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenameToRefString(filename string) string {
|
||||||
|
refString := strings.ReplaceAll(filename, "%", ":")
|
||||||
|
return strings.ReplaceAll(refString, "#", "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
type notFoundError struct {
|
type notFoundError struct {
|
||||||
|
|
|
@ -4139,10 +4139,15 @@ _docker_manifest() {
|
||||||
annotate
|
annotate
|
||||||
create
|
create
|
||||||
inspect
|
inspect
|
||||||
|
ls
|
||||||
push
|
push
|
||||||
rm
|
rm
|
||||||
"
|
"
|
||||||
__docker_subcommands "$subcommands" && return
|
|
||||||
|
local aliases="
|
||||||
|
list
|
||||||
|
"
|
||||||
|
__docker_subcommands "$subcommands $aliases" && return
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
|
@ -4250,6 +4255,24 @@ _docker_manifest_rm() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_docker_manifest_list() {
|
||||||
|
_docker_manifest_ls
|
||||||
|
}
|
||||||
|
|
||||||
|
_docker_manifest_ls() {
|
||||||
|
case "$prev" in
|
||||||
|
--format)
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$cur" in
|
||||||
|
-*)
|
||||||
|
COMPREPLY=( $( compgen -W "--format --help --quiet -q" -- "$cur" ) )
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
_docker_node() {
|
_docker_node() {
|
||||||
local subcommands="
|
local subcommands="
|
||||||
demote
|
demote
|
||||||
|
|
|
@ -21,6 +21,7 @@ For full details on using docker manifest lists, see the registry v2 specificati
|
||||||
| [`annotate`](manifest_annotate.md) | Add additional information to a local image manifest |
|
| [`annotate`](manifest_annotate.md) | Add additional information to a local image manifest |
|
||||||
| [`create`](manifest_create.md) | Create a local manifest list for annotating and pushing to a registry |
|
| [`create`](manifest_create.md) | Create a local manifest list for annotating and pushing to a registry |
|
||||||
| [`inspect`](manifest_inspect.md) | Display an image manifest, or manifest list |
|
| [`inspect`](manifest_inspect.md) | Display an image manifest, or manifest list |
|
||||||
|
| [`ls`](manifest_ls.md) | List local manifest lists |
|
||||||
| [`push`](manifest_push.md) | Push a manifest list to a repository |
|
| [`push`](manifest_push.md) | Push a manifest list to a repository |
|
||||||
| [`rm`](manifest_rm.md) | Delete one or more manifest lists from local storage |
|
| [`rm`](manifest_rm.md) | Delete one or more manifest lists from local storage |
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# docker manifest ls
|
||||||
|
|
||||||
|
<!---MARKER_GEN_START-->
|
||||||
|
List local manifest lists
|
||||||
|
|
||||||
|
### Aliases
|
||||||
|
|
||||||
|
`docker manifest ls`, `docker manifest list`
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `--format` | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
|
||||||
|
| `-q`, `--quiet` | | | Only show manifest list NAMEs |
|
||||||
|
|
||||||
|
|
||||||
|
<!---MARKER_GEN_END-->
|
||||||
|
|
Loading…
Reference in New Issue