diff --git a/cli/command/image/save.go b/cli/command/image/save.go index 64bd72a74d..0b69244ad9 100644 --- a/cli/command/image/save.go +++ b/cli/command/image/save.go @@ -10,6 +10,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/client" "github.com/moby/sys/atomicwriter" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -17,7 +18,7 @@ import ( type saveOptions struct { images []string output string - platform string + platform []string } // NewSaveCommand creates a new `docker save` command @@ -41,7 +42,7 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") - flags.StringVar(&opts.platform, "platform", "", `Save only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`) + flags.StringSliceVar(&opts.platform, "platform", []string{}, `Save only the given platform(s). Formatted as a comma-separated list of "os[/arch[/variant]]" (e.g., "linux/amd64,linux/arm64/v8")`) _ = flags.SetAnnotation("platform", "version", []string{"1.48"}) _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) @@ -51,13 +52,17 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command { // runSave performs a save against the engine based on the specified options func runSave(ctx context.Context, dockerCLI command.Cli, opts saveOptions) error { var options []client.ImageSaveOption - if opts.platform != "" { - p, err := platforms.Parse(opts.platform) + + platformList := []ocispec.Platform{} + for _, p := range opts.platform { + pp, err := platforms.Parse(p) if err != nil { return errors.Wrap(err, "invalid platform") } - // TODO(thaJeztah): change flag-type to support multiple platforms. - options = append(options, client.ImageSaveWithPlatforms(p)) + platformList = append(platformList, pp) + } + if len(platformList) > 0 { + options = append(options, client.ImageSaveWithPlatforms(platformList...)) } var output io.Writer diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go index 7a3e93eb35..0ec4c22bfe 100644 --- a/cli/command/image/save_test.go +++ b/cli/command/image/save_test.go @@ -111,6 +111,26 @@ func TestNewSaveCommandSuccess(t *testing.T) { return io.NopCloser(strings.NewReader("")), nil }, }, + { + args: []string{"--platform", "linux/amd64,linux/arm64/v8,linux/riscv64", "arg1"}, + isTerminal: false, + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) { + assert.Assert(t, is.Len(images, 1)) + assert.Check(t, is.Equal("arg1", images[0])) + assert.Check(t, len(options) > 0) // can be 1 or 2 depending on whether a terminal is attached :/ + return io.NopCloser(strings.NewReader("")), nil + }, + }, + { + args: []string{"--platform", "linux/amd64", "--platform", "linux/arm64/v8", "--platform", "linux/riscv64", "arg1"}, + isTerminal: false, + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) { + assert.Assert(t, is.Len(images, 1)) + assert.Check(t, is.Equal("arg1", images[0])) + assert.Check(t, len(options) > 0) // can be 1 or 2 depending on whether a terminal is attached :/ + return io.NopCloser(strings.NewReader("")), nil + }, + }, } for _, tc := range testCases { t.Run(strings.Join(tc.args, " "), func(t *testing.T) {