Use a cancelReadCloser to automatically close the reader when the context
is cancelled. Consumers are still recommended to manually close the reader,
but the cancelReadCloser makes the Close idempotent.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -11,7 +11,8 @@ type ImageSaveResult interface {
|
| 11 | 11 |
} |
| 12 | 12 |
|
| 13 | 13 |
// ImageSave retrieves one or more images from the docker host as an |
| 14 |
-// [ImageSaveResult]. |
|
| 14 |
+// [ImageSaveResult]. Callers should close the reader, but the underlying |
|
| 15 |
+// [io.ReadCloser] is automatically closed if the context is canceled, |
|
| 15 | 16 |
// |
| 16 | 17 |
// Platforms is an optional parameter that specifies the platforms to save |
| 17 | 18 |
// from the image. Passing a platform only has an effect if the input image |
| ... | ... |
@@ -44,30 +45,15 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts .. |
| 44 | 44 |
return nil, err |
| 45 | 45 |
} |
| 46 | 46 |
return &imageSaveResult{
|
| 47 |
- body: resp.Body, |
|
| 47 |
+ ReadCloser: newCancelReadCloser(ctx, resp.Body), |
|
| 48 | 48 |
}, nil |
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 | 51 |
type imageSaveResult struct {
|
| 52 |
- // body must be closed to avoid a resource leak |
|
| 53 |
- body io.ReadCloser |
|
| 52 |
+ io.ReadCloser |
|
| 54 | 53 |
} |
| 55 | 54 |
|
| 56 | 55 |
var ( |
| 57 | 56 |
_ io.ReadCloser = (*imageSaveResult)(nil) |
| 58 | 57 |
_ ImageSaveResult = (*imageSaveResult)(nil) |
| 59 | 58 |
) |
| 60 |
- |
|
| 61 |
-func (r *imageSaveResult) Read(p []byte) (int, error) {
|
|
| 62 |
- if r == nil || r.body == nil {
|
|
| 63 |
- return 0, io.EOF |
|
| 64 |
- } |
|
| 65 |
- return r.body.Read(p) |
|
| 66 |
-} |
|
| 67 |
- |
|
| 68 |
-func (r *imageSaveResult) Close() error {
|
|
| 69 |
- if r == nil || r.body == nil {
|
|
| 70 |
- return nil |
|
| 71 |
- } |
|
| 72 |
- return r.body.Close() |
|
| 73 |
-} |
| ... | ... |
@@ -16,8 +16,11 @@ func (f imageSaveOptionFunc) Apply(o *imageSaveOpts) error {
|
| 16 | 16 |
return f(o) |
| 17 | 17 |
} |
| 18 | 18 |
|
| 19 |
-// ImageSaveWithPlatforms sets the platforms to be saved from the image. |
|
| 19 |
+// ImageSaveWithPlatforms sets the platforms to be saved from the image. It |
|
| 20 |
+// produces an error if platforms are already set. This option only has an |
|
| 21 |
+// effect if the input image is a multi-platform image. |
|
| 20 | 22 |
func ImageSaveWithPlatforms(platforms ...ocispec.Platform) ImageSaveOption {
|
| 23 |
+ // TODO(thaJeztah): verify the GoDoc; do we produce an error for a single-platform image without the given platform? |
|
| 21 | 24 |
return imageSaveOptionFunc(func(opt *imageSaveOpts) error {
|
| 22 | 25 |
if opt.apiOptions.Platforms != nil {
|
| 23 | 26 |
return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms)
|
| ... | ... |
@@ -1,7 +1,6 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "context" |
|
| 5 | 4 |
"io" |
| 6 | 5 |
"net/http" |
| 7 | 6 |
"net/url" |
| ... | ... |
@@ -17,7 +16,7 @@ func TestImageSaveError(t *testing.T) {
|
| 17 | 17 |
client, err := New(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) |
| 18 | 18 |
assert.NilError(t, err) |
| 19 | 19 |
armv64 := ocispec.Platform{Architecture: "arm64", OS: "linux", Variant: "v8"}
|
| 20 |
- _, err = client.ImageSave(context.Background(), []string{"nothing"}, ImageSaveWithPlatforms(armv64))
|
|
| 20 |
+ _, err = client.ImageSave(t.Context(), []string{"nothing"}, ImageSaveWithPlatforms(armv64))
|
|
| 21 | 21 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) |
| 22 | 22 |
} |
| 23 | 23 |
|
| ... | ... |
@@ -69,7 +68,7 @@ func TestImageSave(t *testing.T) {
|
| 69 | 69 |
return mockResponse(http.StatusOK, nil, expectedOutput)(req) |
| 70 | 70 |
})) |
| 71 | 71 |
assert.NilError(t, err) |
| 72 |
- resp, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"}, tc.options...)
|
|
| 72 |
+ resp, err := client.ImageSave(t.Context(), []string{"image_id1", "image_id2"}, tc.options...)
|
|
| 73 | 73 |
assert.NilError(t, err) |
| 74 | 74 |
defer func() {
|
| 75 | 75 |
assert.NilError(t, resp.Close()) |
| ... | ... |
@@ -75,7 +75,7 @@ func TestBuildUserNamespaceValidateCapabilitiesAreV2(t *testing.T) {
|
| 75 | 75 |
|
| 76 | 76 |
reader, err := clientUserRemap.ImageSave(ctx, []string{imageTag})
|
| 77 | 77 |
assert.NilError(t, err, "failed to download capabilities image") |
| 78 |
- defer reader.Close() |
|
| 78 |
+ defer func() { _ = reader.Close() }()
|
|
| 79 | 79 |
|
| 80 | 80 |
tar, err := os.Create(filepath.Join(tmpDir, "image.tar")) |
| 81 | 81 |
assert.NilError(t, err, "failed to create image tar file") |
| ... | ... |
@@ -130,8 +130,8 @@ func TestMigrateSaveLoad(t *testing.T) {
|
| 130 | 130 |
rdr, err := apiClient.ImageSave(ctx, []string{"busybox:latest"})
|
| 131 | 131 |
assert.NilError(t, err) |
| 132 | 132 |
buf := bytes.NewBuffer(nil) |
| 133 |
- io.Copy(buf, rdr) |
|
| 134 |
- rdr.Close() |
|
| 133 |
+ _, _ = io.Copy(buf, rdr) |
|
| 134 |
+ defer func() { _ = rdr.Close() }()
|
|
| 135 | 135 |
|
| 136 | 136 |
// Delete all images |
| 137 | 137 |
list, err := apiClient.ImageList(ctx, client.ImageListOptions{})
|
| ... | ... |
@@ -44,7 +44,7 @@ func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS {
|
| 44 | 44 |
assert.NilError(t, err) |
| 45 | 45 |
|
| 46 | 46 |
// Do not close at the end of this function otherwise the indexer won't work |
| 47 |
- t.Cleanup(func() { f.Close() })
|
|
| 47 |
+ t.Cleanup(func() { _ = f.Close() })
|
|
| 48 | 48 |
|
| 49 | 49 |
_, err = io.Copy(f, rdr) |
| 50 | 50 |
assert.NilError(t, err) |
| ... | ... |
@@ -64,6 +64,7 @@ func TestSaveCheckTimes(t *testing.T) {
|
| 64 | 64 |
|
| 65 | 65 |
rdr, err := apiClient.ImageSave(ctx, []string{repoName})
|
| 66 | 66 |
assert.NilError(t, err) |
| 67 |
+ defer func() { _ = rdr.Close() }()
|
|
| 67 | 68 |
|
| 68 | 69 |
created, err := time.Parse(time.RFC3339, img.Created) |
| 69 | 70 |
assert.NilError(t, err) |
| ... | ... |
@@ -135,7 +136,7 @@ func TestSaveOCI(t *testing.T) {
|
| 135 | 135 |
|
| 136 | 136 |
rdr, err := apiClient.ImageSave(ctx, []string{tc.image})
|
| 137 | 137 |
assert.NilError(t, err) |
| 138 |
- defer rdr.Close() |
|
| 138 |
+ defer func() { _ = rdr.Close() }()
|
|
| 139 | 139 |
|
| 140 | 140 |
tarfs := tarIndexFS(t, rdr) |
| 141 | 141 |
|
| ... | ... |
@@ -171,7 +172,7 @@ func TestSaveOCI(t *testing.T) {
|
| 171 | 171 |
assert.NilError(t, err) |
| 172 | 172 |
|
| 173 | 173 |
layerDigest, err := testutil.UncompressedTarDigest(f) |
| 174 |
- f.Close() |
|
| 174 |
+ _ = f.Close() |
|
| 175 | 175 |
|
| 176 | 176 |
assert.NilError(t, err) |
| 177 | 177 |
|
| ... | ... |
@@ -426,7 +427,7 @@ func TestSaveRepoWithMultipleImages(t *testing.T) {
|
| 426 | 426 |
|
| 427 | 427 |
rdr, err := apiClient.ImageSave(ctx, []string{repoName, "busybox:latest"})
|
| 428 | 428 |
assert.NilError(t, err) |
| 429 |
- defer rdr.Close() |
|
| 429 |
+ defer func() { _ = rdr.Close() }()
|
|
| 430 | 430 |
|
| 431 | 431 |
tarfs := tarIndexFS(t, rdr) |
| 432 | 432 |
|
| ... | ... |
@@ -483,7 +484,7 @@ RUN touch /opt/a/b/c && chown user:user /opt/a/b/c` |
| 483 | 483 |
|
| 484 | 484 |
rdr, err := apiClient.ImageSave(ctx, []string{imgID})
|
| 485 | 485 |
assert.NilError(t, err) |
| 486 |
- defer rdr.Close() |
|
| 486 |
+ defer func() { _ = rdr.Close() }()
|
|
| 487 | 487 |
|
| 488 | 488 |
tarfs := tarIndexFS(t, rdr) |
| 489 | 489 |
|
| ... | ... |
@@ -425,17 +425,17 @@ func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
|
| 425 | 425 |
} |
| 426 | 426 |
|
| 427 | 427 |
func imageSave(ctx context.Context, apiClient client.APIClient, path, imgRef string) error {
|
| 428 |
- responseReader, err := apiClient.ImageSave(ctx, []string{imgRef})
|
|
| 428 |
+ resp, err := apiClient.ImageSave(ctx, []string{imgRef})
|
|
| 429 | 429 |
if err != nil {
|
| 430 | 430 |
return err |
| 431 | 431 |
} |
| 432 |
- defer responseReader.Close() |
|
| 432 |
+ defer func() { _ = resp.Close() }()
|
|
| 433 | 433 |
file, err := os.Create(path) |
| 434 | 434 |
if err != nil {
|
| 435 | 435 |
return err |
| 436 | 436 |
} |
| 437 |
- defer file.Close() |
|
| 438 |
- _, err = io.Copy(file, responseReader) |
|
| 437 |
+ _, err = io.Copy(file, resp) |
|
| 438 |
+ _ = file.Close() |
|
| 439 | 439 |
return err |
| 440 | 440 |
} |
| 441 | 441 |
|
| ... | ... |
@@ -879,7 +879,7 @@ func (d *Daemon) LoadImage(ctx context.Context, t testing.TB, img string) {
|
| 879 | 879 |
|
| 880 | 880 |
reader, err := clientHost.ImageSave(ctx, []string{img})
|
| 881 | 881 |
assert.NilError(t, err, "[%s] failed to download %s", d.id, img) |
| 882 |
- defer reader.Close() |
|
| 882 |
+ defer func() { _ = reader.Close() }()
|
|
| 883 | 883 |
|
| 884 | 884 |
c := d.NewClientT(t) |
| 885 | 885 |
defer c.Close() |
| ... | ... |
@@ -11,7 +11,8 @@ type ImageSaveResult interface {
|
| 11 | 11 |
} |
| 12 | 12 |
|
| 13 | 13 |
// ImageSave retrieves one or more images from the docker host as an |
| 14 |
-// [ImageSaveResult]. |
|
| 14 |
+// [ImageSaveResult]. Callers should close the reader, but the underlying |
|
| 15 |
+// [io.ReadCloser] is automatically closed if the context is canceled, |
|
| 15 | 16 |
// |
| 16 | 17 |
// Platforms is an optional parameter that specifies the platforms to save |
| 17 | 18 |
// from the image. Passing a platform only has an effect if the input image |
| ... | ... |
@@ -44,30 +45,15 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts .. |
| 44 | 44 |
return nil, err |
| 45 | 45 |
} |
| 46 | 46 |
return &imageSaveResult{
|
| 47 |
- body: resp.Body, |
|
| 47 |
+ ReadCloser: newCancelReadCloser(ctx, resp.Body), |
|
| 48 | 48 |
}, nil |
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 | 51 |
type imageSaveResult struct {
|
| 52 |
- // body must be closed to avoid a resource leak |
|
| 53 |
- body io.ReadCloser |
|
| 52 |
+ io.ReadCloser |
|
| 54 | 53 |
} |
| 55 | 54 |
|
| 56 | 55 |
var ( |
| 57 | 56 |
_ io.ReadCloser = (*imageSaveResult)(nil) |
| 58 | 57 |
_ ImageSaveResult = (*imageSaveResult)(nil) |
| 59 | 58 |
) |
| 60 |
- |
|
| 61 |
-func (r *imageSaveResult) Read(p []byte) (int, error) {
|
|
| 62 |
- if r == nil || r.body == nil {
|
|
| 63 |
- return 0, io.EOF |
|
| 64 |
- } |
|
| 65 |
- return r.body.Read(p) |
|
| 66 |
-} |
|
| 67 |
- |
|
| 68 |
-func (r *imageSaveResult) Close() error {
|
|
| 69 |
- if r == nil || r.body == nil {
|
|
| 70 |
- return nil |
|
| 71 |
- } |
|
| 72 |
- return r.body.Close() |
|
| 73 |
-} |
| ... | ... |
@@ -16,8 +16,11 @@ func (f imageSaveOptionFunc) Apply(o *imageSaveOpts) error {
|
| 16 | 16 |
return f(o) |
| 17 | 17 |
} |
| 18 | 18 |
|
| 19 |
-// ImageSaveWithPlatforms sets the platforms to be saved from the image. |
|
| 19 |
+// ImageSaveWithPlatforms sets the platforms to be saved from the image. It |
|
| 20 |
+// produces an error if platforms are already set. This option only has an |
|
| 21 |
+// effect if the input image is a multi-platform image. |
|
| 20 | 22 |
func ImageSaveWithPlatforms(platforms ...ocispec.Platform) ImageSaveOption {
|
| 23 |
+ // TODO(thaJeztah): verify the GoDoc; do we produce an error for a single-platform image without the given platform? |
|
| 21 | 24 |
return imageSaveOptionFunc(func(opt *imageSaveOpts) error {
|
| 22 | 25 |
if opt.apiOptions.Platforms != nil {
|
| 23 | 26 |
return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms)
|