Skip to content

Commit f483aac

Browse files
authored
Merge pull request #5331 from vvoland/c8d-saveload-platform
c8d: Add `--platform` flag to history, save and load
2 parents 6a78e92 + d085e24 commit f483aac

14 files changed

Lines changed: 262 additions & 30 deletions

cli/command/image/history.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ package image
33
import (
44
"context"
55

6+
"github.com/containerd/platforms"
67
"github.com/docker/cli/cli"
78
"github.com/docker/cli/cli/command"
89
"github.com/docker/cli/cli/command/completion"
910
"github.com/docker/cli/cli/command/formatter"
1011
flagsHelper "github.com/docker/cli/cli/flags"
1112
"github.com/docker/docker/api/types/image"
13+
"github.com/pkg/errors"
1214
"github.com/spf13/cobra"
1315
)
1416

1517
type historyOptions struct {
16-
image string
18+
image string
19+
platform string
1720

1821
human bool
1922
quiet bool
@@ -45,12 +48,24 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
4548
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show image IDs")
4649
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
4750
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp)
51+
flags.StringVar(&opts.platform, "platform", "", `Show history for the given platform. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
52+
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
4853

54+
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
4955
return cmd
5056
}
5157

5258
func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error {
53-
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, image.HistoryOptions{})
59+
var options image.HistoryOptions
60+
if opts.platform != "" {
61+
p, err := platforms.Parse(opts.platform)
62+
if err != nil {
63+
return errors.Wrap(err, "invalid platform")
64+
}
65+
options.Platform = &p
66+
}
67+
68+
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, options)
5469
if err != nil {
5570
return err
5671
}

cli/command/image/history_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88

99
"github.com/docker/cli/internal/test"
1010
"github.com/docker/docker/api/types/image"
11+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1112
"github.com/pkg/errors"
1213
"gotest.tools/v3/assert"
14+
is "gotest.tools/v3/assert/cmp"
1315
"gotest.tools/v3/golden"
1416
)
1517

@@ -33,6 +35,11 @@ func TestNewHistoryCommandErrors(t *testing.T) {
3335
return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong")
3436
},
3537
},
38+
{
39+
name: "invalid platform",
40+
args: []string{"--platform", "<invalid>", "arg1"},
41+
expectedError: `invalid platform`,
42+
},
3643
}
3744
for _, tc := range testCases {
3845
tc := tc
@@ -89,6 +96,17 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
8996
}}, nil
9097
},
9198
},
99+
{
100+
name: "platform",
101+
args: []string{"--platform", "linux/amd64", "image:tag"},
102+
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) {
103+
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
104+
return []image.HistoryResponseItem{{
105+
ID: "1234567890123456789",
106+
Created: time.Now().Unix(),
107+
}}, nil
108+
},
109+
},
92110
}
93111
for _, tc := range testCases {
94112
tc := tc

cli/command/image/load.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"io"
66

7+
"github.com/containerd/platforms"
78
"github.com/docker/cli/cli"
89
"github.com/docker/cli/cli/command"
910
"github.com/docker/cli/cli/command/completion"
@@ -15,8 +16,9 @@ import (
1516
)
1617

1718
type loadOptions struct {
18-
input string
19-
quiet bool
19+
input string
20+
quiet bool
21+
platform string
2022
}
2123

2224
// NewLoadCommand creates a new `docker load` command
@@ -40,7 +42,10 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command {
4042

4143
flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
4244
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
45+
flags.StringVar(&opts.platform, "platform", "", `Load only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
46+
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
4347

48+
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
4449
return cmd
4550
}
4651

@@ -63,12 +68,20 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
6368
return errors.Errorf("requested load from stdin, but stdin is empty")
6469
}
6570

66-
var loadOpts image.LoadOptions
71+
var options image.LoadOptions
6772
if opts.quiet || !dockerCli.Out().IsTerminal() {
68-
loadOpts.Quiet = true
73+
options.Quiet = true
6974
}
7075

