mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00

The nolintlint linter does not deny the use of `//nolint` Instead it allows us to enforce a common nolint style: - force that a linter name must be specified - do not add a space between `//` and `nolint` - make sure nolint is only used when there is actually a problem Signed-off-by: Paul Holzinger <pholzing@redhat.com>
523 lines
18 KiB
Go
523 lines
18 KiB
Go
package containers
|
|
|
|
import (
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
buildahCopiah "github.com/containers/buildah/copier"
|
|
"github.com/containers/podman/v4/cmd/podman/common"
|
|
"github.com/containers/podman/v4/cmd/podman/registry"
|
|
"github.com/containers/podman/v4/pkg/copy"
|
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
|
"github.com/containers/podman/v4/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 = `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 a directory.
|
|
`
|
|
cpCommand = &cobra.Command{
|
|
Use: "cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
|
|
Short: "Copy files/folders between a container and the local filesystem",
|
|
Long: cpDescription,
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: cp,
|
|
ValidArgsFunction: common.AutocompleteCpCommand,
|
|
Example: "podman cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
|
|
}
|
|
|
|
containerCpCommand = &cobra.Command{
|
|
Use: cpCommand.Use,
|
|
Short: cpCommand.Short,
|
|
Long: cpCommand.Long,
|
|
Args: cpCommand.Args,
|
|
RunE: cpCommand.RunE,
|
|
ValidArgsFunction: cpCommand.ValidArgsFunction,
|
|
Example: "podman container cp [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
|
|
}
|
|
)
|
|
|
|
var (
|
|
cpOpts entities.ContainerCpOptions
|
|
chown bool
|
|
)
|
|
|
|
func cpFlags(cmd *cobra.Command) {
|
|
flags := cmd.Flags()
|
|
flags.BoolVar(&cpOpts.OverwriteDirNonDir, "overwrite", false, "Allow to overwrite directories with non-directories and vice versa")
|
|
flags.BoolVarP(&chown, "archive", "a", true, `Chown copied files to the primary uid/gid of the destination container.`)
|
|
|
|
// Deprecated flags (both are NOPs): exist for backwards compat
|
|
flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...")
|
|
_ = flags.MarkHidden("extract")
|
|
flags.BoolVar(&cpOpts.Pause, "pause", true, "Deprecated")
|
|
_ = flags.MarkHidden("pause")
|
|
}
|
|
|
|
func init() {
|
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
|
Command: cpCommand,
|
|
})
|
|
cpFlags(cpCommand)
|
|
|
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
|
Command: containerCpCommand,
|
|
Parent: containerCmd,
|
|
})
|
|
cpFlags(containerCpCommand)
|
|
}
|
|
|
|
func cp(cmd *cobra.Command, args []string) error {
|
|
// Parse user input.
|
|
sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 {
|
|
return copyContainerToContainer(sourceContainerStr, sourcePath, destContainerStr, destPath)
|
|
} else 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
destContainerBaseName, destContainerInfo, destResolvedToParentDir, err := resolvePathOnDestinationContainer(destContainer, destPath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if sourceContainerInfo.IsDir && !destContainerInfo.IsDir {
|
|
return errors.New("destination must be a directory when copying a directory")
|
|
}
|
|
|
|
sourceContainerTarget := sourceContainerInfo.LinkTarget
|
|
destContainerTarget := 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 destResolvedToParentDir && 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, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir}
|
|
if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir {
|
|
// 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 {
|
|
return err
|
|
}
|
|
|
|
isStdout := false
|
|
if hostPath == "-" {
|
|
isStdout = true
|
|
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
|
|
var resolvedToHostParentDir bool
|
|
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)
|
|
resolvedToHostParentDir = true
|
|
} 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)
|
|
}
|
|
|
|
if !isStdout {
|
|
if err := validateFileInfo(hostInfo); err != nil {
|
|
return errors.Wrap(err, "invalid destination")
|
|
}
|
|
}
|
|
|
|
// If we copy a directory via the "." notation and the host path does
|
|
// not exist, we need to make sure that the destination on the host
|
|
// gets created; otherwise the contents of the source directory will be
|
|
// written to the destination's parent directory.
|
|
//
|
|
// While we could cut it short on the host and do create the directory
|
|
// ourselves, we would run into problems trying to that the other way
|
|
// around when copying into a container. Instead, to keep both
|
|
// implementations symmetrical, we need to massage the code a bit to
|
|
// let Buildah's copier package create the destination.
|
|
//
|
|
// 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.
|
|
containerTarget := containerInfo.LinkTarget
|
|
if resolvedToHostParentDir && containerInfo.IsDir && strings.HasSuffix(containerTarget, ".") {
|
|
containerTarget = filepath.Dir(containerTarget)
|
|
}
|
|
|
|
if !isStdout && containerInfo.IsDir && !hostInfo.IsDir {
|
|
return errors.New("destination must be a directory when copying a directory")
|
|
}
|
|
|
|
reader, writer := io.Pipe()
|
|
hostCopy := func() error {
|
|
defer reader.Close()
|
|
if isStdout {
|
|
_, 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,
|
|
IgnoreDevices: true,
|
|
NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir,
|
|
NoOverwriteNonDirDir: !cpOpts.OverwriteDirNonDir,
|
|
}
|
|
if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir {
|
|
// If we're having a file-to-file copy, make sure to
|
|
// rename accordingly.
|
|
putOptions.Rename = map[string]string{filepath.Base(containerTarget): 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, containerTarget, 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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
containerBaseName, containerInfo, containerResolvedToParentDir, err := resolvePathOnDestinationContainer(container, containerPath, isStdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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.
|
|
hostTarget := hostInfo.LinkTarget
|
|
if containerResolvedToParentDir && hostInfo.IsDir && strings.HasSuffix(hostTarget, ".") {
|
|
hostTarget = filepath.Dir(hostTarget)
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
if hostInfo.IsDir && !containerInfo.IsDir {
|
|
return errors.New("destination must be a directory when copying a directory")
|
|
}
|
|
|
|
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 points to ".", we want to
|
|
// copy the base directory.
|
|
KeepDirectoryNames: hostInfo.IsDir && filepath.Base(hostTarget) != ".",
|
|
}
|
|
if (!hostInfo.IsDir && !containerInfo.IsDir) || containerResolvedToParentDir {
|
|
// If we're having a file-to-file copy, make sure to
|
|
// rename accordingly.
|
|
getOptions.Rename = map[string]string{filepath.Base(hostTarget): containerBaseName}
|
|
}
|
|
if err := buildahCopiah.Get("/", "", getOptions, []string{hostTarget}, 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, entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := copyFunc(); err != nil {
|
|
return errors.Wrap(err, "error copying to container")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return doCopy(hostCopy, containerCopy)
|
|
}
|
|
|
|
// resolvePathOnDestinationContainer resolves the specified path on the
|
|
// container. If the path does not exist, it attempts to use the parent
|
|
// directory.
|
|
func resolvePathOnDestinationContainer(container string, containerPath string, isStdin bool) (baseName string, containerInfo *entities.ContainerStatReport, resolvedToParentDir bool, err error) {
|
|
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath)
|
|
if err == nil {
|
|
baseName = filepath.Base(containerInfo.LinkTarget)
|
|
return //nolint: nilerr
|
|
}
|
|
|
|
if strings.HasSuffix(containerPath, "/") {
|
|
err = errors.Wrapf(err, "%q could not be found on container %s", containerPath, container)
|
|
return
|
|
}
|
|
if isStdin {
|
|
err = errors.New("destination must be a directory when copying from stdin")
|
|
return
|
|
}
|
|
|
|
// 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 {
|
|
baseName = filepath.Base(containerInfo.LinkTarget)
|
|
path = containerInfo.LinkTarget
|
|
} else {
|
|
baseName = filepath.Base(containerPath)
|
|
}
|
|
|
|
parentDir, err := containerParentDir(container, path)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "could not determine parent dir of %q on container %s", path, container)
|
|
return
|
|
}
|
|
|
|
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "%q could not be found on container %s", containerPath, container)
|
|
return
|
|
}
|
|
|
|
resolvedToParentDir = true
|
|
return baseName, containerInfo, resolvedToParentDir, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// validateFileInfo returns an error if the specified FileInfo doesn't point to
|
|
// a directory or a regular file.
|
|
func validateFileInfo(info *copy.FileInfo) error {
|
|
if info.Mode.IsDir() || info.Mode.IsRegular() {
|
|
return nil
|
|
}
|
|
return errors.Errorf("%q must be a directory or a regular file", info.LinkTarget)
|
|
}
|