Adding unit tests for cp

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2017-10-30 17:52:08 -04:00
parent 96b8d15bdd
commit b7b62ca26a
3 changed files with 224 additions and 49 deletions

View File

@ -12,12 +12,14 @@ import (
type fakeClient struct { type fakeClient struct {
client.Client client.Client
inspectFunc func(string) (types.ContainerJSON, error) inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error) execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error) execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error) infoFunc func() (types.Info, error)
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
} }
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) { func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
@ -71,3 +73,17 @@ func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
} }
return types.Info{}, nil return types.Info{}, nil
} }
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
if f.containerStatPathFunc != nil {
return f.containerStatPathFunc(container, path)
}
return types.ContainerPathStat{}, nil
}
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
if f.containerCopyFromFunc != nil {
return f.containerCopyFromFunc(container, srcPath)
}
return nil, types.ContainerPathStat{}, nil
}

View File

@ -26,13 +26,17 @@ type copyOptions struct {
type copyDirection int type copyDirection int
const ( const (
fromContainer copyDirection = (1 << iota) fromContainer copyDirection = 1 << iota
toContainer toContainer
acrossContainers = fromContainer | toContainer acrossContainers = fromContainer | toContainer
) )
type cpConfig struct { type cpConfig struct {
followLink bool followLink bool
copyUIDGID bool
sourcePath string
destPath string
container string
} }
// NewCopyCommand creates a new `docker cp` command // NewCopyCommand creates a new `docker cp` command
@ -65,58 +69,57 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
return cmd return cmd
} }
func runCopy(dockerCli command.Cli, opts copyOptions) error { func runCopy(dockerCli command.Cli, opts copyOptions) error {
srcContainer, srcPath := splitCpArg(opts.source) srcContainer, srcPath := splitCpArg(opts.source)
dstContainer, dstPath := splitCpArg(opts.destination) destContainer, destPath := splitCpArg(opts.destination)
copyConfig := cpConfig{
followLink: opts.followLink,
copyUIDGID: opts.copyUIDGID,
sourcePath: srcPath,
destPath: destPath,
}
var direction copyDirection var direction copyDirection
if srcContainer != "" { if srcContainer != "" {
direction |= fromContainer direction |= fromContainer
copyConfig.container = srcContainer
} }
if dstContainer != "" { if destContainer != "" {
direction |= toContainer direction |= toContainer
} copyConfig.container = destContainer
cpParam := &cpConfig{
followLink: opts.followLink,
} }
ctx := context.Background() ctx := context.Background()
switch direction { switch direction {
case fromContainer: case fromContainer:
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam) return copyFromContainer(ctx, dockerCli, copyConfig)
case toContainer: case toContainer:
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID) return copyToContainer(ctx, dockerCli, copyConfig)
case acrossContainers: case acrossContainers:
// Copying between containers isn't supported.
return errors.New("copying between containers is not supported") return errors.New("copying between containers is not supported")
default: default:
// User didn't specify any container.
return errors.New("must specify at least one container source") return errors.New("must specify at least one container source")
} }
} }
func statContainerPath(ctx context.Context, dockerCli command.Cli, containerName, path string) (types.ContainerPathStat, error) {
return dockerCli.Client().ContainerStatPath(ctx, containerName, path)
}
func resolveLocalPath(localPath string) (absPath string, err error) { func resolveLocalPath(localPath string) (absPath string, err error) {
if absPath, err = filepath.Abs(localPath); err != nil { if absPath, err = filepath.Abs(localPath); err != nil {
return return
} }
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
} }
func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) { func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
dstPath := copyConfig.destPath
srcPath := copyConfig.sourcePath
if dstPath != "-" { if dstPath != "-" {
// Get an absolute destination path. // Get an absolute destination path.
dstPath, err = resolveLocalPath(dstPath) dstPath, err = resolveLocalPath(dstPath)
@ -125,10 +128,11 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
} }
} }
client := dockerCli.Client()
// if client requests to follow symbol link, then must decide target file to be copied // if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string var rebaseName string
if cpParam.followLink { if copyConfig.followLink {
srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath) srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
// If the destination is a symbolic link, we should follow it. // If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 { if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
@ -145,20 +149,17 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
} }
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath) content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
if err != nil { if err != nil {
return err return err
} }
defer content.Close() defer content.Close()
if dstPath == "-" { if dstPath == "-" {
// Send the response to STDOUT. _, err = io.Copy(dockerCli.Out(), content)
_, err = io.Copy(os.Stdout, content)
return err return err
} }
// Prepare source copy info.
srcInfo := archive.CopyInfo{ srcInfo := archive.CopyInfo{
Path: srcPath, Path: srcPath,
Exists: true, Exists: true,
@ -171,13 +172,17 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path) _, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
} }
// See comments in the implementation of `archive.CopyTo` for exactly what
// goes into deciding how and whether the source archive needs to be
// altered for the correct copy behavior.
return archive.CopyTo(preArchive, srcInfo, dstPath) return archive.CopyTo(preArchive, srcInfo, dstPath)
} }
func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) { // In order to get the copy behavior right, we need to know information
// about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is.
func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
srcPath := copyConfig.sourcePath
dstPath := copyConfig.destPath
if srcPath != "-" { if srcPath != "-" {
// Get an absolute source path. // Get an absolute source path.
srcPath, err = resolveLocalPath(srcPath) srcPath, err = resolveLocalPath(srcPath)
@ -186,14 +191,10 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
} }
} }
// In order to get the copy behavior right, we need to know information client := dockerCli.Client()
// about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is.
// Prepare destination copy info by stat-ing the container path. // Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath} dstInfo := archive.CopyInfo{Path: dstPath}
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath) dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
// If the destination is a symbolic link, we should evaluate it. // If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 { if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
@ -205,7 +206,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
} }
dstInfo.Path = linkTarget dstInfo.Path = linkTarget
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget) dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
} }
// Ignore any error and assume that the parent directory of the destination // Ignore any error and assume that the parent directory of the destination
@ -224,15 +225,14 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
) )
if srcPath == "-" { if srcPath == "-" {
// Use STDIN.
content = os.Stdin content = os.Stdin
resolvedDstPath = dstInfo.Path resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir { if !dstInfo.IsDir {
return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath) return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
} }
} else { } else {
// Prepare source copy info. // Prepare source copy info.
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink) srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
if err != nil { if err != nil {
return err return err
} }
@ -267,10 +267,9 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
options := types.CopyToContainerOptions{ options := types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false, AllowOverwriteDirWithFile: false,
CopyUIDGID: copyUIDGID, CopyUIDGID: copyConfig.copyUIDGID,
} }
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
} }
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be

