Browse code

image/history: Support `Platform` parameter

Add `Platform` parameter that allows to select a specific platform to
show the history for.

This is a breaking change to the Go client as it changes the signature
of `ImageHistory`.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

Paweł Gronowski authored on 2024/08/09 00:56:27
Showing 13 changed files
... ...
@@ -23,7 +23,7 @@ type Backend interface {
23 23
 
24 24
 type imageBackend interface {
25 25
 	ImageDelete(ctx context.Context, imageRef string, force, prune bool) ([]image.DeleteResponse, error)
26
-	ImageHistory(ctx context.Context, imageName string) ([]*image.HistoryResponseItem, error)
26
+	ImageHistory(ctx context.Context, imageName string, platform *ocispec.Platform) ([]*image.HistoryResponseItem, error)
27 27
 	Images(ctx context.Context, opts image.ListOptions) ([]*image.Summary, error)
28 28
 	GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*dockerimage.Image, error)
29 29
 	ImageInspect(ctx context.Context, refOrID string, options backend.ImageInspectOpts) (*image.InspectResponse, error)
... ...
@@ -398,7 +398,21 @@ func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter,
398 398
 }
399 399
 
400 400
 func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
401
-	history, err := ir.backend.ImageHistory(ctx, vars["name"])
401
+	if err := httputils.ParseForm(r); err != nil {
402
+		return err
403
+	}
404
+
405
+	var platform *ocispec.Platform
406
+	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
407
+		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
408
+			p, err := httputils.DecodePlatform(formPlatform)
409
+			if err != nil {
410
+				return err
411
+			}
412
+			platform = p
413
+		}
414
+	}
415
+	history, err := ir.backend.ImageHistory(ctx, vars["name"], platform)
402 416
 	if err != nil {
403 417
 		return err
404 418
 	}
... ...
@@ -9202,6 +9202,15 @@ paths:
9202 9202
           description: "Image name or ID"
9203 9203
           type: "string"
9204 9204
           required: true
