Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -119,10 +119,10 @@ type ImageAPIClient interface {
|
| 119 | 119 |
ImageTag(ctx context.Context, image, ref string) error |
| 120 | 120 |
ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error) |
| 121 | 121 |
|
| 122 |
- ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) |
|
| 123 |
- ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) |
|
| 124 |
- ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (LoadResponse, error) |
|
| 125 |
- ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) |
|
| 122 |
+ ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error) |
|
| 123 |
+ ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) (ImageHistoryResult, error) |
|
| 124 |
+ ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (ImageLoadResult, error) |
|
| 125 |
+ ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (ImageSaveResult, error) |
|
| 126 | 126 |
} |
| 127 | 127 |
|
| 128 | 128 |
// NetworkAPIClient defines API client methods for the networks |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"fmt" |
| 7 | 7 |
"net/url" |
| 8 | 8 |
|
| 9 |
- "github.com/moby/moby/api/types/image" |
|
| 10 | 9 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 11 | 10 |
) |
| 12 | 11 |
|
| ... | ... |
@@ -22,24 +21,24 @@ func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption {
|
| 22 | 22 |
} |
| 23 | 23 |
|
| 24 | 24 |
// ImageHistory returns the changes in an image in history format. |
| 25 |
-func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) {
|
|
| 25 |
+func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) (ImageHistoryResult, error) {
|
|
| 26 | 26 |
query := url.Values{}
|
| 27 | 27 |
|
| 28 | 28 |
var opts imageHistoryOpts |
| 29 | 29 |
for _, o := range historyOpts {
|
| 30 | 30 |
if err := o.Apply(&opts); err != nil {
|
| 31 |
- return nil, err |
|
| 31 |
+ return ImageHistoryResult{}, err
|
|
| 32 | 32 |
} |
| 33 | 33 |
} |
| 34 | 34 |
|
| 35 | 35 |
if opts.apiOptions.Platform != nil {
|
| 36 | 36 |
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
| 37 |
- return nil, err |
|
| 37 |
+ return ImageHistoryResult{}, err
|
|
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 | 40 |
p, err := encodePlatform(opts.apiOptions.Platform) |
| 41 | 41 |
if err != nil {
|
| 42 |
- return nil, err |
|
| 42 |
+ return ImageHistoryResult{}, err
|
|
| 43 | 43 |
} |
| 44 | 44 |
query.Set("platform", p)
|
| 45 | 45 |
} |
| ... | ... |
@@ -47,10 +46,10 @@ func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts |
| 47 | 47 |
resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) |
| 48 | 48 |
defer ensureReaderClosed(resp) |
| 49 | 49 |
if err != nil {
|
| 50 |
- return nil, err |
|
| 50 |
+ return ImageHistoryResult{}, err
|
|
| 51 | 51 |
} |
| 52 | 52 |
|
| 53 |
- var history []image.HistoryResponseItem |
|
| 54 |
- err = json.NewDecoder(resp.Body).Decode(&history) |
|
| 53 |
+ var history ImageHistoryResult |
|
| 54 |
+ err = json.NewDecoder(resp.Body).Decode(&history.Items) |
|
| 55 | 55 |
return history, err |
| 56 | 56 |
} |
| ... | ... |
@@ -1,6 +1,9 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 |
-import ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 3 |
+import ( |
|
| 4 |
+ "github.com/moby/moby/api/types/image" |
|
| 5 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 6 |
+) |
|
| 4 | 7 |
|
| 5 | 8 |
// ImageHistoryOption is a type representing functional options for the image history operation. |
| 6 | 9 |
type ImageHistoryOption interface {
|
| ... | ... |
@@ -20,3 +23,7 @@ type imageHistoryOptions struct {
|
| 20 | 20 |
// Platform from the manifest list to use for history. |
| 21 | 21 |
Platform *ocispec.Platform |
| 22 | 22 |
} |
| 23 |
+ |
|
| 24 |
+type ImageHistoryResult struct {
|
|
| 25 |
+ Items []image.HistoryResponseItem |
|
| 26 |
+} |
| ... | ... |
@@ -36,16 +36,17 @@ func TestImageHistory(t *testing.T) {
|
| 36 | 36 |
}, nil |
| 37 | 37 |
})) |
| 38 | 38 |
assert.NilError(t, err) |
| 39 |
- expected := []image.HistoryResponseItem{
|
|
| 40 |
- {
|
|
| 41 |
- ID: "image_id1", |
|
| 42 |
- Tags: []string{"tag1", "tag2"},
|
|
| 43 |
- }, |
|
| 44 |
- {
|
|
| 45 |
- ID: "image_id2", |
|
| 46 |
- Tags: []string{"tag1", "tag2"},
|
|
| 47 |
- }, |
|
| 48 |
- } |
|
| 39 |
+ expected := ImageHistoryResult{
|
|
| 40 |
+ Items: []image.HistoryResponseItem{
|
|
| 41 |
+ {
|
|
| 42 |
+ ID: "image_id1", |
|
| 43 |
+ Tags: []string{"tag1", "tag2"},
|
|
| 44 |
+ }, |
|
| 45 |
+ {
|
|
| 46 |
+ ID: "image_id2", |
|
| 47 |
+ Tags: []string{"tag1", "tag2"},
|
|
| 48 |
+ }, |
|
| 49 |
+ }} |
|
| 49 | 50 |
|
| 50 | 51 |
imageHistories, err := client.ImageHistory(context.Background(), "image_id", ImageHistoryWithPlatform(ocispec.Platform{
|
| 51 | 52 |
Architecture: "arm64", |
| ... | ... |
@@ -7,38 +7,36 @@ import ( |
| 7 | 7 |
"fmt" |
| 8 | 8 |
"io" |
| 9 | 9 |
"net/url" |
| 10 |
- |
|
| 11 |
- "github.com/moby/moby/api/types/image" |
|
| 12 | 10 |
) |
| 13 | 11 |
|
| 14 | 12 |
// ImageInspect returns the image information. |
| 15 |
-func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) {
|
|
| 13 |
+func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (ImageInspectResult, error) {
|
|
| 16 | 14 |
if imageID == "" {
|
| 17 |
- return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID}
|
|
| 15 |
+ return ImageInspectResult{}, objectNotFoundError{object: "image", id: imageID}
|
|
| 18 | 16 |
} |
| 19 | 17 |
|
| 20 | 18 |
var opts imageInspectOpts |
| 21 | 19 |
for _, opt := range inspectOpts {
|
| 22 | 20 |
if err := opt.Apply(&opts); err != nil {
|
| 23 |
- return image.InspectResponse{}, fmt.Errorf("error applying image inspect option: %w", err)
|
|
| 21 |
+ return ImageInspectResult{}, fmt.Errorf("error applying image inspect option: %w", err)
|
|
| 24 | 22 |
} |
| 25 | 23 |
} |
| 26 | 24 |
|
| 27 | 25 |
query := url.Values{}
|
| 28 | 26 |
if opts.apiOptions.Manifests {
|
| 29 | 27 |
if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil {
|
| 30 |
- return image.InspectResponse{}, err
|
|
| 28 |
+ return ImageInspectResult{}, err
|
|
| 31 | 29 |
} |
| 32 | 30 |
query.Set("manifests", "1")
|
| 33 | 31 |
} |
| 34 | 32 |
|
| 35 | 33 |
if opts.apiOptions.Platform != nil {
|
| 36 | 34 |
if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
|
| 37 |
- return image.InspectResponse{}, err
|
|
| 35 |
+ return ImageInspectResult{}, err
|
|
| 38 | 36 |
} |
| 39 | 37 |
platform, err := encodePlatform(opts.apiOptions.Platform) |
| 40 | 38 |
if err != nil {
|
| 41 |
- return image.InspectResponse{}, err
|
|
| 39 |
+ return ImageInspectResult{}, err
|
|
| 42 | 40 |
} |
| 43 | 41 |
query.Set("platform", platform)
|
| 44 | 42 |
} |
| ... | ... |
@@ -46,7 +44,7 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts |
| 46 | 46 |
resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) |
| 47 | 47 |
defer ensureReaderClosed(resp) |
| 48 | 48 |
if err != nil {
|
| 49 |
- return image.InspectResponse{}, err
|
|
| 49 |
+ return ImageInspectResult{}, err
|
|
| 50 | 50 |
} |
| 51 | 51 |
|
| 52 | 52 |
buf := opts.raw |
| ... | ... |
@@ -55,10 +53,10 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts |
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 | 57 |
if _, err := io.Copy(buf, resp.Body); err != nil {
|
| 58 |
- return image.InspectResponse{}, err
|
|
| 58 |
+ return ImageInspectResult{}, err
|
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
- var response image.InspectResponse |
|
| 61 |
+ var response ImageInspectResult |
|
| 62 | 62 |
err = json.Unmarshal(buf.Bytes(), &response) |
| 63 | 63 |
return response, err |
| 64 | 64 |
} |
| ... | ... |
@@ -3,6 +3,7 @@ package client |
| 3 | 3 |
import ( |
| 4 | 4 |
"bytes" |
| 5 | 5 |
|
| 6 |
+ "github.com/moby/moby/api/types/image" |
|
| 6 | 7 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 7 | 8 |
) |
| 8 | 9 |
|
| ... | ... |
@@ -62,3 +63,7 @@ type imageInspectOptions struct {
|
| 62 | 62 |
// This option is only available for API version 1.49 and up. |
| 63 | 63 |
Platform *ocispec.Platform |
| 64 | 64 |
} |
| 65 |
+ |
|
| 66 |
+type ImageInspectResult struct {
|
|
| 67 |
+ image.InspectResponse |
|
| 68 |
+} |
| ... | ... |
@@ -9,16 +9,16 @@ import ( |
| 9 | 9 |
|
| 10 | 10 |
// ImageLoad loads an image in the docker host from the client host. |
| 11 | 11 |
// It's up to the caller to close the [io.ReadCloser] in the |
| 12 |
-// [image.LoadResponse] returned by this function. |
|
| 12 |
+// [ImageLoadResult] returned by this function. |
|
| 13 | 13 |
// |
| 14 | 14 |
// Platform is an optional parameter that specifies the platform to load from |
| 15 | 15 |
// the provided multi-platform image. Passing a platform only has an effect |
| 16 | 16 |
// if the input image is a multi-platform image. |
| 17 |
-func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (LoadResponse, error) {
|
|
| 17 |
+func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) {
|
|
| 18 | 18 |
var opts imageLoadOpts |
| 19 | 19 |
for _, opt := range loadOpts {
|
| 20 | 20 |
if err := opt.Apply(&opts); err != nil {
|
| 21 |
- return LoadResponse{}, err
|
|
| 21 |
+ return ImageLoadResult{}, err
|
|
| 22 | 22 |
} |
| 23 | 23 |
} |
| 24 | 24 |
|
| ... | ... |
@@ -29,12 +29,12 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I |
| 29 | 29 |
} |
| 30 | 30 |
if len(opts.apiOptions.Platforms) > 0 {
|
| 31 | 31 |
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
| 32 |
- return LoadResponse{}, err
|
|
| 32 |
+ return ImageLoadResult{}, err
|
|
| 33 | 33 |
} |
| 34 | 34 |
|
| 35 | 35 |
p, err := encodePlatforms(opts.apiOptions.Platforms...) |
| 36 | 36 |
if err != nil {
|
| 37 |
- return LoadResponse{}, err
|
|
| 37 |
+ return ImageLoadResult{}, err
|
|
| 38 | 38 |
} |
| 39 | 39 |
query["platform"] = p |
| 40 | 40 |
} |
| ... | ... |
@@ -43,10 +43,10 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I |
| 43 | 43 |
"Content-Type": {"application/x-tar"},
|
| 44 | 44 |
}) |
| 45 | 45 |
if err != nil {
|
| 46 |
- return LoadResponse{}, err
|
|
| 46 |
+ return ImageLoadResult{}, err
|
|
| 47 | 47 |
} |
| 48 |
- return LoadResponse{
|
|
| 49 |
- Body: resp.Body, |
|
| 48 |
+ return ImageLoadResult{
|
|
| 49 |
+ body: resp.Body, |
|
| 50 | 50 |
JSON: resp.Header.Get("Content-Type") == "application/json",
|
| 51 | 51 |
}, nil |
| 52 | 52 |
} |
| ... | ... |
@@ -73,8 +73,19 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I |
| 73 | 73 |
// |
| 74 | 74 |
// We should deprecated the "quiet" option, as it's really a client |
| 75 | 75 |
// responsibility. |
| 76 |
-type LoadResponse struct {
|
|
| 76 |
+type ImageLoadResult struct {
|
|
| 77 | 77 |
// Body must be closed to avoid a resource leak |
| 78 |
- Body io.ReadCloser |
|
| 78 |
+ body io.ReadCloser |
|
| 79 | 79 |
JSON bool |
| 80 | 80 |
} |
| 81 |
+ |
|
| 82 |
+func (r ImageLoadResult) Read(p []byte) (n int, err error) {
|
|
| 83 |
+ return r.body.Read(p) |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+func (r ImageLoadResult) Close() error {
|
|
| 87 |
+ if r.body == nil {
|
|
| 88 |
+ return nil |
|
| 89 |
+ } |
|
| 90 |
+ return r.body.Close() |
|
| 91 |
+} |
| ... | ... |
@@ -101,7 +101,7 @@ func TestImageLoad(t *testing.T) {
|
| 101 | 101 |
assert.NilError(t, err) |
| 102 | 102 |
assert.Check(t, is.Equal(imageLoadResponse.JSON, tc.expectedResponseJSON)) |
| 103 | 103 |
|
| 104 |
- body, err := io.ReadAll(imageLoadResponse.Body) |
|
| 104 |
+ body, err := io.ReadAll(imageLoadResponse) |
|
| 105 | 105 |
assert.NilError(t, err) |
| 106 | 106 |
assert.Check(t, is.Equal(string(body), expectedOutput)) |
| 107 | 107 |
}) |
| ... | ... |
@@ -2,21 +2,20 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"context" |
| 5 |
- "io" |
|
| 6 | 5 |
"net/url" |
| 7 | 6 |
) |
| 8 | 7 |
|
| 9 | 8 |
// ImageSave retrieves one or more images from the docker host as an |
| 10 |
-// [io.ReadCloser]. |
|
| 9 |
+// [ImageSaveResult]. |
|
| 11 | 10 |
// |
| 12 | 11 |
// Platforms is an optional parameter that specifies the platforms to save |
| 13 | 12 |
// from the image. Passing a platform only has an effect if the input image |
| 14 | 13 |
// is a multi-platform image. |
| 15 |
-func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) {
|
|
| 14 |
+func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (ImageSaveResult, error) {
|
|
| 16 | 15 |
var opts imageSaveOpts |
| 17 | 16 |
for _, opt := range saveOpts {
|
| 18 | 17 |
if err := opt.Apply(&opts); err != nil {
|
| 19 |
- return nil, err |
|
| 18 |
+ return ImageSaveResult{}, err
|
|
| 20 | 19 |
} |
| 21 | 20 |
} |
| 22 | 21 |
|
| ... | ... |
@@ -26,18 +25,18 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts .. |
| 26 | 26 |
|
| 27 | 27 |
if len(opts.apiOptions.Platforms) > 0 {
|
| 28 | 28 |
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
| 29 |
- return nil, err |
|
| 29 |
+ return ImageSaveResult{}, err
|
|
| 30 | 30 |
} |
| 31 | 31 |
p, err := encodePlatforms(opts.apiOptions.Platforms...) |
| 32 | 32 |
if err != nil {
|
| 33 |
- return nil, err |
|
| 33 |
+ return ImageSaveResult{}, err
|
|
| 34 | 34 |
} |
| 35 | 35 |
query["platform"] = p |
| 36 | 36 |
} |
| 37 | 37 |
|
| 38 | 38 |
resp, err := cli.get(ctx, "/images/get", query, nil) |
| 39 | 39 |
if err != nil {
|
| 40 |
- return nil, err |
|
| 40 |
+ return ImageSaveResult{}, err
|
|
| 41 | 41 |
} |
| 42 |
- return resp.Body, nil |
|
| 42 |
+ return newImageSaveResult(resp.Body), nil |
|
| 43 | 43 |
} |
| ... | ... |
@@ -2,6 +2,8 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "io" |
|
| 6 |
+ "sync" |
|
| 5 | 7 |
|
| 6 | 8 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 7 | 9 |
) |
| ... | ... |
@@ -36,3 +38,34 @@ type imageSaveOptions struct {
|
| 36 | 36 |
// multi-platform image and has multiple variants. |
| 37 | 37 |
Platforms []ocispec.Platform |
| 38 | 38 |
} |
| 39 |
+ |
|
| 40 |
+func newImageSaveResult(rc io.ReadCloser) ImageSaveResult {
|
|
| 41 |
+ if rc == nil {
|
|
| 42 |
+ panic("nil io.ReadCloser")
|
|
| 43 |
+ } |
|
| 44 |
+ return ImageSaveResult{
|
|
| 45 |
+ rc: rc, |
|
| 46 |
+ close: sync.OnceValue(rc.Close), |
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+type ImageSaveResult struct {
|
|
| 51 |
+ rc io.ReadCloser |
|
| 52 |
+ close func() error |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+// Read implements io.ReadCloser |
|
| 56 |
+func (r ImageSaveResult) Read(p []byte) (n int, err error) {
|
|
| 57 |
+ if r.rc == nil {
|
|
| 58 |
+ return 0, io.EOF |
|
| 59 |
+ } |
|
| 60 |
+ return r.rc.Read(p) |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+// Close implements io.ReadCloser |
|
| 64 |
+func (r ImageSaveResult) Close() error {
|
|
| 65 |
+ if r.close == nil {
|
|
| 66 |
+ return nil |
|
| 67 |
+ } |
|
| 68 |
+ return r.close() |
|
| 69 |
+} |
| ... | ... |
@@ -111,6 +111,6 @@ func TestBuildSquashParent(t *testing.T) {
|
| 111 | 111 |
|
| 112 | 112 |
inspect, err = apiClient.ImageInspect(ctx, name) |
| 113 | 113 |
assert.NilError(t, err) |
| 114 |
- assert.Check(t, is.Len(testHistory, len(origHistory)+1)) |
|
| 114 |
+ assert.Check(t, is.Len(testHistory.Items, len(origHistory.Items)+1)) |
|
| 115 | 115 |
assert.Check(t, is.Len(inspect.RootFS.Layers, 2)) |
| 116 | 116 |
} |
| ... | ... |
@@ -105,9 +105,9 @@ func TestBuildUserNamespaceValidateCapabilitiesAreV2(t *testing.T) {
|
| 105 | 105 |
tarReader := bufio.NewReader(tarFile) |
| 106 | 106 |
loadResp, err := clientNoUserRemap.ImageLoad(ctx, tarReader) |
| 107 | 107 |
assert.NilError(t, err, "failed to load image tar file") |
| 108 |
- defer loadResp.Body.Close() |
|
| 108 |
+ defer loadResp.Close() |
|
| 109 | 109 |
buf = bytes.NewBuffer(nil) |
| 110 |
- err = jsonmessage.DisplayJSONMessagesStream(loadResp.Body, buf, 0, false, nil) |
|
| 110 |
+ err = jsonmessage.DisplayJSONMessagesStream(loadResp, buf, 0, false, nil) |
|
| 111 | 111 |
assert.NilError(t, err) |
| 112 | 112 |
|
| 113 | 113 |
cid := container.Run(ctx, t, clientNoUserRemap, |
| ... | ... |
@@ -148,8 +148,8 @@ func TestMigrateSaveLoad(t *testing.T) {
|
| 148 | 148 |
// Import |
| 149 | 149 |
lr, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), client.ImageLoadWithQuiet(true)) |
| 150 | 150 |
assert.NilError(t, err) |
| 151 |
- io.Copy(io.Discard, lr.Body) |
|
| 152 |
- lr.Body.Close() |
|
| 151 |
+ io.Copy(io.Discard, lr) |
|
| 152 |
+ lr.Close() |
|
| 153 | 153 |
|
| 154 | 154 |
result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) {
|
| 155 | 155 |
c.Name = "Migration-save-load-" + snapshotter |
| ... | ... |
@@ -25,13 +25,13 @@ func TestAPIImagesHistory(t *testing.T) {
|
| 25 | 25 |
|
| 26 | 26 |
imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile))) |
| 27 | 27 |
|
| 28 |
- historydata, err := client.ImageHistory(ctx, imgID) |
|
| 28 |
+ res, err := client.ImageHistory(ctx, imgID) |
|
| 29 | 29 |
assert.NilError(t, err) |
| 30 | 30 |
|
| 31 |
- assert.Assert(t, len(historydata) != 0) |
|
| 31 |
+ assert.Assert(t, len(res.Items) != 0) |
|
| 32 | 32 |
|
| 33 | 33 |
var found bool |
| 34 |
- for _, imageLayer := range historydata {
|
|
| 34 |
+ for _, imageLayer := range res.Items {
|
|
| 35 | 35 |
if imageLayer.ID == imgID {
|
| 36 | 36 |
found = true |
| 37 | 37 |
break |
| ... | ... |
@@ -107,20 +107,20 @@ func TestAPIImageHistoryCrossPlatform(t *testing.T) {
|
| 107 | 107 |
t.Run(tc.name, func(t *testing.T) {
|
| 108 | 108 |
ctx := testutil.StartSpan(ctx, t) |
| 109 | 109 |
|
| 110 |
- hist, err := apiClient.ImageHistory(ctx, tc.imageRef, tc.options...) |
|
| 110 |
+ res, err := apiClient.ImageHistory(ctx, tc.imageRef, tc.options...) |
|
| 111 | 111 |
|
| 112 | 112 |
assert.NilError(t, err) |
| 113 | 113 |
found := false |
| 114 |
- for _, layer := range hist {
|
|
| 114 |
+ for _, layer := range res.Items {
|
|
| 115 | 115 |
if layer.ID == imgID {
|
| 116 | 116 |
found = true |
| 117 | 117 |
break |
| 118 | 118 |
} |
| 119 | 119 |
} |
| 120 | 120 |
assert.Assert(t, found, "History should contain the built image ID") |
| 121 |
- assert.Assert(t, is.Len(hist, 3)) |
|
| 121 |
+ assert.Assert(t, is.Len(res.Items, 3)) |
|
| 122 | 122 |
|
| 123 |
- for i, layer := range hist {
|
|
| 123 |
+ for i, layer := range res.Items {
|
|
| 124 | 124 |
assert.Assert(t, layer.Size >= 0, "Layer %d should not have negative size", i) |
| 125 | 125 |
} |
| 126 | 126 |
}) |
| ... | ... |
@@ -145,29 +145,29 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
|
| 145 | 145 |
} {
|
| 146 | 146 |
for _, tc := range []struct {
|
| 147 | 147 |
name string |
| 148 |
- imageID func(t *testing.T, inspect image.InspectResponse) string |
|
| 148 |
+ imageID func(t *testing.T, inspect client.ImageInspectResult) string |
|
| 149 | 149 |
}{
|
| 150 | 150 |
{
|
| 151 | 151 |
name: "full id", |
| 152 |
- imageID: func(t *testing.T, inspect image.InspectResponse) string {
|
|
| 152 |
+ imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
|
|
| 153 | 153 |
return inspect.ID |
| 154 | 154 |
}, |
| 155 | 155 |
}, |
| 156 | 156 |
{
|
| 157 | 157 |
name: "full id without sha256 prefix", |
| 158 |
- imageID: func(t *testing.T, inspect image.InspectResponse) string {
|
|
| 158 |
+ imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
|
|
| 159 | 159 |
return strings.TrimPrefix(inspect.ID, "sha256:") |
| 160 | 160 |
}, |
| 161 | 161 |
}, |
| 162 | 162 |
{
|
| 163 | 163 |
name: "truncated id (without sha256 prefix)", |
| 164 |
- imageID: func(t *testing.T, inspect image.InspectResponse) string {
|
|
| 164 |
+ imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
|
|
| 165 | 165 |
return strings.TrimPrefix(inspect.ID, "sha256:")[:8] |
| 166 | 166 |
}, |
| 167 | 167 |
}, |
| 168 | 168 |
{
|
| 169 | 169 |
name: "repo and digest without tag", |
| 170 |
- imageID: func(t *testing.T, inspect image.InspectResponse) string {
|
|
| 170 |
+ imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
|
|
| 171 | 171 |
skip.If(t, !testEnv.UsingSnapshotter()) |
| 172 | 172 |
|
| 173 | 173 |
return "busybox@" + inspect.ID |
| ... | ... |
@@ -175,7 +175,7 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
|
| 175 | 175 |
}, |
| 176 | 176 |
{
|
| 177 | 177 |
name: "tagged and digested", |
| 178 |
- imageID: func(t *testing.T, inspect image.InspectResponse) string {
|
|
| 178 |
+ imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
|
|
| 179 | 179 |
skip.If(t, !testEnv.UsingSnapshotter()) |
| 180 | 180 |
|
| 181 | 181 |
return "busybox:latest@" + inspect.ID |
| ... | ... |
@@ -183,7 +183,7 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
|
| 183 | 183 |
}, |
| 184 | 184 |
{
|
| 185 | 185 |
name: "repo digest", |
| 186 |
- imageID: func(t *testing.T, inspect image.InspectResponse) string {
|
|
| 186 |
+ imageID: func(t *testing.T, inspect client.ImageInspectResult) string {
|
|
| 187 | 187 |
// graphdriver won't have a repo digest |
| 188 | 188 |
skip.If(t, len(inspect.RepoDigests) == 0, "no repo digest") |
| 189 | 189 |
|
| ... | ... |
@@ -94,7 +94,7 @@ func TestRemoveByDigest(t *testing.T) {
|
| 94 | 94 |
|
| 95 | 95 |
inspect, err = apiClient.ImageInspect(ctx, "test-remove-by-digest") |
| 96 | 96 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) |
| 97 |
- assert.Check(t, is.DeepEqual(inspect, image.InspectResponse{}))
|
|
| 97 |
+ assert.Check(t, is.DeepEqual(inspect, client.ImageInspectResult{}))
|
|
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 | 100 |
func TestRemoveWithPlatform(t *testing.T) {
|
| ... | ... |
@@ -328,8 +328,8 @@ func TestSaveAndLoadPlatform(t *testing.T) {
|
| 328 | 328 |
// load the full exported image (all platforms in it) |
| 329 | 329 |
resp, err := apiClient.ImageLoad(ctx, rdr) |
| 330 | 330 |
assert.NilError(t, err) |
| 331 |
- _, err = io.ReadAll(resp.Body) |
|
| 332 |
- resp.Body.Close() |
|
| 331 |
+ _, err = io.ReadAll(resp) |
|
| 332 |
+ resp.Close() |
|
| 333 | 333 |
assert.NilError(t, err) |
| 334 | 334 |
|
| 335 | 335 |
rdr.Close() |
| ... | ... |
@@ -366,8 +366,8 @@ func TestSaveAndLoadPlatform(t *testing.T) {
|
| 366 | 366 |
// load the exported image on the specified platforms only |
| 367 | 367 |
resp, err = apiClient.ImageLoad(ctx, rdr, client.ImageLoadWithPlatforms(tc.loadPlatforms...)) |
| 368 | 368 |
assert.NilError(t, err) |
| 369 |
- _, err = io.ReadAll(resp.Body) |
|
| 370 |
- resp.Body.Close() |
|
| 369 |
+ _, err = io.ReadAll(resp) |
|
| 370 |
+ resp.Close() |
|
| 371 | 371 |
assert.NilError(t, err) |
| 372 | 372 |
|
| 373 | 373 |
rdr.Close() |
| ... | ... |
@@ -30,10 +30,10 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu |
| 30 | 30 |
resp, err := apiClient.ImageLoad(ctx, rc, client.ImageLoadWithQuiet(true)) |
| 31 | 31 |
assert.NilError(t, err, "Failed to load dangling image") |
| 32 | 32 |
|
| 33 |
- defer resp.Body.Close() |
|
| 33 |
+ defer resp.Close() |
|
| 34 | 34 |
|
| 35 | 35 |
if !assert.Check(t, err) {
|
| 36 |
- respBody, err := io.ReadAll(resp.Body) |
|
| 36 |
+ respBody, err := io.ReadAll(resp) |
|
| 37 | 37 |
if err != nil {
|
| 38 | 38 |
t.Fatalf("Failed to read response body: %v", err)
|
| 39 | 39 |
return "" |
| ... | ... |
@@ -41,7 +41,7 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu |
| 41 | 41 |
t.Fatalf("Failed load: %s", string(respBody))
|
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 |
- all, err := io.ReadAll(resp.Body) |
|
| 44 |
+ all, err := io.ReadAll(resp) |
|
| 45 | 45 |
assert.NilError(t, err) |
| 46 | 46 |
|
| 47 | 47 |
decoder := json.NewDecoder(bytes.NewReader(all)) |
| ... | ... |
@@ -886,7 +886,7 @@ func (d *Daemon) LoadImage(ctx context.Context, t testing.TB, img string) {
|
| 886 | 886 |
|
| 887 | 887 |
resp, err := c.ImageLoad(ctx, reader, client.ImageLoadWithQuiet(true)) |
| 888 | 888 |
assert.NilError(t, err, "[%s] failed to load %s", d.id, img) |
| 889 |
- defer resp.Body.Close() |
|
| 889 |
+ defer resp.Close() |
|
| 890 | 890 |
} |
| 891 | 891 |
|
| 892 | 892 |
func (d *Daemon) getClientConfig() (*clientConfig, error) {
|
| ... | ... |
@@ -114,9 +114,9 @@ func loadFrozenImages(ctx context.Context, apiClient client.APIClient) error {
|
| 114 | 114 |
if err != nil {
|
| 115 | 115 |
return errors.Wrap(err, "failed to load frozen images") |
| 116 | 116 |
} |
| 117 |
- defer resp.Body.Close() |
|
| 117 |
+ defer resp.Close() |
|
| 118 | 118 |
fd, isTerminal := term.GetFdInfo(os.Stdout) |
| 119 |
- return jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stdout, fd, isTerminal, nil) |
|
| 119 |
+ return jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, fd, isTerminal, nil) |
|
| 120 | 120 |
} |
| 121 | 121 |
|
| 122 | 122 |
func pullImages(ctx context.Context, client client.APIClient, images []string) error {
|
| ... | ... |
@@ -119,10 +119,10 @@ type ImageAPIClient interface {
|
| 119 | 119 |
ImageTag(ctx context.Context, image, ref string) error |
| 120 | 120 |
ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error) |
| 121 | 121 |
|
| 122 |
- ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) |
|
| 123 |
- ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) |
|
| 124 |
- ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (LoadResponse, error) |
|
| 125 |
- ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) |
|
| 122 |
+ ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error) |
|
| 123 |
+ ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) (ImageHistoryResult, error) |
|
| 124 |
+ ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (ImageLoadResult, error) |
|
| 125 |
+ ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (ImageSaveResult, error) |
|
| 126 | 126 |
} |
| 127 | 127 |
|
| 128 | 128 |
// NetworkAPIClient defines API client methods for the networks |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"fmt" |
| 7 | 7 |
"net/url" |
| 8 | 8 |
|
| 9 |
- "github.com/moby/moby/api/types/image" |
|
| 10 | 9 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 11 | 10 |
) |
| 12 | 11 |
|
| ... | ... |
@@ -22,24 +21,24 @@ func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption {
|
| 22 | 22 |
} |
| 23 | 23 |
|
| 24 | 24 |
// ImageHistory returns the changes in an image in history format. |
| 25 |
-func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) {
|
|
| 25 |
+func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) (ImageHistoryResult, error) {
|
|
| 26 | 26 |
query := url.Values{}
|
| 27 | 27 |
|
| 28 | 28 |
var opts imageHistoryOpts |
| 29 | 29 |
for _, o := range historyOpts {
|
| 30 | 30 |
if err := o.Apply(&opts); err != nil {
|
| 31 |
- return nil, err |
|
| 31 |
+ return ImageHistoryResult{}, err
|
|
| 32 | 32 |
} |
| 33 | 33 |
} |
| 34 | 34 |
|
| 35 | 35 |
if opts.apiOptions.Platform != nil {
|
| 36 | 36 |
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
| 37 |
- return nil, err |
|
| 37 |
+ return ImageHistoryResult{}, err
|
|
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 | 40 |
p, err := encodePlatform(opts.apiOptions.Platform) |
| 41 | 41 |
if err != nil {
|
| 42 |
- return nil, err |
|
| 42 |
+ return ImageHistoryResult{}, err
|
|
| 43 | 43 |
} |
| 44 | 44 |
query.Set("platform", p)
|
| 45 | 45 |
} |
| ... | ... |
@@ -47,10 +46,10 @@ func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts |
| 47 | 47 |
resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) |
| 48 | 48 |
defer ensureReaderClosed(resp) |
| 49 | 49 |
if err != nil {
|
| 50 |
- return nil, err |
|
| 50 |
+ return ImageHistoryResult{}, err
|
|
| 51 | 51 |
} |
| 52 | 52 |
|
| 53 |
- var history []image.HistoryResponseItem |
|
| 54 |
- err = json.NewDecoder(resp.Body).Decode(&history) |
|
| 53 |
+ var history ImageHistoryResult |
|
| 54 |
+ err = json.NewDecoder(resp.Body).Decode(&history.Items) |
|
| 55 | 55 |
return history, err |
| 56 | 56 |
} |
| ... | ... |
@@ -1,6 +1,9 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 |
-import ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 3 |
+import ( |
|
| 4 |
+ "github.com/moby/moby/api/types/image" |
|
| 5 |
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
|
| 6 |
+) |
|
| 4 | 7 |
|
| 5 | 8 |
// ImageHistoryOption is a type representing functional options for the image history operation. |
| 6 | 9 |
type ImageHistoryOption interface {
|
| ... | ... |
@@ -20,3 +23,7 @@ type imageHistoryOptions struct {
|
| 20 | 20 |
// Platform from the manifest list to use for history. |
| 21 | 21 |
Platform *ocispec.Platform |
| 22 | 22 |
} |
| 23 |
+ |
|
| 24 |
+type ImageHistoryResult struct {
|
|
| 25 |
+ Items []image.HistoryResponseItem |
|
| 26 |
+} |
| ... | ... |
@@ -7,38 +7,36 @@ import ( |
| 7 | 7 |
"fmt" |
| 8 | 8 |
"io" |
| 9 | 9 |
"net/url" |
| 10 |
- |
|
| 11 |
- "github.com/moby/moby/api/types/image" |
|
| 12 | 10 |
) |
| 13 | 11 |
|
| 14 | 12 |
// ImageInspect returns the image information. |
| 15 |
-func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) {
|
|
| 13 |
+func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (ImageInspectResult, error) {
|
|
| 16 | 14 |
if imageID == "" {
|
| 17 |
- return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID}
|
|
| 15 |
+ return ImageInspectResult{}, objectNotFoundError{object: "image", id: imageID}
|
|
| 18 | 16 |
} |
| 19 | 17 |
|
| 20 | 18 |
var opts imageInspectOpts |
| 21 | 19 |
for _, opt := range inspectOpts {
|
| 22 | 20 |
if err := opt.Apply(&opts); err != nil {
|
| 23 |
- return image.InspectResponse{}, fmt.Errorf("error applying image inspect option: %w", err)
|
|
| 21 |
+ return ImageInspectResult{}, fmt.Errorf("error applying image inspect option: %w", err)
|
|
| 24 | 22 |
} |
| 25 | 23 |
} |
| 26 | 24 |
|
| 27 | 25 |
query := url.Values{}
|
| 28 | 26 |
if opts.apiOptions.Manifests {
|
| 29 | 27 |
if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil {
|
| 30 |
- return image.InspectResponse{}, err
|
|
| 28 |
+ return ImageInspectResult{}, err
|
|
| 31 | 29 |
} |
| 32 | 30 |
query.Set("manifests", "1")
|
| 33 | 31 |
} |
| 34 | 32 |
|
| 35 | 33 |
if opts.apiOptions.Platform != nil {
|
| 36 | 34 |
if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
|
| 37 |
- return image.InspectResponse{}, err
|
|
| 35 |
+ return ImageInspectResult{}, err
|
|
| 38 | 36 |
} |
| 39 | 37 |
platform, err := encodePlatform(opts.apiOptions.Platform) |
| 40 | 38 |
if err != nil {
|
| 41 |
- return image.InspectResponse{}, err
|
|
| 39 |
+ return ImageInspectResult{}, err
|
|
| 42 | 40 |
} |
| 43 | 41 |
query.Set("platform", platform)
|
| 44 | 42 |
} |
| ... | ... |
@@ -46,7 +44,7 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts |
| 46 | 46 |
resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) |
| 47 | 47 |
defer ensureReaderClosed(resp) |
| 48 | 48 |
if err != nil {
|
| 49 |
- return image.InspectResponse{}, err
|
|
| 49 |
+ return ImageInspectResult{}, err
|
|
| 50 | 50 |
} |
| 51 | 51 |
|
| 52 | 52 |
buf := opts.raw |
| ... | ... |
@@ -55,10 +53,10 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts |
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 | 57 |
if _, err := io.Copy(buf, resp.Body); err != nil {
|
| 58 |
- return image.InspectResponse{}, err
|
|
| 58 |
+ return ImageInspectResult{}, err
|
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
- var response image.InspectResponse |
|
| 61 |
+ var response ImageInspectResult |
|
| 62 | 62 |
err = json.Unmarshal(buf.Bytes(), &response) |
| 63 | 63 |
return response, err |
| 64 | 64 |
} |
| ... | ... |
@@ -3,6 +3,7 @@ package client |
| 3 | 3 |
import ( |
| 4 | 4 |
"bytes" |
| 5 | 5 |
|
| 6 |
+ "github.com/moby/moby/api/types/image" |
|
| 6 | 7 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 7 | 8 |
) |
| 8 | 9 |
|
| ... | ... |
@@ -62,3 +63,7 @@ type imageInspectOptions struct {
|
| 62 | 62 |
// This option is only available for API version 1.49 and up. |
| 63 | 63 |
Platform *ocispec.Platform |
| 64 | 64 |
} |
| 65 |
+ |
|
| 66 |
+type ImageInspectResult struct {
|
|
| 67 |
+ image.InspectResponse |
|
| 68 |
+} |
| ... | ... |
@@ -9,16 +9,16 @@ import ( |
| 9 | 9 |
|
| 10 | 10 |
// ImageLoad loads an image in the docker host from the client host. |
| 11 | 11 |
// It's up to the caller to close the [io.ReadCloser] in the |
| 12 |
-// [image.LoadResponse] returned by this function. |
|
| 12 |
+// [ImageLoadResult] returned by this function. |
|
| 13 | 13 |
// |
| 14 | 14 |
// Platform is an optional parameter that specifies the platform to load from |
| 15 | 15 |
// the provided multi-platform image. Passing a platform only has an effect |
| 16 | 16 |
// if the input image is a multi-platform image. |
| 17 |
-func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (LoadResponse, error) {
|
|
| 17 |
+func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) {
|
|
| 18 | 18 |
var opts imageLoadOpts |
| 19 | 19 |
for _, opt := range loadOpts {
|
| 20 | 20 |
if err := opt.Apply(&opts); err != nil {
|
| 21 |
- return LoadResponse{}, err
|
|
| 21 |
+ return ImageLoadResult{}, err
|
|
| 22 | 22 |
} |
| 23 | 23 |
} |
| 24 | 24 |
|
| ... | ... |
@@ -29,12 +29,12 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I |
| 29 | 29 |
} |
| 30 | 30 |
if len(opts.apiOptions.Platforms) > 0 {
|
| 31 | 31 |
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
| 32 |
- return LoadResponse{}, err
|
|
| 32 |
+ return ImageLoadResult{}, err
|
|
| 33 | 33 |
} |
| 34 | 34 |
|
| 35 | 35 |
p, err := encodePlatforms(opts.apiOptions.Platforms...) |
| 36 | 36 |
if err != nil {
|
| 37 |
- return LoadResponse{}, err
|
|
| 37 |
+ return ImageLoadResult{}, err
|
|
| 38 | 38 |
} |
| 39 | 39 |
query["platform"] = p |
| 40 | 40 |
} |
| ... | ... |
@@ -43,10 +43,10 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I |
| 43 | 43 |
"Content-Type": {"application/x-tar"},
|
| 44 | 44 |
}) |
| 45 | 45 |
if err != nil {
|
| 46 |
- return LoadResponse{}, err
|
|
| 46 |
+ return ImageLoadResult{}, err
|
|
| 47 | 47 |
} |
| 48 |
- return LoadResponse{
|
|
| 49 |
- Body: resp.Body, |
|
| 48 |
+ return ImageLoadResult{
|
|
| 49 |
+ body: resp.Body, |
|
| 50 | 50 |
JSON: resp.Header.Get("Content-Type") == "application/json",
|
| 51 | 51 |
}, nil |
| 52 | 52 |
} |
| ... | ... |
@@ -73,8 +73,19 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I |
| 73 | 73 |
// |
| 74 | 74 |
// We should deprecated the "quiet" option, as it's really a client |
| 75 | 75 |
// responsibility. |
| 76 |
-type LoadResponse struct {
|
|
| 76 |
+type ImageLoadResult struct {
|
|
| 77 | 77 |
// Body must be closed to avoid a resource leak |
| 78 |
- Body io.ReadCloser |
|
| 78 |
+ body io.ReadCloser |
|
| 79 | 79 |
JSON bool |
| 80 | 80 |
} |
| 81 |
+ |
|
| 82 |
+func (r ImageLoadResult) Read(p []byte) (n int, err error) {
|
|
| 83 |
+ return r.body.Read(p) |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+func (r ImageLoadResult) Close() error {
|
|
| 87 |
+ if r.body == nil {
|
|
| 88 |
+ return nil |
|
| 89 |
+ } |
|
| 90 |
+ return r.body.Close() |
|
| 91 |
+} |
| ... | ... |
@@ -2,21 +2,20 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"context" |
| 5 |
- "io" |
|
| 6 | 5 |
"net/url" |
| 7 | 6 |
) |
| 8 | 7 |
|
| 9 | 8 |
// ImageSave retrieves one or more images from the docker host as an |
| 10 |
-// [io.ReadCloser]. |
|
| 9 |
+// [ImageSaveResult]. |
|
| 11 | 10 |
// |
| 12 | 11 |
// Platforms is an optional parameter that specifies the platforms to save |
| 13 | 12 |
// from the image. Passing a platform only has an effect if the input image |
| 14 | 13 |
// is a multi-platform image. |
| 15 |
-func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) {
|
|
| 14 |
+func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (ImageSaveResult, error) {
|
|
| 16 | 15 |
var opts imageSaveOpts |
| 17 | 16 |
for _, opt := range saveOpts {
|
| 18 | 17 |
if err := opt.Apply(&opts); err != nil {
|
| 19 |
- return nil, err |
|
| 18 |
+ return ImageSaveResult{}, err
|
|
| 20 | 19 |
} |
| 21 | 20 |
} |
| 22 | 21 |
|
| ... | ... |
@@ -26,18 +25,18 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts .. |
| 26 | 26 |
|
| 27 | 27 |
if len(opts.apiOptions.Platforms) > 0 {
|
| 28 | 28 |
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
|
| 29 |
- return nil, err |
|
| 29 |
+ return ImageSaveResult{}, err
|
|
| 30 | 30 |
} |
| 31 | 31 |
p, err := encodePlatforms(opts.apiOptions.Platforms...) |
| 32 | 32 |
if err != nil {
|
| 33 |
- return nil, err |
|
| 33 |
+ return ImageSaveResult{}, err
|
|
| 34 | 34 |
} |
| 35 | 35 |
query["platform"] = p |
| 36 | 36 |
} |
| 37 | 37 |
|
| 38 | 38 |
resp, err := cli.get(ctx, "/images/get", query, nil) |
| 39 | 39 |
if err != nil {
|
| 40 |
- return nil, err |
|
| 40 |
+ return ImageSaveResult{}, err
|
|
| 41 | 41 |
} |
| 42 |
- return resp.Body, nil |
|
| 42 |
+ return newImageSaveResult(resp.Body), nil |
|
| 43 | 43 |
} |
| ... | ... |
@@ -2,6 +2,8 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "io" |
|
| 6 |
+ "sync" |
|
| 5 | 7 |
|
| 6 | 8 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 7 | 9 |
) |
| ... | ... |
@@ -36,3 +38,34 @@ type imageSaveOptions struct {
|
| 36 | 36 |
// multi-platform image and has multiple variants. |
| 37 | 37 |
Platforms []ocispec.Platform |
| 38 | 38 |
} |
| 39 |
+ |
|
| 40 |
+func newImageSaveResult(rc io.ReadCloser) ImageSaveResult {
|
|
| 41 |
+ if rc == nil {
|
|
| 42 |
+ panic("nil io.ReadCloser")
|
|
| 43 |
+ } |
|
| 44 |
+ return ImageSaveResult{
|
|
| 45 |
+ rc: rc, |
|
| 46 |
+ close: sync.OnceValue(rc.Close), |
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+type ImageSaveResult struct {
|
|
| 51 |
+ rc io.ReadCloser |
|
| 52 |
+ close func() error |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+// Read implements io.ReadCloser |
|
| 56 |
+func (r ImageSaveResult) Read(p []byte) (n int, err error) {
|
|
| 57 |
+ if r.rc == nil {
|
|
| 58 |
+ return 0, io.EOF |
|
| 59 |
+ } |
|
| 60 |
+ return r.rc.Read(p) |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+// Close implements io.ReadCloser |
|
| 64 |
+func (r ImageSaveResult) Close() error {
|
|
| 65 |
+ if r.close == nil {
|
|
| 66 |
+ return nil |
|
| 67 |
+ } |
|
| 68 |
+ return r.close() |
|
| 69 |
+} |