client: Client.ImageLoad: 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-25 19:14:45 +02:00
parent be96014740
commit 849239cedf
8 changed files with 25 additions and 53 deletions

View File

@@ -15,6 +15,8 @@ type ImageLoadResult interface {
// ImageLoad loads an image in the docker host from the client host. It's up // ImageLoad loads an image in the docker host from the client host. It's up
// to the caller to close the [ImageLoadResult] returned by this function. // to the caller to close the [ImageLoadResult] returned by this function.
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) { func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) {
var opts imageLoadOpts var opts imageLoadOpts
for _, opt := range loadOpts { for _, opt := range loadOpts {
@@ -47,31 +49,16 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I
return nil, err return nil, err
} }
return &imageLoadResult{ return &imageLoadResult{
body: resp.Body, ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil }, nil
} }
// imageLoadResult returns information to the client about a load process. // imageLoadResult returns information to the client about a load process.
type imageLoadResult struct { type imageLoadResult struct {
// body must be closed to avoid a resource leak io.ReadCloser
body io.ReadCloser
} }
var ( var (
_ io.ReadCloser = (*imageLoadResult)(nil) _ io.ReadCloser = (*imageLoadResult)(nil)
_ ImageLoadResult = (*imageLoadResult)(nil) _ ImageLoadResult = (*imageLoadResult)(nil)
) )
func (r *imageLoadResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *imageLoadResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}

View File

@@ -106,8 +106,8 @@ func TestBuildUserNamespaceValidateCapabilitiesAreV2(t *testing.T) {
loadResp, err := clientNoUserRemap.ImageLoad(ctx, tarReader) loadResp, err := clientNoUserRemap.ImageLoad(ctx, tarReader)
assert.NilError(t, err, "failed to load image tar file") assert.NilError(t, err, "failed to load image tar file")
defer loadResp.Close() defer loadResp.Close()
buf = bytes.NewBuffer(nil) var buf2 bytes.Buffer
err = jsonmessage.DisplayJSONMessagesStream(loadResp, buf, 0, false, nil) err = jsonmessage.DisplayJSONMessagesStream(loadResp, &buf2, 0, false, nil)
assert.NilError(t, err) assert.NilError(t, err)
cid := container.Run(ctx, t, clientNoUserRemap, cid := container.Run(ctx, t, clientNoUserRemap,

View File

@@ -146,10 +146,10 @@ func TestMigrateSaveLoad(t *testing.T) {
assert.Equal(t, info.Images, 0) assert.Equal(t, info.Images, 0)
// Import // Import
lr, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), client.ImageLoadWithQuiet(true)) resp, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), client.ImageLoadWithQuiet(true))
assert.NilError(t, err) assert.NilError(t, err)
io.Copy(io.Discard, lr) _, _ = io.Copy(io.Discard, resp)
lr.Close() _ = resp.Close()
result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) { result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) {
c.Name = "Migration-save-load-" + snapshotter c.Name = "Migration-save-load-" + snapshotter

View File

@@ -343,11 +343,9 @@ func TestSaveAndLoadPlatform(t *testing.T) {
// load the full exported image (all platforms in it) // load the full exported image (all platforms in it)
resp, err := apiClient.ImageLoad(ctx, rdr) resp, err := apiClient.ImageLoad(ctx, rdr)
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.ReadAll(resp) _, _ = io.Copy(io.Discard, resp)
resp.Close() _ = resp.Close()
assert.NilError(t, err) _ = rdr.Close()
rdr.Close()
// verify the loaded image has all the expected platforms // verify the loaded image has all the expected platforms
for _, p := range tc.expectedSavedPlatforms { for _, p := range tc.expectedSavedPlatforms {
@@ -381,11 +379,9 @@ func TestSaveAndLoadPlatform(t *testing.T) {
// load the exported image on the specified platforms only // load the exported image on the specified platforms only
resp, err = apiClient.ImageLoad(ctx, rdr, client.ImageLoadWithPlatforms(tc.loadPlatforms...)) resp, err = apiClient.ImageLoad(ctx, rdr, client.ImageLoadWithPlatforms(tc.loadPlatforms...))
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.ReadAll(resp) _, _ = io.Copy(io.Discard, resp)
resp.Close() _ = resp.Close()
assert.NilError(t, err) _ = rdr.Close()
rdr.Close()
// verify the image was loaded for the specified platforms // verify the image was loaded for the specified platforms
for _, p := range tc.expectedLoadedPlatforms { for _, p := range tc.expectedLoadedPlatforms {

View File

@@ -30,7 +30,7 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu
resp, err := apiClient.ImageLoad(ctx, rc, client.ImageLoadWithQuiet(true)) resp, err := apiClient.ImageLoad(ctx, rc, client.ImageLoadWithQuiet(true))
assert.NilError(t, err, "Failed to load dangling image") assert.NilError(t, err, "Failed to load dangling image")
defer resp.Close() defer func() { _ = resp.Close() }()
if !assert.Check(t, err) { if !assert.Check(t, err) {
respBody, err := io.ReadAll(resp) respBody, err := io.ReadAll(resp)

View File

@@ -445,11 +445,12 @@ func imageLoad(ctx context.Context, apiClient client.APIClient, path string) err
return err return err
} }
defer file.Close() defer file.Close()
response, err := apiClient.ImageLoad(ctx, file, client.ImageLoadWithQuiet(true)) resp, err := apiClient.ImageLoad(ctx, file, client.ImageLoadWithQuiet(true))
if err != nil { if err != nil {
return err return err
} }
defer response.Close() _, _ = io.Copy(io.Discard, resp)
_ = resp.Close()
return nil return nil
} }

View File

@@ -886,7 +886,8 @@ func (d *Daemon) LoadImage(ctx context.Context, t testing.TB, img string) {
resp, err := c.ImageLoad(ctx, reader, client.ImageLoadWithQuiet(true)) resp, err := c.ImageLoad(ctx, reader, client.ImageLoadWithQuiet(true))
assert.NilError(t, err, "[%s] failed to load %s", d.id, img) assert.NilError(t, err, "[%s] failed to load %s", d.id, img)
defer resp.Close() _, _ = io.Copy(io.Discard, resp)
_ = resp.Close()
} }
func (d *Daemon) getClientConfig() (*clientConfig, error) { func (d *Daemon) getClientConfig() (*clientConfig, error) {

View File

@@ -15,6 +15,8 @@ type ImageLoadResult interface {
// ImageLoad loads an image in the docker host from the client host. It's up // ImageLoad loads an image in the docker host from the client host. It's up
// to the caller to close the [ImageLoadResult] returned by this function. // to the caller to close the [ImageLoadResult] returned by this function.
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) { func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (ImageLoadResult, error) {
var opts imageLoadOpts var opts imageLoadOpts
for _, opt := range loadOpts { for _, opt := range loadOpts {
@@ -47,31 +49,16 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...I
return nil, err return nil, err
} }
return &imageLoadResult{ return &imageLoadResult{
body: resp.Body, ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil }, nil
} }
// imageLoadResult returns information to the client about a load process. // imageLoadResult returns information to the client about a load process.
type imageLoadResult struct { type imageLoadResult struct {
// body must be closed to avoid a resource leak io.ReadCloser
body io.ReadCloser
} }
var ( var (
_ io.ReadCloser = (*imageLoadResult)(nil) _ io.ReadCloser = (*imageLoadResult)(nil)
_ ImageLoadResult = (*imageLoadResult)(nil) _ ImageLoadResult = (*imageLoadResult)(nil)
) )
func (r *imageLoadResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *imageLoadResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}