71-
response, err := dockerCli.Client().ImageLoad(ctx, input, loadOpts)
76+
if opts.platform != "" {
77+
p, err := platforms.Parse(opts.platform)
78+
if err != nil {
79+
return errors.Wrap(err, "invalid platform")
80+
}
81+
options.Platform = &p
82+
}
83+
84+
response, err := dockerCli.Client().ImageLoad(ctx, input, options)
7285
if err != nil {
7386
return err
7487
}

cli/command/image/load_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88

99
"github.com/docker/cli/internal/test"
1010
"github.com/docker/docker/api/types/image"
11+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1112
"github.com/pkg/errors"
1213
"gotest.tools/v3/assert"
14+
is "gotest.tools/v3/assert/cmp"
1315
"gotest.tools/v3/golden"
1416
)
1517

@@ -40,6 +42,14 @@ func TestNewLoadCommandErrors(t *testing.T) {
4042
return image.LoadResponse{}, errors.Errorf("something went wrong")
4143
},
4244
},
45+
{
46+
name: "invalid platform",
47+
args: []string{"--platform", "<invalid>"},
48+
expectedError: `invalid platform`,
49+
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
50+
return image.LoadResponse{}, nil
51+
},
52+
},
4353
}
4454
for _, tc := range testCases {
4555
tc := tc
@@ -96,6 +106,14 @@ func TestNewLoadCommandSuccess(t *testing.T) {
96106
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
97107
},
98108
},
109+
{
110+
name: "with platform",
111+
args: []string{"--platform", "linux/amd64"},
112+
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
113+
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
114+
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
115+
},
116+
},
99117
}
100118
for _, tc := range testCases {
101119
tc := tc

cli/command/image/save.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"io"
66

7+
"github.com/containerd/platforms"
78
"github.com/docker/cli/cli"
89
"github.com/docker/cli/cli/command"
910
"github.com/docker/cli/cli/command/completion"
@@ -13,8 +14,9 @@ import (
1314
)
1415

1516
type saveOptions struct {
16-
images []string
17-
output string
17+
images []string
18+
output string
19+
platform string
1820
}
1921

2022
// NewSaveCommand creates a new `docker save` command
@@ -38,7 +40,10 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
3840
flags := cmd.Flags()
3941

4042
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
43+
flags.StringVar(&opts.platform, "platform", "", `Save only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
44+
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
4145

46+
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
4247
return cmd
4348
}
4449

@@ -52,7 +57,16 @@ func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
5257
return errors.Wrap(err, "failed to save image")
5358
}
5459

55-
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, image.SaveOptions{})
60+
var options image.SaveOptions
61+
if opts.platform != "" {
62+
p, err := platforms.Parse(opts.platform)
63+
if err != nil {
64+
return errors.Wrap(err, "invalid platform")
65+
}
66+
options.Platform = &p
67+
}
68+
69+
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options)
5670
if err != nil {
5771
return err
5872
}

cli/command/image/save_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/docker/cli/internal/test"
1010
"github.com/docker/docker/api/types/image"
11+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1112
"github.com/pkg/errors"
1213
"gotest.tools/v3/assert"
1314
is "gotest.tools/v3/assert/cmp"
@@ -51,6 +52,11 @@ func TestNewSaveCommandErrors(t *testing.T) {
5152
args: []string{"-o", "/dev/null", "arg1"},
5253
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
5354
},
55+
{
56+
name: "invalid platform",
57+
args: []string{"--platform", "<invalid>", "arg1"},
58+
expectedError: `invalid platform`,
59+
},
5460
}
5561
for _, tc := range testCases {
5662
tc := tc
@@ -95,6 +101,16 @@ func TestNewSaveCommandSuccess(t *testing.T) {
95101
return io.NopCloser(strings.NewReader("")), nil
96102
},
97103
},
104+
{
105+
args: []string{"--platform", "linux/amd64", "arg1"},
106+
isTerminal: false,
107+
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) {
108+
assert.Assert(t, is.Len(images, 1))
109+
assert.Check(t, is.Equal("arg1", images[0]))
110+
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
111+
return io.NopCloser(strings.NewReader("")), nil
112+
},
113+
},
98114
}
99115
for _, tc := range testCases {
100116
tc := tc
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
IMAGE CREATED CREATED BY SIZE COMMENT
2+
123456789012 Less than a second ago 0B
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Success

docs/reference/commandline/history.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Show the history of an image
1414
| `--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 |
1515
| `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format |
1616
| `--no-trunc` | `bool` | | Don't truncate output |
17+
| `--platform` | `string` | | Show history for the given platform. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
1718
| `-q`, `--quiet` | `bool` | | Only show image IDs |
1819

1920

docs/reference/commandline/image_history.md

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ Show the history of an image
99

1010
### Options
1111

12-
| Name | Type | Default | Description |
13-
|:----------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14-
| [`--format`](#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 |
15-
| `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format |
16-
| `--no-trunc` | `bool` | | Don't truncate output |
17-
| `-q`, `--quiet` | `bool` | | Only show image IDs |
12+
| Name | Type | Default | Description |
13+
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14+
| [`--format`](#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 |
15+
| `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format |
16+
| `--no-trunc` | `bool` | | Don't truncate output |
17+
| [`--platform`](#platform) | `string` | | Show history for the given platform. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
18+
| `-q`, `--quiet` | `bool` | | Only show image IDs |
1819

1920

2021
<!---MARKER_GEN_END-->
@@ -76,3 +77,54 @@ $ docker history --format "{{.ID}}: {{.CreatedSince}}" busybox
7677
f6e427c148a7: 4 weeks ago
7778
<missing>: 4 weeks ago
7879
```
80+
81+
### <a name="platform"></a> Show history for a specific platform (--platform)
82+
83+
The `--platform` option allows you to specify which platform variant to show
84+
history for if multiple platforms are present. By default, `docker history`
85+
shows the history for the daemon's native platform or if not present, the
86+
first available platform.
87+
88+
If the local image store has multiple platform variants of an image, the
89+
`--platform` option selects which variant to show the history for. An error
90+
is produced if the given platform is not present in the local image cache.
91+
92+
The platform option takes the `os[/arch[/variant]]` format; for example,
93+
`linux/amd64` or `linux/arm64/v8`. Architecture and variant are optional,
94+
and if omitted falls back to the daemon's defaults.
95+
96+
97+
The following example pulls the RISC-V variant of the `alpine:latest` image
98+
and shows its history.
99+
100+
101+
```console
102+
$ docker image pull --quiet --platform=linux/riscv64 alpine
103+
docker.io/library/alpine:latest
104+
105+
$ docker image history --platform=linux/s390x alpine
106+
IMAGE CREATED CREATED BY SIZE COMMENT
107+
beefdbd8a1da 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
108+
<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:ba2637314e600db5a… 8.46MB
109+
```
110+
111+
The following example attempts to show the history for a platform variant of
112+
`alpine:latest` that doesn't exist in the local image store, resulting in
113+
an error.
114+
115+
```console
116+
$ docker image ls --tree
117+
IMAGE ID DISK USAGE CONTENT SIZE IN USE
118+
alpine:latest beefdbd8a1da 10.6MB 3.37MB
119+
├─ linux/riscv64 80cde017a105 10.6MB 3.37MB
120+
├─ linux/amd64 33735bd63cf8 0B 0B
121+
├─ linux/arm/v6 50f635c8b04d 0B 0B
122+
├─ linux/arm/v7 f2f82d424957 0B 0B
123+
├─ linux/arm64/v8 9cee2b382fe2 0B 0B
124+
├─ linux/386 b3e87f642f5c 0B 0B
125+
├─ linux/ppc64le c7a6800e3dc5 0B 0B
126+
└─ linux/s390x 2b5b26e09ca2 0B 0B
127+
128+
$ docker image history --platform=linux/s390x alpine
129+
Error response from daemon: image with reference alpine:latest was found but does not match the specified platform: wanted linux/s390x
130+
```

0 commit comments

Comments
 (0)