Skip to content

Commit 8633197

Browse files
authored
Merge pull request #5914 from thaJeztah/use_atomicwriter
cli/command: deprecate CopyToFile and reimplement with atomicwriter
2 parents 94afbc1 + 7cc6b8e commit 8633197

5 files changed

Lines changed: 52 additions & 62 deletions

File tree

cli/command/container/export.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/docker/cli/cli"
88
"github.com/docker/cli/cli/command"
99
"github.com/docker/cli/cli/command/completion"
10+
"github.com/moby/sys/atomicwriter"
1011
"github.com/pkg/errors"
1112
"github.com/spf13/cobra"
1213
)
@@ -41,27 +42,28 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
4142
return cmd
4243
}
4344

44-
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
45-
if opts.output == "" && dockerCli.Out().IsTerminal() {
46-
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
47-
}
48-
49-
if err := command.ValidateOutputPath(opts.output); err != nil {
50-
return errors.Wrap(err, "failed to export container")
45+
func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) error {
46+
var output io.Writer
47+
if opts.output == "" {
48+
if dockerCLI.Out().IsTerminal() {
49+
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
50+
}
51+
output = dockerCLI.Out()
52+
} else {
53+
writer, err := atomicwriter.New(opts.output, 0o600)
54+
if err != nil {
55+
return errors.Wrap(err, "failed to export container")
56+
}
57+
defer writer.Close()
58+
output = writer
5159
}
5260

53-
clnt := dockerCli.Client()
54-
55-
responseBody, err := clnt.ContainerExport(ctx, opts.container)
61+
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
5662
if err != nil {
5763
return err
5864
}
5965
defer responseBody.Close()
6066

61-
if opts.output == "" {
62-
_, err := io.Copy(dockerCli.Out(), responseBody)
63-
return err
64-
}
65-
66-
return command.CopyToFile(opts.output, responseBody)
67+
_, err = io.Copy(output, responseBody)
68+
return err
6769
}

cli/command/container/export_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
4242
cmd.SetErr(io.Discard)
4343
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
4444

45-
err := cmd.Execute()
46-
assert.Assert(t, err != nil)
47-
expected := `"/dev/random" must be a directory or a regular file`
48-
assert.ErrorContains(t, err, expected)
45+
const expected = `failed to export container: cannot write to a character device file`
46+
assert.Error(t, cmd.Execute(), expected)
4947
}

cli/command/image/save.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/docker/cli/cli/command"
1010
"github.com/docker/cli/cli/command/completion"
1111
"github.com/docker/docker/client"
12+
"github.com/moby/sys/atomicwriter"
1213
"github.com/pkg/errors"
1314
"github.com/spf13/cobra"
1415
)
@@ -48,15 +49,7 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
4849
}
4950

5051
// runSave performs a save against the engine based on the specified options
51-
func runSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error {
52-
if opts.output == "" && dockerCli.Out().IsTerminal() {
53-
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
54-
}
55-
56-
if err := command.ValidateOutputPath(opts.output); err != nil {
57-
return errors.Wrap(err, "failed to save image")
58-
}
59-
52+
func runSave(ctx context.Context, dockerCLI command.Cli, opts saveOptions) error {
6053
var options []client.ImageSaveOption
6154
if opts.platform != "" {
6255
p, err := platforms.Parse(opts.platform)
@@ -67,16 +60,27 @@ func runSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
6760
options = append(options, client.ImageSaveWithPlatforms(p))
6861
}
6962

70-
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options...)
71-
if err != nil {
72-
return err
63+
var output io.Writer
64+
if opts.output == "" {
65+
if dockerCLI.Out().IsTerminal() {
66+
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
67+
}
68+
output = dockerCLI.Out()
69+
} else {
70+
writer, err := atomicwriter.New(opts.output, 0o600)
71+
if err != nil {
72+
return errors.Wrap(err, "failed to save image")
73+
}
74+
defer writer.Close()
75+
output = writer
7376
}
74-
defer responseBody.Close()
7577

76-
if opts.output == "" {
77-
_, err := io.Copy(dockerCli.Out(), responseBody)
78+
responseBody, err := dockerCLI.Client().ImageSave(ctx, opts.images, options...)
79+
if err != nil {
7880
return err
7981
}
82+
defer responseBody.Close()
8083

81-
return command.CopyToFile(opts.output, responseBody)
84+
_, err = io.Copy(output, responseBody)
85+
return err
8286
}

cli/command/image/save_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ func TestNewSaveCommandErrors(t *testing.T) {
4444
{
4545
name: "output directory does not exist",
4646
args: []string{"-o", "fakedir/out.tar", "arg1"},
47-
expectedError: "failed to save image: invalid output path: directory \"fakedir\" does not exist",
47+
expectedError: `failed to save image: invalid output path: stat fakedir: no such file or directory`,
4848
},
4949
{
5050
name: "output file is irregular",
5151
args: []string{"-o", "/dev/null", "arg1"},
52-
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
52+
expectedError: `failed to save image: cannot write to a character device file`,
5353
},
5454
{
5555
name: "invalid platform",

cli/command/utils.go

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,23 @@ import (
1717
"github.com/docker/cli/cli/streams"
1818
"github.com/docker/docker/api/types/filters"
1919
"github.com/docker/docker/errdefs"
20-
"github.com/moby/sys/sequential"
20+
"github.com/moby/sys/atomicwriter"
2121
"github.com/moby/term"
2222
"github.com/pkg/errors"
2323
"github.com/spf13/pflag"
2424
)
2525

2626
// CopyToFile writes the content of the reader to the specified file
27+
//
28+
// Deprecated: use [atomicwriter.New].
2729
func CopyToFile(outfile string, r io.Reader) error {
28-
// We use sequential file access here to avoid depleting the standby list
29-
// on Windows. On Linux, this is a call directly to os.CreateTemp
30-
tmpFile, err := sequential.CreateTemp(filepath.Dir(outfile), ".docker_temp_")
31-
if err != nil {
32-
return err
33-
}
34-
35-
tmpPath := tmpFile.Name()
36-
37-
_, err = io.Copy(tmpFile, r)
38-
tmpFile.Close()
39-
30+
writer, err := atomicwriter.New(outfile, 0o600)
4031
if err != nil {
41-
os.Remove(tmpPath)
42-
return err
43-
}
44-
45-
if err = os.Rename(tmpPath, outfile); err != nil {
46-
os.Remove(tmpPath)
4732
return err
4833
}
49-
50-
return nil
34+
defer writer.Close()
35+
_, err = io.Copy(writer, r)
36+
return err
5137
}
5238

5339
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
@@ -187,7 +173,7 @@ func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
187173
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
188174
}
189175

190-
// ValidateOutputPath validates the output paths of the `export` and `save` commands.
176+
// ValidateOutputPath validates the output paths of the "docker cp" command.
191177
func ValidateOutputPath(path string) error {
192178
dir := filepath.Dir(filepath.Clean(path))
193179
if dir != "" && dir != "." {
@@ -213,8 +199,8 @@ func ValidateOutputPath(path string) error {
213199
return nil
214200
}
215201

216-
// ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a
217-
// helper to `ValidateOutputPath`
202+
// ValidateOutputPathFileMode validates the output paths of the "docker cp" command
203+
// and serves as a helper to [ValidateOutputPath]
218204
func ValidateOutputPathFileMode(fileMode os.FileMode) error {
219205
switch {
220206
case fileMode&os.ModeDevice != 0:

0 commit comments

Comments
 (0)