Skip to content

Commit

Permalink
support container to container copy
Browse files Browse the repository at this point in the history
Implement container to container copy.  Previously data could only be
copied from/to the host.

Fixes: containers#7370
Co-authored-by: Mehul Arora <[email protected]>
Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
Mehul Arora authored and vrothberg committed Jul 27, 2021
1 parent b6c279b commit 6fe03b2
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 26 deletions.
108 changes: 107 additions & 1 deletion cmd/podman/containers/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 6 additions & 5 deletions docs/source/markdown/podman-cp.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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., `/`).

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion libpod/container_copy_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 14 additions & 3 deletions pkg/api/handlers/compat/containers_archive.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compat

import (
"encoding/json"
"fmt"
"net/http"
"os"
Expand Down Expand Up @@ -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"`
}{
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/server/register_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/bindings/containers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 16 additions & 0 deletions pkg/bindings/containers/types_copy_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
12 changes: 0 additions & 12 deletions pkg/copy/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/domain/entities/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/infra/abi/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/domain/infra/tunnel/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 6fe03b2

Please sign in to comment.