Browse code

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>

Sebastiaan van Stijn authored on 2024/10/29 02:03:54
Showing 7 changed files
... ...
@@ -248,6 +248,10 @@ func (ir *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter,
248 248
 
249 249
 	var platform *ocispec.Platform
250 250
 	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
251
+		if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
252
+			// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
253
+			return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
254
+		}
251 255
 		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
252 256
 			p, err := httputils.DecodePlatform(formPlatform)
253 257
 			if err != nil {
... ...
@@ -273,6 +277,10 @@ func (ir *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter
273 273
 
274 274
 	var platform *ocispec.Platform
275 275
 	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
276
+		if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
277
+			// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
278
+			return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
279
+		}
276 280
 		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
277 281
 			p, err := httputils.DecodePlatform(formPlatform)
278 282
 			if err != nil {
... ...
@@ -98,12 +98,14 @@ type LoadOptions struct {
98 98
 	// Quiet suppresses progress output
99 99
 	Quiet bool
100 100
 
101
-	// Platform is a specific platform to load when the image is a multi-platform
102
-	Platform *ocispec.Platform
101
+	// Platforms selects the platforms to load if the image is a
102
+	// multi-platform image and has multiple variants.
103
+	Platforms []ocispec.Platform
103 104
 }
104 105
 
105 106
 // SaveOptions holds parameters to save images.
106 107
 type SaveOptions struct {
107
-	// Platform is a specific platform to save if the image is a multi-platform image.
108
-	Platform *ocispec.Platform
108
+	// Platforms selects the platforms to save if the image is a
109
+	// multi-platform image and has multiple variants.
110
+	Platforms []ocispec.Platform
109 111
 }
... ...
@@ -22,16 +22,16 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, opts image.Lo
22 22
 	if opts.Quiet {
23 23
 		query.Set("quiet", "1")
24 24
 	}
25
-	if opts.Platform != nil {
25
+	if len(opts.Platforms) > 0 {
26 26
 		if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
27 27
 			return image.LoadResponse{}, err
28 28
 		}
29 29
 
30
-		p, err := encodePlatform(opts.Platform)
30
+		p, err := encodePlatforms(opts.Platforms...)
31 31
 		if err != nil {
32 32
 			return image.LoadResponse{}, err
33 33
 		}
34
-		query.Set("platform", p)
34
+		query["platform"] = p
35 35
 	}
36 36
 
37 37
 	resp, err := cli.postRaw(ctx, "/images/load", query, input, http.Header{
... ...
@@ -34,7 +34,7 @@ func TestImageLoad(t *testing.T) {
34 34
 	tests := []struct {
35 35
 		doc                  string
36 36
 		quiet                bool
37
-		platform             *ocispec.Platform
37
+		platforms            []ocispec.Platform
38 38
 		responseContentType  string
39 39
 		expectedResponseJSON bool
40 40
 		expectedQueryParams  url.Values
... ...
@@ -59,7 +59,7 @@ func TestImageLoad(t *testing.T) {
59 59
 		},
60 60
 		{
61 61
 			doc:                  "json with platform",
62
-			platform:             &ocispec.Platform{Architecture: "arm64", OS: "linux", Variant: "v8"},
62
+			platforms:            []ocispec.Platform{{Architecture: "arm64", OS: "linux", Variant: "v8"}},
63 63
 			responseContentType:  "application/json",
64 64
 			expectedResponseJSON: true,
65 65
 			expectedQueryParams: url.Values{
... ...
@@ -67,6 +67,19 @@ func TestImageLoad(t *testing.T) {
67 67
 				"quiet":    {"0"},
68 68
 			},
69 69
 		},
70
+		{
71
+			doc: "json with multiple platforms",
72
+			platforms: []ocispec.Platform{
73
+				{Architecture: "arm64", OS: "linux", Variant: "v8"},
74
+				{Architecture: "amd64", OS: "linux"},
75
+			},
76
+			responseContentType:  "application/json",
77
+			expectedResponseJSON: true,
78
+			expectedQueryParams: url.Values{
79
+				"platform": {`{"architecture":"arm64","os":"linux","variant":"v8"}`, `{"architecture":"amd64","os":"linux"}`},
80
+				"quiet":    {"0"},
81
+			},
82
+		},
70 83
 	}
71 84
 	for _, tc := range tests {
72 85
 		t.Run(tc.doc, func(t *testing.T) {
... ...
@@ -85,8 +98,8 @@ func TestImageLoad(t *testing.T) {
85 85
 
86 86
 			input := bytes.NewReader([]byte(expectedInput))
87 87
 			imageLoadResponse, err := client.ImageLoad(context.Background(), input, image.LoadOptions{
88
-				Quiet:    tc.quiet,
89
-				Platform: tc.platform,
88
+				Quiet:     tc.quiet,
89
+				Platforms: tc.platforms,
90 90
 			})
91 91
 			assert.NilError(t, err)
92 92
 			assert.Check(t, is.Equal(imageLoadResponse.JSON, tc.expectedResponseJSON))
... ...
@@ -15,16 +15,15 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, opts image.
15 15
 		"names": imageIDs,
16 16
 	}
17 17
 
18
-	if opts.Platform != nil {
18
+	if len(opts.Platforms) > 0 {
19 19
 		if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
20 20
 			return nil, err
21 21
 		}
22
-
23
-		p, err := encodePlatform(opts.Platform)
22
+		p, err := encodePlatforms(opts.Platforms...)
24 23
 		if err != nil {
25 24
 			return nil, err
26 25
 		}
27
-		query.Set("platform", p)
26
+		query["platform"] = p
28 27
 	}
29 28
 
30 29
 	resp, err := cli.get(ctx, "/images/get", query, nil)
... ...
@@ -42,13 +42,26 @@ func TestImageSave(t *testing.T) {
42 42
 		{
43 43
 			doc: "platform",
44 44
 			options: image.SaveOptions{
45
-				Platform: &ocispec.Platform{Architecture: "arm64", OS: "linux", Variant: "v8"},
45
+				Platforms: []ocispec.Platform{{Architecture: "arm64", OS: "linux", Variant: "v8"}},
46 46
 			},
47 47
 			expectedQueryParams: url.Values{
48 48
 				"names":    {"image_id1", "image_id2"},
49 49
 				"platform": {`{"architecture":"arm64","os":"linux","variant":"v8"}`},
50 50
 			},
51 51
 		},
52
+		{
53
+			doc: "multiple platforms",
54
+			options: image.SaveOptions{
55
+				Platforms: []ocispec.Platform{
56
+					{Architecture: "arm64", OS: "linux", Variant: "v8"},
57
+					{Architecture: "amd64", OS: "linux"},
58
+				},
59
+			},
60
+			expectedQueryParams: url.Values{
61
+				"names":    {"image_id1", "image_id2"},
62
+				"platform": {`{"architecture":"arm64","os":"linux","variant":"v8"}`, `{"architecture":"amd64","os":"linux"}`},
63
+			},
64
+		},
52 65
 	}
53 66
 	for _, tc := range tests {
54 67
 		t.Run(tc.doc, func(t *testing.T) {
... ...
@@ -17,6 +17,7 @@ import (
17 17
 	containertypes "github.com/docker/docker/api/types/container"
18 18
 	"github.com/docker/docker/api/types/image"
19 19
 	"github.com/docker/docker/api/types/versions"
20
+	"github.com/docker/docker/errdefs"
20 21
 	"github.com/docker/docker/integration/internal/build"
21 22
 	"github.com/docker/docker/integration/internal/container"
22 23
 	"github.com/docker/docker/internal/testutils"
... ...
@@ -212,6 +213,27 @@ func TestSaveOCI(t *testing.T) {
212 212
 	}
213 213
 }
214 214
 
215
+// 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.
216
+func TestSavePlatform(t *testing.T) {
217
+	ctx := setupTest(t)
218
+
219
+	t.Parallel()
220
+	client := testEnv.APIClient()
221
+
222
+	const repoName = "busybox:latest"
223
+	_, _, err := client.ImageInspectWithRaw(ctx, repoName)
224
+	assert.NilError(t, err)
225
+
226
+	_, err = client.ImageSave(ctx, []string{repoName}, image.SaveOptions{
227
+		Platforms: []ocispec.Platform{
228
+			{Architecture: "amd64", OS: "linux"},
229
+			{Architecture: "arm64", OS: "linux", Variant: "v8"},
230
+		},
231
+	})
232
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
233
+	assert.Check(t, is.Error(err, "Error response from daemon: multiple platform parameters not supported"))
234
+}
235
+
215 236
 func TestSaveRepoWithMultipleImages(t *testing.T) {
216 237
 	ctx := setupTest(t)
217 238
 	client := testEnv.APIClient()