client/image_(inspect,history,load,save): Wrap return values

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Paweł Gronowski
2025-10-20 23:37:29 +02:00
committed by Sebastiaan van Stijn
parent 7066eb3736
commit 2d69edd28a
29 changed files with 231 additions and 126 deletions

View File

@@ -119,10 +119,10 @@ type ImageAPIClient interface {
ImageTag(ctx context.Context, image, ref string) error ImageTag(ctx context.Context, image, ref string) error
ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error) ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error)
ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error)
ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) (ImageHistoryResult, error)
ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (LoadResponse, error) ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (ImageLoadResult, error)
ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (ImageSaveResult, error)
} }
// NetworkAPIClient defines API client methods for the networks // NetworkAPIClient defines API client methods for the networks

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"github.com/moby/moby/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" 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. // 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{} query := url.Values{}
var opts imageHistoryOpts var opts imageHistoryOpts
for _, o := range historyOpts { for _, o := range historyOpts {
if err := o.Apply(&opts); err != nil { if err := o.Apply(&opts); err != nil {
return nil, err return ImageHistoryResult{}, err
} }
} }
if opts.apiOptions.Platform != nil { if opts.apiOptions.Platform != nil {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err return ImageHistoryResult{}, err
} }
p, err := encodePlatform(opts.apiOptions.Platform) p, err := encodePlatform(opts.apiOptions.Platform)
if err != nil { if err != nil {
return nil, err return ImageHistoryResult{}, err
} }
query.Set("platform", p) 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) resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return ImageHistoryResult{}, err
} }
var history []image.HistoryResponseItem var history ImageHistoryResult
err = json.NewDecoder(resp.Body).Decode(&history) err = json.NewDecoder(resp.Body).Decode(&history.Items)
return history, err return history, err
} }

View File

@@ -1,6 +1,9 @@
package client 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. // ImageHistoryOption is a type representing functional options for the image history operation.
type ImageHistoryOption interface { type ImageHistoryOption interface {
@@ -20,3 +23,7 @@ type imageHistoryOptions struct {
// Platform from the manifest list to use for history. // Platform from the manifest list to use for history.
Platform *ocispec.Platform Platform *ocispec.Platform
} }
type ImageHistoryResult struct {
Items []image.HistoryResponseItem
}

View File

@@ -36,16 +36,17 @@ func TestImageHistory(t *testing.T) {
}, nil }, nil
})) }))
assert.NilError(t, err) assert.NilError(t, err)
expected := []image.HistoryResponseItem{ expected := ImageHistoryResult{
{ Items: []image.HistoryResponseItem{
ID: "image_id1", {
Tags: []string{"tag1", "tag2"}, ID: "image_id1",
}, Tags: []string{"tag1", "tag2"},
{ },
ID: "image_id2", {
Tags: []string{"tag1", "tag2"}, ID: "image_id2",
}, Tags: []string{"tag1", "tag2"},
} },
}}
imageHistories, err := client.ImageHistory(context.Background(), "image_id", ImageHistoryWithPlatform(ocispec.Platform{ imageHistories, err := client.ImageHistory(context.Background(), "image_id", ImageHistoryWithPlatform(ocispec.Platform{
Architecture: "arm64", Architecture: "arm64",

View File

@@ -7,38 +7,36 @@ import (
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
"github.com/moby/moby/api/types/image"
) )
// ImageInspect returns the image information. // 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 == "" { if imageID == "" {
return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID} return ImageInspectResult{}, objectNotFoundError{object: "image", id: imageID}
} }
var opts imageInspectOpts var opts imageInspectOpts
for _, opt := range inspectOpts { for _, opt := range inspectOpts {
if err := opt.Apply(&opts); err != nil { 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{} query := url.Values{}
if opts.apiOptions.Manifests { if opts.apiOptions.Manifests {
if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
query.Set("manifests", "1") query.Set("manifests", "1")
} }
if opts.apiOptions.Platform != nil { if opts.apiOptions.Platform != nil {
if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
platform, err := encodePlatform(opts.apiOptions.Platform) platform, err := encodePlatform(opts.apiOptions.Platform)
if err != nil { if err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
query.Set("platform", platform) 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) resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
buf := opts.raw 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 { 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) err = json.Unmarshal(buf.Bytes(), &response)
return response, err return response, err
} }

View File

@@ -3,6 +3,7 @@ package client
import ( import (
"bytes" "bytes"
"github.com/moby/moby/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" 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. // This option is only available for API version 1.49 and up.
Platform *ocispec.Platform Platform *ocispec.Platform
} }
type ImageInspectResult struct {
image.InspectResponse
}

View File

@@ -9,16 +9,16 @@ import (
// ImageLoad loads an image in the docker host from the client host. // 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 // 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 // Platform is an optional parameter that specifies the platform to load from
// the provided multi-platform image. Passing a platform only has an effect // the provided multi-platform image. Passing a platform only has an effect
// if the input image is a multi-platform image. // 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 var opts imageLoadOpts
for _, opt := range loadOpts { for _, opt := range loadOpts {
if err := opt.Apply(&opts); err != nil { 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 len(opts.apiOptions.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return LoadResponse{}, err return ImageLoadResult{}, err
} }
p, err := encodePlatforms(opts.apiOptions.Platforms...) p, err := encodePlatforms(opts.apiOptions.Platforms...)
if err != nil { if err != nil {
return LoadResponse{}, err return ImageLoadResult{}, err
} }
query["platform"] = p query["platform"] = p
} }
@@ -43,10 +43,10 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I
"Content-Type": {"application/x-tar"}, "Content-Type": {"application/x-tar"},
}) })
if err != nil { if err != nil {
return LoadResponse{}, err return ImageLoadResult{}, err
} }
return LoadResponse{ return ImageLoadResult{
Body: resp.Body, body: resp.Body,
JSON: resp.Header.Get("Content-Type") == "application/json", JSON: resp.Header.Get("Content-Type") == "application/json",
}, nil }, 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 // We should deprecated the "quiet" option, as it's really a client
// responsibility. // responsibility.
type LoadResponse struct { type ImageLoadResult struct {
// Body must be closed to avoid a resource leak // Body must be closed to avoid a resource leak
Body io.ReadCloser body io.ReadCloser
JSON bool 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()
}

View File

@@ -101,7 +101,7 @@ func TestImageLoad(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(imageLoadResponse.JSON, tc.expectedResponseJSON)) 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.NilError(t, err)
assert.Check(t, is.Equal(string(body), expectedOutput)) assert.Check(t, is.Equal(string(body), expectedOutput))
}) })

