Files
Paul Holzinger 41528739ce golangci-lint: enable nolintlint
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>
2022-06-14 16:29:42 +02:00

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)
}