client: Client.ImageImport: 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-27 16:50:14 +01:00
parent 849239cedf
commit 08cd02cab6
4 changed files with 20 additions and 41 deletions

View File

@@ -13,8 +13,10 @@ type ImageImportResult interface {
io.ReadCloser io.ReadCloser
} }
// ImageImport creates a new image based on the source options. // ImageImport creates a new image based on the source options. It returns the
// It returns the JSON content in the response body. // JSON content in the [ImageImportResult].
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (ImageImportResult, error) { func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (ImageImportResult, error) {
if ref != "" { if ref != "" {
// Check if the given image name can be resolved // Check if the given image name can be resolved
@@ -48,30 +50,17 @@ func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, re
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &imageImportResult{body: resp.Body}, nil return &imageImportResult{
ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil
} }
// ImageImportResult holds the response body returned by the daemon for image import. // ImageImportResult holds the response body returned by the daemon for image import.
type imageImportResult struct { type imageImportResult struct {
// body must be closed to avoid a resource leak io.ReadCloser
body io.ReadCloser
} }
var ( var (
_ io.ReadCloser = (*imageImportResult)(nil) _ io.ReadCloser = (*imageImportResult)(nil)
_ ImageImportResult = (*imageImportResult)(nil) _ ImageImportResult = (*imageImportResult)(nil)
) )
func (r *imageImportResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *imageImportResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}

View File

@@ -34,6 +34,7 @@ func TestExportContainerAndImportImage(t *testing.T) {
SourceName: "-", SourceName: "-",
}, reference, client.ImageImportOptions{}) }, reference, client.ImageImportOptions{})
assert.NilError(t, err) assert.NilError(t, err)
defer func() { _ = importRes.Close() }()
// If the import is successfully, then the message output should contain // If the import is successfully, then the message output should contain
// the image ID and match with the output from `docker images`. // the image ID and match with the output from `docker images`.

View File

@@ -460,17 +460,17 @@ func imageImport(ctx context.Context, apiClient client.APIClient, path string) e
return err return err
} }
defer file.Close() defer file.Close()
options := client.ImageImportOptions{}
ref := "" ref := ""
source := client.ImageImportSource{ source := client.ImageImportSource{
Source: file, Source: file,
SourceName: "-", SourceName: "-",
} }
responseReader, err := apiClient.ImageImport(ctx, source, ref, options) resp, err := apiClient.ImageImport(ctx, source, ref, client.ImageImportOptions{})
if err != nil { if err != nil {
return err return err
} }
defer responseReader.Close() _, _ = io.Copy(io.Discard, resp)
_ = resp.Close()
return nil return nil
} }

View File

@@ -13,8 +13,10 @@ type ImageImportResult interface {
io.ReadCloser io.ReadCloser
} }
// ImageImport creates a new image based on the source options. // ImageImport creates a new image based on the source options. It returns the
// It returns the JSON content in the response body. // JSON content in the [ImageImportResult].
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (ImageImportResult, error) { func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (ImageImportResult, error) {
if ref != "" { if ref != "" {
// Check if the given image name can be resolved // Check if the given image name can be resolved
@@ -48,30 +50,17 @@ func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, re
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &imageImportResult{body: resp.Body}, nil return &imageImportResult{
ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil
} }
// ImageImportResult holds the response body returned by the daemon for image import. // ImageImportResult holds the response body returned by the daemon for image import.
type imageImportResult struct { type imageImportResult struct {
// body must be closed to avoid a resource leak io.ReadCloser
body io.ReadCloser
} }
var ( var (
_ io.ReadCloser = (*imageImportResult)(nil) _ io.ReadCloser = (*imageImportResult)(nil)
_ ImageImportResult = (*imageImportResult)(nil) _ ImageImportResult = (*imageImportResult)(nil)
) )
func (r *imageImportResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *imageImportResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}