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>
| ... | ... |
@@ -19,6 +19,8 @@ type ContainerExportResult interface {
|
| 19 | 19 |
// ContainerExport retrieves the raw contents of a container |
| 20 | 20 |
// and returns them as an [io.ReadCloser]. It's up to the caller |
| 21 | 21 |
// to close the stream. |
| 22 |
+// |
|
| 23 |
+// The underlying [io.ReadCloser] is automatically closed if the context is canceled, |
|
| 22 | 24 |
func (cli *Client) ContainerExport(ctx context.Context, containerID string, options ContainerExportOptions) (ContainerExportResult, error) {
|
| 23 | 25 |
containerID, err := trimID("container", containerID)
|
| 24 | 26 |
if err != nil {
|
| ... | ... |
@@ -31,30 +33,15 @@ func (cli *Client) ContainerExport(ctx context.Context, containerID string, opti |
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 | 33 |
return &containerExportResult{
|
| 34 |
- body: resp.Body, |
|
| 34 |
+ ReadCloser: newCancelReadCloser(ctx, resp.Body), |
|
| 35 | 35 |
}, nil |
| 36 | 36 |
} |
| 37 | 37 |
|
| 38 | 38 |
type containerExportResult struct {
|
| 39 |
- // body must be closed to avoid a resource leak |
|
| 40 |
- body io.ReadCloser |
|
| 39 |
+ io.ReadCloser |
|
| 41 | 40 |
} |
| 42 | 41 |
|
| 43 | 42 |
var ( |
| 44 | 43 |
_ io.ReadCloser = (*containerExportResult)(nil) |
| 45 | 44 |
_ ContainerExportResult = (*containerExportResult)(nil) |
| 46 | 45 |
) |
| 47 |
- |
|
| 48 |
-func (r *containerExportResult) Read(p []byte) (int, error) {
|
|
| 49 |
- if r == nil || r.body == nil {
|
|
| 50 |
- return 0, io.EOF |
|
| 51 |
- } |
|
| 52 |
- return r.body.Read(p) |
|
| 53 |
-} |
|
| 54 |
- |
|
| 55 |
-func (r *containerExportResult) Close() error {
|
|
| 56 |
- if r == nil || r.body == nil {
|
|
| 57 |
- return nil |
|
| 58 |
- } |
|
| 59 |
- return r.body.Close() |
|
| 60 |
-} |
| ... | ... |
@@ -1,7 +1,6 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "context" |
|
| 5 | 4 |
"io" |
| 6 | 5 |
"net/http" |
| 7 | 6 |
"testing" |
| ... | ... |
@@ -17,14 +16,14 @@ func TestContainerExportError(t *testing.T) {
|
| 17 | 17 |
) |
| 18 | 18 |
assert.NilError(t, err) |
| 19 | 19 |
|
| 20 |
- _, err = client.ContainerExport(context.Background(), "nothing", ContainerExportOptions{})
|
|
| 20 |
+ _, err = client.ContainerExport(t.Context(), "nothing", ContainerExportOptions{})
|
|
| 21 | 21 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) |
| 22 | 22 |
|
| 23 |
- _, err = client.ContainerExport(context.Background(), "", ContainerExportOptions{})
|
|
| 23 |
+ _, err = client.ContainerExport(t.Context(), "", ContainerExportOptions{})
|
|
| 24 | 24 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 25 | 25 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 26 | 26 |
|
| 27 |
- _, err = client.ContainerExport(context.Background(), " ", ContainerExportOptions{})
|
|
| 27 |
+ _, err = client.ContainerExport(t.Context(), " ", ContainerExportOptions{})
|
|
| 28 | 28 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 29 | 29 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 30 | 30 |
} |
| ... | ... |
@@ -40,10 +39,10 @@ func TestContainerExport(t *testing.T) {
|
| 40 | 40 |
}), |
| 41 | 41 |
) |
| 42 | 42 |
assert.NilError(t, err) |
| 43 |
- body, err := client.ContainerExport(context.Background(), "container_id", ContainerExportOptions{})
|
|
| 43 |
+ res, err := client.ContainerExport(t.Context(), "container_id", ContainerExportOptions{})
|
|
| 44 | 44 |
assert.NilError(t, err) |
| 45 |
- defer body.Close() |
|
| 46 |
- content, err := io.ReadAll(body) |
|
| 45 |
+ defer res.Close() |
|
| 46 |
+ content, err := io.ReadAll(res) |
|
| 47 | 47 |
assert.NilError(t, err) |
| 48 | 48 |
assert.Check(t, is.Equal(string(content), "response")) |
| 49 | 49 |
} |
| ... | ... |
@@ -103,11 +103,11 @@ func (s *DockerAPISuite) TestContainerAPIGetExport(c *testing.T) {
|
| 103 | 103 |
assert.NilError(c, err) |
| 104 | 104 |
defer apiClient.Close() |
| 105 | 105 |
|
| 106 |
- body, err := apiClient.ContainerExport(testutil.GetContext(c), name, client.ContainerExportOptions{})
|
|
| 106 |
+ res, err := apiClient.ContainerExport(testutil.GetContext(c), name, client.ContainerExportOptions{})
|
|
| 107 | 107 |
assert.NilError(c, err) |
| 108 |
- defer body.Close() |
|
| 108 |
+ defer res.Close() |
|
| 109 | 109 |
found := false |
| 110 |
- for tarReader := tar.NewReader(body); ; {
|
|
| 110 |
+ for tarReader := tar.NewReader(res); ; {
|
|
| 111 | 111 |
h, err := tarReader.Next() |
| 112 | 112 |
if errors.Is(err, io.EOF) {
|
| 113 | 113 |
break |
| ... | ... |
@@ -71,6 +71,7 @@ func TestExportContainerAfterDaemonRestart(t *testing.T) {
|
| 71 | 71 |
|
| 72 | 72 |
d.Restart(t) |
| 73 | 73 |
|
| 74 |
- _, err := c.ContainerExport(ctx, ctrID, client.ContainerExportOptions{})
|
|
| 74 |
+ res, err := c.ContainerExport(ctx, ctrID, client.ContainerExportOptions{})
|
|
| 75 | 75 |
assert.NilError(t, err) |
| 76 |
+ _ = res.Close() |
|
| 76 | 77 |
} |
| ... | ... |
@@ -32,10 +32,10 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
|
| 32 | 32 |
return err |
| 33 | 33 |
}}, |
| 34 | 34 |
{name: "export", operation: func(*testing.T) error {
|
| 35 |
- rc, err := apiClient.ContainerExport(ctx, cID, client.ContainerExportOptions{})
|
|
| 35 |
+ res, err := apiClient.ContainerExport(ctx, cID, client.ContainerExportOptions{})
|
|
| 36 | 36 |
if err == nil {
|
| 37 |
- defer rc.Close() |
|
| 38 |
- _, err = io.Copy(io.Discard, rc) |
|
| 37 |
+ _, err = io.Copy(io.Discard, res) |
|
| 38 |
+ _ = res.Close() |
|
| 39 | 39 |
} |
| 40 | 40 |
return err |
| 41 | 41 |
}}, |
| ... | ... |
@@ -48,8 +48,8 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
|
| 48 | 48 |
{name: "cp from container", operation: func(*testing.T) error {
|
| 49 | 49 |
res, err := apiClient.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/file"})
|
| 50 | 50 |
if err == nil {
|
| 51 |
- defer res.Content.Close() |
|
| 52 | 51 |
_, err = io.Copy(io.Discard, res.Content) |
| 52 |
+ _ = res.Content.Close() |
|
| 53 | 53 |
} |
| 54 | 54 |
|
| 55 | 55 |
return err |
| ... | ... |
@@ -363,13 +363,13 @@ func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
|
| 363 | 363 |
|
| 364 | 364 |
cID := container.Run(ctx, t, c) |
| 365 | 365 |
|
| 366 |
- responseReader, err := c.ContainerExport(ctx, cID, client.ContainerExportOptions{})
|
|
| 366 |
+ res, err := c.ContainerExport(ctx, cID, client.ContainerExportOptions{})
|
|
| 367 | 367 |
assert.NilError(t, err) |
| 368 |
- defer responseReader.Close() |
|
| 368 |
+ defer func() { _ = res.Close() }()
|
|
| 369 | 369 |
file, err := os.Create(exportedImagePath) |
| 370 | 370 |
assert.NilError(t, err) |
| 371 |
- defer file.Close() |
|
| 372 |
- _, err = io.Copy(file, responseReader) |
|
| 371 |
+ defer func() { _ = file.Close() }()
|
|
| 372 |
+ _, err = io.Copy(file, res) |
|
| 373 | 373 |
assert.NilError(t, err) |
| 374 | 374 |
|
| 375 | 375 |
err = imageImport(ctx, c, exportedImagePath) |
| ... | ... |
@@ -19,6 +19,8 @@ type ContainerExportResult interface {
|
| 19 | 19 |
// ContainerExport retrieves the raw contents of a container |
| 20 | 20 |
// and returns them as an [io.ReadCloser]. It's up to the caller |
| 21 | 21 |
// to close the stream. |
| 22 |
+// |
|
| 23 |
+// The underlying [io.ReadCloser] is automatically closed if the context is canceled, |
|
| 22 | 24 |
func (cli *Client) ContainerExport(ctx context.Context, containerID string, options ContainerExportOptions) (ContainerExportResult, error) {
|
| 23 | 25 |
containerID, err := trimID("container", containerID)
|
| 24 | 26 |
if err != nil {
|
| ... | ... |
@@ -31,30 +33,15 @@ func (cli *Client) ContainerExport(ctx context.Context, containerID string, opti |
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 | 33 |
return &containerExportResult{
|
| 34 |
- body: resp.Body, |
|
| 34 |
+ ReadCloser: newCancelReadCloser(ctx, resp.Body), |
|
| 35 | 35 |
}, nil |
| 36 | 36 |
} |
| 37 | 37 |
|
| 38 | 38 |
type containerExportResult struct {
|
| 39 |
- // body must be closed to avoid a resource leak |
|
| 40 |
- body io.ReadCloser |
|
| 39 |
+ io.ReadCloser |
|
| 41 | 40 |
} |
| 42 | 41 |
|
| 43 | 42 |
var ( |
| 44 | 43 |
_ io.ReadCloser = (*containerExportResult)(nil) |
| 45 | 44 |
_ ContainerExportResult = (*containerExportResult)(nil) |
| 46 | 45 |
) |
| 47 |
- |
|
| 48 |
-func (r *containerExportResult) Read(p []byte) (int, error) {
|
|
| 49 |
- if r == nil || r.body == nil {
|
|
| 50 |
- return 0, io.EOF |
|
| 51 |
- } |
|
| 52 |
- return r.body.Read(p) |
|
| 53 |
-} |
|
| 54 |
- |
|
| 55 |
-func (r *containerExportResult) Close() error {
|
|
| 56 |
- if r == nil || r.body == nil {
|
|
| 57 |
- return nil |
|
| 58 |
- } |
|
| 59 |
- return r.body.Close() |
|
| 60 |
-} |