Implement --archive flag for podman cp

Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
Matej Vasek
2021-06-28 21:17:13 +02:00
parent fd1715568b
commit 86c6014145
16 changed files with 184 additions and 31 deletions

View File

@ -28,13 +28,13 @@ var (
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. 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{ cpCommand = &cobra.Command{
Use: "cp [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH", Use: "cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
Short: "Copy files/folders between a container and the local filesystem", Short: "Copy files/folders between a container and the local filesystem",
Long: cpDescription, Long: cpDescription,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: cp, RunE: cp,
ValidArgsFunction: common.AutocompleteCpCommand, ValidArgsFunction: common.AutocompleteCpCommand,
Example: "podman cp [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH", Example: "podman cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
} }
containerCpCommand = &cobra.Command{ containerCpCommand = &cobra.Command{
@ -50,12 +50,14 @@ var (
var ( var (
cpOpts entities.ContainerCpOptions cpOpts entities.ContainerCpOptions
chown bool
) )
func cpFlags(cmd *cobra.Command) { func cpFlags(cmd *cobra.Command) {
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...") flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...")
flags.BoolVar(&cpOpts.Pause, "pause", true, "Deprecated") flags.BoolVar(&cpOpts.Pause, "pause", true, "Deprecated")
flags.BoolVarP(&chown, "archive", "a", true, `Chown copied files to the primary uid/gid of the destination container.`)
_ = flags.MarkHidden("extract") _ = flags.MarkHidden("extract")
_ = flags.MarkHidden("pause") _ = flags.MarkHidden("pause")
} }
@ -378,7 +380,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er
target = filepath.Dir(target) target = filepath.Dir(target)
} }
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader) copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader, entities.CopyOptions{Chown: chown})
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,9 +4,9 @@
podman\-cp - Copy files/folders between a container and the local filesystem podman\-cp - Copy files/folders between a container and the local filesystem
## SYNOPSIS ## SYNOPSIS
**podman cp** [*container*:]*src_path* [*container*:]*dest_path* **podman cp** [*options*] [*container*:]*src_path* [*container*:]*dest_path*
**podman container cp** [*container*:]*src_path* [*container*:]*dest_path* **podman container cp** [*options*] [*container*:]*src_path* [*container*:]*dest_path*
## DESCRIPTION ## DESCRIPTION
Copy the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container. Copy the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container.
@ -61,6 +61,13 @@ Note that `podman cp` ignores permission errors when copying from a running root
## OPTIONS ## OPTIONS
#### **--archive**, **-a**
Archive mode (copy all uid/gid information).
When set to true, files copied to a container will have changed ownership to the primary uid/gid of the container.
When set to false, maintain uid/gid from archive sources instead of changing them to the primary uid/gid of the destination container.
The default is *true*.
## ALTERNATIVES ## ALTERNATIVES
Podman has much stronger capabilities than just `podman cp` to achieve copy files between host and container. Podman has much stronger capabilities than just `podman cp` to achieve copy files between host and container.

View File

@ -840,7 +840,7 @@ func (c *Container) ShouldRestart(ctx context.Context) bool {
// CopyFromArchive copies the contents from the specified tarStream to path // CopyFromArchive copies the contents from the specified tarStream to path
// *inside* the container. // *inside* the container.
func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, tarStream io.Reader) (func() error, error) { func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, chown bool, tarStream io.Reader) (func() error, error) {
if !c.batched { if !c.batched {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
@ -850,7 +850,7 @@ func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, t
} }
} }
return c.copyFromArchive(ctx, containerPath, tarStream) return c.copyFromArchive(ctx, containerPath, chown, tarStream)
} }
// CopyToArchive copies the contents from the specified path *inside* the // CopyToArchive copies the contents from the specified path *inside* the

View File