View File

@@ -2,21 +2,20 @@ package client
import ( import (
"context" "context"
"io"
"net/url" "net/url"
) )
// ImageSave retrieves one or more images from the docker host as an // 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 // 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 // from the image. Passing a platform only has an effect if the input image
// is a multi-platform 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 var opts imageSaveOpts
for _, opt := range saveOpts { for _, opt := range saveOpts {
if err := opt.Apply(&opts); err != nil { 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 len(opts.apiOptions.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err return ImageSaveResult{}, err
} }
p, err := encodePlatforms(opts.apiOptions.Platforms...) p, err := encodePlatforms(opts.apiOptions.Platforms...)
if err != nil { if err != nil {
return nil, err return ImageSaveResult{}, err
} }
query["platform"] = p query["platform"] = p
} }
resp, err := cli.get(ctx, "/images/get", query, nil) resp, err := cli.get(ctx, "/images/get", query, nil)
if err != nil { if err != nil {
return nil, err return ImageSaveResult{}, err
} }
return resp.Body, nil return newImageSaveResult(resp.Body), nil
} }

View File

@@ -2,6 +2,8 @@ package client
import ( import (
"fmt" "fmt"
"io"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
@@ -36,3 +38,34 @@ type imageSaveOptions struct {
// multi-platform image and has multiple variants. // multi-platform image and has multiple variants.
Platforms []ocispec.Platform 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()
}

View File

@@ -111,6 +111,6 @@ func TestBuildSquashParent(t *testing.T) {
inspect, err = apiClient.ImageInspect(ctx, name) inspect, err = apiClient.ImageInspect(ctx, name)
assert.NilError(t, err) 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)) assert.Check(t, is.Len(inspect.RootFS.Layers, 2))
} }

View File

