From ab075ecd10505a38b5b228c24850c69a4e4ec4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Thu, 8 Aug 2024 17:56:27 +0200 Subject: [PATCH] image/history: Support `Platform` parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- api/server/router/image/backend.go | 2 +- api/server/router/image/image_routes.go | 16 +++++++++++++++- api/swagger.yaml | 9 +++++++++ api/types/image/opts.go | 6 ++++++ client/image_history.go | 18 ++++++++++++++++-- client/image_history_test.go | 4 ++-- client/interface.go | 2 +- daemon/containerd/image_history.go | 5 ++--- daemon/image_service.go | 2 +- daemon/images/image_history.go | 5 +++-- docs/api/version-history.md | 4 ++++ integration-cli/docker_api_images_test.go | 2 +- integration/build/build_squash_test.go | 5 +++-- 13 files changed, 64 insertions(+), 16 deletions(-) diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go index cb120c9e12..f943816ed5 100644 --- a/api/server/router/image/backend.go +++ b/api/server/router/image/backend.go @@ -23,7 +23,7 @@ type Backend interface { type imageBackend interface { ImageDelete(ctx context.Context, imageRef string, force, prune bool) ([]image.DeleteResponse, error) - ImageHistory(ctx context.Context, imageName string) ([]*image.HistoryResponseItem, error) + ImageHistory(ctx context.Context, imageName string, platform *ocispec.Platform) ([]*image.HistoryResponseItem, error) Images(ctx context.Context, opts image.ListOptions) ([]*image.Summary, error) GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*dockerimage.Image, error) ImageInspect(ctx context.Context, refOrID string, options backend.ImageInspectOpts) (*image.InspectResponse, error) diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index 67db9097f7..5601164ea9 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -398,7 +398,21 @@ func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, } func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - history, err := ir.backend.ImageHistory(ctx, vars["name"]) + if err := httputils.ParseForm(r); err != nil { + return err + } + + var platform *ocispec.Platform + if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") { + if formPlatform := r.Form.Get("platform"); formPlatform != "" { + p, err := httputils.DecodePlatform(formPlatform) + if err != nil { + return err + } + platform = p + } + } + history, err := ir.backend.ImageHistory(ctx, vars["name"], platform) if err != nil { return err } diff --git a/api/swagger.yaml b/api/swagger.yaml index 5d7e1e3a97..26cf84e9f4 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -9202,6 +9202,15 @@ paths: description: "Image name or ID" type: "string" required: true + - name: "platform" + type: "string" + in: "query" + description: | + JSON encoded OCI platform describing platform to show the history for. + If not provided, the host platform will be used. If it's not + available, any present platform will be picked. + + Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}` tags: ["Image"] /images/{name}/push: post: diff --git a/api/types/image/opts.go b/api/types/image/opts.go index 3949eae219..e99756eeb1 100644 --- a/api/types/image/opts.go +++ b/api/types/image/opts.go @@ -86,3 +86,9 @@ type RemoveOptions struct { Force bool PruneChildren bool } + +// HistoryOptions holds parameters to get image history. +type HistoryOptions struct { + // Platform from the manifest list to use for history. + Platform *ocispec.Platform +} diff --git a/client/image_history.go b/client/image_history.go index b5bea10d8f..779f4cfb22 100644 --- a/client/image_history.go +++ b/client/image_history.go @@ -3,15 +3,29 @@ package client // import "github.com/docker/docker/client" import ( "context" "encoding/json" + "fmt" "net/url" "github.com/docker/docker/api/types/image" ) // ImageHistory returns the changes in an image in history format. -func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) { +func (cli *Client) ImageHistory(ctx context.Context, imageID string, opts image.HistoryOptions) ([]image.HistoryResponseItem, error) { + values := url.Values{} + if opts.Platform != nil { + if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { + return nil, err + } + + p, err := json.Marshal(*opts.Platform) + if err != nil { + return nil, fmt.Errorf("invalid platform: %v", err) + } + values.Set("platform", string(p)) + } + var history []image.HistoryResponseItem - serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil) + serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", values, nil) defer ensureReaderClosed(serverResp) if err != nil { return history, err diff --git a/client/image_history_test.go b/client/image_history_test.go index 81ffbcbdf6..1d2d558eaf 100644 --- a/client/image_history_test.go +++ b/client/image_history_test.go @@ -20,7 +20,7 @@ func TestImageHistoryError(t *testing.T) { client := &Client{ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.ImageHistory(context.Background(), "nothing") + _, err := client.ImageHistory(context.Background(), "nothing", image.HistoryOptions{}) assert.Check(t, is.ErrorType(err, errdefs.IsSystem)) } @@ -51,7 +51,7 @@ func TestImageHistory(t *testing.T) { }, nil }), } - imageHistories, err := client.ImageHistory(context.Background(), "image_id") + imageHistories, err := client.ImageHistory(context.Background(), "image_id", image.HistoryOptions{}) if err != nil { t.Fatal(err) } diff --git a/client/interface.go b/client/interface.go index f96ca98e4a..142b79c347 100644 --- a/client/interface.go +++ b/client/interface.go @@ -91,7 +91,7 @@ type ImageAPIClient interface { BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) BuildCancel(ctx context.Context, id string) error ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) - ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error) + ImageHistory(ctx context.Context, image string, opts image.HistoryOptions) ([]image.HistoryResponseItem, error) ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) diff --git a/daemon/containerd/image_history.go b/daemon/containerd/image_history.go index a6f76bd859..776e428186 100644 --- a/daemon/containerd/image_history.go +++ b/daemon/containerd/image_history.go @@ -18,15 +18,14 @@ import ( // ImageHistory returns a slice of HistoryResponseItem structures for the // specified image name by walking the image lineage. -func (i *ImageService) ImageHistory(ctx context.Context, name string) ([]*imagetype.HistoryResponseItem, error) { +func (i *ImageService) ImageHistory(ctx context.Context, name string, platform *ocispec.Platform) ([]*imagetype.HistoryResponseItem, error) { start := time.Now() img, err := i.resolveImage(ctx, name) if err != nil { return nil, err } - // TODO: pass platform in from the CLI - pm := matchAllWithPreference(platforms.Default()) + pm := i.matchRequestedOrDefault(platforms.Only, platform) im, err := i.getBestPresentImageManifest(ctx, img, pm) if err != nil { diff --git a/daemon/image_service.go b/daemon/image_service.go index 6f25470d6d..c3580fcdfa 100644 --- a/daemon/image_service.go +++ b/daemon/image_service.go @@ -40,7 +40,7 @@ type ImageService interface { ImportImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, msg string, layerReader io.Reader, changes []string) (image.ID, error) TagImage(ctx context.Context, imageID image.ID, newTag reference.Named) error GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*image.Image, error) - ImageHistory(ctx context.Context, name string) ([]*imagetype.HistoryResponseItem, error) + ImageHistory(ctx context.Context, name string, platform *ocispec.Platform) ([]*imagetype.HistoryResponseItem, error) CommitImage(ctx context.Context, c backend.CommitConfig) (image.ID, error) SquashImage(id, parent string) (string, error) ImageInspect(ctx context.Context, refOrID string, opts backend.ImageInspectOpts) (*imagetype.InspectResponse, error) diff --git a/daemon/images/image_history.go b/daemon/images/image_history.go index f621ceae13..3368133a8c 100644 --- a/daemon/images/image_history.go +++ b/daemon/images/image_history.go @@ -9,13 +9,14 @@ import ( "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/image" "github.com/docker/docker/layer" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // ImageHistory returns a slice of ImageHistory structures for the specified image // name by walking the image lineage. -func (i *ImageService) ImageHistory(ctx context.Context, name string) ([]*image.HistoryResponseItem, error) { +func (i *ImageService) ImageHistory(ctx context.Context, name string, platform *ocispec.Platform) ([]*image.HistoryResponseItem, error) { start := time.Now() - img, err := i.GetImage(ctx, name, backend.GetImageOpts{}) + img, err := i.GetImage(ctx, name, backend.GetImageOpts{Platform: platform}) if err != nil { return nil, err } diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 3a9fc62663..a1b184f9de 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -17,6 +17,10 @@ keywords: "API, Docker, rcli, REST, documentation" [Docker Engine API v1.48](https://docs.docker.com/engine/api/v1.48/) documentation +* `GET /images/{name}/history` now supports a `platform` parameter (JSON + encoded OCI Platform type) that allows to specify a platform to show the + history of. + ## v1.47 API changes [Docker Engine API v1.47](https://docs.docker.com/engine/api/v1.47/) documentation diff --git a/integration-cli/docker_api_images_test.go b/integration-cli/docker_api_images_test.go index e03ad4df4d..c0f1fa2ca8 100644 --- a/integration-cli/docker_api_images_test.go +++ b/integration-cli/docker_api_images_test.go @@ -73,7 +73,7 @@ func (s *DockerAPISuite) TestAPIImagesHistory(c *testing.T) { buildImageSuccessfully(c, name, build.WithDockerfile("FROM busybox\nENV FOO bar")) id := getIDByName(c, name) - historydata, err := apiClient.ImageHistory(testutil.GetContext(c), id) + historydata, err := apiClient.ImageHistory(testutil.GetContext(c), id, image.HistoryOptions{}) assert.NilError(c, err) assert.Assert(c, len(historydata) != 0) diff --git a/integration/build/build_squash_test.go b/integration/build/build_squash_test.go index 2a2d034e4d..827988fe75 100644 --- a/integration/build/build_squash_test.go +++ b/integration/build/build_squash_test.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" dclient "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/pkg/stdcopy" @@ -105,9 +106,9 @@ func TestBuildSquashParent(t *testing.T) { container.WithCmd("/bin/sh", "-c", `[ "$(echo $HELLO)" = "world" ]`), ) - origHistory, err := client.ImageHistory(ctx, origID) + origHistory, err := client.ImageHistory(ctx, origID, image.HistoryOptions{}) assert.NilError(t, err) - testHistory, err := client.ImageHistory(ctx, name) + testHistory, err := client.ImageHistory(ctx, name, image.HistoryOptions{}) assert.NilError(t, err) inspect, _, err = client.ImageInspectWithRaw(ctx, name)