Browse code

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>

Sebastiaan van Stijn authored on 2025/10/28 00:50:14
Showing 4 changed files
... ...
@@ -13,8 +13,10 @@ type ImageImportResult interface {
13 13
 	io.ReadCloser
14 14
 }
15 15
 
16
-// ImageImport creates a new image based on the source options.
17
-// It returns the JSON content in the response body.
16
+// ImageImport creates a new image based on the source options. It returns the
17
+// JSON content in the [ImageImportResult].
18
+//
19
+// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
18 20
 func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (ImageImportResult, error) {
19 21
 	if ref != "" {
20 22
 		// Check if the given image name can be resolved
... ...
@@ -48,30 +50,17 @@ func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, re
48 48
 	if err != nil {
49 49
 		return nil, err
50 50
 	}
51
-	return &imageImportResult{body: resp.Body}, nil
51
+	return &imageImportResult{
52
+		ReadCloser: newCancelReadCloser(ctx, resp.Body),
53
+	}, nil
52 54
 }
53 55
 
54 56
 // ImageImportResult holds the response body returned by the daemon for image import.
55 57
 type imageImportResult struct {
56
-	// body must be closed to avoid a resource leak
57
-	body io.ReadCloser
58
+	io.ReadCloser
58 59
 }
59 60
 
60 61
 var (
61 62
 	_ io.ReadCloser     = (*imageImportResult)(nil)
62 63
 	_ ImageImportResult = (*imageImportResult)(nil)
63 64
 )
64
-
65
-func (r *imageImportResult) Read(p []byte) (int, error) {
66
-	if r == nil || r.body == nil {
67
-		return 0, io.EOF
68
-	}
69
-	return r.body.Read(p)
70
-}
71
-
72
-func (r *imageImportResult) Close() error {
73
-	if r == nil || r.body == nil {
74
-		return nil
75
-	}
76
-	return r.body.Close()
77
-}
... ...
@@ -34,6 +34,7 @@ func TestExportContainerAndImportImage(t *testing.T) {
34 34
 		SourceName: "-",
35 35
 	}, reference, client.ImageImportOptions{})
36 36
 	assert.NilError(t, err)
37
+	defer func() { _ = importRes.Close() }()
37 38
 
38 39
 	// If the import is successfully, then the message output should contain
39 40
 	// the image ID and match with the output from `docker images`.
... ...
@@ -460,17 +460,17 @@ func imageImport(ctx context.Context, apiClient client.APIClient, path string) e
460 460
 		return err
461 461
 	}
462 462
 	defer file.Close()
463
-	options := client.ImageImportOptions{}
464 463
 	ref := ""
465 464
 	source := client.ImageImportSource{
466 465
 		Source:     file,
467 466
 		SourceName: "-",
468 467
 	}
469
-	responseReader, err := apiClient.ImageImport(ctx, source, ref, options)
468
+	resp, err := apiClient.ImageImport(ctx, source, ref, client.ImageImportOptions{})
470 469
 	if err != nil {
471 470
 		return err
472 471
 	}
473
-	defer responseReader.Close()
472
+	_, _ = io.Copy(io.Discard, resp)
473
+	_ = resp.Close()
474 474
 	return nil
475 475
 }
476 476
 
... ...
@@ -13,8 +13,10 @@ type ImageImportResult interface {
13 13
 	io.ReadCloser
14 14
 }
15 15
 
16
-// ImageImport creates a new image based on the source options.
17
-// It returns the JSON content in the response body.
16
+// ImageImport creates a new image based on the source options. It returns the
17
+// JSON content in the [ImageImportResult].
18
+//
19
+// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
18 20
 func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (ImageImportResult, error) {
19 21
 	if ref != "" {
20 22
 		// Check if the given image name can be resolved
... ...
@@ -48,30 +50,17 @@ func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, re
48 48
 	if err != nil {
49 49
 		return nil, err
50 50
 	}
51
-	return &imageImportResult{body: resp.Body}, nil
51
+	return &imageImportResult{
52
+		ReadCloser: newCancelReadCloser(ctx, resp.Body),
53
+	}, nil
52 54
 }
53 55
 
54 56
 // ImageImportResult holds the response body returned by the daemon for image import.
55 57
 type imageImportResult struct {
56
-	// body must be closed to avoid a resource leak
57
-	body io.ReadCloser
58
+	io.ReadCloser
58 59
 }
59 60
 
60 61
 var (
61 62
 	_ io.ReadCloser     = (*imageImportResult)(nil)
62 63
 	_ ImageImportResult = (*imageImportResult)(nil)
63 64
 )
64
-
65
-func (r *imageImportResult) Read(p []byte) (int, error) {
66
-	if r == nil || r.body == nil {
67
-		return 0, io.EOF
68
-	}
69
-	return r.body.Read(p)
70
-}
71
-
72
-func (r *imageImportResult) Close() error {
73
-	if r == nil || r.body == nil {
74
-		return nil
75
-	}
76
-	return r.body.Close()
77
-}