client: support multiple platforms on save and load

We don't yet support this at the API level, so for now it returns
an error when trying to set multiple, but this makes sure that the
client types are already ready for this.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2024-10-28 18:03:54 +01:00
parent 84ad184fe5
commit 9c9eccfb23
7 changed files with 73 additions and 16 deletions

View File

@@ -248,6 +248,10 @@ func (ir *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter,
var platform *ocispec.Platform
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
}
if formPlatform := r.Form.Get("platform"); formPlatform != "" {
p, err := httputils.DecodePlatform(formPlatform)
if err != nil {
@@ -273,6 +277,10 @@ func (ir *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter
var platform *ocispec.Platform
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
}
if formPlatform := r.Form.Get("platform"); formPlatform != "" {
p, err := httputils.DecodePlatform(formPlatform)
if err != nil {

View File

@@ -98,12 +98,14 @@ type LoadOptions struct {
// Quiet suppresses progress output
Quiet bool
// Platform is a specific platform to load when the image is a multi-platform
Platform *ocispec.Platform
// Platforms selects the platforms to load if the image is a
// multi-platform image and has multiple variants.
Platforms []ocispec.Platform
}
// SaveOptions holds parameters to save images.
type SaveOptions struct {
// Platform is a specific platform to save if the image is a multi-platform image.
Platform *ocispec.Platform
// Platforms selects the platforms to save if the image is a
// multi-platform image and has multiple variants.
Platforms []ocispec.Platform
}

View File

@@ -22,16 +22,16 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, opts image.Lo
if opts.Quiet {
query.Set("quiet", "1")
}
if opts.Platform != nil {
if len(opts.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return image.LoadResponse{}, err
}
p, err := encodePlatform(opts.Platform)
p, err := encodePlatforms(opts.Platforms...)
if err != nil {
return image.LoadResponse{}, err
}
query.Set("platform", p)
query["platform"] = p
}
resp, err := cli.postRaw(ctx, "/images/load", query, input, http.Header{

View File

@@ -34,7 +34,7 @@ func TestImageLoad(t *testing.T) {
tests := []struct {
doc string
quiet bool
platform *ocispec.Platform
platforms []ocispec.Platform
responseContentType string
expectedResponseJSON bool
expectedQueryParams url.Values
@@ -59,7 +59,7 @@ func TestImageLoad(t *testing.T) {
},
{
doc: "json with platform",
platform: &ocispec.Platform{Architecture: "arm64", OS: "linux", Variant: "v8"},
platforms: []ocispec.Platform{{Architecture: "arm64", OS: "linux", Variant: "v8"}},
responseContentType: "application/json",
expectedResponseJSON: true,
expectedQueryParams: url.Values{
@@ -67,6 +67,19 @@ func TestImageLoad(t *testing.T) {
"quiet": {"0"},
},
},
{
doc: "json with multiple platforms",
platforms: []ocispec.Platform{
{Architecture: "arm64", OS: "linux", Variant: "v8"},
{Architecture: "amd64", OS: "linux"},
},
responseContentType: "application/json",
expectedResponseJSON: true,
expectedQueryParams: url.Values{
"platform": {`{"architecture":"arm64","os":"linux","variant":"v8"}`, `{"architecture":"amd64","os":"linux"}`},
"quiet": {"0"},
},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
@@ -85,8 +98,8 @@ func TestImageLoad(t *testing.T) {
input := bytes.NewReader([]byte(expectedInput))
imageLoadResponse, err := client.ImageLoad(context.Background(), input, image.LoadOptions{
Quiet: tc.quiet,
Platform: tc.platform,
Quiet: tc.quiet,
Platforms: tc.platforms,
})
assert.NilError(t, err)
assert.Check(t, is.Equal(imageLoadResponse.JSON, tc.expectedResponseJSON))

View File

@@ -15,16 +15,15 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, opts image.
"names": imageIDs,
}
if opts.Platform != nil {
if len(opts.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err
}
p, err := encodePlatform(opts.Platform)
p, err := encodePlatforms(opts.Platforms...)
if err != nil {
return nil, err
}
query.Set("platform", p)
query["platform"] = p
}
resp, err := cli.get(ctx, "/images/get", query, nil)

View File

@@ -42,13 +42,26 @@ func TestImageSave(t *testing.T) {
{
doc: "platform",
options: image.SaveOptions{
Platform: &ocispec.Platform{Architecture: "arm64", OS: "linux", Variant: "v8"},
Platforms: []ocispec.Platform{{Architecture: "arm64", OS: "linux", Variant: "v8"}},
},
expectedQueryParams: url.Values{
"names": {"image_id1", "image_id2"},
"platform": {`{"architecture":"arm64","os":"linux","variant":"v8"}`},
},
},
{
doc: "multiple platforms",
options: image.SaveOptions{
Platforms: []ocispec.Platform{
{Architecture: "arm64", OS: "linux", Variant: "v8"},
{Architecture: "amd64", OS: "linux"},
},
},
expectedQueryParams: url.Values{
"names": {"image_id1", "image_id2"},
"platform": {`{"architecture":"arm64","os":"linux","variant":"v8"}`, `{"architecture":"amd64","os":"linux"}`},
},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {

View File

@@ -17,6 +17,7 @@ import (
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/integration/internal/build"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/internal/testutils"
@@ -212,6 +213,27 @@ func TestSaveOCI(t *testing.T) {
}
}
// TODO(thaJeztah): this test currently only checks invalid cases; update this test to use a table-test and test both valid and invalid platform options.
func TestSavePlatform(t *testing.T) {
ctx := setupTest(t)
t.Parallel()
client := testEnv.APIClient()
const repoName = "busybox:latest"
_, _, err := client.ImageInspectWithRaw(ctx, repoName)
assert.NilError(t, err)
_, err = client.ImageSave(ctx, []string{repoName}, image.SaveOptions{
Platforms: []ocispec.Platform{
{Architecture: "amd64", OS: "linux"},
{Architecture: "arm64", OS: "linux", Variant: "v8"},
},
})
assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
assert.Check(t, is.Error(err, "Error response from daemon: multiple platform parameters not supported"))
}
func TestSaveRepoWithMultipleImages(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()