client/container_copy: Wrap options and result struct
| ... | ... |
@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
|
| 67 | 67 |
ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error) |
| 68 | 68 |
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error) |
| 69 | 69 |
ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error) |
| 70 |
- ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error) |
|
| 70 |
+ ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error) |
|
| 71 | 71 |
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error) |
| 72 | 72 |
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error) |
| 73 | 73 |
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error) |
| ... | ... |
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
|
| 75 | 75 |
ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error) |
| 76 | 76 |
ContainerUpdate(ctx context.Context, container string, updateConfig ContainerUpdateOptions) (ContainerUpdateResult, error) |
| 77 | 77 |
ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult |
| 78 |
- CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error) |
|
| 79 |
- CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error |
|
| 78 |
+ CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error) |
|
| 79 |
+ CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error) |
|
| 80 | 80 |
ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error) |
| 81 | 81 |
} |
| 82 | 82 |
|
| ... | ... |
@@ -14,41 +14,57 @@ import ( |
| 14 | 14 |
"github.com/moby/moby/api/types/container" |
| 15 | 15 |
) |
| 16 | 16 |
|
| 17 |
+type ContainerStatPathOptions struct {
|
|
| 18 |
+ Path string |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+type ContainerStatPathResult struct {
|
|
| 22 |
+ Stat container.PathStat |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 17 | 25 |
// ContainerStatPath returns stat information about a path inside the container filesystem. |
| 18 |
-func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
|
|
| 26 |
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
|
|
| 19 | 27 |
containerID, err := trimID("container", containerID)
|
| 20 | 28 |
if err != nil {
|
| 21 |
- return container.PathStat{}, err
|
|
| 29 |
+ return ContainerStatPathResult{}, err
|
|
| 22 | 30 |
} |
| 23 | 31 |
|
| 24 | 32 |
query := url.Values{}
|
| 25 |
- query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
|
|
| 33 |
+ query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
|
|
| 26 | 34 |
|
| 27 | 35 |
resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) |
| 28 | 36 |
defer ensureReaderClosed(resp) |
| 29 | 37 |
if err != nil {
|
| 30 |
- return container.PathStat{}, err
|
|
| 38 |
+ return ContainerStatPathResult{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ stat, err := getContainerPathStatFromHeader(resp.Header) |
|
| 41 |
+ if err != nil {
|
|
| 42 |
+ return ContainerStatPathResult{}, err
|
|
| 31 | 43 |
} |
| 32 |
- return getContainerPathStatFromHeader(resp.Header) |
|
| 44 |
+ return ContainerStatPathResult{Stat: stat}, nil
|
|
| 33 | 45 |
} |
| 34 | 46 |
|
| 35 | 47 |
// CopyToContainerOptions holds information |
| 36 | 48 |
// about files to copy into a container |
| 37 | 49 |
type CopyToContainerOptions struct {
|
| 50 |
+ DestinationPath string |
|
| 51 |
+ Content io.Reader |
|
| 38 | 52 |
AllowOverwriteDirWithFile bool |
| 39 | 53 |
CopyUIDGID bool |
| 40 | 54 |
} |
| 41 | 55 |
|
| 56 |
+type CopyToContainerResult struct{}
|
|
| 57 |
+ |
|
| 42 | 58 |
// CopyToContainer copies content into the container filesystem. |
| 43 | 59 |
// Note that `content` must be a Reader for a TAR archive |
| 44 |
-func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error {
|
|
| 60 |
+func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
|
|
| 45 | 61 |
containerID, err := trimID("container", containerID)
|
| 46 | 62 |
if err != nil {
|
| 47 |
- return err |
|
| 63 |
+ return CopyToContainerResult{}, err
|
|
| 48 | 64 |
} |
| 49 | 65 |
|
| 50 | 66 |
query := url.Values{}
|
| 51 |
- query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
|
|
| 67 |
+ query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
|
|
| 52 | 68 |
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa. |
| 53 | 69 |
if !options.AllowOverwriteDirWithFile {
|
| 54 | 70 |
query.Set("noOverwriteDirNonDir", "true")
|
| ... | ... |
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str |
| 58 | 58 |
query.Set("copyUIDGID", "true")
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
- response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil) |
|
| 61 |
+ response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil) |
|
| 62 | 62 |
defer ensureReaderClosed(response) |
| 63 | 63 |
if err != nil {
|
| 64 |
- return err |
|
| 64 |
+ return CopyToContainerResult{}, err
|
|
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
- return nil |
|
| 67 |
+ return CopyToContainerResult{}, nil
|
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+type CopyFromContainerOptions struct {
|
|
| 71 |
+ SourcePath string |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+type CopyFromContainerResult struct {
|
|
| 75 |
+ Content io.ReadCloser |
|
| 76 |
+ Stat container.PathStat |
|
| 68 | 77 |
} |
| 69 | 78 |
|
| 70 | 79 |
// CopyFromContainer gets the content from the container and returns it as a Reader |
| 71 | 80 |
// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader. |
| 72 |
-func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
|
| 81 |
+func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
|
|
| 73 | 82 |
containerID, err := trimID("container", containerID)
|
| 74 | 83 |
if err != nil {
|
| 75 |
- return nil, container.PathStat{}, err
|
|
| 84 |
+ return CopyFromContainerResult{}, err
|
|
| 76 | 85 |
} |
| 77 | 86 |
|
| 78 | 87 |
query := make(url.Values, 1) |
| 79 |
- query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
|
|
| 88 |
+ query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
|
|
| 80 | 89 |
|
| 81 | 90 |
resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) |
| 82 | 91 |
if err != nil {
|
| 83 |
- return nil, container.PathStat{}, err
|
|
| 92 |
+ return CopyFromContainerResult{}, err
|
|
| 84 | 93 |
} |
| 85 | 94 |
|
| 86 | 95 |
// In order to get the copy behavior right, we need to know information |
| ... | ... |
@@ -91,9 +116,10 @@ func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath s |
| 91 | 91 |
// can be when copying a file/dir from one location to another file/dir. |
| 92 | 92 |
stat, err := getContainerPathStatFromHeader(resp.Header) |
| 93 | 93 |
if err != nil {
|
| 94 |
- return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
|
|
| 94 |
+ ensureReaderClosed(resp) |
|
| 95 |
+ return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
|
|
| 95 | 96 |
} |
| 96 |
- return resp.Body, stat, err |
|
| 97 |
+ return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
|
|
| 97 | 98 |
} |
| 98 | 99 |
|
| 99 | 100 |
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {
|
| ... | ... |
@@ -24,14 +24,14 @@ func TestContainerStatPathError(t *testing.T) {
|
| 24 | 24 |
) |
| 25 | 25 |
assert.NilError(t, err) |
| 26 | 26 |
|
| 27 |
- _, err = client.ContainerStatPath(context.Background(), "container_id", "path") |
|
| 27 |
+ _, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
|
|
| 28 | 28 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) |
| 29 | 29 |
|
| 30 |
- _, err = client.ContainerStatPath(context.Background(), "", "path") |
|
| 30 |
+ _, err = client.ContainerStatPath(context.Background(), "", ContainerStatPathOptions{Path: "path"})
|
|
| 31 | 31 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 32 | 32 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 33 | 33 |
|
| 34 |
- _, err = client.ContainerStatPath(context.Background(), " ", "path") |
|
| 34 |
+ _, err = client.ContainerStatPath(context.Background(), " ", ContainerStatPathOptions{Path: "path"})
|
|
| 35 | 35 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 36 | 36 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 37 | 37 |
} |
| ... | ... |
@@ -42,7 +42,7 @@ func TestContainerStatPathNotFoundError(t *testing.T) {
|
| 42 | 42 |
) |
| 43 | 43 |
assert.NilError(t, err) |
| 44 | 44 |
|
| 45 |
- _, err = client.ContainerStatPath(context.Background(), "container_id", "path") |
|
| 45 |
+ _, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
|
|
| 46 | 46 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) |
| 47 | 47 |
} |
| 48 | 48 |
|
| ... | ... |
@@ -52,7 +52,7 @@ func TestContainerStatPathNoHeaderError(t *testing.T) {
|
| 52 | 52 |
) |
| 53 | 53 |
assert.NilError(t, err) |
| 54 | 54 |
|
| 55 |
- _, err = client.ContainerStatPath(context.Background(), "container_id", "path/to/file") |
|
| 55 |
+ _, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path/to/file"})
|
|
| 56 | 56 |
assert.Check(t, err != nil, "expected an error, got nothing") |
| 57 | 57 |
} |
| 58 | 58 |
|
| ... | ... |
@@ -86,10 +86,10 @@ func TestContainerStatPath(t *testing.T) {
|
| 86 | 86 |
}), |
| 87 | 87 |
) |
| 88 | 88 |
assert.NilError(t, err) |
| 89 |
- stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath) |
|
| 89 |
+ res, err := client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: expectedPath})
|
|
| 90 | 90 |
assert.NilError(t, err) |
| 91 |
- assert.Check(t, is.Equal(stat.Name, "name")) |
|
| 92 |
- assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700))) |
|
| 91 |
+ assert.Check(t, is.Equal(res.Stat.Name, "name")) |
|
| 92 |
+ assert.Check(t, is.Equal(res.Stat.Mode, os.FileMode(0o700))) |
|
| 93 | 93 |
} |
| 94 | 94 |
|
| 95 | 95 |
func TestCopyToContainerError(t *testing.T) {
|
| ... | ... |
@@ -98,14 +98,23 @@ func TestCopyToContainerError(t *testing.T) {
|
| 98 | 98 |
) |
| 99 | 99 |
assert.NilError(t, err) |
| 100 | 100 |
|
| 101 |
- err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
|
|
| 101 |
+ _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
|
|
| 102 |
+ DestinationPath: "path/to/file", |
|
| 103 |
+ Content: bytes.NewReader([]byte("")),
|
|
| 104 |
+ }) |
|
| 102 | 105 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) |
| 103 | 106 |
|
| 104 |
- err = client.CopyToContainer(context.Background(), "", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
|
|
| 107 |
+ _, err = client.CopyToContainer(context.Background(), "", CopyToContainerOptions{
|
|
| 108 |
+ DestinationPath: "path/to/file", |
|
| 109 |
+ Content: bytes.NewReader([]byte("")),
|
|
| 110 |
+ }) |
|
| 105 | 111 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 106 | 112 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 107 | 113 |
|
| 108 |
- err = client.CopyToContainer(context.Background(), " ", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
|
|
| 114 |
+ _, err = client.CopyToContainer(context.Background(), " ", CopyToContainerOptions{
|
|
| 115 |
+ DestinationPath: "path/to/file", |
|
| 116 |
+ Content: bytes.NewReader([]byte("")),
|
|
| 117 |
+ }) |
|
| 109 | 118 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 110 | 119 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 111 | 120 |
} |
| ... | ... |
@@ -116,7 +125,10 @@ func TestCopyToContainerNotFoundError(t *testing.T) {
|
| 116 | 116 |
) |
| 117 | 117 |
assert.NilError(t, err) |
| 118 | 118 |
|
| 119 |
- err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
|
|
| 119 |
+ _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
|
|
| 120 |
+ DestinationPath: "path/to/file", |
|
| 121 |
+ Content: bytes.NewReader([]byte("")),
|
|
| 122 |
+ }) |
|
| 120 | 123 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) |
| 121 | 124 |
} |
| 122 | 125 |
|
| ... | ... |
@@ -128,7 +140,10 @@ func TestCopyToContainerEmptyResponse(t *testing.T) {
|
| 128 | 128 |
) |
| 129 | 129 |
assert.NilError(t, err) |
| 130 | 130 |
|
| 131 |
- err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
|
|
| 131 |
+ _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
|
|
| 132 |
+ DestinationPath: "path/to/file", |
|
| 133 |
+ Content: bytes.NewReader([]byte("")),
|
|
| 134 |
+ }) |
|
| 132 | 135 |
assert.NilError(t, err) |
| 133 | 136 |
} |
| 134 | 137 |
|
| ... | ... |
@@ -168,7 +183,9 @@ func TestCopyToContainer(t *testing.T) {
|
| 168 | 168 |
) |
| 169 | 169 |
assert.NilError(t, err) |
| 170 | 170 |
|
| 171 |
- err = client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), CopyToContainerOptions{
|
|
| 171 |
+ _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
|
|
| 172 |
+ DestinationPath: expectedPath, |
|
| 173 |
+ Content: bytes.NewReader([]byte("content")),
|
|
| 172 | 174 |
AllowOverwriteDirWithFile: false, |
| 173 | 175 |
}) |
| 174 | 176 |
assert.NilError(t, err) |
| ... | ... |
@@ -180,14 +197,14 @@ func TestCopyFromContainerError(t *testing.T) {
|
| 180 | 180 |
) |
| 181 | 181 |
assert.NilError(t, err) |
| 182 | 182 |
|
| 183 |
- _, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") |
|
| 183 |
+ _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
|
|
| 184 | 184 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) |
| 185 | 185 |
|
| 186 |
- _, _, err = client.CopyFromContainer(context.Background(), "", "path/to/file") |
|
| 186 |
+ _, err = client.CopyFromContainer(context.Background(), "", CopyFromContainerOptions{SourcePath: "path/to/file"})
|
|
| 187 | 187 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 188 | 188 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 189 | 189 |
|
| 190 |
- _, _, err = client.CopyFromContainer(context.Background(), " ", "path/to/file") |
|
| 190 |
+ _, err = client.CopyFromContainer(context.Background(), " ", CopyFromContainerOptions{SourcePath: "path/to/file"})
|
|
| 191 | 191 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) |
| 192 | 192 |
assert.Check(t, is.ErrorContains(err, "value is empty")) |
| 193 | 193 |
} |
| ... | ... |
@@ -198,7 +215,7 @@ func TestCopyFromContainerNotFoundError(t *testing.T) {
|
| 198 | 198 |
) |
| 199 | 199 |
assert.NilError(t, err) |
| 200 | 200 |
|
| 201 |
- _, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") |
|
| 201 |
+ _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
|
|
| 202 | 202 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) |
| 203 | 203 |
} |
| 204 | 204 |
|
| ... | ... |
@@ -223,7 +240,7 @@ func TestCopyFromContainerEmptyResponse(t *testing.T) {
|
| 223 | 223 |
) |
| 224 | 224 |
assert.NilError(t, err) |
| 225 | 225 |
|
| 226 |
- _, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") |
|
| 226 |
+ _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
|
|
| 227 | 227 |
assert.NilError(t, err) |
| 228 | 228 |
} |
| 229 | 229 |
|
| ... | ... |
@@ -233,7 +250,7 @@ func TestCopyFromContainerNoHeaderError(t *testing.T) {
|
| 233 | 233 |
) |
| 234 | 234 |
assert.NilError(t, err) |
| 235 | 235 |
|
| 236 |
- _, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") |
|
| 236 |
+ _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
|
|
| 237 | 237 |
assert.Check(t, err != nil, "expected an error, got nothing") |
| 238 | 238 |
} |
| 239 | 239 |
|
| ... | ... |
@@ -268,13 +285,13 @@ func TestCopyFromContainer(t *testing.T) {
|
| 268 | 268 |
}), |
| 269 | 269 |
) |
| 270 | 270 |
assert.NilError(t, err) |
| 271 |
- r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath) |
|
| 271 |
+ res2, err := client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: expectedPath})
|
|
| 272 | 272 |
assert.NilError(t, err) |
| 273 |
- assert.Check(t, is.Equal(stat.Name, "name")) |
|
| 274 |
- assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700))) |
|
| 273 |
+ assert.Check(t, is.Equal(res2.Stat.Name, "name")) |
|
| 274 |
+ assert.Check(t, is.Equal(res2.Stat.Mode, os.FileMode(0o700))) |
|
| 275 | 275 |
|
| 276 |
- content, err := io.ReadAll(r) |
|
| 276 |
+ content, err := io.ReadAll(res2.Content) |
|
| 277 | 277 |
assert.NilError(t, err) |
| 278 | 278 |
assert.Check(t, is.Equal(string(content), "content")) |
| 279 |
- assert.NilError(t, r.Close()) |
|
| 279 |
+ assert.NilError(t, res2.Content.Close()) |
|
| 280 | 280 |
} |
| ... | ... |
@@ -991,7 +991,7 @@ func (s *DockerAPISuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRoot |
| 991 | 991 |
apiClient, err := client.NewClientWithOpts(client.FromEnv) |
| 992 | 992 |
assert.NilError(c, err) |
| 993 | 993 |
|
| 994 |
- err = apiClient.CopyToContainer(testutil.GetContext(c), cID, "/vol2/symlinkToAbsDir", nil, client.CopyToContainerOptions{})
|
|
| 994 |
+ _, err = apiClient.CopyToContainer(testutil.GetContext(c), cID, client.CopyToContainerOptions{DestinationPath: "/vol2/symlinkToAbsDir"})
|
|
| 995 | 995 |
assert.ErrorContains(c, err, "container rootfs is marked read-only") |
| 996 | 996 |
} |
| 997 | 997 |
|
| ... | ... |
@@ -30,7 +30,7 @@ func TestCopyFromContainerPathDoesNotExist(t *testing.T) {
|
| 30 | 30 |
apiClient := testEnv.APIClient() |
| 31 | 31 |
cid := container.Create(ctx, t, apiClient) |
| 32 | 32 |
|
| 33 |
- _, _, err := apiClient.CopyFromContainer(ctx, cid, "/dne") |
|
| 33 |
+ _, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/dne"})
|
|
| 34 | 34 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) |
| 35 | 35 |
assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid)) |
| 36 | 36 |
} |
| ... | ... |
@@ -58,7 +58,7 @@ func TestCopyFromContainerPathIsNotDir(t *testing.T) {
|
| 58 | 58 |
"The filename, directory name, or volume label syntax is incorrect.", // ERROR_INVALID_NAME |
| 59 | 59 |
} |
| 60 | 60 |
} |
| 61 |
- _, _, err := apiClient.CopyFromContainer(ctx, cid, existingFile) |
|
| 61 |
+ _, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: existingFile})
|
|
| 62 | 62 |
var found bool |
| 63 | 63 |
for _, expErr := range expected {
|
| 64 | 64 |
if err != nil && strings.Contains(err.Error(), expErr) {
|
| ... | ... |
@@ -75,7 +75,7 @@ func TestCopyToContainerPathDoesNotExist(t *testing.T) {
|
| 75 | 75 |
apiClient := testEnv.APIClient() |
| 76 | 76 |
cid := container.Create(ctx, t, apiClient) |
| 77 | 77 |
|
| 78 |
- err := apiClient.CopyToContainer(ctx, cid, "/dne", nil, client.CopyToContainerOptions{})
|
|
| 78 |
+ _, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: "/dne", Content: bytes.NewReader([]byte(""))})
|
|
| 79 | 79 |
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) |
| 80 | 80 |
assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid)) |
| 81 | 81 |
} |
| ... | ... |
@@ -88,23 +88,22 @@ func TestCopyEmptyFile(t *testing.T) {
|
| 88 | 88 |
|
| 89 | 89 |
// empty content |
| 90 | 90 |
dstDir, _ := makeEmptyArchive(t) |
| 91 |
- err := apiClient.CopyToContainer(ctx, cid, dstDir, bytes.NewReader([]byte("")), client.CopyToContainerOptions{})
|
|
| 91 |
+ _, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: bytes.NewReader([]byte(""))})
|
|
| 92 | 92 |
assert.NilError(t, err) |
| 93 | 93 |
|
| 94 | 94 |
// tar with empty file |
| 95 | 95 |
dstDir, preparedArchive := makeEmptyArchive(t) |
| 96 |
- err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{})
|
|
| 96 |
+ _, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
|
|
| 97 | 97 |
assert.NilError(t, err) |
| 98 | 98 |
|
| 99 | 99 |
// tar with empty file archive mode |
| 100 | 100 |
dstDir, preparedArchive = makeEmptyArchive(t) |
| 101 |
- err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{
|
|
| 102 |
- CopyUIDGID: true, |
|
| 103 |
- }) |
|
| 101 |
+ _, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
|
|
| 104 | 102 |
assert.NilError(t, err) |
| 105 | 103 |
|
| 106 | 104 |
// copy from empty file |
| 107 |
- rdr, _, err := apiClient.CopyFromContainer(ctx, cid, dstDir) |
|
| 105 |
+ res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: dstDir})
|
|
| 106 |
+ rdr := res.Content |
|
| 108 | 107 |
assert.NilError(t, err) |
| 109 | 108 |
defer rdr.Close() |
| 110 | 109 |
} |
| ... | ... |
@@ -189,9 +188,7 @@ func TestCopyToContainerCopyUIDGID(t *testing.T) {
|
| 189 | 189 |
|
| 190 | 190 |
// tar with empty file |
| 191 | 191 |
dstDir, preparedArchive := makeEmptyArchive(t) |
| 192 |
- err := apiClient.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{
|
|
| 193 |
- CopyUIDGID: true, |
|
| 194 |
- }) |
|
| 192 |
+ _, err := apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
|
|
| 195 | 193 |
assert.NilError(t, err) |
| 196 | 194 |
|
| 197 | 195 |
res, err := container.Exec(ctx, apiClient, cID, []string{"stat", "-c", "%u:%g", "/empty-file.txt"})
|
| ... | ... |
@@ -264,7 +261,7 @@ func TestCopyToContainerPathIsNotDir(t *testing.T) {
|
| 264 | 264 |
if testEnv.DaemonInfo.OSType == "windows" {
|
| 265 | 265 |
path = "c:/windows/system32/drivers/etc/hosts/" |
| 266 | 266 |
} |
| 267 |
- err := apiClient.CopyToContainer(ctx, cid, path, nil, client.CopyToContainerOptions{})
|
|
| 267 |
+ _, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: path})
|
|
| 268 | 268 |
assert.Check(t, is.ErrorContains(err, "not a directory")) |
| 269 | 269 |
} |
| 270 | 270 |
|
| ... | ... |
@@ -327,7 +324,8 @@ func TestCopyFromContainer(t *testing.T) {
|
| 327 | 327 |
{"bar/notarget", map[string]string{"notarget": ""}},
|
| 328 | 328 |
} {
|
| 329 | 329 |
t.Run(x.src, func(t *testing.T) {
|
| 330 |
- rdr, _, err := apiClient.CopyFromContainer(ctx, cid, x.src) |
|
| 330 |
+ res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: x.src})
|
|
| 331 |
+ rdr := res.Content |
|
| 331 | 332 |
assert.NilError(t, err) |
| 332 | 333 |
defer rdr.Close() |
| 333 | 334 |
|
| ... | ... |
@@ -480,7 +480,7 @@ func TestContainerCopyLeaksMounts(t *testing.T) {
|
| 480 | 480 |
|
| 481 | 481 |
mountsBefore := getMounts() |
| 482 | 482 |
|
| 483 |
- _, _, err := apiClient.CopyFromContainer(ctx, cid, "/etc/passwd") |
|
| 483 |
+ _, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/etc/passwd"})
|
|
| 484 | 484 |
assert.NilError(t, err) |
| 485 | 485 |
|
| 486 | 486 |
mountsAfter := getMounts() |
| ... | ... |
@@ -42,13 +42,14 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
|
| 42 | 42 |
{name: "cp to container", operation: func(t *testing.T) error {
|
| 43 | 43 |
archiveReader, err := archive.Generate("new-file", "hello-world")
|
| 44 | 44 |
assert.NilError(t, err, "failed to create a temporary archive") |
| 45 |
- return apiClient.CopyToContainer(ctx, cID, "/", archiveReader, client.CopyToContainerOptions{})
|
|
| 45 |
+ _, err = apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: "/", Content: archiveReader})
|
|
| 46 |
+ return err |
|
| 46 | 47 |
}}, |
| 47 | 48 |
{name: "cp from container", operation: func(*testing.T) error {
|
| 48 |
- rc, _, err := apiClient.CopyFromContainer(ctx, cID, "/file") |
|
| 49 |
+ res, err := apiClient.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/file"})
|
|
| 49 | 50 |
if err == nil {
|
| 50 |
- defer rc.Close() |
|
| 51 |
- _, err = io.Copy(io.Discard, rc) |
|
| 51 |
+ defer res.Content.Close() |
|
| 52 |
+ _, err = io.Copy(io.Discard, res.Content) |
|
| 52 | 53 |
} |
| 53 | 54 |
|
| 54 | 55 |
return err |
| ... | ... |
@@ -607,7 +607,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
|
| 607 | 607 |
|
| 608 | 608 |
// Wait until container creates a file in the volume. |
| 609 | 609 |
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
| 610 |
- stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt") |
|
| 610 |
+ res, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/foo/test.txt"})
|
|
| 611 | 611 |
if err != nil {
|
| 612 | 612 |
if cerrdefs.IsNotFound(err) {
|
| 613 | 613 |
return poll.Continue("file doesn't yet exist")
|
| ... | ... |
@@ -615,8 +615,8 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
|
| 615 | 615 |
return poll.Error(err) |
| 616 | 616 |
} |
| 617 | 617 |
|
| 618 |
- if int(stat.Size) != len(testContent)+1 {
|
|
| 619 |
- return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size))
|
|
| 618 |
+ if int(res.Stat.Size) != len(testContent)+1 {
|
|
| 619 |
+ return poll.Error(fmt.Errorf("unexpected test file size: %d", res.Stat.Size))
|
|
| 620 | 620 |
} |
| 621 | 621 |
|
| 622 | 622 |
return poll.Success() |
| ... | ... |
@@ -680,7 +680,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
|
| 680 | 680 |
defer c.ContainerRemove(ctx, cID, client.ContainerRemoveOptions{Force: true})
|
| 681 | 681 |
|
| 682 | 682 |
waitFn := func(t poll.LogT) poll.Result {
|
| 683 |
- _, err := c.ContainerStatPath(ctx, cID, "/image/hello") |
|
| 683 |
+ _, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/image/hello"})
|
|
| 684 | 684 |
if err != nil {
|
| 685 | 685 |
if cerrdefs.IsNotFound(err) {
|
| 686 | 686 |
return poll.Continue("file doesn't yet exist")
|
| ... | ... |
@@ -415,12 +415,12 @@ func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
|
| 415 | 415 |
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
|
| 416 | 416 |
assert.NilError(t, err) |
| 417 | 417 |
|
| 418 |
- err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{})
|
|
| 418 |
+ _, err = c.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
|
|
| 419 | 419 |
assert.NilError(t, err) |
| 420 | 420 |
|
| 421 |
- rdr, _, err := c.CopyFromContainer(ctx, cID, "/test") |
|
| 421 |
+ res, err := c.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/test"})
|
|
| 422 | 422 |
assert.NilError(t, err) |
| 423 |
- _, err = io.Copy(io.Discard, rdr) |
|
| 423 |
+ _, err = io.Copy(io.Discard, res.Content) |
|
| 424 | 424 |
assert.NilError(t, err) |
| 425 | 425 |
} |
| 426 | 426 |
|
| ... | ... |
@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
|
| 67 | 67 |
ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error) |
| 68 | 68 |
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error) |
| 69 | 69 |
ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error) |
| 70 |
- ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error) |
|
| 70 |
+ ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error) |
|
| 71 | 71 |
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error) |
| 72 | 72 |
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error) |
| 73 | 73 |
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error) |
| ... | ... |
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
|
| 75 | 75 |
ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error) |
| 76 | 76 |
ContainerUpdate(ctx context.Context, container string, updateConfig ContainerUpdateOptions) (ContainerUpdateResult, error) |
| 77 | 77 |
ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult |
| 78 |
- CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error) |
|
| 79 |
- CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error |
|
| 78 |
+ CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error) |
|
| 79 |
+ CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error) |
|
| 80 | 80 |
ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error) |
| 81 | 81 |
} |
| 82 | 82 |
|
| ... | ... |
@@ -14,41 +14,57 @@ import ( |
| 14 | 14 |
"github.com/moby/moby/api/types/container" |
| 15 | 15 |
) |
| 16 | 16 |
|
| 17 |
+type ContainerStatPathOptions struct {
|
|
| 18 |
+ Path string |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+type ContainerStatPathResult struct {
|
|
| 22 |
+ Stat container.PathStat |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 17 | 25 |
// ContainerStatPath returns stat information about a path inside the container filesystem. |
| 18 |
-func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
|
|
| 26 |
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
|
|
| 19 | 27 |
containerID, err := trimID("container", containerID)
|
| 20 | 28 |
if err != nil {
|
| 21 |
- return container.PathStat{}, err
|
|
| 29 |
+ return ContainerStatPathResult{}, err
|
|
| 22 | 30 |
} |
| 23 | 31 |
|
| 24 | 32 |
query := url.Values{}
|
| 25 |
- query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
|
|
| 33 |
+ query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
|
|
| 26 | 34 |
|
| 27 | 35 |
resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) |
| 28 | 36 |
defer ensureReaderClosed(resp) |
| 29 | 37 |
if err != nil {
|
| 30 |
- return container.PathStat{}, err
|
|
| 38 |
+ return ContainerStatPathResult{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ stat, err := getContainerPathStatFromHeader(resp.Header) |
|
| 41 |
+ if err != nil {
|
|
| 42 |
+ return ContainerStatPathResult{}, err
|
|
| 31 | 43 |
} |
| 32 |
- return getContainerPathStatFromHeader(resp.Header) |
|
| 44 |
+ return ContainerStatPathResult{Stat: stat}, nil
|
|
| 33 | 45 |
} |
| 34 | 46 |
|
| 35 | 47 |
// CopyToContainerOptions holds information |
| 36 | 48 |
// about files to copy into a container |
| 37 | 49 |
type CopyToContainerOptions struct {
|
| 50 |
+ DestinationPath string |
|
| 51 |
+ Content io.Reader |
|
| 38 | 52 |
AllowOverwriteDirWithFile bool |
| 39 | 53 |
CopyUIDGID bool |
| 40 | 54 |
} |
| 41 | 55 |
|
| 56 |
+type CopyToContainerResult struct{}
|
|
| 57 |
+ |
|
| 42 | 58 |
// CopyToContainer copies content into the container filesystem. |
| 43 | 59 |
// Note that `content` must be a Reader for a TAR archive |
| 44 |
-func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error {
|
|
| 60 |
+func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
|
|
| 45 | 61 |
containerID, err := trimID("container", containerID)
|
| 46 | 62 |
if err != nil {
|
| 47 |
- return err |
|
| 63 |
+ return CopyToContainerResult{}, err
|
|
| 48 | 64 |
} |
| 49 | 65 |
|
| 50 | 66 |
query := url.Values{}
|
| 51 |
- query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
|
|
| 67 |
+ query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
|
|
| 52 | 68 |
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa. |
| 53 | 69 |
if !options.AllowOverwriteDirWithFile {
|
| 54 | 70 |
query.Set("noOverwriteDirNonDir", "true")
|
| ... | ... |
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str |
| 58 | 58 |
query.Set("copyUIDGID", "true")
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
- response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil) |
|
| 61 |
+ response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil) |
|
| 62 | 62 |
defer ensureReaderClosed(response) |
| 63 | 63 |
if err != nil {
|
| 64 |
- return err |
|
| 64 |
+ return CopyToContainerResult{}, err
|
|
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
- return nil |
|
| 67 |
+ return CopyToContainerResult{}, nil
|
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+type CopyFromContainerOptions struct {
|
|
| 71 |
+ SourcePath string |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+type CopyFromContainerResult struct {
|
|
| 75 |
+ Content io.ReadCloser |
|
| 76 |
+ Stat container.PathStat |
|
| 68 | 77 |
} |
| 69 | 78 |
|
| 70 | 79 |
// CopyFromContainer gets the content from the container and returns it as a Reader |
| 71 | 80 |
// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader. |
| 72 |
-func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
|
| 81 |
+func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
|
|
| 73 | 82 |
containerID, err := trimID("container", containerID)
|
| 74 | 83 |
if err != nil {
|
| 75 |
- return nil, container.PathStat{}, err
|
|
| 84 |
+ return CopyFromContainerResult{}, err
|
|
| 76 | 85 |
} |
| 77 | 86 |
|
| 78 | 87 |
query := make(url.Values, 1) |
| 79 |
- query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
|
|
| 88 |
+ query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
|
|
| 80 | 89 |
|
| 81 | 90 |
resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) |
| 82 | 91 |
if err != nil {
|
| 83 |
- return nil, container.PathStat{}, err
|
|
| 92 |
+ return CopyFromContainerResult{}, err
|
|
| 84 | 93 |
} |
| 85 | 94 |
|
| 86 | 95 |
// In order to get the copy behavior right, we need to know information |
| ... | ... |
@@ -91,9 +116,10 @@ func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath s |
| 91 | 91 |
// can be when copying a file/dir from one location to another file/dir. |
| 92 | 92 |
stat, err := getContainerPathStatFromHeader(resp.Header) |
| 93 | 93 |
if err != nil {
|
| 94 |
- return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
|
|
| 94 |
+ ensureReaderClosed(resp) |
|
| 95 |
+ return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
|
|
| 95 | 96 |
} |
| 96 |
- return resp.Body, stat, err |
|
| 97 |
+ return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
|
|
| 97 | 98 |
} |
| 98 | 99 |
|
| 99 | 100 |
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {
|