Add build-tagged splits for cp command to support podman-remote

The original cp.go uses buildah/copier which depends on libimage,
making it unavailable in remote builds. This change splits cp
implementation into separate local and remote variants:

- cmd/podman/containers/cp.go: Shared command definitions, flags, and
  init function (no build tag)
- cmd/podman/containers/cp_local.go: Local implementation using
  buildah/copier (//go:build !remote)
- cmd/podman/containers/cp_remote.go: Remote implementation using
  stdlib archive/tar (//go:build remote)

The remote implementation:
- Uses ContainerEngine interface methods that work over REST API:
  ContainerStat, ContainerCopyToArchive, ContainerCopyFromArchive
- Properly handles file and directory copying in both directions
- Uses CopyOptions.Rename for file renaming when copying to specific
  filenames
- Returns error for container-to-container copying (not supported with
  podman-remote)

This follows the same pattern used for other libimage-dependent code
in commit 4231526e18.

Fixes system tests that require cp functionality with podman-remote.

Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
This commit is contained in:
Lokesh Mandvekar 2025-12-19 18:56:10 -05:00
parent ca1a0db4e7
commit 8fd34d5bfd
No known key found for this signature in database
GPG key ID: 1C1EDD7CC7C3A0DD
3 changed files with 758 additions and 461 deletions

View file

@ -1,27 +1,9 @@
//go:build !remote
package containers
import (
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"errors"
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/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -80,446 +62,3 @@ func init() {
})
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 fmt.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 fmt.Errorf("%q could not be found on container %s: %w", sourcePath, sourceContainer, err)
}
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 && filepath.Base(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 fmt.Errorf("copying from container: %w", err)
}
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 fmt.Errorf("copying to container: %w", err)
}
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 fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
}
var hostBaseName string
var resolvedToHostParentDir bool
hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath)
if hostInfoErr != nil {
if strings.HasSuffix(hostPath, "/") {
return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr)
}
// 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 fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr)
}
// 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 fmt.Errorf("invalid destination: %w", err)
}
}
// 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 && filepath.Base(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 fmt.Errorf("copying to host: %w", err)
}
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 fmt.Errorf("copying from container: %w", err)
}
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 fmt.Errorf("%q could not be found on the host: %w", hostPath, err)
}
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 && filepath.Base(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 := os.CreateTemp("", "")
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 fmt.Errorf("copying from host: %w", err)
}
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 fmt.Errorf("copying to container: %w", err)
}
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 = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
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 = fmt.Errorf("could not determine parent dir of %q on container %s: %w", path, container, err)
return
}
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir)
if err != nil {
err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
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 "", fmt.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 fmt.Errorf("%q must be a directory or a regular file", info.LinkTarget)
}

View file

@ -0,0 +1,468 @@
//go:build !remote
package containers
import (
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"errors"
buildahCopiah "github.com/containers/buildah/copier"
"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/sirupsen/logrus"
"github.com/spf13/cobra"
)
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 fmt.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 fmt.Errorf("%q could not be found on container %s: %w", sourcePath, sourceContainer, err)
}
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 && filepath.Base(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 fmt.Errorf("copying from container: %w", err)
}
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 fmt.Errorf("copying to container: %w", err)
}
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 fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
}
var hostBaseName string
var resolvedToHostParentDir bool
hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath)
if hostInfoErr != nil {
if strings.HasSuffix(hostPath, "/") {
return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr)
}
// 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 fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr)
}
// 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 fmt.Errorf("invalid destination: %w", err)
}
}
// 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 && filepath.Base(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 fmt.Errorf("copying to host: %w", err)
}
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 fmt.Errorf("copying from container: %w", err)
}
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 fmt.Errorf("%q could not be found on the host: %w", hostPath, err)
}
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 && filepath.Base(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 := os.CreateTemp("", "")
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 fmt.Errorf("copying from host: %w", err)
}
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 fmt.Errorf("copying to container: %w", err)
}
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 = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
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 = fmt.Errorf("could not determine parent dir of %q on container %s: %w", path, container, err)
return
}
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir)
if err != nil {
err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
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 "", fmt.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 fmt.Errorf("%q must be a directory or a regular file", info.LinkTarget)
}

View file

