client/container_copy: Wrap options and result struct

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-29 13:05:23 +01:00
parent c755b9635d
commit 1cc2ab16ce
11 changed files with 160 additions and 92 deletions

View File

@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error) ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error)
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error) ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error)
ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error) ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error)
ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error) ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error)
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error) ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error) ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error)
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error) ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error)
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error) ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error)
ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error) ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error)
ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error) CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error)
CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error)
ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error) ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error)
} }

View File

@@ -14,41 +14,57 @@ import (
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
) )
type ContainerStatPathOptions struct {
Path string
}
type ContainerStatPathResult struct {
Stat container.PathStat
}
// ContainerStatPath returns stat information about a path inside the container filesystem. // ContainerStatPath returns stat information about a path inside the container filesystem.
func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) { func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
containerID, err := trimID("container", containerID) containerID, err := trimID("container", containerID)
if err != nil { if err != nil {
return container.PathStat{}, err return ContainerStatPathResult{}, err
} }
query := url.Values{} query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.PathStat{}, err return ContainerStatPathResult{}, err
} }
return getContainerPathStatFromHeader(resp.Header) stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil {
return ContainerStatPathResult{}, err
}
return ContainerStatPathResult{Stat: stat}, nil
} }
// CopyToContainerOptions holds information // CopyToContainerOptions holds information
// about files to copy into a container // about files to copy into a container
type CopyToContainerOptions struct { type CopyToContainerOptions struct {
DestinationPath string
Content io.Reader
AllowOverwriteDirWithFile bool AllowOverwriteDirWithFile bool
CopyUIDGID bool CopyUIDGID bool
} }
type CopyToContainerResult struct{}
// CopyToContainer copies content into the container filesystem. // CopyToContainer copies content into the container filesystem.
// Note that `content` must be a Reader for a TAR archive // Note that `content` must be a Reader for a TAR archive
func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error { func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
containerID, err := trimID("container", containerID) containerID, err := trimID("container", containerID)
if err != nil { if err != nil {
return err return CopyToContainerResult{}, err
} }
query := url.Values{} query := url.Values{}
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa. // Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
if !options.AllowOverwriteDirWithFile { if !options.AllowOverwriteDirWithFile {
query.Set("noOverwriteDirNonDir", "true") query.Set("noOverwriteDirNonDir", "true")
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
query.Set("copyUIDGID", "true") query.Set("copyUIDGID", "true")
} }
response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil) response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil)
defer ensureReaderClosed(response) defer ensureReaderClosed(response)
if err != nil { if err != nil {
return err return CopyToContainerResult{}, err
} }
return nil return CopyToContainerResult{}, nil
}
type CopyFromContainerOptions struct {
SourcePath string
}
type CopyFromContainerResult struct {
Content io.ReadCloser
Stat container.PathStat
} }
// CopyFromContainer gets the content from the container and returns it as a Reader // CopyFromContainer gets the content from the container and returns it as a Reader
// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader. // for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) { func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
containerID, err := trimID("container", containerID) containerID, err := trimID("container", containerID)
if err != nil { if err != nil {
return nil, container.PathStat{}, err return CopyFromContainerResult{}, err
} }
query := make(url.Values, 1) query := make(url.Values, 1)
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
if err != nil { if err != nil {
return nil, container.PathStat{}, err return CopyFromContainerResult{}, err
} }
// In order to get the copy behavior right, we need to know information // 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
// can be when copying a file/dir from one location to another file/dir. // can be when copying a file/dir from one location to another file/dir.
stat, err := getContainerPathStatFromHeader(resp.Header) stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil { if err != nil {
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) ensureReaderClosed(resp)
return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
} }
return resp.Body, stat, err return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
} }
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) { func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {

View File

@@ -24,14 +24,14 @@ func TestContainerStatPathError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.ContainerStatPath(context.Background(), "container_id", "path") _, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.ContainerStatPath(context.Background(), "", "path") _, err = client.ContainerStatPath(context.Background(), "", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.ContainerStatPath(context.Background(), " ", "path") _, err = client.ContainerStatPath(context.Background(), " ", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -42,7 +42,7 @@ func TestContainerStatPathNotFoundError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.ContainerStatPath(context.Background(), "container_id", "path") _, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
} }
@@ -52,7 +52,7 @@ func TestContainerStatPathNoHeaderError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.ContainerStatPath(context.Background(), "container_id", "path/to/file") _, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path/to/file"})
assert.Check(t, err != nil, "expected an error, got nothing") assert.Check(t, err != nil, "expected an error, got nothing")
} }
@@ -86,10 +86,10 @@ func TestContainerStatPath(t *testing.T) {
}), }),
) )
assert.NilError(t, err) assert.NilError(t, err)
stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath) res, err := client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: expectedPath})
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(stat.Name, "name")) assert.Check(t, is.Equal(res.Stat.Name, "name"))
assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700))) assert.Check(t, is.Equal(res.Stat.Mode, os.FileMode(0o700)))
} }
func TestCopyToContainerError(t *testing.T) { func TestCopyToContainerError(t *testing.T) {
@@ -98,14 +98,23 @@ func TestCopyToContainerError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{}) _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.CopyToContainer(context.Background(), "", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{}) _, err = client.CopyToContainer(context.Background(), "", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.CopyToContainer(context.Background(), " ", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{}) _, err = client.CopyToContainer(context.Background(), " ", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -116,7 +125,10 @@ func TestCopyToContainerNotFoundError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{}) _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
} }
@@ -128,7 +140,10 @@ func TestCopyToContainerEmptyResponse(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{}) _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -168,7 +183,9 @@ func TestCopyToContainer(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), CopyToContainerOptions{ _, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: expectedPath,
Content: bytes.NewReader([]byte("content")),
AllowOverwriteDirWithFile: false, AllowOverwriteDirWithFile: false,
}) })
assert.NilError(t, err) assert.NilError(t, err)
@@ -180,14 +197,14 @@ func TestCopyFromContainerError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, _, err = client.CopyFromContainer(context.Background(), "", "path/to/file") _, err = client.CopyFromContainer(context.Background(), "", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
_, _, err = client.CopyFromContainer(context.Background(), " ", "path/to/file") _, err = client.CopyFromContainer(context.Background(), " ", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -198,7 +215,7 @@ func TestCopyFromContainerNotFoundError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
} }
@@ -223,7 +240,7 @@ func TestCopyFromContainerEmptyResponse(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -233,7 +250,7 @@ func TestCopyFromContainerNoHeaderError(t *testing.T) {
) )
assert.NilError(t, err) assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file") _, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, err != nil, "expected an error, got nothing") assert.Check(t, err != nil, "expected an error, got nothing")
} }
@@ -268,13 +285,13 @@ func TestCopyFromContainer(t *testing.T) {
}), }),
) )
assert.NilError(t, err) assert.NilError(t, err)
r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath) res2, err := client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: expectedPath})
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(stat.Name, "name")) assert.Check(t, is.Equal(res2.Stat.Name, "name"))
assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700))) assert.Check(t, is.Equal(res2.Stat.Mode, os.FileMode(0o700)))
content, err := io.ReadAll(r) content, err := io.ReadAll(res2.Content)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(string(content), "content")) assert.Check(t, is.Equal(string(content), "content"))
assert.NilError(t, r.Close()) assert.NilError(t, res2.Content.Close())
} }