View File

@ -0,0 +1,160 @@
package container
import (
"io"
"io/ioutil"
"runtime"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunCopyWithInvalidArguments(t *testing.T) {
var testcases = []struct {
doc string
options copyOptions
expectedErr string
}{
{
doc: "copy between container",
options: copyOptions{
source: "first:/path",
destination: "second:/path",
},
expectedErr: "copying between containers is not supported",
},
{
doc: "copy without a container",
options: copyOptions{
source: "./source",
destination: "./dest",
},
expectedErr: "must specify at least one container source",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
err := runCopy(test.NewFakeCli(nil), testcase.options)
assert.EqualError(t, err, testcase.expectedErr)
})
}
}
func TestRunCopyFromContainerToStdout(t *testing.T) {
tarContent := "the tar content"
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Equal(t, "container", container)
return ioutil.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
},
}
options := copyOptions{source: "container:/path", destination: "-"}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
require.NoError(t, err)
assert.Equal(t, tarContent, cli.OutBuffer().String())
assert.Equal(t, "", cli.ErrBuffer().String())
}
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
destDir := fs.NewDir(t, "cp-test",
fs.WithFile("file1", "content\n"))
defer destDir.Remove()
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Equal(t, "container", container)
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, types.ContainerPathStat{}, err
},
}
options := copyOptions{source: "container:/path", destination: destDir.Path()}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
require.NoError(t, err)
assert.Equal(t, "", cli.OutBuffer().String())
assert.Equal(t, "", cli.ErrBuffer().String())
content, err := ioutil.ReadFile(destDir.Join("file1"))
require.NoError(t, err)
assert.Equal(t, "content\n", string(content))
}
func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.T) {
destDir := fs.NewDir(t, "cp-test",
fs.WithFile("file1", "content\n"))
defer destDir.Remove()
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Equal(t, "container", container)
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, types.ContainerPathStat{}, err
},
}
options := copyOptions{
source: "container:/path",
destination: destDir.Join("missing", "foo"),
}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
testutil.ErrorContains(t, err, destDir.Join("missing"))
}
func TestSplitCpArg(t *testing.T) {
var testcases = []struct {
doc string
path string
os string
expectedContainer string
expectedPath string
}{
{
doc: "absolute path with colon",
os: "linux",
path: "/abs/path:withcolon",
expectedPath: "/abs/path:withcolon",
},
{
doc: "relative path with colon",
path: "./relative:path",
expectedPath: "./relative:path",
},
{
doc: "absolute path with drive",
os: "windows",
path: `d:\abs\path`,
expectedPath: `d:\abs\path`,
},
{
doc: "no separator",
path: "relative/path",
expectedPath: "relative/path",
},
{
doc: "with separator",
path: "container:/opt/foo",
expectedPath: "/opt/foo",
expectedContainer: "container",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
skip.IfCondition(t, testcase.os != "" && testcase.os != runtime.GOOS)
container, path := splitCpArg(testcase.path)
assert.Equal(t, testcase.expectedContainer, container)
assert.Equal(t, testcase.expectedPath, path)
})
}
}