client: Client.ContainerExport: close reader on context cancellation

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>
This commit is contained in:
Sebastiaan van Stijn
2025-10-30 09:55:45 +01:00
parent 08cd02cab6
commit c5aedacb4f
7 changed files with 27 additions and 53 deletions

View File

@@ -19,6 +19,8 @@ type ContainerExportResult interface {
// ContainerExport retrieves the raw contents of a container
// and returns them as an [io.ReadCloser]. It's up to the caller
// to close the stream.
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) ContainerExport(ctx context.Context, containerID string, options ContainerExportOptions) (ContainerExportResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
@@ -31,30 +33,15 @@ func (cli *Client) ContainerExport(ctx context.Context, containerID string, opti
}
return &containerExportResult{
body: resp.Body,
ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil
}
type containerExportResult struct {
// body must be closed to avoid a resource leak
body io.ReadCloser
io.ReadCloser
}
var (
_ io.ReadCloser = (*containerExportResult)(nil)
_ ContainerExportResult = (*containerExportResult)(nil)
)
func (r *containerExportResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *containerExportResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}

View File

@@ -1,7 +1,6 @@
package client
import (
"context"
"io"
"net/http"
"testing"
@@ -17,14 +16,14 @@ func TestContainerExportError(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerExport(context.Background(), "nothing", ContainerExportOptions{})
_, err = client.ContainerExport(t.Context(), "nothing", ContainerExportOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.ContainerExport(context.Background(), "", ContainerExportOptions{})
_, err = client.ContainerExport(t.Context(), "", ContainerExportOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.ContainerExport(context.Background(), " ", ContainerExportOptions{})
_, err = client.ContainerExport(t.Context(), " ", ContainerExportOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -40,10 +39,10 @@ func TestContainerExport(t *testing.T) {
}),
)
assert.NilError(t, err)
body, err := client.ContainerExport(context.Background(), "container_id", ContainerExportOptions{})
res, err := client.ContainerExport(t.Context(), "container_id", ContainerExportOptions{})
assert.NilError(t, err)
defer body.Close()
content, err := io.ReadAll(body)
defer res.Close()
content, err := io.ReadAll(res)
assert.NilError(t, err)
assert.Check(t, is.Equal(string(content), "response"))
}