diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index c1f1e27f58..67bb13aa19 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -82,7 +82,9 @@ func cp(cmd *cobra.Command, args []string) error { return err } - if len(sourceContainerStr) > 0 { + if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 { + return copyContainerToContainer(sourceContainerStr, sourcePath, destContainerStr, destPath) + } else if len(sourceContainerStr) > 0 { return copyFromContainer(sourceContainerStr, sourcePath, destPath) } @@ -115,6 +117,110 @@ func doCopy(funcA func() error, funcB func() error) error { return errorhandling.JoinErrors(copyErrors) } +func copyContainerToContainer(sourceContainer string, sourcePath string, destContainer string, destPath string) error { + if err := containerMustExist(sourceContainer); err != nil { + return err + } + + if err := containerMustExist(destContainer); err != nil { + return err + } + + sourceContainerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), sourceContainer, sourcePath) + if err != nil { + return errors.Wrapf(err, "%q could not be found on container %s", sourcePath, sourceContainer) + } + + var destContainerBaseName string + destContainerInfo, destContainerInfoErr := registry.ContainerEngine().ContainerStat(registry.GetContext(), destContainer, destPath) + if destContainerInfoErr != nil { + if strings.HasSuffix(destPath, "/") { + return errors.Wrapf(destContainerInfoErr, "%q could not be found on container %s", destPath, destContainer) + } + // NOTE: containerInfo may actually be set. That happens when + // the container path is a symlink into nirvana. In that case, + // we must use the symlinked path instead. + path := destPath + if destContainerInfo != nil { + destContainerBaseName = filepath.Base(destContainerInfo.LinkTarget) + path = destContainerInfo.LinkTarget + } else { + destContainerBaseName = filepath.Base(destPath) + } + + parentDir, err := containerParentDir(destContainer, path) + if err != nil { + return errors.Wrapf(err, "could not determine parent dir of %q on container %s", path, destContainer) + } + destContainerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), destContainer, parentDir) + if err != nil { + return errors.Wrapf(err, "%q could not be found on container %s", destPath, destContainer) + } + } else { + // If the specified path exists on the container, we must use + // its base path as it may have changed due to symlink + // evaluations. + destContainerBaseName = filepath.Base(destContainerInfo.LinkTarget) + } + + if sourceContainerInfo.IsDir && !destContainerInfo.IsDir { + return errors.New("destination must be a directory when copying a directory") + } + + sourceContainerTarget, destContainerTarget := sourceContainerInfo.LinkTarget, destContainerInfo.LinkTarget + if !destContainerInfo.IsDir { + destContainerTarget = filepath.Dir(destPath) + } + + // If we copy a directory via the "." notation and the container path + // does not exist, we need to make sure that the destination on the + // container gets created; otherwise the contents of the source + // directory will be written to the destination's parent directory. + // + // Hence, whenever "." is the source and the destination does not + // exist, we copy the source's parent and let the copier package create + // the destination via the Rename option. + if destContainerInfoErr != nil && sourceContainerInfo.IsDir && strings.HasSuffix(sourcePath, ".") { + sourceContainerTarget = filepath.Dir(sourceContainerTarget) + } + + reader, writer := io.Pipe() + + sourceContainerCopy := func() error { + defer writer.Close() + copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), sourceContainer, sourceContainerTarget, writer) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return errors.Wrap(err, "error copying from container") + } + return nil + } + + destContainerCopy := func() error { + defer reader.Close() + + copyOptions := entities.CopyOptions{Chown: chown} + if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destContainerInfoErr != nil { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + copyOptions.Rename = map[string]string{filepath.Base(sourceContainerTarget): destContainerBaseName} + } + + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), destContainer, destContainerTarget, reader, copyOptions) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return errors.Wrap(err, "error copying to container") + } + return nil + } + + return doCopy(sourceContainerCopy, destContainerCopy) +} + // copyFromContainer copies from the containerPath on the container to hostPath. func copyFromContainer(container string, containerPath string, hostPath string) error { if err := containerMustExist(container); err != nil { diff --git a/docs/source/markdown/podman-cp.1.md b/docs/source/markdown/podman-cp.1.md index 43ee4cdff0..e04542aa28 100644 --- a/docs/source/markdown/podman-cp.1.md +++ b/docs/source/markdown/podman-cp.1.md @@ -9,10 +9,10 @@ podman\-cp - Copy files/folders between a container and the local filesystem **podman container cp** [*options*] [*container*:]*src_path* [*container*:]*dest_path* ## DESCRIPTION -Copy the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container. +Copy the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container or between two containers. If `-` is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. -The CONTAINER can be a running or stopped container. The **src_path** or **dest_path** can be a file or directory. +The containers can be a running or stopped. The **src_path** or **dest_path** can be a file or directory. The **podman cp** command assumes container paths are relative to the container's root directory (i.e., `/`). @@ -70,10 +70,9 @@ The default is *true*. ## ALTERNATIVES -Podman has much stronger capabilities than just `podman cp` to achieve copy files between host and container. +Podman has much stronger capabilities than just `podman cp` to achieve copying files between the host and containers. -Using standard podman-mount and podman-umount takes advantage of the entire linux tool chain, rather -then just cp. +Using standard podman-mount and podman-umount takes advantage of the entire linux tool chain, rather than just cp. If a user wants to copy contents out of a container or into a container, they can execute a few simple commands. @@ -113,6 +112,8 @@ podman cp containerID:/myapp/ /myapp/ podman cp containerID:/home/myuser/. /home/myuser/ +podman cp containerA:/myapp containerB:/yourapp + podman cp - containerID:/myfiles.tar.gz < myfiles.tar.gz ## SEE ALSO diff --git a/libpod/container_api.go b/libpod/container_api.go index 390bba7bbf..d221baa776 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -840,7 +840,7 @@ func (c *Container) ShouldRestart(ctx context.Context) bool { // CopyFromArchive copies the contents from the specified tarStream to path // *inside* the container. -func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, chown bool, tarStream io.Reader) (func() error, error) { +func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, chown bool, rename map[string]string, tarStream io.Reader) (func() error, error) { if !c.batched { c.lock.Lock() defer c.lock.Unlock() @@ -850,7 +850,7 @@ func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, c } } - return c.copyFromArchive(ctx, containerPath, chown, tarStream) + return c.copyFromArchive(ctx, containerPath, chown, rename, tarStream) } // CopyToArchive copies the contents from the specified path *inside* the diff --git a/libpod/container_copy_linux.go b/libpod/container_copy_linux.go index 01e7ecacba..a358242893 100644 --- a/libpod/container_copy_linux.go +++ b/libpod/container_copy_linux.go @@ -23,7 +23,7 @@ import ( "golang.org/x/sys/unix" ) -func (c *Container) copyFromArchive(ctx context.Context, path string, chown bool, reader io.Reader) (func() error, error) { +func (c *Container) copyFromArchive(ctx context.Context, path string, chown bool, rename map[string]string, reader io.Reader) (func() error, error) { var ( mountPoint string resolvedRoot string @@ -89,6 +89,7 @@ func (c *Container) copyFromArchive(ctx context.Context, path string, chown bool GIDMap: c.config.IDMappings.GIDMap, ChownDirs: idPair, ChownFiles: idPair, + Rename: rename, } return c.joinMountAndExec(ctx, diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go index a9d74e5f4e..541f702e75 100644 --- a/pkg/api/handlers/compat/containers_archive.go +++ b/pkg/api/handlers/compat/containers_archive.go @@ -1,6 +1,7 @@ package compat import ( + "encoding/json" "fmt" "net/http" "os" @@ -93,8 +94,9 @@ func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.De func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { query := struct { - Path string `schema:"path"` - Chown bool `schema:"copyUIDGID"` + Path string `schema:"path"` + Chown bool `schema:"copyUIDGID"` + Rename string `schema:"rename"` // TODO handle params below NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` }{ @@ -107,10 +109,19 @@ func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, return } + var rename map[string]string + if query.Rename != "" { + if err := json.Unmarshal([]byte(query.Rename), &rename); err != nil { + utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) + return + } + } + containerName := utils.GetName(r) containerEngine := abi.ContainerEngine{Libpod: runtime} - copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body, entities.CopyOptions{Chown: query.Chown}) + copyOptions := entities.CopyOptions{Chown: query.Chown, Rename: rename} + copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body, copyOptions) if errors.Cause(err) == define.ErrNoSuchCtr || os.IsNotExist(err) { // 404 is returned for an absent container and path. The // clients must deal with it accordingly. diff --git a/pkg/api/server/register_archive.go b/pkg/api/server/register_archive.go index ee7449fbb2..82d72ee6a5 100644 --- a/pkg/api/server/register_archive.go +++ b/pkg/api/server/register_archive.go @@ -151,6 +151,10 @@ func (s *APIServer) registerArchiveHandlers(r *mux.Router) error { // type: string // description: Path to a directory in the container to extract // required: true + // - in: query + // name: rename + // type: string + // description: JSON encoded map[string]string to translate paths // responses: // 200: // description: no error diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index 1058c7a486..39492077bd 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -263,4 +263,6 @@ type CopyOptions struct { // If used with CopyFromArchive and set to true it will change ownership of files from the source tar archive // to the primary uid/gid of the target container. Chown *bool `schema:"copyUIDGID"` + // Map to translate path names. + Rename map[string]string } diff --git a/pkg/bindings/containers/types_copy_options.go b/pkg/bindings/containers/types_copy_options.go index 12ad085fd1..0624b450e8 100644 --- a/pkg/bindings/containers/types_copy_options.go +++ b/pkg/bindings/containers/types_copy_options.go @@ -35,3 +35,19 @@ func (o *CopyOptions) GetChown() bool { } return *o.Chown } + +// WithRename +func (o *CopyOptions) WithRename(value map[string]string) *CopyOptions { + v := value + o.Rename = v + return o +} + +// GetRename +func (o *CopyOptions) GetRename() map[string]string { + var rename map[string]string + if o.Rename == nil { + return rename + } + return o.Rename +} diff --git a/pkg/copy/parse.go b/pkg/copy/parse.go index 39e0e15471..93edec5fa6 100644 --- a/pkg/copy/parse.go +++ b/pkg/copy/parse.go @@ -18,18 +18,6 @@ func ParseSourceAndDestination(source, destination string) (string, string, stri sourceContainer, sourcePath := parseUserInput(source) destContainer, destPath := parseUserInput(destination) - numContainers := 0 - if len(sourceContainer) > 0 { - numContainers++ - } - if len(destContainer) > 0 { - numContainers++ - } - - if numContainers != 1 { - return "", "", "", "", errors.Errorf("invalid arguments %q, %q: exactly 1 container expected but %d specified", source, destination, numContainers) - } - if len(sourcePath) == 0 || len(destPath) == 0 { return "", "", "", "", errors.Errorf("invalid arguments %q, %q: you must specify paths", source, destination) } diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 4d85941cd6..7655e2e63e 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -165,6 +165,8 @@ type CopyOptions struct { // it will change ownership of files from the source tar archive // to the primary uid/gid of the destination container. Chown bool + // Map to translate path names. + Rename map[string]string } type CommitReport struct { diff --git a/pkg/domain/infra/abi/archive.go b/pkg/domain/infra/abi/archive.go index 1a5bb6dc42..b60baa935d 100644 --- a/pkg/domain/infra/abi/archive.go +++ b/pkg/domain/infra/abi/archive.go @@ -12,7 +12,7 @@ func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrI if err != nil { return nil, err } - return container.CopyFromArchive(ctx, containerPath, options.Chown, reader) + return container.CopyFromArchive(ctx, containerPath, options.Chown, options.Rename, reader) } func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) { diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index d7e8a4e46d..58f9c5fb0e 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -853,7 +853,8 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o } func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID, path string, reader io.Reader, options entities.CopyOptions) (entities.ContainerCopyFunc, error) { - return containers.CopyFromArchiveWithOptions(ic.ClientCtx, nameOrID, path, reader, new(containers.CopyOptions).WithChown(options.Chown)) + copyOptions := new(containers.CopyOptions).WithChown(options.Chown).WithRename(options.Rename) + return containers.CopyFromArchiveWithOptions(ic.ClientCtx, nameOrID, path, reader, copyOptions) } func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) { diff --git a/test/system/065-cp.bats b/test/system/065-cp.bats index 5778eb46e9..f589a1290f 100644 --- a/test/system/065-cp.bats +++ b/test/system/065-cp.bats @@ -226,6 +226,96 @@ load helpers } +@test "podman cp file from container to container" { + # Create 3 files with random content in the container. + local -a randomcontent=( + random-0-$(random_string 10) + random-1-$(random_string 15) + random-2-$(random_string 20) + ) + run_podman run -d --name cpcontainer --workdir=/srv $IMAGE sleep infinity + run_podman exec cpcontainer sh -c "echo ${randomcontent[0]} > /tmp/containerfile" + run_podman exec cpcontainer sh -c "echo ${randomcontent[0]} > /tmp/dotfile." + run_podman exec cpcontainer sh -c "echo ${randomcontent[1]} > /srv/containerfile1" + run_podman exec cpcontainer sh -c "mkdir /srv/subdir; echo ${randomcontent[2]} > /srv/subdir/containerfile2" + + # Commit the image for testing non-running containers + run_podman commit -q cpcontainer + cpimage="$output" + + # format is: | | | | + tests=" +0 | /tmp/containerfile | | /containerfile | / +0 | /tmp/dotfile. | | /dotfile. | / +0 | /tmp/containerfile | / | /containerfile | / +0 | /tmp/containerfile | /. | /containerfile | /. +0 | /tmp/containerfile | /newfile | /newfile | /newfile +1 | containerfile1 | / | /containerfile1 | copy from workdir (rel path) to / +2 | subdir/containerfile2 | / | /containerfile2 | copy from workdir/subdir (rel path) to / +" + + # From RUNNING container + while read id src dest dest_fullname description; do + # dest may be "''" for empty table cells + if [[ $dest == "''" ]];then + unset dest + fi + + # To RUNNING container + run_podman run -d $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman exec $destcontainer cat "/$dest_fullname" + is "$output" "${randomcontent[$id]}" "$description (cp ctr:$src to /$dest)" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + + # To CREATED container + run_podman create $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman start $destcontainer + run_podman exec $destcontainer cat "/$dest_fullname" + is "$output" "${randomcontent[$id]}" "$description (cp ctr:$src to /$dest)" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + done < <(parse_table "$tests") + run_podman kill cpcontainer + run_podman rm -f cpcontainer + + # From CREATED container + run_podman create --name cpcontainer --workdir=/srv $cpimage + while read id src dest dest_fullname description; do + # dest may be "''" for empty table cells + if [[ $dest == "''" ]];then + unset dest + fi + + # To RUNNING container + run_podman run -d $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman exec $destcontainer cat "/$dest_fullname" + is "$output" "${randomcontent[$id]}" "$description (cp ctr:$src to /$dest)" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + + # To CREATED container + run_podman create $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman start $destcontainer + run_podman exec $destcontainer cat "/$dest_fullname" + is "$output" "${randomcontent[$id]}" "$description (cp ctr:$src to /$dest)" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + done < <(parse_table "$tests") + run_podman rm -f cpcontainer + + run_podman rmi -f $cpimage +} + + @test "podman cp dir from host to container" { srcdir=$PODMAN_TMPDIR mkdir -p $srcdir/dir/sub @@ -377,6 +467,110 @@ load helpers } +@test "podman cp dir from container to container" { + # Create 2 files with random content in the container. + local -a randomcontent=( + random-0-$(random_string 10) + random-1-$(random_string 15) + ) + run_podman run -d --name cpcontainer --workdir=/srv $IMAGE sleep infinity + run_podman exec cpcontainer sh -c "mkdir /srv/subdir; echo ${randomcontent[0]} > /srv/subdir/containerfile0" + run_podman exec cpcontainer sh -c "echo ${randomcontent[1]} > /srv/subdir/containerfile1" + # "." and "dir/." will copy the contents, so make sure that a dir ending + # with dot is treated correctly. + run_podman exec cpcontainer sh -c 'mkdir /tmp/subdir.; cp /srv/subdir/* /tmp/subdir./' + + # Commit the image for testing non-running containers + run_podman commit -q cpcontainer + cpimage="$output" + + # format is: | | | + tests=" +/srv | | /srv/subdir | copy /srv +/srv | /newdir | /newdir/subdir | copy /srv to /newdir +/srv/ | | /srv/subdir | copy /srv/ +/srv/. | | /subdir | copy /srv/. +/srv/. | /newdir | /newdir/subdir | copy /srv/. to /newdir +/srv/subdir/. | | | copy /srv/subdir/. +/tmp/subdir. | | /subdir. | copy /tmp/subdir. +" + + # From RUNNING container + while read src dest dest_fullname description; do + if [[ $src == "''" ]];then + unset src + fi + if [[ $dest == "''" ]];then + unset dest + fi + if [[ $dest_fullname == "''" ]];then + unset dest_fullname + fi + + # To RUNNING container + run_podman run -d $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman exec $destcontainer cat "/$dest_fullname/containerfile0" "/$dest_fullname/containerfile1" + is "$output" "${randomcontent[0]} +${randomcontent[1]}" "$description" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + + # To CREATED container + run_podman create $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman start $destcontainer + run_podman exec $destcontainer cat "/$dest_fullname/containerfile0" "/$dest_fullname/containerfile1" + is "$output" "${randomcontent[0]} +${randomcontent[1]}" "$description" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + done < <(parse_table "$tests") + run_podman kill cpcontainer + run_podman rm -f cpcontainer + + # From CREATED container + run_podman create --name cpcontainer --workdir=/srv $cpimage + while read src dest dest_fullname description; do + if [[ $src == "''" ]];then + unset src + fi + if [[ $dest == "''" ]];then + unset dest + fi + if [[ $dest_fullname == "''" ]];then + unset dest_fullname + fi + + # To RUNNING container + run_podman run -d $IMAGE sleep infinity + destcontainer="$output" + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman exec $destcontainer cat "/$dest_fullname/containerfile0" "/$dest_fullname/containerfile1" + is "$output" "${randomcontent[0]} +${randomcontent[1]}" "$description" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + + # To CREATED container + run_podman create $IMAGE sleep infinity + destcontainer="$output" + run_podman start $destcontainer + run_podman cp cpcontainer:$src $destcontainer:"/$dest" + run_podman exec $destcontainer cat "/$dest_fullname/containerfile0" "/$dest_fullname/containerfile1" + is "$output" "${randomcontent[0]} +${randomcontent[1]}" "$description" + run_podman kill $destcontainer + run_podman rm -f $destcontainer + done < <(parse_table "$tests") + + run_podman rm -f cpcontainer + run_podman rmi -f $cpimage +} + + @test "podman cp symlinked directory from container" { destdir=$PODMAN_TMPDIR/cp-weird-symlink mkdir -p $destdir