@ -23,7 +23,7 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func (c *Container) copyFromArchive(ctx context.Context, path string, reader io.Reader) (func() error, error) { func (c *Container) copyFromArchive(ctx context.Context, path string, chown bool, reader io.Reader) (func() error, error) {
var ( var (
mountPoint string mountPoint string
resolvedRoot string resolvedRoot string
@ -62,13 +62,16 @@ func (c *Container) copyFromArchive(ctx context.Context, path string, reader io.
} }
} }
// Make sure we chown the files to the container's main user and group ID. var idPair *idtools.IDPair
user, err := getContainerUser(c, mountPoint) if chown {
if err != nil { // Make sure we chown the files to the container's main user and group ID.
unmount() user, err := getContainerUser(c, mountPoint)
return nil, err if err != nil {
unmount()
return nil, err
}
idPair = &idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
} }
idPair := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
decompressed, err := archive.DecompressStream(reader) decompressed, err := archive.DecompressStream(reader)
if err != nil { if err != nil {
@ -84,8 +87,8 @@ func (c *Container) copyFromArchive(ctx context.Context, path string, reader io.
putOptions := buildahCopiah.PutOptions{ putOptions := buildahCopiah.PutOptions{
UIDMap: c.config.IDMappings.UIDMap, UIDMap: c.config.IDMappings.UIDMap,
GIDMap: c.config.IDMappings.GIDMap, GIDMap: c.config.IDMappings.GIDMap,
ChownDirs: &idPair, ChownDirs: idPair,
ChownFiles: &idPair, ChownFiles: idPair,
} }
return c.joinMountAndExec(ctx, return c.joinMountAndExec(ctx,

View File

@ -9,6 +9,7 @@ import (
"github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/api/handlers/utils" "github.com/containers/podman/v3/pkg/api/handlers/utils"
"github.com/containers/podman/v3/pkg/copy" "github.com/containers/podman/v3/pkg/copy"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/containers/podman/v3/pkg/domain/infra/abi" "github.com/containers/podman/v3/pkg/domain/infra/abi"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -92,11 +93,13 @@ func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.De
func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) {
query := struct { query := struct {
Path string `schema:"path"` Path string `schema:"path"`
Chown bool `schema:"copyUIDGID"`
// TODO handle params below // TODO handle params below
NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"`
CopyUIDGID bool `schema:"copyUIDGID"` }{
}{} Chown: utils.IsLibpodRequest(r), // backward compatibility
}
err := decoder.Decode(&query, r.URL.Query()) err := decoder.Decode(&query, r.URL.Query())
if err != nil { if err != nil {
@ -107,7 +110,7 @@ func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder,
containerName := utils.GetName(r) containerName := utils.GetName(r)
containerEngine := abi.ContainerEngine{Libpod: runtime} containerEngine := abi.ContainerEngine{Libpod: runtime}
copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body) copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body, entities.CopyOptions{Chown: query.Chown})
if errors.Cause(err) == define.ErrNoSuchCtr || os.IsNotExist(err) { if errors.Cause(err) == define.ErrNoSuchCtr || os.IsNotExist(err) {
// 404 is returned for an absent container and path. The // 404 is returned for an absent container and path. The
// clients must deal with it accordingly. // clients must deal with it accordingly.

View File

@ -50,11 +50,21 @@ func Stat(ctx context.Context, nameOrID string, path string) (*entities.Containe
} }
func CopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) { func CopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
return CopyFromArchiveWithOptions(ctx, nameOrID, path, reader, nil)
}
// CopyFromArchiveWithOptions FIXME: remove this function and make CopyFromArchive accept the option as the last parameter in podman 4.0
func CopyFromArchiveWithOptions(ctx context.Context, nameOrID string, path string, reader io.Reader, options *CopyOptions) (entities.ContainerCopyFunc, error) {
conn, err := bindings.GetClient(ctx) conn, err := bindings.GetClient(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
params := url.Values{}
params, err := options.ToParams()
if err != nil {
return nil, err
}
params.Set("path", path) params.Set("path", path)
return func() error { return func() error {

View File

@ -251,3 +251,11 @@ type ExistsOptions struct {
// External checks for containers created outside of Podman // External checks for containers created outside of Podman
External *bool External *bool
} }
//go:generate go run ../generator/generator.go CopyOptions
// CopyOptions are options for copying to containers.
type CopyOptions struct {
// If used with CopyFromArchive and set to true it will change ownership of files from the source tar archive
// to the primary uid/gid of the target container.
Chown *bool `schema:"copyUIDGID"`
}

View File

@ -0,0 +1,37 @@
package containers
import (
"net/url"
"github.com/containers/podman/v3/pkg/bindings/internal/util"
)
/*
This file is generated automatically by go generate. Do not edit.
*/
// Changed
func (o *CopyOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams
func (o *CopyOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithChown
func (o *CopyOptions) WithChown(value bool) *CopyOptions {
v := &value
o.Chown = v
return o
}
// GetChown
func (o *CopyOptions) GetChown() bool {
var chown bool
if o.Chown == nil {
return chown
}
return *o.Chown
}

View File

@ -72,14 +72,18 @@ func ToParams(o interface{}) (url.Values, error) {
if reflect.Ptr == f.Kind() { if reflect.Ptr == f.Kind() {
f = f.Elem() f = f.Elem()
} }
paramName := fieldName
if pn, ok := sType.Field(i).Tag.Lookup("schema"); ok {
paramName = pn
}
switch { switch {
case IsSimpleType(f): case IsSimpleType(f):
params.Set(fieldName, SimpleTypeToParam(f)) params.Set(paramName, SimpleTypeToParam(f))
case f.Kind() == reflect.Slice: case f.Kind() == reflect.Slice:
for i := 0; i < f.Len(); i++ { for i := 0; i < f.Len(); i++ {
elem := f.Index(i) elem := f.Index(i)
if IsSimpleType(elem) { if IsSimpleType(elem) {
params.Add(fieldName, SimpleTypeToParam(elem)) params.Add(paramName, SimpleTypeToParam(elem))
} else { } else {
return nil, errors.New("slices must contain only simple types") return nil, errors.New("slices must contain only simple types")
} }
@ -95,7 +99,7 @@ func ToParams(o interface{}) (url.Values, error) {
return nil, err return nil, err
} }
params.Set(fieldName, s) params.Set(paramName, s)
} }
} }
return params, nil return params, nil

View File

@ -160,6 +160,13 @@ type CommitOptions struct {
Writer io.Writer Writer io.Writer
} }
type CopyOptions struct {
// If used with ContainerCopyFromArchive and set to true
// it will change ownership of files from the source tar archive
// to the primary uid/gid of the destination container.
Chown bool
}
type CommitReport struct { type CommitReport struct {
Id string //nolint Id string //nolint
} }

View File

@ -20,7 +20,7 @@ type ContainerEngine interface {
ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error)
ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error) ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error)
ContainerCommit(ctx context.Context, nameOrID string, options CommitOptions) (*CommitReport, error) ContainerCommit(ctx context.Context, nameOrID string, options CommitOptions) (*CommitReport, error)
ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (ContainerCopyFunc, error) ContainerCopyFromArchive(ctx context.Context, nameOrID, path string, reader io.Reader, options CopyOptions) (ContainerCopyFunc, error)
ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (ContainerCopyFunc, error) ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (ContainerCopyFunc, error)
ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error)
ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error) ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error)

View File

@ -7,12 +7,12 @@ import (
"github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities"
) )
func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, containerPath string, reader io.Reader) (entities.ContainerCopyFunc, error) { func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID, containerPath string, reader io.Reader, options entities.CopyOptions) (entities.ContainerCopyFunc, error) {
container, err := ic.Libpod.LookupContainer(nameOrID) container, err := ic.Libpod.LookupContainer(nameOrID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return container.CopyFromArchive(ctx, containerPath, reader) return container.CopyFromArchive(ctx, containerPath, options.Chown, reader)
} }
func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) { func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) {

View File

@ -833,8 +833,8 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o
return reports, nil return reports, nil
} }
func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) { func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID, path string, reader io.Reader, options entities.CopyOptions) (entities.ContainerCopyFunc, error) {
return containers.CopyFromArchive(ic.ClientCtx, nameOrID, path, reader) return containers.CopyFromArchiveWithOptions(ic.ClientCtx, nameOrID, path, reader, new(containers.CopyOptions).WithChown(options.Chown))
} }
func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) { func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) {

View File

@ -16,7 +16,7 @@ CTR="ArchiveTestingCtr"
TMPD=$(mktemp -d podman-apiv2-test.archive.XXXXXXXX) TMPD=$(mktemp -d podman-apiv2-test.archive.XXXXXXXX)
HELLO_TAR="${TMPD}/hello.tar" HELLO_TAR="${TMPD}/hello.tar"
echo "Hello" > $TMPD/hello.txt echo "Hello" > $TMPD/hello.txt
tar --format=posix -C $TMPD -cvf ${HELLO_TAR} hello.txt &> /dev/null tar --owner=1042 --group=1043 --format=posix -C $TMPD -cvf ${HELLO_TAR} hello.txt &> /dev/null
podman run -d --name "${CTR}" "${IMAGE}" top podman run -d --name "${CTR}" "${IMAGE}" top
@ -72,6 +72,20 @@ if [ "$(tar -xf "${TMPD}/body.tar" hello.txt --to-stdout)" != "Hello" ]; then
ARCHIVE_TEST_ERROR="1" ARCHIVE_TEST_ERROR="1"
fi fi
# test if uid/gid was set correctly in the server
uidngid=$($PODMAN_BIN --root $WORKDIR/server_root exec "${CTR}" stat -c "%u:%g" "/tmp/hello.txt")
if [[ "${uidngid}" != "1042:1043" ]]; then
echo -e "${red}NOK: UID/GID of the file doesn't match.${nc}" 1>&2;
ARCHIVE_TEST_ERROR="1"
fi
# TODO: uid/gid should be also preserved on way back (GET request)
# right now it ends up as root:root instead of 1042:1043
#if [[ "$(tar -tvf "${TMPD}/body.tar")" != *"1042/1043"* ]]; then
# echo -e "${red}NOK: UID/GID of the file doesn't match.${nc}" 1>&2;
# ARCHIVE_TEST_ERROR="1"
#fi
cleanUpArchiveTest cleanUpArchiveTest
if [[ "${ARCHIVE_TEST_ERROR}" ]] ; then if [[ "${ARCHIVE_TEST_ERROR}" ]] ; then
exit 1; exit 1;

View File

@ -1,13 +1,18 @@
import io
import subprocess import subprocess
import sys import sys
import time import time
import unittest import unittest
from typing import IO, Optional
from docker import DockerClient, errors from docker import DockerClient, errors
from docker.models.containers import Container
from test.python.docker import Podman from test.python.docker import Podman
from test.python.docker.compat import common, constant from test.python.docker.compat import common, constant
import tarfile
class TestContainers(unittest.TestCase): class TestContainers(unittest.TestCase):
podman = None # initialized podman configuration for tests podman = None # initialized podman configuration for tests
@ -198,3 +203,37 @@ class TestContainers(unittest.TestCase):
filters = {"name": "top"} filters = {"name": "top"}
ctnrs = self.client.containers.list(all=True, filters=filters) ctnrs = self.client.containers.list(all=True, filters=filters)
self.assertEqual(len(ctnrs), 1) self.assertEqual(len(ctnrs), 1)
def test_copy_to_container(self):
ctr: Optional[Container] = None
try:
test_file_content = b"Hello World!"
ctr = self.client.containers.create(image="alpine", detach=True, command="top")
ctr.start()
buff: IO[bytes] = io.BytesIO()
with tarfile.open(fileobj=buff, mode="w:") as tf:
ti: tarfile.TarInfo = tarfile.TarInfo()
ti.uid = 1042
ti.gid = 1043
ti.name = "a.txt"
ti.path = "a.txt"
ti.mode = 0o644
ti.type = tarfile.REGTYPE
ti.size = len(test_file_content)
tf.addfile(ti, fileobj=io.BytesIO(test_file_content))
buff.seek(0)
ctr.put_archive("/tmp/", buff)
ret, out = ctr.exec_run(["stat", "-c", "%u:%g", "/tmp/a.txt"])
self.assertEqual(ret, 0)
self.assertTrue(out.startswith(b'1042:1043'), "assert correct uid/gid")
ret, out = ctr.exec_run(["cat", "/tmp/a.txt"])
self.assertEqual(ret, 0)
self.assertTrue(out.startswith(test_file_content), "assert file content")
finally:
if ctr is not None:
ctr.stop()
ctr.remove()

View File

@ -114,7 +114,7 @@ load helpers
} }
@test "podman cp file from host to container and check ownership" { @test "podman cp (-a=true) file from host to container and check ownership" {
srcdir=$PODMAN_TMPDIR/cp-test-file-host-to-ctr srcdir=$PODMAN_TMPDIR/cp-test-file-host-to-ctr
mkdir -p $srcdir mkdir -p $srcdir
content=cp-user-test-$(random_string 10) content=cp-user-test-$(random_string 10)
@ -129,6 +129,25 @@ load helpers
run_podman rm -f cpcontainer run_podman rm -f cpcontainer
} }
@test "podman cp (-a=false) file from host to container and check ownership" {
local tmpdir="${PODMAN_TMPDIR}/cp-test-file-host-to-ctr"
mkdir -p "${tmpdir}"
pushd "${tmpdir}"
touch a.txt
tar --owner=1042 --group=1043 -cf a.tar a.txt
popd
userid=$(id -u)
run_podman run --user="$userid" --userns=keep-id -d --name cpcontainer $IMAGE sleep infinity
run_podman cp -a=false - cpcontainer:/tmp/ < "${tmpdir}/a.tar"
run_podman exec cpcontainer stat -c "%u:%g" /tmp/a.txt
is "$output" "1042:1043" "copied file retains uid/gid from the tar"
run_podman kill cpcontainer
run_podman rm -f cpcontainer
}
@test "podman cp file from/to host while --pid=host" { @test "podman cp file from/to host while --pid=host" {
if is_rootless && ! is_cgroupsv2; then if is_rootless && ! is_cgroupsv2; then