support container to container copy

Implement container to container copy.  Previously data could only be
copied from/to the host.

Fixes: #7370
Co-authored-by: Mehul Arora <aroram18@mcmaster.ca>
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Mehul Arora
2021-06-19 11:27:24 +05:30
committed by Valentin Rothberg
parent b6c279be22
commit 6fe03b25ab
13 changed files with 352 additions and 26 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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) {

View File

@ -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: <id> | <source arg to cp> | <destination arg (appended to $srcdir) to cp> | <full dest path (appended to $srcdir)> | <test name>
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: <source arg to cp (appended to /srv)> | <dest> | <full dest path> | <test name>
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