Browse code

Merge pull request #51316 from austinvazquez/refactor-client-container-export

client: refactor `ContainerExport` to wrap options/result structs

Sebastiaan van Stijn authored on 2025/10/29 18:01:07
Showing 9 changed files
... ...
@@ -59,7 +59,7 @@ type ContainerAPIClient interface {
59 59
 	ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error)
60 60
 	ContainerDiff(ctx context.Context, container string, options ContainerDiffOptions) (ContainerDiffResult, error)
61 61
 	ExecAPIClient
62
-	ContainerExport(ctx context.Context, container string) (io.ReadCloser, error)
62
+	ContainerExport(ctx context.Context, container string, options ContainerExportOptions) (ContainerExportResult, error)
63 63
 	ContainerInspect(ctx context.Context, container string, options ContainerInspectOptions) (ContainerInspectResult, error)
64 64
 	ContainerKill(ctx context.Context, container string, options ContainerKillOptions) (ContainerKillResult, error)
65 65
 	ContainerList(ctx context.Context, options ContainerListOptions) (ContainerListResult, error)
... ...
@@ -4,21 +4,59 @@ import (
4 4
 	"context"
5 5
 	"io"
6 6
 	"net/url"
7
+	"sync"
7 8
 )
8 9
 
10
+// ContainerExportOptions specifies options for container export operations.
11
+type ContainerExportOptions struct {
12
+	// Currently no options are defined for ContainerExport
13
+}
14
+
15
+// ContainerExportResult represents the result of a container export operation.
16
+type ContainerExportResult struct {
17
+	rc    io.ReadCloser
18
+	close func() error
19
+}
20
+
9 21
 // ContainerExport retrieves the raw contents of a container
10 22
 // and returns them as an [io.ReadCloser]. It's up to the caller
11 23
 // to close the stream.
12
-func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
24
+func (cli *Client) ContainerExport(ctx context.Context, containerID string, options ContainerExportOptions) (ContainerExportResult, error) {
13 25
 	containerID, err := trimID("container", containerID)
14 26
 	if err != nil {
15
-		return nil, err
27
+		return ContainerExportResult{}, err
16 28
 	}
17 29
 
18 30
 	resp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
19 31
 	if err != nil {
20
-		return nil, err
32
+		return ContainerExportResult{}, err
33
+	}
34
+
35
+	return newContainerExportResult(resp.Body), nil
36
+}
37
+
38
+func newContainerExportResult(rc io.ReadCloser) ContainerExportResult {
39
+	if rc == nil {
40
+		panic("nil io.ReadCloser")
41
+	}
42
+	return ContainerExportResult{
43
+		rc:    rc,
44
+		close: sync.OnceValue(rc.Close),
21 45
 	}
46
+}
22 47
 
23
-	return resp.Body, nil
48
+// Read implements io.ReadCloser
49
+func (r ContainerExportResult) Read(p []byte) (n int, err error) {
50
+	if r.rc == nil {
51
+		return 0, io.EOF
52
+	}
53
+	return r.rc.Read(p)
54
+}
55
+
56
+// Close implements io.ReadCloser
57
+func (r ContainerExportResult) Close() error {
58
+	if r.close == nil {
59
+		return nil
60
+	}
61
+	return r.close()
24 62
 }
... ...
@@ -17,14 +17,14 @@ func TestContainerExportError(t *testing.T) {
17 17
 	)
18 18
 	assert.NilError(t, err)
19 19
 
20
-	_, err = client.ContainerExport(context.Background(), "nothing")
20
+	_, err = client.ContainerExport(context.Background(), "nothing", ContainerExportOptions{})
21 21
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
22 22
 
23
-	_, err = client.ContainerExport(context.Background(), "")
23
+	_, err = client.ContainerExport(context.Background(), "", 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(), "    ")
27
+	_, err = client.ContainerExport(context.Background(), "    ", 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,7 +40,7 @@ func TestContainerExport(t *testing.T) {
40 40
 		}),
41 41
 	)
42 42
 	assert.NilError(t, err)
43
-	body, err := client.ContainerExport(context.Background(), "container_id")
43
+	body, err := client.ContainerExport(context.Background(), "container_id", ContainerExportOptions{})
44 44
 	assert.NilError(t, err)
45 45
 	defer body.Close()
46 46
 	content, err := io.ReadAll(body)
... ...
@@ -103,7 +103,7 @@ 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)
106
+	body, err := apiClient.ContainerExport(testutil.GetContext(c), name, client.ContainerExportOptions{})
107 107
 	assert.NilError(c, err)
108 108
 	defer body.Close()
109 109
 	found := false
