From d47d2338b718b7a5bb18ea940dd04488a2c8023d Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 9 Mar 2025 21:24:49 +0100 Subject: [PATCH] image save: implement file-write with atomicwriter Same functionality, but implemented with atomicwriter. There's a slight difference in error-messages produced (but can be adjusted if we want). Before: docker image save -o ./no/such/foo busybox:latest failed to save image: invalid output path: directory "no/such" does not exist docker image save -o /no/permissions busybox:latest failed to save image: stat /no/permissions: permission denied After: docker image save -o ./no/such/foo busybox:latest failed to save image: invalid file path: stat no/such: no such file or directory docker image save -o /no/permissions busybox:latest failed to save image: failed to stat output path: lstat /no/permissions: permission denied Signed-off-by: Sebastiaan van Stijn --- cli/command/image/save.go | 36 +++++++++++++++++++--------------- cli/command/image/save_test.go | 4 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/cli/command/image/save.go b/cli/command/image/save.go index 813052278c..64bd72a74d 100644 --- a/cli/command/image/save.go +++ b/cli/command/image/save.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/client" + "github.com/moby/sys/atomicwriter" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -48,15 +49,7 @@ 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 { - if opts.output == "" && dockerCli.Out().IsTerminal() { - return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect") - } - - if err := command.ValidateOutputPath(opts.output); err != nil { - return errors.Wrap(err, "failed to save image") - } - +func runSave(ctx context.Context, dockerCLI command.Cli, opts saveOptions) error { var options []client.ImageSaveOption if opts.platform != "" { p, err := platforms.Parse(opts.platform) @@ -67,16 +60,27 @@ func runSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error options = append(options, client.ImageSaveWithPlatforms(p)) } - responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options...) + var output io.Writer + if opts.output == "" { + if dockerCLI.Out().IsTerminal() { + return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect") + } + output = dockerCLI.Out() + } else { + writer, err := atomicwriter.New(opts.output, 0o600) + if err != nil { + return errors.Wrap(err, "failed to save image") + } + defer writer.Close() + output = writer + } + + responseBody, err := dockerCLI.Client().ImageSave(ctx, opts.images, options...) if err != nil { return err } defer responseBody.Close() - if opts.output == "" { - _, err := io.Copy(dockerCli.Out(), responseBody) - return err - } - - return command.CopyToFile(opts.output, responseBody) + _, err = io.Copy(output, responseBody) + return err } diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go index aa0bb5139d..7a3e93eb35 100644 --- a/cli/command/image/save_test.go +++ b/cli/command/image/save_test.go @@ -44,12 +44,12 @@ func TestNewSaveCommandErrors(t *testing.T) { { name: "output directory does not exist", args: []string{"-o", "fakedir/out.tar", "arg1"}, - expectedError: "failed to save image: invalid output path: directory \"fakedir\" does not exist", + expectedError: `failed to save image: invalid output path: stat fakedir: no such file or directory`, }, { name: "output file is irregular", args: []string{"-o", "/dev/null", "arg1"}, - expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file", + expectedError: `failed to save image: cannot write to a character device file`, }, { name: "invalid platform",