9205
+        - name: "platform"
9206
+          type: "string"
9207
+          in: "query"
9208
+          description: |
9209
+            JSON encoded OCI platform describing platform to show the history for.
9210
+            If not provided, the host platform will be used. If it's not
9211
+            available, any present platform will be picked.
9212
+
9213
+            Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}`
9205 9214
       tags: ["Image"]
9206 9215
   /images/{name}/push:
9207 9216
     post:
... ...
@@ -86,3 +86,9 @@ type RemoveOptions struct {
86 86
 	Force         bool
87 87
 	PruneChildren bool
88 88
 }
89
+
90
+// HistoryOptions holds parameters to get image history.
91
+type HistoryOptions struct {
92
+	// Platform from the manifest list to use for history.
93
+	Platform *ocispec.Platform
94
+}
... ...
@@ -3,15 +3,29 @@ package client // import "github.com/docker/docker/client"
3 3
 import (
4 4
 	"context"
5 5
 	"encoding/json"
6
+	"fmt"
6 7
 	"net/url"
7 8
 
8 9
 	"github.com/docker/docker/api/types/image"
9 10
 )
10 11
 
11 12
 // ImageHistory returns the changes in an image in history format.
12
-func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) {
13
+func (cli *Client) ImageHistory(ctx context.Context, imageID string, opts image.HistoryOptions) ([]image.HistoryResponseItem, error) {
14
+	values := url.Values{}
15
+	if opts.Platform != nil {
16
+		if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
17
+			return nil, err
18
+		}
19
+
20
+		p, err := json.Marshal(*opts.Platform)
21
+		if err != nil {
22
+			return nil, fmt.Errorf("invalid platform: %v", err)
23
+		}
24
+		values.Set("platform", string(p))
25
+	}
26
+
13 27
 	var history []image.HistoryResponseItem
14
-	serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil)
28
+	serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", values, nil)
15 29
 	defer ensureReaderClosed(serverResp)
16 30
 	if err != nil {
17 31
 		return history, err
... ...
@@ -20,7 +20,7 @@ func TestImageHistoryError(t *testing.T) {
20 20
 	client := &Client{
21 21
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
22 22
 	}
23
-	_, err := client.ImageHistory(context.Background(), "nothing")
23
+	_, err := client.ImageHistory(context.Background(), "nothing", image.HistoryOptions{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25 25
 }
26 26
 
... ...
@@ -51,7 +51,7 @@ func TestImageHistory(t *testing.T) {
51 51
 			}, nil
52 52
 		}),
53 53
 	}
54
-	imageHistories, err := client.ImageHistory(context.Background(), "image_id")
54
+	imageHistories, err := client.ImageHistory(context.Background(), "image_id", image.HistoryOptions{})
55 55
 	if err != nil {
56 56
 		t.Fatal(err)
57 57
 	}
... ...
@@ -91,7 +91,7 @@ type ImageAPIClient interface {
91 91
 	BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
92 92
 	BuildCancel(ctx context.Context, id string) error
93 93
 	ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
94
-	ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error)
94
+	ImageHistory(ctx context.Context, image string, opts image.HistoryOptions) ([]image.HistoryResponseItem, error)
95 95
 	ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
96 96
 	ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error)
97 97
 	ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error)
... ...
@@ -18,15 +18,14 @@ import (
18 18
 
19 19
 // ImageHistory returns a slice of HistoryResponseItem structures for the
20 20
 // specified image name by walking the image lineage.
21
-func (i *ImageService) ImageHistory(ctx context.Context, name string) ([]*imagetype.HistoryResponseItem, error) {
21
+func (i *ImageService) ImageHistory(ctx context.Context, name string, platform *ocispec.Platform) ([]*imagetype.HistoryResponseItem, error) {
22 22
 	start := time.Now()
23 23
 	img, err := i.resolveImage(ctx, name)
24 24
 	if err != nil {
25 25
 		return nil, err
26 26
 	}
27 27
 
28
-	// TODO: pass platform in from the CLI
29
-	pm := matchAllWithPreference(platforms.Default())
28
+	pm := i.matchRequestedOrDefault(platforms.Only, platform)
30 29
 
31 30
 	im, err := i.getBestPresentImageManifest(ctx, img, pm)
32 31
 	if err != nil {
... ...
@@ -40,7 +40,7 @@ type ImageService interface {
40 40
 	ImportImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, msg string, layerReader io.Reader, changes []string) (image.ID, error)
41 41
 	TagImage(ctx context.Context, imageID image.ID, newTag reference.Named) error
42 42
 	GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*image.Image, error)
43
-	ImageHistory(ctx context.Context, name string) ([]*imagetype.HistoryResponseItem, error)
43
+	ImageHistory(ctx context.Context, name string, platform *ocispec.Platform) ([]*imagetype.HistoryResponseItem, error)
44 44
 	CommitImage(ctx context.Context, c backend.CommitConfig) (image.ID, error)
45 45
 	SquashImage(id, parent string) (string, error)
46 46
 	ImageInspect(ctx context.Context, refOrID string, opts backend.ImageInspectOpts) (*imagetype.InspectResponse, error)
... ...
@@ -9,13 +9,14 @@ import (
9 9
 	"github.com/docker/docker/api/types/backend"
10 10
 	"github.com/docker/docker/api/types/image"
11 11
 	"github.com/docker/docker/layer"
12
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
12 13
 )
13 14
 
14 15
 // ImageHistory returns a slice of ImageHistory structures for the specified image
15 16
 // name by walking the image lineage.
16
-func (i *ImageService) ImageHistory(ctx context.Context, name string) ([]*image.HistoryResponseItem, error) {
17
+func (i *ImageService) ImageHistory(ctx context.Context, name string, platform *ocispec.Platform) ([]*image.HistoryResponseItem, error) {
17 18
 	start := time.Now()
18
-	img, err := i.GetImage(ctx, name, backend.GetImageOpts{})
19
+	img, err := i.GetImage(ctx, name, backend.GetImageOpts{Platform: platform})
19 20
 	if err != nil {
20 21
 		return nil, err
21 22
 	}
... ...
@@ -17,6 +17,10 @@ keywords: "API, Docker, rcli, REST, documentation"
17 17
 
18 18
 [Docker Engine API v1.48](https://docs.docker.com/engine/api/v1.48/) documentation
19 19
 
20
+* `GET /images/{name}/history` now supports a `platform` parameter (JSON
21
+  encoded OCI Platform type) that allows to specify a platform to show the
22
+  history of.
23
+
20 24
 ## v1.47 API changes
21 25
 
22 26
 [Docker Engine API v1.47](https://docs.docker.com/engine/api/v1.47/) documentation
... ...
@@ -73,7 +73,7 @@ func (s *DockerAPISuite) TestAPIImagesHistory(c *testing.T) {
73 73
 	buildImageSuccessfully(c, name, build.WithDockerfile("FROM busybox\nENV FOO bar"))
74 74
 	id := getIDByName(c, name)
75 75
 
76
-	historydata, err := apiClient.ImageHistory(testutil.GetContext(c), id)
76
+	historydata, err := apiClient.ImageHistory(testutil.GetContext(c), id, image.HistoryOptions{})
77 77
 	assert.NilError(c, err)
78 78
 
79 79
 	assert.Assert(c, len(historydata) != 0)
... ...
@@ -8,6 +8,7 @@ import (
8 8
 
9 9
 	"github.com/docker/docker/api/types"
10 10
 	containertypes "github.com/docker/docker/api/types/container"
11
+	"github.com/docker/docker/api/types/image"
11 12
 	dclient "github.com/docker/docker/client"
12 13
 	"github.com/docker/docker/integration/internal/container"
13 14
 	"github.com/docker/docker/pkg/stdcopy"
... ...
@@ -105,9 +106,9 @@ func TestBuildSquashParent(t *testing.T) {
105 105
 		container.WithCmd("/bin/sh", "-c", `[ "$(echo $HELLO)" = "world" ]`),
106 106
 	)
107 107
 
108
-	origHistory, err := client.ImageHistory(ctx, origID)
108
+	origHistory, err := client.ImageHistory(ctx, origID, image.HistoryOptions{})
109 109
 	assert.NilError(t, err)
110
-	testHistory, err := client.ImageHistory(ctx, name)
110
+	testHistory, err := client.ImageHistory(ctx, name, image.HistoryOptions{})
111 111
 	assert.NilError(t, err)
112 112
 
113 113
 	inspect, _, err = client.ImageInspectWithRaw(ctx, name)