Skip to content

Commit

Permalink
Merge pull request #11049 from vrothberg/fix-7370
Browse files Browse the repository at this point in the history
support container to container copy
  • Loading branch information
openshift-merge-robot authored Jul 27, 2021
2 parents 22b3fe5 + 0aec93e commit a5de831
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 46 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 a5de831

Please sign in to comment.