Browse code

client: add utilities to encode platforms

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2024/10/31 21:46:57
Showing 5 changed files
... ...
@@ -17,11 +17,11 @@ func (cli *Client) ImageHistory(ctx context.Context, imageID string, opts image.
17 17
 			return nil, err
18 18
 		}
19 19
 
20
-		p, err := json.Marshal(*opts.Platform)
20
+		p, err := encodePlatform(opts.Platform)
21 21
 		if err != nil {
22 22
 			return nil, fmt.Errorf("invalid platform: %v", err)
23 23
 		}
24
-		query.Set("platform", string(p))
24
+		query.Set("platform", p)
25 25
 	}
26 26
 
27 27
 	var history []image.HistoryResponseItem
... ...
@@ -2,7 +2,6 @@ package client // import "github.com/docker/docker/client"
2 2
 
3 3
 import (
4 4
 	"context"
5
-	"encoding/json"
6 5
 	"io"
7 6
 	"net/http"
8 7
 	"net/url"
... ...
@@ -28,11 +27,11 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, opts image.Lo
28 28
 			return image.LoadResponse{}, err
29 29
 		}
30 30
 
31
-		p, err := json.Marshal(*opts.Platform)
31
+		p, err := encodePlatform(opts.Platform)
32 32
 		if err != nil {
33 33
 			return image.LoadResponse{}, err
34 34
 		}
35
-		query.Set("platform", string(p))
35
+		query.Set("platform", p)
36 36
 	}
37 37
 
38 38
 	resp, err := cli.postRaw(ctx, "/images/load", query, input, http.Header{
... ...
@@ -2,8 +2,6 @@ package client // import "github.com/docker/docker/client"
2 2
 
3 3
 import (
4 4
 	"context"
5
-	"encoding/json"
6
-	"fmt"
7 5
 	"io"
8 6
 	"net/url"
9 7
 
... ...
@@ -22,11 +20,11 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, opts image.
22 22
 			return nil, err
23 23
 		}
24 24
 
25
-		p, err := json.Marshal(*opts.Platform)
25
+		p, err := encodePlatform(opts.Platform)
26 26
 		if err != nil {
27
-			return nil, fmt.Errorf("invalid platform: %v", err)
27
+			return nil, err
28 28
 		}
29
-		query.Set("platform", string(p))
29
+		query.Set("platform", p)
30 30
 	}
31 31
 
32 32
 	resp, err := cli.get(ctx, "/images/get", query, nil)
... ...
@@ -1,10 +1,14 @@
1 1
 package client // import "github.com/docker/docker/client"
2 2
 
3 3
 import (
4
+	"encoding/json"
5
+	"fmt"
4 6
 	"net/url"
5 7
 	"regexp"
6 8
 
7 9
 	"github.com/docker/docker/api/types/filters"
10
+	"github.com/docker/docker/errdefs"
11
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
8 12
 )
9 13
 
10 14
 var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)
... ...
@@ -32,3 +36,43 @@ func getFiltersQuery(f filters.Args) (url.Values, error) {
32 32
 	}
33 33
 	return query, nil
34 34
 }
35
+
36
+// encodePlatforms marshals the given platform(s) to JSON format, to
37
+// be used for query-parameters for filtering / selecting platforms.
38
+func encodePlatforms(platform ...ocispec.Platform) ([]string, error) {
39
+	if len(platform) == 0 {
40
+		return []string{}, nil
41
+	}
42
+	if len(platform) == 1 {
43
+		p, err := encodePlatform(&platform[0])
44
+		if err != nil {
45
+			return nil, err
46
+		}
47
+		return []string{p}, nil
48
+	}
49
+
50
+	seen := make(map[string]struct{}, len(platform))
51
+	out := make([]string, 0, len(platform))
52
+	for i := range platform {
53
+		p, err := encodePlatform(&platform[i])
54
+		if err != nil {
55
+			return nil, err
56
+		}
57
+		if _, ok := seen[p]; !ok {
58
+			out = append(out, p)
59
+			seen[p] = struct{}{}
60
+		}
61
+	}
62
+	return out, nil
63
+}
64
+
65
+// encodePlatforms marshals the given platform to JSON format, to
66
+// be used for query-parameters for filtering / selecting platforms. It
67
+// is used as a helper for encodePlatforms,
68
+func encodePlatform(platform *ocispec.Platform) (string, error) {
69
+	p, err := json.Marshal(platform)
70
+	if err != nil {
71
+		return "", errdefs.InvalidParameter(fmt.Errorf("invalid platform: %v", err))
72
+	}
73
+	return string(p), nil
74
+}
35 75
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package client
1
+
2
+import (
3
+	"testing"
4
+
5
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
6
+	"gotest.tools/v3/assert"
7
+)
8
+
9
+func TestEncodePlatforms(t *testing.T) {
10
+	tests := []struct {
11
+		doc       string
12
+		platforms []ocispec.Platform
13
+		expected  []string
14
+	}{
15
+		{
16
+			doc: "single platform",
17
+			platforms: []ocispec.Platform{
18
+				{Architecture: "arm64", OS: "windows", Variant: "v8", OSVersion: "99.99.99"},
19
+			},
20
+			expected: []string{
21
+				`{"architecture":"arm64","os":"windows","os.version":"99.99.99","variant":"v8"}`,
22
+			},
23
+		},
24
+		{
25
+			doc: "multiple platforms",
26
+			platforms: []ocispec.Platform{
27
+				{Architecture: "arm64", OS: "linux", Variant: "v8"},
28
+				{Architecture: "arm64", OS: "windows", Variant: "v8", OSVersion: "99.99.99"},
29
+			},
30
+			expected: []string{
31
+				`{"architecture":"arm64","os":"linux","variant":"v8"}`,
32
+				`{"architecture":"arm64","os":"windows","os.version":"99.99.99","variant":"v8"}`,
33
+			},
34
+		},
35
+		{
36
+			doc: "multiple platforms with duplicates",
37
+			platforms: []ocispec.Platform{
38
+				{Architecture: "arm64", OS: "linux", Variant: "v8"},
39
+				{Architecture: "arm64", OS: "windows", Variant: "v8", OSVersion: "99.99.99"},
40
+				{Architecture: "arm64", OS: "linux", Variant: "v8"},
41
+			},
42
+			expected: []string{
43
+				`{"architecture":"arm64","os":"linux","variant":"v8"}`,
44
+				`{"architecture":"arm64","os":"windows","os.version":"99.99.99","variant":"v8"}`,
45
+			},
46
+		},
47
+	}
48
+	for _, tc := range tests {
49
+		t.Run(tc.doc, func(t *testing.T) {
50
+			out, err := encodePlatforms(tc.platforms...)
51
+			assert.NilError(t, err)
52
+			assert.DeepEqual(t, out, tc.expected)
53
+		})
54
+	}
55
+}