diff options
author | Valentin Rothberg <rothberg@redhat.com> | 2020-12-11 11:00:25 +0100 |
---|---|---|
committer | Valentin Rothberg <rothberg@redhat.com> | 2020-12-18 12:08:49 +0100 |
commit | adcb3a7a609ba756f8b9de17521f0e3dce3b778e (patch) | |
tree | 4572b8ed243ffa33648a9190dcf4c2ac9a419099 /cmd | |
parent | f56865879ccffeddce3b9e36f585fe67c37591d5 (diff) | |
download | podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.tar.gz podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.tar.bz2 podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.zip |
remote copy
Implement `podman-remote cp` and break out the logic from the previously
added `pkg/copy` into it's basic building blocks and move them up into
the `ContainerEngine` interface and `cmd/podman`.
The `--pause` and `--extract` flags are now deprecated and turned into
nops.
Note that this commit is vendoring a non-release version of Buildah to
pull in updates to the copier package.
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/podman/containers/cp.go | 318 |
1 files changed, 310 insertions, 8 deletions
diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index 9b0a01a2f..e0161824f 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -1,19 +1,34 @@ package containers import ( + "io" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" "github.com/containers/podman/v2/cmd/podman/common" "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/pkg/copy" "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/errorhandling" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var ( - cpDescription = `Command copies the contents of SRC_PATH to the DEST_PATH. + cpDescription = `Copy the contents of SRC_PATH to the DEST_PATH. - You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. 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. + You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. 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 a directory. ` cpCommand = &cobra.Command{ - Use: "cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH", + Use: "cp [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH", Short: "Copy files/folders between a container and the local filesystem", Long: cpDescription, Args: cobra.ExactArgs(2), @@ -39,19 +54,21 @@ var ( func cpFlags(cmd *cobra.Command) { flags := cmd.Flags() - flags.BoolVar(&cpOpts.Extract, "extract", false, "Extract the tar file into the destination directory.") - flags.BoolVar(&cpOpts.Pause, "pause", true, "Pause the container while copying") + flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...") + flags.BoolVar(&cpOpts.Pause, "pause", true, "Deorecated") + _ = flags.MarkHidden("extract") + _ = flags.MarkHidden("pause") } func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode}, + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: cpCommand, }) cpFlags(cpCommand) registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode}, + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: containerCpCommand, Parent: containerCmd, }) @@ -59,5 +76,290 @@ func init() { } func cp(cmd *cobra.Command, args []string) error { - return registry.ContainerEngine().ContainerCp(registry.GetContext(), args[0], args[1], cpOpts) + // Parse user input. + sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1]) + if err != nil { + return err + } + + if len(sourceContainerStr) > 0 { + return copyFromContainer(sourceContainerStr, sourcePath, destPath) + } + + return copyToContainer(destContainerStr, destPath, sourcePath) +} + +// containerMustExist returns an error if the specified container does not +// exist. +func containerMustExist(container string) error { + exists, err := registry.ContainerEngine().ContainerExists(registry.GetContext(), container, entities.ContainerExistsOptions{}) + if err != nil { + return err + } + if !exists.Value { + return errors.Errorf("container %q does not exist", container) + } + return nil +} + +// doCopy executes the two functions in parallel to copy data from A to B and +// joins the errors if any. +func doCopy(funcA func() error, funcB func() error) error { + errChan := make(chan error) + go func() { + errChan <- funcA() + }() + var copyErrors []error + copyErrors = append(copyErrors, funcB()) + copyErrors = append(copyErrors, <-errChan) + return errorhandling.JoinErrors(copyErrors) +} + +// 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 { + return err + } + + if hostPath == "-" { + hostPath = os.Stdout.Name() + } + + containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + if err != nil { + return errors.Wrapf(err, "%q could not be found on container %s", containerPath, container) + } + + var hostBaseName string + hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath) + if hostInfoErr != nil { + if strings.HasSuffix(hostPath, "/") { + return errors.Wrapf(hostInfoErr, "%q could not be found on the host", hostPath) + } + // If it doesn't exist, then let's have a look at the parent dir. + parentDir := filepath.Dir(hostPath) + hostInfo, err = copy.ResolveHostPath(parentDir) + if err != nil { + return errors.Wrapf(hostInfoErr, "%q could not be found on the host", hostPath) + } + // If the specified path does not exist, we need to assume that + // it'll be created while copying. Hence, we use it as the + // base path. + hostBaseName = filepath.Base(hostPath) + } else { + // If the specified path exists on the host, we must use its + // base path as it may have changed due to symlink evaluations. + hostBaseName = filepath.Base(hostInfo.LinkTarget) + } + + reader, writer := io.Pipe() + hostCopy := func() error { + defer reader.Close() + if hostInfo.LinkTarget == os.Stdout.Name() { + _, err := io.Copy(os.Stdout, reader) + return err + } + + groot, err := user.Current() + if err != nil { + return err + } + + // Set the {G,U}ID. Let's be tolerant towards the different + // operating systems and only log the errors, so we can debug + // if necessary. + idPair := idtools.IDPair{} + if i, err := strconv.Atoi(groot.Uid); err == nil { + idPair.UID = i + } else { + logrus.Debugf("Error converting UID %q to int: %v", groot.Uid, err) + } + if i, err := strconv.Atoi(groot.Gid); err == nil { + idPair.GID = i + } else { + logrus.Debugf("Error converting GID %q to int: %v", groot.Gid, err) + } + + putOptions := buildahCopiah.PutOptions{ + ChownDirs: &idPair, + ChownFiles: &idPair, + } + if !containerInfo.IsDir && (!hostInfo.IsDir || hostInfoErr != nil) { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + putOptions.Rename = map[string]string{filepath.Base(containerInfo.LinkTarget): hostBaseName} + } + dir := hostInfo.LinkTarget + if !hostInfo.IsDir { + dir = filepath.Dir(dir) + } + if err := buildahCopiah.Put(dir, "", putOptions, reader); err != nil { + return errors.Wrap(err, "error copying to host") + } + return nil + } + + containerCopy := func() error { + defer writer.Close() + copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), container, containerInfo.LinkTarget, writer) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return errors.Wrap(err, "error copying from container") + } + return nil + } + return doCopy(containerCopy, hostCopy) +} + +// copyToContainer copies the hostPath to containerPath on the container. +func copyToContainer(container string, containerPath string, hostPath string) error { + if err := containerMustExist(container); err != nil { + return err + } + + isStdin := false + if hostPath == "-" { + hostPath = os.Stdin.Name() + isStdin = true + } else if hostPath == os.Stdin.Name() { + isStdin = true + } + + // Make sure that host path exists. + hostInfo, err := copy.ResolveHostPath(hostPath) + if err != nil { + return errors.Wrapf(err, "%q could not be found on the host", hostPath) + } + + // If the path on the container does not exist. We need to make sure + // that it's parent directory exists. The destination may be created + // while copying. + var containerBaseName string + containerInfo, containerInfoErr := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + if containerInfoErr != nil { + if strings.HasSuffix(containerPath, "/") { + return errors.Wrapf(containerInfoErr, "%q could not be found on container %s", containerPath, container) + } + if isStdin { + return errors.New("destination must be a directory when copying from stdin") + } + // 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 := containerPath + if containerInfo != nil { + containerBaseName = filepath.Base(containerInfo.LinkTarget) + path = containerInfo.LinkTarget + } else { + containerBaseName = filepath.Base(containerPath) + } + + parentDir, err := containerParentDir(container, path) + if err != nil { + return errors.Wrapf(err, "could not determine parent dir of %q on container %s", path, container) + } + containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir) + if err != nil { + return errors.Wrapf(err, "%q could not be found on container %s", containerPath, container) + } + } else { + // If the specified path exists on the container, we must use + // its base path as it may have changed due to symlink + // evaluations. + containerBaseName = filepath.Base(containerInfo.LinkTarget) + } + + var stdinFile string + if isStdin { + if !containerInfo.IsDir { + return errors.New("destination must be a directory when copying from stdin") + } + + // Copy from stdin to a temporary file *before* throwing it + // over the wire. This allows for proper client-side error + // reporting. + tmpFile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + _, err = io.Copy(tmpFile, os.Stdin) + if err != nil { + return err + } + if err = tmpFile.Close(); err != nil { + return err + } + if !archive.IsArchivePath(tmpFile.Name()) { + return errors.New("source must be a (compressed) tar archive when copying from stdin") + } + stdinFile = tmpFile.Name() + } + + reader, writer := io.Pipe() + hostCopy := func() error { + defer writer.Close() + if isStdin { + stream, err := os.Open(stdinFile) + if err != nil { + return err + } + defer stream.Close() + _, err = io.Copy(writer, stream) + return err + } + + getOptions := buildahCopiah.GetOptions{ + // Unless the specified path ends with ".", we want to copy the base directory. + KeepDirectoryNames: !strings.HasSuffix(hostPath, "."), + } + if !hostInfo.IsDir && (!containerInfo.IsDir || containerInfoErr != nil) { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + getOptions.Rename = map[string]string{filepath.Base(hostInfo.LinkTarget): containerBaseName} + } + if err := buildahCopiah.Get("/", "", getOptions, []string{hostInfo.LinkTarget}, writer); err != nil { + return errors.Wrap(err, "error copying from host") + } + return nil + } + + containerCopy := func() error { + defer reader.Close() + target := containerInfo.FileInfo.LinkTarget + if !containerInfo.IsDir { + target = filepath.Dir(target) + } + + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return errors.Wrap(err, "error copying to container") + } + return nil + } + + return doCopy(hostCopy, containerCopy) +} + +// containerParentDir returns the parent directory of the specified path on the +// container. If the path is relative, it will be resolved relative to the +// container's working directory (or "/" if the work dir isn't set). +func containerParentDir(container string, containerPath string) (string, error) { + if filepath.IsAbs(containerPath) { + return filepath.Dir(containerPath), nil + } + inspectData, _, err := registry.ContainerEngine().ContainerInspect(registry.GetContext(), []string{container}, entities.InspectOptions{}) + if err != nil { + return "", err + } + if len(inspectData) != 1 { + return "", errors.Errorf("inspecting container %q: expected 1 data item but got %d", container, len(inspectData)) + } + workDir := filepath.Join("/", inspectData[0].Config.WorkingDir) + workDir = filepath.Join(workDir, containerPath) + return filepath.Dir(workDir), nil } |