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 <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-03-09 21:24:49 +01:00
parent 410c0baadd
commit d47d2338b7
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
2 changed files with 22 additions and 18 deletions

View File

@ -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
}

View File

@ -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",