From 2d69edd28a8a7bda3f1c25d4aad331481e8ad15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 20 Oct 2025 23:37:29 +0200 Subject: [PATCH] client/image_(inspect,history,load,save): Wrap return values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski Signed-off-by: Sebastiaan van Stijn --- client/client_interfaces.go | 8 ++--- client/image_history.go | 15 ++++----- client/image_history_opts.go | 9 ++++- client/image_history_test.go | 21 ++++++------ client/image_inspect.go | 20 +++++------ client/image_inspect_opts.go | 5 +++ client/image_load.go | 31 +++++++++++------ client/image_load_test.go | 2 +- client/image_save.go | 15 ++++----- client/image_save_opts.go | 33 +++++++++++++++++++ integration/build/build_squash_test.go | 2 +- integration/build/build_userns_linux_test.go | 4 +-- integration/daemon/migration_test.go | 4 +-- integration/image/history_test.go | 14 ++++---- integration/image/prune_test.go | 14 ++++---- integration/image/remove_test.go | 2 +- integration/image/save_test.go | 8 ++--- integration/internal/image/load.go | 6 ++-- integration/plugin/authz/authz_plugin_test.go | 2 +- internal/testutil/daemon/daemon.go | 2 +- internal/testutil/fixtures/load/frozen.go | 4 +-- .../moby/moby/client/client_interfaces.go | 8 ++--- .../moby/moby/client/image_history.go | 15 ++++----- .../moby/moby/client/image_history_opts.go | 9 ++++- .../moby/moby/client/image_inspect.go | 20 +++++------ .../moby/moby/client/image_inspect_opts.go | 5 +++ .../github.com/moby/moby/client/image_load.go | 31 +++++++++++------ .../github.com/moby/moby/client/image_save.go | 15 ++++----- .../moby/moby/client/image_save_opts.go | 33 +++++++++++++++++++ 29 files changed, 231 insertions(+), 126 deletions(-) diff --git a/client/client_interfaces.go b/client/client_interfaces.go index fcb00553fc..92d3071c27 100644 --- a/client/client_interfaces.go +++ b/client/client_interfaces.go @@ -119,10 +119,10 @@ type ImageAPIClient interface { ImageTag(ctx context.Context, image, ref string) error ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error) - ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) - ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) - ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (LoadResponse, error) - ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) + ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error) + ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) (ImageHistoryResult, error) + ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (ImageLoadResult, error) + ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (ImageSaveResult, error) } // NetworkAPIClient defines API client methods for the networks diff --git a/client/image_history.go b/client/image_history.go index 42c2b134bd..9bf627d4c7 100644 --- a/client/image_history.go +++ b/client/image_history.go @@ -6,7 +6,6 @@ import ( "fmt" "net/url" - "github.com/moby/moby/api/types/image" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -22,24 +21,24 @@ func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption { } // ImageHistory returns the changes in an image in history format. -func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) { +func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) (ImageHistoryResult, error) { query := url.Values{} var opts imageHistoryOpts for _, o := range historyOpts { if err := o.Apply(&opts); err != nil { - return nil, err + return ImageHistoryResult{}, err } } if opts.apiOptions.Platform != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return nil, err + return ImageHistoryResult{}, err } p, err := encodePlatform(opts.apiOptions.Platform) if err != nil { - return nil, err + return ImageHistoryResult{}, err } query.Set("platform", p) } @@ -47,10 +46,10 @@ func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) defer ensureReaderClosed(resp) if err != nil { - return nil, err + return ImageHistoryResult{}, err } - var history []image.HistoryResponseItem - err = json.NewDecoder(resp.Body).Decode(&history) + var history ImageHistoryResult + err = json.NewDecoder(resp.Body).Decode(&history.Items) return history, err } diff --git a/client/image_history_opts.go b/client/image_history_opts.go index 744d9fac9e..7fc57afd1c 100644 --- a/client/image_history_opts.go +++ b/client/image_history_opts.go @@ -1,6 +1,9 @@ package client -import ocispec "github.com/opencontainers/image-spec/specs-go/v1" +import ( + "github.com/moby/moby/api/types/image" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) // ImageHistoryOption is a type representing functional options for the image history operation. type ImageHistoryOption interface { @@ -20,3 +23,7 @@ type imageHistoryOptions struct { // Platform from the manifest list to use for history. Platform *ocispec.Platform } + +type ImageHistoryResult struct { + Items []image.HistoryResponseItem +} diff --git a/client/image_history_test.go b/client/image_history_test.go index 784e7ec7e3..484b435e89 100644 --- a/client/image_history_test.go +++ b/client/image_history_test.go @@ -36,16 +36,17 @@ func TestImageHistory(t *testing.T) { }, nil })) assert.NilError(t, err) - expected := []image.HistoryResponseItem{ - { - ID: "image_id1", - Tags: []string{"tag1", "tag2"}, - }, - { - ID: "image_id2", - Tags: []string{"tag1", "tag2"}, - }, - } + expected := ImageHistoryResult{ + Items: []image.HistoryResponseItem{ + { + ID: "image_id1", + Tags: []string{"tag1", "tag2"}, + }, + { + ID: "image_id2", + Tags: []string{"tag1", "tag2"}, + }, + }} imageHistories, err := client.ImageHistory(context.Background(), "image_id", ImageHistoryWithPlatform(ocispec.Platform{ Architecture: "arm64", diff --git a/client/image_inspect.go b/client/image_inspect.go index 30579ddbe2..b31ce14282 100644 --- a/client/image_inspect.go +++ b/client/image_inspect.go @@ -7,38 +7,36 @@ import ( "fmt" "io" "net/url" - - "github.com/moby/moby/api/types/image" ) // ImageInspect returns the image information. -func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) { +func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (ImageInspectResult, error) { if imageID == "" { - return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID} + return ImageInspectResult{}, objectNotFoundError{object: "image", id: imageID} } var opts imageInspectOpts for _, opt := range inspectOpts { if err := opt.Apply(&opts); err != nil { - return image.InspectResponse{}, fmt.Errorf("error applying image inspect option: %w", err) + return ImageInspectResult{}, fmt.Errorf("error applying image inspect option: %w", err) } } query := url.Values{} if opts.apiOptions.Manifests { if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } query.Set("manifests", "1") } if opts.apiOptions.Platform != nil { if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } platform, err := encodePlatform(opts.apiOptions.Platform) if err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } query.Set("platform", platform) } @@ -46,7 +44,7 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) defer ensureReaderClosed(resp) if err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } buf := opts.raw @@ -55,10 +53,10 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts } if _, err := io.Copy(buf, resp.Body); err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } - var response image.InspectResponse + var response ImageInspectResult err = json.Unmarshal(buf.Bytes(), &response) return response, err } diff --git a/client/image_inspect_opts.go b/client/image_inspect_opts.go index c2cc6eea84..266c1fe815 100644 --- a/client/image_inspect_opts.go +++ b/client/image_inspect_opts.go @@ -3,6 +3,7 @@ package client import ( "bytes" + "github.com/moby/moby/api/types/image" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -62,3 +63,7 @@ type imageInspectOptions struct { // This option is only available for API version 1.49 and up. Platform *ocispec.Platform } + +type ImageInspectResult struct { + image.InspectResponse +} diff --git a/client/image_load.go b/client/image_load.go index 6c51f61e46..f87b46dc2a 100644 --- a/client/image_load.go +++ b/client/image_load.go @@ -9,16 +9,16 @@ import ( // ImageLoad loads an image in the docker host from the client host. // It's up to the caller to close the [io.ReadCloser] in the -// [image.LoadResponse] returned by this function. +// [ImageLoadResult] returned by this function. // // Platform is an optional parameter that specifies the platform to load from // the provided multi-platform image. Passing a platform only has an effect // if the input image is a multi-platform image. -func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (LoadResponse, error) { +func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) { var opts imageLoadOpts for _, opt := range loadOpts { if err := opt.Apply(&opts); err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } } @@ -29,12 +29,12 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I } if len(opts.apiOptions.Platforms) > 0 { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } p, err := encodePlatforms(opts.apiOptions.Platforms...) if err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } query["platform"] = p } @@ -43,10 +43,10 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I "Content-Type": {"application/x-tar"}, }) if err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } - return LoadResponse{ - Body: resp.Body, + return ImageLoadResult{ + body: resp.Body, JSON: resp.Header.Get("Content-Type") == "application/json", }, nil } @@ -73,8 +73,19 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I // // We should deprecated the "quiet" option, as it's really a client // responsibility. -type LoadResponse struct { +type ImageLoadResult struct { // Body must be closed to avoid a resource leak - Body io.ReadCloser + body io.ReadCloser JSON bool } + +func (r ImageLoadResult) Read(p []byte) (n int, err error) { + return r.body.Read(p) +} + +func (r ImageLoadResult) Close() error { + if r.body == nil { + return nil + } + return r.body.Close() +} diff --git a/client/image_load_test.go b/client/image_load_test.go index 4415d746fc..45616fbcbd 100644 --- a/client/image_load_test.go +++ b/client/image_load_test.go @@ -101,7 +101,7 @@ func TestImageLoad(t *testing.T) { assert.NilError(t, err) assert.Check(t, is.Equal(imageLoadResponse.JSON, tc.expectedResponseJSON)) - body, err := io.ReadAll(imageLoadResponse.Body) + body, err := io.ReadAll(imageLoadResponse) assert.NilError(t, err) assert.Check(t, is.Equal(string(body), expectedOutput)) }) diff --git a/client/image_save.go b/client/image_save.go index ad32b0d65c..6ea4a8ec2f 100644 --- a/client/image_save.go +++ b/client/image_save.go @@ -2,21 +2,20 @@ package client import ( "context" - "io" "net/url" ) // ImageSave retrieves one or more images from the docker host as an -// [io.ReadCloser]. +// [ImageSaveResult]. // // Platforms is an optional parameter that specifies the platforms to save // from the image. Passing a platform only has an effect if the input image // is a multi-platform image. -func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) { +func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (ImageSaveResult, error) { var opts imageSaveOpts for _, opt := range saveOpts { if err := opt.Apply(&opts); err != nil { - return nil, err + return ImageSaveResult{}, err } } @@ -26,18 +25,18 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts .. if len(opts.apiOptions.Platforms) > 0 { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return nil, err + return ImageSaveResult{}, err } p, err := encodePlatforms(opts.apiOptions.Platforms...) if err != nil { - return nil, err + return ImageSaveResult{}, err } query["platform"] = p } resp, err := cli.get(ctx, "/images/get", query, nil) if err != nil { - return nil, err + return ImageSaveResult{}, err } - return resp.Body, nil + return newImageSaveResult(resp.Body), nil } diff --git a/client/image_save_opts.go b/client/image_save_opts.go index c51c2d5354..480126544d 100644 --- a/client/image_save_opts.go +++ b/client/image_save_opts.go @@ -2,6 +2,8 @@ package client import ( "fmt" + "io" + "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -36,3 +38,34 @@ type imageSaveOptions struct { // multi-platform image and has multiple variants. Platforms []ocispec.Platform } + +func newImageSaveResult(rc io.ReadCloser) ImageSaveResult { + if rc == nil { + panic("nil io.ReadCloser") + } + return ImageSaveResult{ + rc: rc, + close: sync.OnceValue(rc.Close), + } +} + +type ImageSaveResult struct { + rc io.ReadCloser + close func() error +} + +// Read implements io.ReadCloser +func (r ImageSaveResult) Read(p []byte) (n int, err error) { + if r.rc == nil { + return 0, io.EOF + } + return r.rc.Read(p) +} + +// Close implements io.ReadCloser +func (r ImageSaveResult) Close() error { + if r.close == nil { + return nil + } + return r.close() +} diff --git a/integration/build/build_squash_test.go b/integration/build/build_squash_test.go index 64e6bd52e9..2fccfc584f 100644 --- a/integration/build/build_squash_test.go +++ b/integration/build/build_squash_test.go @@ -111,6 +111,6 @@ func TestBuildSquashParent(t *testing.T) { inspect, err = apiClient.ImageInspect(ctx, name) assert.NilError(t, err) - assert.Check(t, is.Len(testHistory, len(origHistory)+1)) + assert.Check(t, is.Len(testHistory.Items, len(origHistory.Items)+1)) assert.Check(t, is.Len(inspect.RootFS.Layers, 2)) } diff --git a/integration/build/build_userns_linux_test.go b/integration/build/build_userns_linux_test.go index 18e0f177ca..120ab4a625 100644 --- a/integration/build/build_userns_linux_test.go +++ b/integration/build/build_userns_linux_test.go @@ -105,9 +105,9 @@ func TestBuildUserNamespaceValidateCapabilitiesAreV2(t *testing.T) { tarReader := bufio.NewReader(tarFile) loadResp, err := clientNoUserRemap.ImageLoad(ctx, tarReader) assert.NilError(t, err, "failed to load image tar file") - defer loadResp.Body.Close() + defer loadResp.Close() buf = bytes.NewBuffer(nil) - err = jsonmessage.DisplayJSONMessagesStream(loadResp.Body, buf, 0, false, nil) + err = jsonmessage.DisplayJSONMessagesStream(loadResp, buf, 0, false, nil) assert.NilError(t, err) cid := container.Run(ctx, t, clientNoUserRemap, diff --git a/integration/daemon/migration_test.go b/integration/daemon/migration_test.go index 1307384b90..91f26312ec 100644 --- a/integration/daemon/migration_test.go +++ b/integration/daemon/migration_test.go @@ -148,8 +148,8 @@ func TestMigrateSaveLoad(t *testing.T) { // Import lr, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), client.ImageLoadWithQuiet(true)) assert.NilError(t, err) - io.Copy(io.Discard, lr.Body) - lr.Body.Close() + io.Copy(io.Discard, lr) + lr.Close() result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) { c.Name = "Migration-save-load-" + snapshotter diff --git a/integration/image/history_test.go b/integration/image/history_test.go index d04c2c741b..5891b97bb1 100644 --- a/integration/image/history_test.go +++ b/integration/image/history_test.go @@ -25,13 +25,13 @@ func TestAPIImagesHistory(t *testing.T) { imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile))) - historydata, err := client.ImageHistory(ctx, imgID) + res, err := client.ImageHistory(ctx, imgID) assert.NilError(t, err) - assert.Assert(t, len(historydata) != 0) + assert.Assert(t, len(res.Items) != 0) var found bool - for _, imageLayer := range historydata { + for _, imageLayer := range res.Items { if imageLayer.ID == imgID { found = true break @@ -107,20 +107,20 @@ func TestAPIImageHistoryCrossPlatform(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) - hist, err := apiClient.ImageHistory(ctx, tc.imageRef, tc.options...) + res, err := apiClient.ImageHistory(ctx, tc.imageRef, tc.options...) assert.NilError(t, err) found := false - for _, layer := range hist { + for _, layer := range res.Items { if layer.ID == imgID { found = true break } } assert.Assert(t, found, "History should contain the built image ID") - assert.Assert(t, is.Len(hist, 3)) + assert.Assert(t, is.Len(res.Items, 3)) - for i, layer := range hist { + for i, layer := range res.Items { assert.Assert(t, layer.Size >= 0, "Layer %d should not have negative size", i) } }) diff --git a/integration/image/prune_test.go b/integration/image/prune_test.go index 20e79b1f9e..5c1e222ece 100644 --- a/integration/image/prune_test.go +++ b/integration/image/prune_test.go @@ -145,29 +145,29 @@ func TestPruneDontDeleteUsedImage(t *testing.T) { } { for _, tc := range []struct { name string - imageID func(t *testing.T, inspect image.InspectResponse) string + imageID func(t *testing.T, inspect client.ImageInspectResult) string }{ { name: "full id", - imageID: func(t *testing.T, inspect image.InspectResponse) string { + imageID: func(t *testing.T, inspect client.ImageInspectResult) string { return inspect.ID }, }, { name: "full id without sha256 prefix", - imageID: func(t *testing.T, inspect image.InspectResponse) string { + imageID: func(t *testing.T, inspect client.ImageInspectResult) string { return strings.TrimPrefix(inspect.ID, "sha256:") }, }, { name: "truncated id (without sha256 prefix)", - imageID: func(t *testing.T, inspect image.InspectResponse) string { + imageID: func(t *testing.T, inspect client.ImageInspectResult) string { return strings.TrimPrefix(inspect.ID, "sha256:")[:8] }, }, { name: "repo and digest without tag", - imageID: func(t *testing.T, inspect image.InspectResponse) string { + imageID: func(t *testing.T, inspect client.ImageInspectResult) string { skip.If(t, !testEnv.UsingSnapshotter()) return "busybox@" + inspect.ID @@ -175,7 +175,7 @@ func TestPruneDontDeleteUsedImage(t *testing.T) { }, { name: "tagged and digested", - imageID: func(t *testing.T, inspect image.InspectResponse) string { + imageID: func(t *testing.T, inspect client.ImageInspectResult) string { skip.If(t, !testEnv.UsingSnapshotter()) return "busybox:latest@" + inspect.ID @@ -183,7 +183,7 @@ func TestPruneDontDeleteUsedImage(t *testing.T) { }, { name: "repo digest", - imageID: func(t *testing.T, inspect image.InspectResponse) string { + imageID: func(t *testing.T, inspect client.ImageInspectResult) string { // graphdriver won't have a repo digest skip.If(t, len(inspect.RepoDigests) == 0, "no repo digest") diff --git a/integration/image/remove_test.go b/integration/image/remove_test.go index ceb147d10d..bc842b8a97 100644 --- a/integration/image/remove_test.go +++ b/integration/image/remove_test.go @@ -94,7 +94,7 @@ func TestRemoveByDigest(t *testing.T) { inspect, err = apiClient.ImageInspect(ctx, "test-remove-by-digest") assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) - assert.Check(t, is.DeepEqual(inspect, image.InspectResponse{})) + assert.Check(t, is.DeepEqual(inspect, client.ImageInspectResult{})) } func TestRemoveWithPlatform(t *testing.T) { diff --git a/integration/image/save_test.go b/integration/image/save_test.go index b832fa6275..43ab5ac812 100644 --- a/integration/image/save_test.go +++ b/integration/image/save_test.go @@ -328,8 +328,8 @@ func TestSaveAndLoadPlatform(t *testing.T) { // load the full exported image (all platforms in it) resp, err := apiClient.ImageLoad(ctx, rdr) assert.NilError(t, err) - _, err = io.ReadAll(resp.Body) - resp.Body.Close() + _, err = io.ReadAll(resp) + resp.Close() assert.NilError(t, err) rdr.Close() @@ -366,8 +366,8 @@ func TestSaveAndLoadPlatform(t *testing.T) { // load the exported image on the specified platforms only resp, err = apiClient.ImageLoad(ctx, rdr, client.ImageLoadWithPlatforms(tc.loadPlatforms...)) assert.NilError(t, err) - _, err = io.ReadAll(resp.Body) - resp.Body.Close() + _, err = io.ReadAll(resp) + resp.Close() assert.NilError(t, err) rdr.Close() diff --git a/integration/internal/image/load.go b/integration/internal/image/load.go index 2533ac9624..44769d50e3 100644 --- a/integration/internal/image/load.go +++ b/integration/internal/image/load.go @@ -30,10 +30,10 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu resp, err := apiClient.ImageLoad(ctx, rc, client.ImageLoadWithQuiet(true)) assert.NilError(t, err, "Failed to load dangling image") - defer resp.Body.Close() + defer resp.Close() if !assert.Check(t, err) { - respBody, err := io.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp) if err != nil { t.Fatalf("Failed to read response body: %v", err) return "" @@ -41,7 +41,7 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu t.Fatalf("Failed load: %s", string(respBody)) } - all, err := io.ReadAll(resp.Body) + all, err := io.ReadAll(resp) assert.NilError(t, err) decoder := json.NewDecoder(bytes.NewReader(all)) diff --git a/integration/plugin/authz/authz_plugin_test.go b/integration/plugin/authz/authz_plugin_test.go index c4b84453e5..26eaffafaf 100644 --- a/integration/plugin/authz/authz_plugin_test.go +++ b/integration/plugin/authz/authz_plugin_test.go @@ -448,7 +448,7 @@ func imageLoad(ctx context.Context, apiClient client.APIClient, path string) err if err != nil { return err } - defer response.Body.Close() + defer response.Close() return nil } diff --git a/internal/testutil/daemon/daemon.go b/internal/testutil/daemon/daemon.go index db99ad34c9..0e0fdb2ba5 100644 --- a/internal/testutil/daemon/daemon.go +++ b/internal/testutil/daemon/daemon.go @@ -886,7 +886,7 @@ func (d *Daemon) LoadImage(ctx context.Context, t testing.TB, img string) { resp, err := c.ImageLoad(ctx, reader, client.ImageLoadWithQuiet(true)) assert.NilError(t, err, "[%s] failed to load %s", d.id, img) - defer resp.Body.Close() + defer resp.Close() } func (d *Daemon) getClientConfig() (*clientConfig, error) { diff --git a/internal/testutil/fixtures/load/frozen.go b/internal/testutil/fixtures/load/frozen.go index 6e6e21d760..634d08a0fa 100644 --- a/internal/testutil/fixtures/load/frozen.go +++ b/internal/testutil/fixtures/load/frozen.go @@ -114,9 +114,9 @@ func loadFrozenImages(ctx context.Context, apiClient client.APIClient) error { if err != nil { return errors.Wrap(err, "failed to load frozen images") } - defer resp.Body.Close() + defer resp.Close() fd, isTerminal := term.GetFdInfo(os.Stdout) - return jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stdout, fd, isTerminal, nil) + return jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, fd, isTerminal, nil) } func pullImages(ctx context.Context, client client.APIClient, images []string) error { diff --git a/vendor/github.com/moby/moby/client/client_interfaces.go b/vendor/github.com/moby/moby/client/client_interfaces.go index fcb00553fc..92d3071c27 100644 --- a/vendor/github.com/moby/moby/client/client_interfaces.go +++ b/vendor/github.com/moby/moby/client/client_interfaces.go @@ -119,10 +119,10 @@ type ImageAPIClient interface { ImageTag(ctx context.Context, image, ref string) error ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error) - ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) - ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) - ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (LoadResponse, error) - ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) + ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error) + ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) (ImageHistoryResult, error) + ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (ImageLoadResult, error) + ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (ImageSaveResult, error) } // NetworkAPIClient defines API client methods for the networks diff --git a/vendor/github.com/moby/moby/client/image_history.go b/vendor/github.com/moby/moby/client/image_history.go index 42c2b134bd..9bf627d4c7 100644 --- a/vendor/github.com/moby/moby/client/image_history.go +++ b/vendor/github.com/moby/moby/client/image_history.go @@ -6,7 +6,6 @@ import ( "fmt" "net/url" - "github.com/moby/moby/api/types/image" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -22,24 +21,24 @@ func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption { } // ImageHistory returns the changes in an image in history format. -func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) { +func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) (ImageHistoryResult, error) { query := url.Values{} var opts imageHistoryOpts for _, o := range historyOpts { if err := o.Apply(&opts); err != nil { - return nil, err + return ImageHistoryResult{}, err } } if opts.apiOptions.Platform != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return nil, err + return ImageHistoryResult{}, err } p, err := encodePlatform(opts.apiOptions.Platform) if err != nil { - return nil, err + return ImageHistoryResult{}, err } query.Set("platform", p) } @@ -47,10 +46,10 @@ func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) defer ensureReaderClosed(resp) if err != nil { - return nil, err + return ImageHistoryResult{}, err } - var history []image.HistoryResponseItem - err = json.NewDecoder(resp.Body).Decode(&history) + var history ImageHistoryResult + err = json.NewDecoder(resp.Body).Decode(&history.Items) return history, err } diff --git a/vendor/github.com/moby/moby/client/image_history_opts.go b/vendor/github.com/moby/moby/client/image_history_opts.go index 744d9fac9e..7fc57afd1c 100644 --- a/vendor/github.com/moby/moby/client/image_history_opts.go +++ b/vendor/github.com/moby/moby/client/image_history_opts.go @@ -1,6 +1,9 @@ package client -import ocispec "github.com/opencontainers/image-spec/specs-go/v1" +import ( + "github.com/moby/moby/api/types/image" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) // ImageHistoryOption is a type representing functional options for the image history operation. type ImageHistoryOption interface { @@ -20,3 +23,7 @@ type imageHistoryOptions struct { // Platform from the manifest list to use for history. Platform *ocispec.Platform } + +type ImageHistoryResult struct { + Items []image.HistoryResponseItem +} diff --git a/vendor/github.com/moby/moby/client/image_inspect.go b/vendor/github.com/moby/moby/client/image_inspect.go index 30579ddbe2..b31ce14282 100644 --- a/vendor/github.com/moby/moby/client/image_inspect.go +++ b/vendor/github.com/moby/moby/client/image_inspect.go @@ -7,38 +7,36 @@ import ( "fmt" "io" "net/url" - - "github.com/moby/moby/api/types/image" ) // ImageInspect returns the image information. -func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) { +func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (ImageInspectResult, error) { if imageID == "" { - return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID} + return ImageInspectResult{}, objectNotFoundError{object: "image", id: imageID} } var opts imageInspectOpts for _, opt := range inspectOpts { if err := opt.Apply(&opts); err != nil { - return image.InspectResponse{}, fmt.Errorf("error applying image inspect option: %w", err) + return ImageInspectResult{}, fmt.Errorf("error applying image inspect option: %w", err) } } query := url.Values{} if opts.apiOptions.Manifests { if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } query.Set("manifests", "1") } if opts.apiOptions.Platform != nil { if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } platform, err := encodePlatform(opts.apiOptions.Platform) if err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } query.Set("platform", platform) } @@ -46,7 +44,7 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) defer ensureReaderClosed(resp) if err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } buf := opts.raw @@ -55,10 +53,10 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts } if _, err := io.Copy(buf, resp.Body); err != nil { - return image.InspectResponse{}, err + return ImageInspectResult{}, err } - var response image.InspectResponse + var response ImageInspectResult err = json.Unmarshal(buf.Bytes(), &response) return response, err } diff --git a/vendor/github.com/moby/moby/client/image_inspect_opts.go b/vendor/github.com/moby/moby/client/image_inspect_opts.go index c2cc6eea84..266c1fe815 100644 --- a/vendor/github.com/moby/moby/client/image_inspect_opts.go +++ b/vendor/github.com/moby/moby/client/image_inspect_opts.go @@ -3,6 +3,7 @@ package client import ( "bytes" + "github.com/moby/moby/api/types/image" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -62,3 +63,7 @@ type imageInspectOptions struct { // This option is only available for API version 1.49 and up. Platform *ocispec.Platform } + +type ImageInspectResult struct { + image.InspectResponse +} diff --git a/vendor/github.com/moby/moby/client/image_load.go b/vendor/github.com/moby/moby/client/image_load.go index 6c51f61e46..f87b46dc2a 100644 --- a/vendor/github.com/moby/moby/client/image_load.go +++ b/vendor/github.com/moby/moby/client/image_load.go @@ -9,16 +9,16 @@ import ( // ImageLoad loads an image in the docker host from the client host. // It's up to the caller to close the [io.ReadCloser] in the -// [image.LoadResponse] returned by this function. +// [ImageLoadResult] returned by this function. // // Platform is an optional parameter that specifies the platform to load from // the provided multi-platform image. Passing a platform only has an effect // if the input image is a multi-platform image. -func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (LoadResponse, error) { +func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) { var opts imageLoadOpts for _, opt := range loadOpts { if err := opt.Apply(&opts); err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } } @@ -29,12 +29,12 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I } if len(opts.apiOptions.Platforms) > 0 { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } p, err := encodePlatforms(opts.apiOptions.Platforms...) if err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } query["platform"] = p } @@ -43,10 +43,10 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I "Content-Type": {"application/x-tar"}, }) if err != nil { - return LoadResponse{}, err + return ImageLoadResult{}, err } - return LoadResponse{ - Body: resp.Body, + return ImageLoadResult{ + body: resp.Body, JSON: resp.Header.Get("Content-Type") == "application/json", }, nil } @@ -73,8 +73,19 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I // // We should deprecated the "quiet" option, as it's really a client // responsibility. -type LoadResponse struct { +type ImageLoadResult struct { // Body must be closed to avoid a resource leak - Body io.ReadCloser + body io.ReadCloser JSON bool } + +func (r ImageLoadResult) Read(p []byte) (n int, err error) { + return r.body.Read(p) +} + +func (r ImageLoadResult) Close() error { + if r.body == nil { + return nil + } + return r.body.Close() +} diff --git a/vendor/github.com/moby/moby/client/image_save.go b/vendor/github.com/moby/moby/client/image_save.go index ad32b0d65c..6ea4a8ec2f 100644 --- a/vendor/github.com/moby/moby/client/image_save.go +++ b/vendor/github.com/moby/moby/client/image_save.go @@ -2,21 +2,20 @@ package client import ( "context" - "io" "net/url" ) // ImageSave retrieves one or more images from the docker host as an -// [io.ReadCloser]. +// [ImageSaveResult]. // // Platforms is an optional parameter that specifies the platforms to save // from the image. Passing a platform only has an effect if the input image // is a multi-platform image. -func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) { +func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (ImageSaveResult, error) { var opts imageSaveOpts for _, opt := range saveOpts { if err := opt.Apply(&opts); err != nil { - return nil, err + return ImageSaveResult{}, err } } @@ -26,18 +25,18 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts .. if len(opts.apiOptions.Platforms) > 0 { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return nil, err + return ImageSaveResult{}, err } p, err := encodePlatforms(opts.apiOptions.Platforms...) if err != nil { - return nil, err + return ImageSaveResult{}, err } query["platform"] = p } resp, err := cli.get(ctx, "/images/get", query, nil) if err != nil { - return nil, err + return ImageSaveResult{}, err } - return resp.Body, nil + return newImageSaveResult(resp.Body), nil } diff --git a/vendor/github.com/moby/moby/client/image_save_opts.go b/vendor/github.com/moby/moby/client/image_save_opts.go index c51c2d5354..480126544d 100644 --- a/vendor/github.com/moby/moby/client/image_save_opts.go +++ b/vendor/github.com/moby/moby/client/image_save_opts.go @@ -2,6 +2,8 @@ package client import ( "fmt" + "io" + "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -36,3 +38,34 @@ type imageSaveOptions struct { // multi-platform image and has multiple variants. Platforms []ocispec.Platform } + +func newImageSaveResult(rc io.ReadCloser) ImageSaveResult { + if rc == nil { + panic("nil io.ReadCloser") + } + return ImageSaveResult{ + rc: rc, + close: sync.OnceValue(rc.Close), + } +} + +type ImageSaveResult struct { + rc io.ReadCloser + close func() error +} + +// Read implements io.ReadCloser +func (r ImageSaveResult) Read(p []byte) (n int, err error) { + if r.rc == nil { + return 0, io.EOF + } + return r.rc.Read(p) +} + +// Close implements io.ReadCloser +func (r ImageSaveResult) Close() error { + if r.close == nil { + return nil + } + return r.close() +}