@@ -105,9 +105,9 @@ func TestBuildUserNamespaceValidateCapabilitiesAreV2(t *testing.T) {
tarReader := bufio.NewReader(tarFile) tarReader := bufio.NewReader(tarFile)
loadResp, err := clientNoUserRemap.ImageLoad(ctx, tarReader) loadResp, err := clientNoUserRemap.ImageLoad(ctx, tarReader)
assert.NilError(t, err, "failed to load image tar file") assert.NilError(t, err, "failed to load image tar file")
defer loadResp.Body.Close() defer loadResp.Close()
buf = bytes.NewBuffer(nil) 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) assert.NilError(t, err)
cid := container.Run(ctx, t, clientNoUserRemap, cid := container.Run(ctx, t, clientNoUserRemap,

View File

@@ -148,8 +148,8 @@ func TestMigrateSaveLoad(t *testing.T) {
// Import // Import
lr, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), client.ImageLoadWithQuiet(true)) lr, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), client.ImageLoadWithQuiet(true))
assert.NilError(t, err) assert.NilError(t, err)
io.Copy(io.Discard, lr.Body) io.Copy(io.Discard, lr)
lr.Body.Close() lr.Close()
result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) { result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) {
c.Name = "Migration-save-load-" + snapshotter c.Name = "Migration-save-load-" + snapshotter

View File

@@ -25,13 +25,13 @@ func TestAPIImagesHistory(t *testing.T) {
imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile))) 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.NilError(t, err)
assert.Assert(t, len(historydata) != 0) assert.Assert(t, len(res.Items) != 0)
var found bool var found bool
for _, imageLayer := range historydata { for _, imageLayer := range res.Items {
if imageLayer.ID == imgID { if imageLayer.ID == imgID {
found = true found = true
break break
@@ -107,20 +107,20 @@ func TestAPIImageHistoryCrossPlatform(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, 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) assert.NilError(t, err)
found := false found := false
for _, layer := range hist { for _, layer := range res.Items {
if layer.ID == imgID { if layer.ID == imgID {
found = true found = true
break break
} }
} }
assert.Assert(t, found, "History should contain the built image ID") 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) assert.Assert(t, layer.Size >= 0, "Layer %d should not have negative size", i)
} }
}) })

View File