... ...
@@ -27,7 +27,7 @@ func TestExportContainerAndImportImage(t *testing.T) {
27 27
 	poll.WaitOn(t, container.IsStopped(ctx, apiClient, cID))
28 28
 
29 29
 	reference := "repo/" + strings.ToLower(t.Name()) + ":v1"
30
-	exportRes, err := apiClient.ContainerExport(ctx, cID)
30
+	exportRes, err := apiClient.ContainerExport(ctx, cID, client.ContainerExportOptions{})
31 31
 	assert.NilError(t, err)
32 32
 	importRes, err := apiClient.ImageImport(ctx, client.ImageImportSource{
33 33
 		Source:     exportRes,
... ...
@@ -70,6 +70,6 @@ func TestExportContainerAfterDaemonRestart(t *testing.T) {
70 70
 
71 71
 	d.Restart(t)
72 72
 
73
-	_, err := c.ContainerExport(ctx, ctrID)
73
+	_, err := c.ContainerExport(ctx, ctrID, client.ContainerExportOptions{})
74 74
 	assert.NilError(t, err)
75 75
 }
... ...
@@ -32,7 +32,7 @@ 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)
35
+			rc, err := apiClient.ContainerExport(ctx, cID, client.ContainerExportOptions{})
36 36
 			if err == nil {
37 37
 				defer rc.Close()
38 38
 				_, err = io.Copy(io.Discard, rc)
... ...
@@ -362,7 +362,7 @@ func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
362 362
 
363 363
 	cID := container.Run(ctx, t, c)
364 364
 
365
-	responseReader, err := c.ContainerExport(ctx, cID)
365
+	responseReader, err := c.ContainerExport(ctx, cID, client.ContainerExportOptions{})
366 366
 	assert.NilError(t, err)
367 367
 	defer responseReader.Close()
368 368
 	file, err := os.Create(exportedImagePath)
... ...
@@ -59,7 +59,7 @@ type ContainerAPIClient interface {
59 59
 	ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error)
60 60
 	ContainerDiff(ctx context.Context, container string, options ContainerDiffOptions) (ContainerDiffResult, error)
61 61
 	ExecAPIClient
62
-	ContainerExport(ctx context.Context, container string) (io.ReadCloser, error)
62
+	ContainerExport(ctx context.Context, container string, options ContainerExportOptions) (ContainerExportResult, error)
63 63
 	ContainerInspect(ctx context.Context, container string, options ContainerInspectOptions) (ContainerInspectResult, error)
64 64
 	ContainerKill(ctx context.Context, container string, options ContainerKillOptions) (ContainerKillResult, error)
65 65
 	ContainerList(ctx context.Context, options ContainerListOptions) (ContainerListResult, error)
... ...
@@ -4,21 +4,59 @@ import (
4 4
 	"context"
5 5
 	"io"
6 6
 	"net/url"
7
+	"sync"
7 8
 )
8 9
 
10
+// ContainerExportOptions specifies options for container export operations.
11
+type ContainerExportOptions struct {
12
+	// Currently no options are defined for ContainerExport
13
+}
14
+
15
+// ContainerExportResult represents the result of a container export operation.
16
+type ContainerExportResult struct {
17
+	rc    io.ReadCloser
18
+	close func() error
19
+}
20
+
9 21
 // ContainerExport retrieves the raw contents of a container
10 22
 // and returns them as an [io.ReadCloser]. It's up to the caller
11 23
 // to close the stream.
12
-func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
24
+func (cli *Client) ContainerExport(ctx context.Context, containerID string, options ContainerExportOptions) (ContainerExportResult, error) {
13 25
 	containerID, err := trimID("container", containerID)
14 26
 	if err != nil {
15
-		return nil, err
27
+		return ContainerExportResult{}, err
16 28
 	}
17 29
 
18 30
 	resp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
19 31
 	if err != nil {
20
-		return nil, err
32
+		return ContainerExportResult{}, err
33
+	}
34
+
35
+	return newContainerExportResult(resp.Body), nil
36
+}
37
+
38
+func newContainerExportResult(rc io.ReadCloser) ContainerExportResult {
39
+	if rc == nil {
40
+		panic("nil io.ReadCloser")
41
+	}
42
+	return ContainerExportResult{
43
+		rc:    rc,
44
+		close: sync.OnceValue(rc.Close),
21 45
 	}
46
+}
22 47
 
23
-	return resp.Body, nil
48
+// Read implements io.ReadCloser
49
+func (r ContainerExportResult) Read(p []byte) (n int, err error) {
50
+	if r.rc == nil {
51
+		return 0, io.EOF
52
+	}
53
+	return r.rc.Read(p)
54
+}
55
+
56
+// Close implements io.ReadCloser
57
+func (r ContainerExportResult) Close() error {
58
+	if r.close == nil {
59
+		return nil
60
+	}
61
+	return r.close()
24 62
 }