View File

@@ -991,7 +991,7 @@ func (s *DockerAPISuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRoot
apiClient, err := client.NewClientWithOpts(client.FromEnv) apiClient, err := client.NewClientWithOpts(client.FromEnv)
assert.NilError(c, err) assert.NilError(c, err)
err = apiClient.CopyToContainer(testutil.GetContext(c), cID, "/vol2/symlinkToAbsDir", nil, client.CopyToContainerOptions{}) _, err = apiClient.CopyToContainer(testutil.GetContext(c), cID, client.CopyToContainerOptions{DestinationPath: "/vol2/symlinkToAbsDir"})
assert.ErrorContains(c, err, "container rootfs is marked read-only") assert.ErrorContains(c, err, "container rootfs is marked read-only")
} }

View File

@@ -30,7 +30,7 @@ func TestCopyFromContainerPathDoesNotExist(t *testing.T) {
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
cid := container.Create(ctx, t, apiClient) cid := container.Create(ctx, t, apiClient)
_, _, err := apiClient.CopyFromContainer(ctx, cid, "/dne") _, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/dne"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid)) assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid))
} }
@@ -58,7 +58,7 @@ func TestCopyFromContainerPathIsNotDir(t *testing.T) {
"The filename, directory name, or volume label syntax is incorrect.", // ERROR_INVALID_NAME "The filename, directory name, or volume label syntax is incorrect.", // ERROR_INVALID_NAME
} }
} }
_, _, err := apiClient.CopyFromContainer(ctx, cid, existingFile) _, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: existingFile})
var found bool var found bool
for _, expErr := range expected { for _, expErr := range expected {
if err != nil && strings.Contains(err.Error(), expErr) { if err != nil && strings.Contains(err.Error(), expErr) {
@@ -75,7 +75,7 @@ func TestCopyToContainerPathDoesNotExist(t *testing.T) {
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
cid := container.Create(ctx, t, apiClient) cid := container.Create(ctx, t, apiClient)
err := apiClient.CopyToContainer(ctx, cid, "/dne", nil, client.CopyToContainerOptions{}) _, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: "/dne", Content: bytes.NewReader([]byte(""))})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid)) assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid))
} }
@@ -88,23 +88,22 @@ func TestCopyEmptyFile(t *testing.T) {
// empty content // empty content
dstDir, _ := makeEmptyArchive(t) dstDir, _ := makeEmptyArchive(t)
err := apiClient.CopyToContainer(ctx, cid, dstDir, bytes.NewReader([]byte("")), client.CopyToContainerOptions{}) _, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: bytes.NewReader([]byte(""))})
assert.NilError(t, err) assert.NilError(t, err)
// tar with empty file // tar with empty file
dstDir, preparedArchive := makeEmptyArchive(t) dstDir, preparedArchive := makeEmptyArchive(t)
err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{}) _, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
assert.NilError(t, err) assert.NilError(t, err)
// tar with empty file archive mode // tar with empty file archive mode
dstDir, preparedArchive = makeEmptyArchive(t) dstDir, preparedArchive = makeEmptyArchive(t)
err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{ _, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
CopyUIDGID: true,
})
assert.NilError(t, err) assert.NilError(t, err)
// copy from empty file // copy from empty file
rdr, _, err := apiClient.CopyFromContainer(ctx, cid, dstDir) res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: dstDir})
rdr := res.Content
assert.NilError(t, err) assert.NilError(t, err)
defer rdr.Close() defer rdr.Close()
} }
@@ -189,9 +188,7 @@ func TestCopyToContainerCopyUIDGID(t *testing.T) {
// tar with empty file // tar with empty file
dstDir, preparedArchive := makeEmptyArchive(t) dstDir, preparedArchive := makeEmptyArchive(t)
err := apiClient.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{ _, err := apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
CopyUIDGID: true,
})
assert.NilError(t, err) assert.NilError(t, err)
res, err := container.Exec(ctx, apiClient, cID, []string{"stat", "-c", "%u:%g", "/empty-file.txt"}) res, err := container.Exec(ctx, apiClient, cID, []string{"stat", "-c", "%u:%g", "/empty-file.txt"})
@@ -264,7 +261,7 @@ func TestCopyToContainerPathIsNotDir(t *testing.T) {
if testEnv.DaemonInfo.OSType == "windows" { if testEnv.DaemonInfo.OSType == "windows" {
path = "c:/windows/system32/drivers/etc/hosts/" path = "c:/windows/system32/drivers/etc/hosts/"
} }
err := apiClient.CopyToContainer(ctx, cid, path, nil, client.CopyToContainerOptions{}) _, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: path})
assert.Check(t, is.ErrorContains(err, "not a directory")) assert.Check(t, is.ErrorContains(err, "not a directory"))
} }
@@ -327,7 +324,8 @@ func TestCopyFromContainer(t *testing.T) {
{"bar/notarget", map[string]string{"notarget": ""}}, {"bar/notarget", map[string]string{"notarget": ""}},
} { } {
t.Run(x.src, func(t *testing.T) { t.Run(x.src, func(t *testing.T) {
rdr, _, err := apiClient.CopyFromContainer(ctx, cid, x.src) res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: x.src})
rdr := res.Content
assert.NilError(t, err) assert.NilError(t, err)
defer rdr.Close() defer rdr.Close()

View File

@@ -480,7 +480,7 @@ func TestContainerCopyLeaksMounts(t *testing.T) {
mountsBefore := getMounts() mountsBefore := getMounts()
_, _, err := apiClient.CopyFromContainer(ctx, cid, "/etc/passwd") _, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/etc/passwd"})
assert.NilError(t, err) assert.NilError(t, err)
mountsAfter := getMounts() mountsAfter := getMounts()

View File

@@ -42,13 +42,14 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
{name: "cp to container", operation: func(t *testing.T) error { {name: "cp to container", operation: func(t *testing.T) error {
archiveReader, err := archive.Generate("new-file", "hello-world") archiveReader, err := archive.Generate("new-file", "hello-world")
assert.NilError(t, err, "failed to create a temporary archive") assert.NilError(t, err, "failed to create a temporary archive")
return apiClient.CopyToContainer(ctx, cID, "/", archiveReader, client.CopyToContainerOptions{}) _, err = apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: "/", Content: archiveReader})
return err
}}, }},
{name: "cp from container", operation: func(*testing.T) error { {name: "cp from container", operation: func(*testing.T) error {
rc, _, err := apiClient.CopyFromContainer(ctx, cID, "/file") res, err := apiClient.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/file"})
if err == nil { if err == nil {
defer rc.Close() defer res.Content.Close()
_, err = io.Copy(io.Discard, rc) _, err = io.Copy(io.Discard, res.Content)
} }
return err return err

View File

@@ -607,7 +607,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
// Wait until container creates a file in the volume. // Wait until container creates a file in the volume.
poll.WaitOn(t, func(t poll.LogT) poll.Result { poll.WaitOn(t, func(t poll.LogT) poll.Result {
stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt") res, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/foo/test.txt"})
if err != nil { if err != nil {
if cerrdefs.IsNotFound(err) { if cerrdefs.IsNotFound(err) {
return poll.Continue("file doesn't yet exist") return poll.Continue("file doesn't yet exist")
@@ -615,8 +615,8 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
return poll.Error(err) return poll.Error(err)
} }
if int(stat.Size) != len(testContent)+1 { if int(res.Stat.Size) != len(testContent)+1 {
return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size)) return poll.Error(fmt.Errorf("unexpected test file size: %d", res.Stat.Size))
} }
return poll.Success() return poll.Success()
@@ -680,7 +680,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
defer c.ContainerRemove(ctx, cID, client.ContainerRemoveOptions{Force: true}) defer c.ContainerRemove(ctx, cID, client.ContainerRemoveOptions{Force: true})
waitFn := func(t poll.LogT) poll.Result { waitFn := func(t poll.LogT) poll.Result {
_, err := c.ContainerStatPath(ctx, cID, "/image/hello") _, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/image/hello"})
if err != nil { if err != nil {
if cerrdefs.IsNotFound(err) { if cerrdefs.IsNotFound(err) {
return poll.Continue("file doesn't yet exist") return poll.Continue("file doesn't yet exist")

View File

@@ -415,12 +415,12 @@ func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"}) dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
assert.NilError(t, err) assert.NilError(t, err)
err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{}) _, err = c.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
assert.NilError(t, err) assert.NilError(t, err)
rdr, _, err := c.CopyFromContainer(ctx, cID, "/test") res, err := c.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/test"})
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.Copy(io.Discard, rdr) _, err = io.Copy(io.Discard, res.Content)
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error) ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error)
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error) ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error)
ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error) ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error)
ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error) ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error)
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error) ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error) ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error)
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error) ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error)
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error) ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error)
ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error) ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error)
ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error) CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error)
CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error)
ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error) ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error)
} }