@@ -145,29 +145,29 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
} { } {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
imageID func(t *testing.T, inspect image.InspectResponse) string imageID func(t *testing.T, inspect client.ImageInspectResult) string
}{ }{
{ {
name: "full id", name: "full id",
imageID: func(t *testing.T, inspect image.InspectResponse) string { imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
return inspect.ID return inspect.ID
}, },
}, },
{ {
name: "full id without sha256 prefix", 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:") return strings.TrimPrefix(inspect.ID, "sha256:")
}, },
}, },
{ {
name: "truncated id (without sha256 prefix)", 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] return strings.TrimPrefix(inspect.ID, "sha256:")[:8]
}, },
}, },
{ {
name: "repo and digest without tag", 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()) skip.If(t, !testEnv.UsingSnapshotter())
return "busybox@" + inspect.ID return "busybox@" + inspect.ID
@@ -175,7 +175,7 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
}, },
{ {
name: "tagged and digested", 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()) skip.If(t, !testEnv.UsingSnapshotter())
return "busybox:latest@" + inspect.ID return "busybox:latest@" + inspect.ID
@@ -183,7 +183,7 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
}, },
{ {
name: "repo digest", 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 // graphdriver won't have a repo digest
skip.If(t, len(inspect.RepoDigests) == 0, "no repo digest") skip.If(t, len(inspect.RepoDigests) == 0, "no repo digest")

View File

@@ -94,7 +94,7 @@ func TestRemoveByDigest(t *testing.T) {
inspect, err = apiClient.ImageInspect(ctx, "test-remove-by-digest") inspect, err = apiClient.ImageInspect(ctx, "test-remove-by-digest")
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) 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) { func TestRemoveWithPlatform(t *testing.T) {

View File

@@ -328,8 +328,8 @@ func TestSaveAndLoadPlatform(t *testing.T) {
// load the full exported image (all platforms in it) // load the full exported image (all platforms in it)
resp, err := apiClient.ImageLoad(ctx, rdr) resp, err := apiClient.ImageLoad(ctx, rdr)
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.ReadAll(resp.Body) _, err = io.ReadAll(resp)
resp.Body.Close() resp.Close()
assert.NilError(t, err) assert.NilError(t, err)
rdr.Close() rdr.Close()
@@ -366,8 +366,8 @@ func TestSaveAndLoadPlatform(t *testing.T) {
// load the exported image on the specified platforms only // load the exported image on the specified platforms only
resp, err = apiClient.ImageLoad(ctx, rdr, client.ImageLoadWithPlatforms(tc.loadPlatforms...)) resp, err = apiClient.ImageLoad(ctx, rdr, client.ImageLoadWithPlatforms(tc.loadPlatforms...))
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.ReadAll(resp.Body) _, err = io.ReadAll(resp)
resp.Body.Close() resp.Close()
assert.NilError(t, err) assert.NilError(t, err)
rdr.Close() rdr.Close()

View File

@@ -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)) resp, err := apiClient.ImageLoad(ctx, rc, client.ImageLoadWithQuiet(true))
assert.NilError(t, err, "Failed to load dangling image") assert.NilError(t, err, "Failed to load dangling image")
defer resp.Body.Close() defer resp.Close()
if !assert.Check(t, err) { if !assert.Check(t, err) {
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(resp)
if err != nil { if err != nil {
t.Fatalf("Failed to read response body: %v", err) t.Fatalf("Failed to read response body: %v", err)
return "" return ""
@@ -41,7 +41,7 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu
t.Fatalf("Failed load: %s", string(respBody)) t.Fatalf("Failed load: %s", string(respBody))
} }
all, err := io.ReadAll(resp.Body) all, err := io.ReadAll(resp)
assert.NilError(t, err) assert.NilError(t, err)
decoder := json.NewDecoder(bytes.NewReader(all)) decoder := json.NewDecoder(bytes.NewReader(all))

View File

@@ -448,7 +448,7 @@ func imageLoad(ctx context.Context, apiClient client.APIClient, path string) err
if err != nil { if err != nil {
return err return err
} }
defer response.Body.Close() defer response.Close()
return nil return nil
} }

View File

@@ -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)) resp, err := c.ImageLoad(ctx, reader, client.ImageLoadWithQuiet(true))
assert.NilError(t, err, "[%s] failed to load %s", d.id, img) 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) { func (d *Daemon) getClientConfig() (*clientConfig, error) {

View File

@@ -114,9 +114,9 @@ func loadFrozenImages(ctx context.Context, apiClient client.APIClient) error {
if err != nil { if err != nil {
return errors.Wrap(err, "failed to load frozen images") return errors.Wrap(err, "failed to load frozen images")
} }
defer resp.Body.Close() defer resp.Close()
fd, isTerminal := term.GetFdInfo(os.Stdout) 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 { func pullImages(ctx context.Context, client client.APIClient, images []string) error {

View File

@@ -119,10 +119,10 @@ type ImageAPIClient interface {
ImageTag(ctx context.Context, image, ref string) error ImageTag(ctx context.Context, image, ref string) error
ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error) ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error)
ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error)
ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) (ImageHistoryResult, error)
ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (LoadResponse, error) ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (ImageLoadResult, error)
ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (ImageSaveResult, error)
} }
// NetworkAPIClient defines API client methods for the networks // NetworkAPIClient defines API client methods for the networks

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"github.com/moby/moby/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" 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. // 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{} query := url.Values{}
var opts imageHistoryOpts var opts imageHistoryOpts
for _, o := range historyOpts { for _, o := range historyOpts {
if err := o.Apply(&opts); err != nil { if err := o.Apply(&opts); err != nil {
return nil, err return ImageHistoryResult{}, err
} }
} }
if opts.apiOptions.Platform != nil { if opts.apiOptions.Platform != nil {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err return ImageHistoryResult{}, err
} }
p, err := encodePlatform(opts.apiOptions.Platform) p, err := encodePlatform(opts.apiOptions.Platform)
if err != nil { if err != nil {
return nil, err return ImageHistoryResult{}, err
} }
query.Set("platform", p) 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) resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return ImageHistoryResult{}, err
} }
var history []image.HistoryResponseItem var history ImageHistoryResult
err = json.NewDecoder(resp.Body).Decode(&history) err = json.NewDecoder(resp.Body).Decode(&history.Items)
return history, err return history, err
} }

View File

@@ -1,6 +1,9 @@
package client 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. // ImageHistoryOption is a type representing functional options for the image history operation.
type ImageHistoryOption interface { type ImageHistoryOption interface {
@@ -20,3 +23,7 @@ type imageHistoryOptions struct {
// Platform from the manifest list to use for history. // Platform from the manifest list to use for history.
Platform *ocispec.Platform Platform *ocispec.Platform
} }
type ImageHistoryResult struct {
Items []image.HistoryResponseItem
}

View File

@@ -7,38 +7,36 @@ import (
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
"github.com/moby/moby/api/types/image"
) )
// ImageInspect returns the image information. // 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 == "" { if imageID == "" {
return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID} return ImageInspectResult{}, objectNotFoundError{object: "image", id: imageID}
} }
var opts imageInspectOpts var opts imageInspectOpts
for _, opt := range inspectOpts { for _, opt := range inspectOpts {
if err := opt.Apply(&opts); err != nil { 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{} query := url.Values{}
if opts.apiOptions.Manifests { if opts.apiOptions.Manifests {
if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
query.Set("manifests", "1") query.Set("manifests", "1")
} }
if opts.apiOptions.Platform != nil { if opts.apiOptions.Platform != nil {
if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
platform, err := encodePlatform(opts.apiOptions.Platform) platform, err := encodePlatform(opts.apiOptions.Platform)
if err != nil { if err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
query.Set("platform", platform) 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) resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return image.InspectResponse{}, err return ImageInspectResult{}, err
} }
buf := opts.raw 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 { 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) err = json.Unmarshal(buf.Bytes(), &response)
return response, err return response, err
} }

View File

@@ -3,6 +3,7 @@ package client
import ( import (
"bytes" "bytes"
"github.com/moby/moby/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" 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. // This option is only available for API version 1.49 and up.
Platform *ocispec.Platform Platform *ocispec.Platform
} }
type ImageInspectResult struct {
image.InspectResponse
}

View File

@@ -9,16 +9,16 @@ import (
// ImageLoad loads an image in the docker host from the client host. // 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 // 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 // Platform is an optional parameter that specifies the platform to load from
// the provided multi-platform image. Passing a platform only has an effect // the provided multi-platform image. Passing a platform only has an effect
// if the input image is a multi-platform image. // 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 var opts imageLoadOpts
for _, opt := range loadOpts { for _, opt := range loadOpts {
if err := opt.Apply(&opts); err != nil { 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 len(opts.apiOptions.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return LoadResponse{}, err return ImageLoadResult{}, err
} }
p, err := encodePlatforms(opts.apiOptions.Platforms...) p, err := encodePlatforms(opts.apiOptions.Platforms...)
if err != nil { if err != nil {
return LoadResponse{}, err return ImageLoadResult{}, err
} }
query["platform"] = p query["platform"] = p
} }
@@ -43,10 +43,10 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I
"Content-Type": {"application/x-tar"}, "Content-Type": {"application/x-tar"},
}) })
if err != nil { if err != nil {
return LoadResponse{}, err return ImageLoadResult{}, err
} }
return LoadResponse{ return ImageLoadResult{
Body: resp.Body, body: resp.Body,
JSON: resp.Header.Get("Content-Type") == "application/json", JSON: resp.Header.Get("Content-Type") == "application/json",
}, nil }, 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 // We should deprecated the "quiet" option, as it's really a client
// responsibility. // responsibility.
type LoadResponse struct { type ImageLoadResult struct {
// Body must be closed to avoid a resource leak // Body must be closed to avoid a resource leak
Body io.ReadCloser body io.ReadCloser
JSON bool 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()
}

View File

@@ -2,21 +2,20 @@ package client
import ( import (
"context" "context"
"io"
"net/url" "net/url"
) )
// ImageSave retrieves one or more images from the docker host as an // 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 // 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 // from the image. Passing a platform only has an effect if the input image
// is a multi-platform 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 var opts imageSaveOpts
for _, opt := range saveOpts { for _, opt := range saveOpts {
if err := opt.Apply(&opts); err != nil { 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 len(opts.apiOptions.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err return ImageSaveResult{}, err
} }
p, err := encodePlatforms(opts.apiOptions.Platforms...) p, err := encodePlatforms(opts.apiOptions.Platforms...)
if err != nil { if err != nil {
return nil, err return ImageSaveResult{}, err
} }
query["platform"] = p query["platform"] = p
} }
resp, err := cli.get(ctx, "/images/get", query, nil) resp, err := cli.get(ctx, "/images/get", query, nil)
if err != nil { if err != nil {
return nil, err return ImageSaveResult{}, err
} }
return resp.Body, nil return newImageSaveResult(resp.Body), nil
} }

View File

@@ -2,6 +2,8 @@ package client
import ( import (
"fmt" "fmt"
"io"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
@@ -36,3 +38,34 @@ type imageSaveOptions struct {
// multi-platform image and has multiple variants. // multi-platform image and has multiple variants.
Platforms []ocispec.Platform 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()
}