Merge pull request #51236 from vvoland/client-image-opts-functionalwrap

client/image_(inspect,history,load,save): Wrap return values
This commit is contained in:
Sebastiaan van Stijn
2025-10-21 16:54:02 +02:00
committed by GitHub
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
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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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))
})

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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))
}

View File

@@ -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,

View File

@@ -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

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)))
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)
}
})

View File

@@ -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")

View File

@@ -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) {

View File

@@ -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()

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))
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))

View File

@@ -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
}

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))
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) {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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()
}