@ -0,0 +1,290 @@
//go:build remote
package containers
import (
"archive/tar"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"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/spf13/cobra"
)
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 errors.New("copying between containers is not supported with podman-remote")
} else if len(sourceContainerStr) > 0 {
return copyFromContainerRemote(sourceContainerStr, sourcePath, destPath)
}
return copyToContainerRemote(destContainerStr, destPath, sourcePath)
}
// copyFromContainerRemote copies from the containerPath on the container to hostPath.
func copyFromContainerRemote(container string, containerPath string, hostPath string) error {
isStdout := hostPath == "-"
if isStdout {
hostPath = os.Stdout.Name()
}
// Get the file info from the container to know what we're copying
containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath)
if err != nil {
return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
}
reader, writer := io.Pipe()
// Start copying from the container in a goroutine
var copyErr error
go func() {
defer writer.Close()
copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), container, containerInfo.LinkTarget, writer)
if err != nil {
copyErr = err
return
}
copyErr = copyFunc()
}()
// Extract the tar archive to the host
defer reader.Close()
if isStdout {
_, err := io.Copy(os.Stdout, reader)
return err
}
// Extract tar to destination
if err := extractTar(reader, hostPath, containerInfo.IsDir); err != nil {
return err
}
return copyErr
}
// copyToContainerRemote copies from hostPath to the containerPath on the container.
func copyToContainerRemote(container string, containerPath string, hostPath string) error {
isStdin := hostPath == "-"
if isStdin {
hostPath = os.Stdin.Name()
}
// Get info about the host path
hostInfo, err := os.Stat(hostPath)
if err != nil {
return fmt.Errorf("%q could not be found on the host: %w", hostPath, err)
}
// Get info about the container destination path
containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath)
var containerExists bool
var containerIsDir bool
var containerResolvedToParentDir bool
var targetPath string
var containerBaseName string
if err != nil {
// Container path doesn't exist
// If path has trailing /, it must be a directory (error if it doesn't exist)
if strings.HasSuffix(containerPath, "/") {
return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
}
containerExists = false
// If we're copying contents only (source ends with /.), use the dest path directly
// The server will create it as a directory
if strings.HasSuffix(hostPath, "/.") {
targetPath = containerPath
containerResolvedToParentDir = false
} else {
// Otherwise, use parent directory and rename
containerResolvedToParentDir = true
targetPath = filepath.Dir(containerPath)
containerBaseName = filepath.Base(containerPath)
}
} else {
containerExists = true
containerIsDir = containerInfo.IsDir
if containerIsDir {
// Destination is a directory - extract into it
targetPath = containerPath
} else {
// Destination is a file - use parent directory
targetPath = filepath.Dir(containerInfo.LinkTarget)
containerBaseName = filepath.Base(containerInfo.LinkTarget)
}
}
// Validate: can't copy directory to a file
if hostInfo.IsDir() && containerExists && !containerIsDir {
return errors.New("destination must be a directory when copying a directory")
}
reader, writer := io.Pipe()
// Create tar archive from host path in a goroutine
var tarErr error
go func() {
defer writer.Close()
if isStdin {
_, tarErr = io.Copy(writer, os.Stdin)
} else {
tarErr = createTar(hostPath, writer)
}
}()
// Copy the archive to the container
defer reader.Close()
copyOptions := entities.CopyOptions{
Chown: chown,
}
// If we're copying to a non-existent path or file-to-file, use Rename
// But NOT when copying contents only (hostPath ends with /.)
if ((!hostInfo.IsDir() && !containerIsDir) || containerResolvedToParentDir) && !strings.HasSuffix(hostPath, "/.") {
copyOptions.Rename = map[string]string{filepath.Base(hostPath): containerBaseName}
}
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, targetPath, reader, copyOptions)
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return err
}
return tarErr
}
// createTar creates a tar archive from the specified path
func createTar(sourcePath string, writer io.Writer) error {
tw := tar.NewWriter(writer)
defer tw.Close()
// Check if we should copy contents only (path ends with /.)
// vs copying the directory itself
// Must check BEFORE cleaning, as Clean will remove the trailing /.
keepDirName := !strings.HasSuffix(sourcePath, "/.")
cleanPath := filepath.Clean(sourcePath)
baseDir := filepath.Dir(cleanPath)
return filepath.Walk(cleanPath, func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return err
}
// Update the name to be relative
var relPath string
if keepDirName {
// Include the directory name in the archive
relPath, err = filepath.Rel(baseDir, file)
} else {
// Copy contents only, exclude the directory name
relPath, err = filepath.Rel(cleanPath, file)
if relPath == "." {
// Skip the directory itself when copying contents
return nil
}
}
if err != nil {
return err
}
header.Name = relPath
if err := tw.WriteHeader(header); err != nil {
return err
}
if !fi.IsDir() {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(tw, f); err != nil {
return err
}
}
return nil
})
}
// extractTar extracts a tar archive to the specified destination
func extractTar(reader io.Reader, destPath string, isDir bool) error {
tr := tar.NewReader(reader)
// Check if destination exists
destInfo, destErr := os.Stat(destPath)
destExists := destErr == nil
destIsDir := destExists && destInfo.IsDir()
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
var target string
// If dest doesn't exist and we're extracting a single file, use dest as the filename
if !destExists && !isDir && header.Typeflag == tar.TypeReg {
target = destPath
} else if destIsDir {
// Dest is a directory, extract into it
target = filepath.Join(destPath, header.Name)
} else {
// Dest exists but isn't a directory, or we're extracting a directory
target = filepath.Join(destPath, header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
case tar.TypeReg:
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
f.Close()
}
}
return nil
}