View File

@@ -14,41 +14,57 @@ import (
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
) )
type ContainerStatPathOptions struct {
Path string
}
type ContainerStatPathResult struct {
Stat container.PathStat
}
// ContainerStatPath returns stat information about a path inside the container filesystem. // ContainerStatPath returns stat information about a path inside the container filesystem.
func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) { func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
containerID, err := trimID("container", containerID) containerID, err := trimID("container", containerID)
if err != nil { if err != nil {
return container.PathStat{}, err return ContainerStatPathResult{}, err
} }
query := url.Values{} query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.PathStat{}, err return ContainerStatPathResult{}, err
} }
return getContainerPathStatFromHeader(resp.Header) stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil {
return ContainerStatPathResult{}, err
}
return ContainerStatPathResult{Stat: stat}, nil
} }
// CopyToContainerOptions holds information // CopyToContainerOptions holds information
// about files to copy into a container // about files to copy into a container
type CopyToContainerOptions struct { type CopyToContainerOptions struct {
DestinationPath string
Content io.Reader
AllowOverwriteDirWithFile bool AllowOverwriteDirWithFile bool
CopyUIDGID bool CopyUIDGID bool
} }
type CopyToContainerResult struct{}
// CopyToContainer copies content into the container filesystem. // CopyToContainer copies content into the container filesystem.
// Note that `content` must be a Reader for a TAR archive // Note that `content` must be a Reader for a TAR archive
func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error { func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
containerID, err := trimID("container", containerID) containerID, err := trimID("container", containerID)
if err != nil { if err != nil {
return err return CopyToContainerResult{}, err
} }
query := url.Values{} query := url.Values{}
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa. // Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
if !options.AllowOverwriteDirWithFile { if !options.AllowOverwriteDirWithFile {
query.Set("noOverwriteDirNonDir", "true") query.Set("noOverwriteDirNonDir", "true")
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
query.Set("copyUIDGID", "true") query.Set("copyUIDGID", "true")
} }
response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil) response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil)
defer ensureReaderClosed(response) defer ensureReaderClosed(response)
if err != nil { if err != nil {
return err return CopyToContainerResult{}, err
} }
return nil return CopyToContainerResult{}, nil
}
type CopyFromContainerOptions struct {
SourcePath string
}
type CopyFromContainerResult struct {
Content io.ReadCloser
Stat container.PathStat
} }
// CopyFromContainer gets the content from the container and returns it as a Reader // CopyFromContainer gets the content from the container and returns it as a Reader
// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader. // for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) { func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
containerID, err := trimID("container", containerID) containerID, err := trimID("container", containerID)
if err != nil { if err != nil {
return nil, container.PathStat{}, err return CopyFromContainerResult{}, err
} }
query := make(url.Values, 1) query := make(url.Values, 1)
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
if err != nil { if err != nil {
return nil, container.PathStat{}, err return CopyFromContainerResult{}, err
} }
// In order to get the copy behavior right, we need to know information // 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
// can be when copying a file/dir from one location to another file/dir. // can be when copying a file/dir from one location to another file/dir.
stat, err := getContainerPathStatFromHeader(resp.Header) stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil { if err != nil {
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) ensureReaderClosed(resp)
return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
} }
return resp.Body, stat, err return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
} }
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) { func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {