Merge pull request #5331 from vvoland/c8d-saveload-platform

c8d: Add `--platform` flag to history, save and load
This commit is contained in:
Sebastiaan van Stijn 2024-10-10 17:33:08 +02:00 committed by GitHub
commit f483aacd6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 262 additions and 30 deletions

View File

@ -3,17 +3,20 @@ package image
import (
"context"
"github.com/containerd/platforms"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type historyOptions struct {
image string
image string
platform string
human bool
quiet bool
@ -45,12 +48,24 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show image IDs")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp)
flags.StringVar(&opts.platform, "platform", "", `Show history for the given platform. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd
}
func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error {
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, image.HistoryOptions{})
var options image.HistoryOptions
if opts.platform != "" {
p, err := platforms.Parse(opts.platform)
if err != nil {
return errors.Wrap(err, "invalid platform")
}
options.Platform = &p
}
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, options)
if err != nil {
return err
}

View File

@ -8,8 +8,10 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -33,6 +35,11 @@ func TestNewHistoryCommandErrors(t *testing.T) {
return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong")
},
},
{
name: "invalid platform",
args: []string{"--platform", "<invalid>", "arg1"},
expectedError: `invalid platform`,
},
}
for _, tc := range testCases {
tc := tc
@ -89,6 +96,17 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
}}, nil
},
},
{
name: "platform",
args: []string{"--platform", "linux/amd64", "image:tag"},
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) {
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
return []image.HistoryResponseItem{{
ID: "1234567890123456789",
Created: time.Now().Unix(),
}}, nil
},
},
}
for _, tc := range testCases {
tc := tc

View File

@ -4,6 +4,7 @@ import (
"context"
"io"
"github.com/containerd/platforms"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
@ -15,8 +16,9 @@ import (
)
type loadOptions struct {
input string
quiet bool
input string
quiet bool
platform string
}
// NewLoadCommand creates a new `docker load` command
@ -40,7 +42,10 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
flags.StringVar(&opts.platform, "platform", "", `Load only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd
}
@ -63,12 +68,20 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
return errors.Errorf("requested load from stdin, but stdin is empty")
}
var loadOpts image.LoadOptions
var options image.LoadOptions
if opts.quiet || !dockerCli.Out().IsTerminal() {
loadOpts.Quiet = true
options.Quiet = true
}
response, err := dockerCli.Client().ImageLoad(ctx, input, loadOpts)
if opts.platform != "" {
p, err := platforms.Parse(opts.platform)
if err != nil {
return errors.Wrap(err, "invalid platform")
}
options.Platform = &p
}
response, err := dockerCli.Client().ImageLoad(ctx, input, options)
if err != nil {
return err
}

View File

@ -8,8 +8,10 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -40,6 +42,14 @@ func TestNewLoadCommandErrors(t *testing.T) {
return image.LoadResponse{}, errors.Errorf("something went wrong")
},
},
{
name: "invalid platform",
args: []string{"--platform", "<invalid>"},
expectedError: `invalid platform`,
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
return image.LoadResponse{}, nil
},
},
}
for _, tc := range testCases {
tc := tc
@ -96,6 +106,14 @@ func TestNewLoadCommandSuccess(t *testing.T) {
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
},
},
{
name: "with platform",
args: []string{"--platform", "linux/amd64"},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
},
},
}
for _, tc := range testCases {
tc := tc

View File

@ -4,6 +4,7 @@ import (
"context"
"io"
"github.com/containerd/platforms"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
@ -13,8 +14,9 @@ import (
)
type saveOptions struct {
images []string
output string
images []string
output string
platform string
}
// NewSaveCommand creates a new `docker save` command
@ -38,7 +40,10 @@ 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.SetAnnotation("platform", "version", []string{"1.48"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd
}
@ -52,7 +57,16 @@ func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
return errors.Wrap(err, "failed to save image")
}
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, image.SaveOptions{})
var options image.SaveOptions
if opts.platform != "" {
p, err := platforms.Parse(opts.platform)
if err != nil {
return errors.Wrap(err, "invalid platform")
}
options.Platform = &p
}
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options)
if err != nil {
return err
}

View File

@ -8,6 +8,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -51,6 +52,11 @@ func TestNewSaveCommandErrors(t *testing.T) {
args: []string{"-o", "/dev/null", "arg1"},
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
},
{
name: "invalid platform",
args: []string{"--platform", "<invalid>", "arg1"},
expectedError: `invalid platform`,
},
}
for _, tc := range testCases {
tc := tc
@ -95,6 +101,16 @@ func TestNewSaveCommandSuccess(t *testing.T) {
return io.NopCloser(strings.NewReader("")), nil
},
},
{
args: []string{"--platform", "linux/amd64", "arg1"},
isTerminal: false,
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 1))
assert.Check(t, is.Equal("arg1", images[0]))
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
return io.NopCloser(strings.NewReader("")), nil
},
},
}
for _, tc := range testCases {
tc := tc

View File

@ -0,0 +1,2 @@
IMAGE CREATED CREATED BY SIZE COMMENT
123456789012 Less than a second ago 0B

View File

@ -0,0 +1 @@
Success

View File

@ -14,6 +14,7 @@ Show the history of an image
| `--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 |
| `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format |
| `--no-trunc` | `bool` | | Don't truncate output |
| `--platform` | `string` | | Show history for the given platform. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
| `-q`, `--quiet` | `bool` | | Only show image IDs |

View File

@ -9,12 +9,13 @@ Show the history of an image
### Options
| Name | Type | Default | Description |
|:----------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`--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 |
| `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format |
| `--no-trunc` | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`--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 |
| `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format |
| `--no-trunc` | `bool` | | Don't truncate output |
| [`--platform`](#platform) | `string` | | Show history for the given platform. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
<!---MARKER_GEN_END-->
@ -76,3 +77,54 @@ $ docker history --format "{{.ID}}: {{.CreatedSince}}" busybox
f6e427c148a7: 4 weeks ago
<missing>: 4 weeks ago
```
### <a name="platform"></a> Show history for a specific platform (--platform)
The `--platform` option allows you to specify which platform variant to show
history for if multiple platforms are present. By default, `docker history`
shows the history for the daemon's native platform or if not present, the
first available platform.
If the local image store has multiple platform variants of an image, the
`--platform` option selects which variant to show the history for. An error
is produced if the given platform is not present in the local image cache.
The platform option takes the `os[/arch[/variant]]` format; for example,
`linux/amd64` or `linux/arm64/v8`. Architecture and variant are optional,
and if omitted falls back to the daemon's defaults.
The following example pulls the RISC-V variant of the `alpine:latest` image
and shows its history.
```console
$ docker image pull --quiet --platform=linux/riscv64 alpine
docker.io/library/alpine:latest
$ docker image history --platform=linux/s390x alpine
IMAGE CREATED CREATED BY SIZE COMMENT
beefdbd8a1da 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:ba2637314e600db5a… 8.46MB
```
The following example attempts to show the history for a platform variant of
`alpine:latest` that doesn't exist in the local image store, resulting in
an error.
```console
$ docker image ls --tree
IMAGE ID DISK USAGE CONTENT SIZE IN USE
alpine:latest beefdbd8a1da 10.6MB 3.37MB
├─ linux/riscv64 80cde017a105 10.6MB 3.37MB
├─ linux/amd64 33735bd63cf8 0B 0B
├─ linux/arm/v6 50f635c8b04d 0B 0B
├─ linux/arm/v7 f2f82d424957 0B 0B
├─ linux/arm64/v8 9cee2b382fe2 0B 0B
├─ linux/386 b3e87f642f5c 0B 0B
├─ linux/ppc64le c7a6800e3dc5 0B 0B
└─ linux/s390x 2b5b26e09ca2 0B 0B
$ docker image history --platform=linux/s390x alpine
Error response from daemon: image with reference alpine:latest was found but does not match the specified platform: wanted linux/s390x
```

View File

@ -9,10 +9,11 @@ Load an image from a tar archive or STDIN
### Options
| Name | Type | Default | Description |
|:------------------------------------|:---------|:--------|:---------------------------------------------|
| [`-i`](#input), [`--input`](#input) | `string` | | Read from tar archive file, instead of STDIN |
| `-q`, `--quiet` | `bool` | | Suppress the load output |
| Name | Type | Default | Description |
|:------------------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------|
| [`-i`](#input), [`--input`](#input) | `string` | | Read from tar archive file, instead of STDIN |
| [`--platform`](#platform) | `string` | | Load only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
| `-q`, `--quiet` | `bool` | | Suppress the load output |
<!---MARKER_GEN_END-->
@ -58,3 +59,32 @@ fedora 20 58394af37342 7 weeks ago
fedora heisenbug 58394af37342 7 weeks ago 385.5 MB
fedora latest 58394af37342 7 weeks ago 385.5 MB
```
### <a name="platform"></a> Load a specific platform (--platform)
The `--platform` option allows you to specify which platform variant of the
image to load. By default, `docker load` loads all platform variants that
are present in the archive. Use the `--platform` option to specify which
platform variant of the image to load. An error is produced if the given
platform is not present in the archive.
The platform option takes the `os[/arch[/variant]]` format; for example,
`linux/amd64` or `linux/arm64/v8`. Architecture and variant are optional,
and default to the daemon's native architecture if omitted.
The following example loads the `linux/amd64` variant of an `alpine` image
from an archive that contains multiple platform variants.
```console
$ docker image load -i image.tar --platform=linux/amd64
Loaded image: alpine:latest
```
The following example attempts to load a `linux/ppc64le` image from an
archive, but the given platform is not present in the archive;
```console
$ docker image load -i image.tar --platform=linux/ppc64le
requested platform (linux/ppc64le) not found: image might be filtered out
```

View File

@ -9,9 +9,10 @@ Save one or more images to a tar archive (streamed to STDOUT by default)
### Options
| Name | Type | Default | Description |
|:-----------------|:---------|:--------|:-----------------------------------|
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------|
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
| [`--platform`](#platform) | `string` | | Save only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
<!---MARKER_GEN_END-->
@ -59,3 +60,52 @@ You can even cherry-pick particular tags of an image repository.
```console
$ docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy
```
### <a name="platform"></a> Save a specific platform (--platform)
The `--platform` option allows you to specify which platform variant of the
image to save. By default, `docker save` saves all platform variants that
are present in the daemon's image store. Use the `--platform` option
to specify which platform variant of the image to save. An error is produced
if the given platform is not present in the local image store.
The platform option takes the `os[/arch[/variant]]` format; for example,
`linux/amd64` or `linux/arm64/v8`. Architecture and variant are optional,
and default to the daemon's native architecture if omitted.
The following example pulls the RISC-V variant of the `alpine:latest` image
and saves it to a tar archive.
```console
$ docker pull --platform=linux/riscv64 alpine:latest
latest: Pulling from library/alpine
8c4a05189a5f: Download complete
Digest: sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest
$ docker image save --platform=linux/riscv64 -o alpine-riscv.tar alpine:latest
$ ls -lh image.tar
-rw------- 1 thajeztah staff 3.9M Oct 7 11:06 alpine-riscv.tar
```
The following example attempts to save a platform variant of `alpine:latest`
that doesn't exist in the local image store, resulting in an error.
```console
$ docker image ls --tree
IMAGE ID DISK USAGE CONTENT SIZE IN USE
alpine:latest beefdbd8a1da 10.6MB 3.37MB
├─ linux/riscv64 80cde017a105 10.6MB 3.37MB
├─ linux/amd64 33735bd63cf8 0B 0B
├─ linux/arm/v6 50f635c8b04d 0B 0B
├─ linux/arm/v7 f2f82d424957 0B 0B
├─ linux/arm64/v8 9cee2b382fe2 0B 0B
├─ linux/386 b3e87f642f5c 0B 0B
├─ linux/ppc64le c7a6800e3dc5 0B 0B
└─ linux/s390x 2b5b26e09ca2 0B 0B
$ docker image save --platform=linux/s390x -o alpine-s390x.tar alpine:latest
Error response from daemon: no suitable export target found for platform linux/s390x
```

View File

@ -9,10 +9,11 @@ Load an image from a tar archive or STDIN
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:---------------------------------------------|
| `-i`, `--input` | `string` | | Read from tar archive file, instead of STDIN |
| `-q`, `--quiet` | `bool` | | Suppress the load output |
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-----------------------------------------------------------------------------------------------|
| `-i`, `--input` | `string` | | Read from tar archive file, instead of STDIN |
| `--platform` | `string` | | Load only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
| `-q`, `--quiet` | `bool` | | Suppress the load output |
<!---MARKER_GEN_END-->

View File

@ -9,9 +9,10 @@ Save one or more images to a tar archive (streamed to STDOUT by default)
### Options
| Name | Type | Default | Description |
|:-----------------|:---------|:--------|:-----------------------------------|
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
| Name | Type | Default | Description |
|:-----------------|:---------|:--------|:-----------------------------------------------------------------------------------------------|
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
| `--platform` | `string` | | Save only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) |
<!---MARKER_GEN_END-->