summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorValentin Rothberg <rothberg@redhat.com>2020-12-11 11:00:25 +0100
committerValentin Rothberg <rothberg@redhat.com>2020-12-18 12:08:49 +0100
commitadcb3a7a609ba756f8b9de17521f0e3dce3b778e (patch)
tree4572b8ed243ffa33648a9190dcf4c2ac9a419099 /cmd
parentf56865879ccffeddce3b9e36f585fe67c37591d5 (diff)
downloadpodman-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.go318
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
}