diff options
author | Matej Vasek <mvasek@redhat.com> | 2021-06-28 21:17:13 +0200 |
---|---|---|
committer | Matej Vasek <mvasek@redhat.com> | 2021-07-01 12:01:46 +0200 |
commit | 86c6014145d5b8d4ea51f338beb9bddaa8b5a334 (patch) | |
tree | 056769253cf35f00bdd46389bddd9c076c31a00e | |
parent | fd1715568b7c14451dcf2581c385c8d3e307d30e (diff) | |
download | podman-86c6014145d5b8d4ea51f338beb9bddaa8b5a334.tar.gz podman-86c6014145d5b8d4ea51f338beb9bddaa8b5a334.tar.bz2 podman-86c6014145d5b8d4ea51f338beb9bddaa8b5a334.zip |
Implement --archive flag for podman cp
Signed-off-by: Matej Vasek <mvasek@redhat.com>
-rw-r--r-- | cmd/podman/containers/cp.go | 8 | ||||
-rw-r--r-- | docs/source/markdown/podman-cp.1.md | 11 | ||||
-rw-r--r-- | libpod/container_api.go | 4 | ||||
-rw-r--r-- | libpod/container_copy_linux.go | 21 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_archive.go | 11 | ||||
-rw-r--r-- | pkg/bindings/containers/archive.go | 12 | ||||
-rw-r--r-- | pkg/bindings/containers/types.go | 8 | ||||
-rw-r--r-- | pkg/bindings/containers/types_copy_options.go | 37 | ||||
-rw-r--r-- | pkg/bindings/internal/util/util.go | 10 | ||||
-rw-r--r-- | pkg/domain/entities/containers.go | 7 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/archive.go | 4 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/containers.go | 4 | ||||
-rw-r--r-- | test/apiv2/23-containersArchive.at | 16 | ||||
-rw-r--r-- | test/python/docker/compat/test_containers.py | 39 | ||||
-rw-r--r-- | test/system/065-cp.bats | 21 |
16 files changed, 184 insertions, 31 deletions
diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index 0ad258824..c1f1e27f5 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -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. ` 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", Long: cpDescription, Args: cobra.ExactArgs(2), RunE: cp, 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{ @@ -50,12 +50,14 @@ var ( var ( cpOpts entities.ContainerCpOptions + chown bool ) func cpFlags(cmd *cobra.Command) { flags := cmd.Flags() flags.BoolVar(&cpOpts.Extract, "extract", false, "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("pause") } @@ -378,7 +380,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er 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 { return err } diff --git a/docs/source/markdown/podman-cp.1.md b/docs/source/markdown/podman-cp.1.md index f245ad1aa..43ee4cdff 100644 --- a/docs/source/markdown/podman-cp.1.md +++ b/docs/source/markdown/podman-cp.1.md @@ -4,9 +4,9 @@ podman\-cp - Copy files/folders between a container and the local filesystem ## 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 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 +#### **--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 Podman has much stronger capabilities than just `podman cp` to achieve copy files between host and container. diff --git a/libpod/container_api.go b/libpod/container_api.go index b75d0b41d..390bba7bb 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -840,7 +840,7 @@ func (c *Container) ShouldRestart(ctx context.Context) bool { // CopyFromArchive copies the contents from the specified tarStream to path // *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 { c.lock.Lock() 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 diff --git a/libpod/container_copy_linux.go b/libpod/container_copy_linux.go index 0ab322829..01e7ecacb 100644 --- a/libpod/container_copy_linux.go +++ b/libpod/container_copy_linux.go @@ -23,7 +23,7 @@ import ( "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 ( mountPoint 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. - user, err := getContainerUser(c, mountPoint) - if err != nil { - unmount() - return nil, err + var idPair *idtools.IDPair + if chown { + // Make sure we chown the files to the container's main user and group ID. + user, err := getContainerUser(c, mountPoint) + 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) if err != nil { @@ -84,8 +87,8 @@ func (c *Container) copyFromArchive(ctx context.Context, path string, reader io. putOptions := buildahCopiah.PutOptions{ UIDMap: c.config.IDMappings.UIDMap, GIDMap: c.config.IDMappings.GIDMap, - ChownDirs: &idPair, - ChownFiles: &idPair, + ChownDirs: idPair, + ChownFiles: idPair, } return c.joinMountAndExec(ctx, diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go index e119dc7cb..a9d74e5f4 100644 --- a/pkg/api/handlers/compat/containers_archive.go +++ b/pkg/api/handlers/compat/containers_archive.go @@ -9,6 +9,7 @@ import ( "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/api/handlers/utils" "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/gorilla/schema" "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) { query := struct { - Path string `schema:"path"` + Path string `schema:"path"` + Chown bool `schema:"copyUIDGID"` // TODO handle params below NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` - CopyUIDGID bool `schema:"copyUIDGID"` - }{} + }{ + Chown: utils.IsLibpodRequest(r), // backward compatibility + } err := decoder.Decode(&query, r.URL.Query()) if err != nil { @@ -107,7 +110,7 @@ func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, containerName := utils.GetName(r) 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) { // 404 is returned for an absent container and path. The // clients must deal with it accordingly. diff --git a/pkg/bindings/containers/archive.go b/pkg/bindings/containers/archive.go index 0178f63c3..52b73662b 100644 --- a/pkg/bindings/containers/archive.go +++ b/pkg/bindings/containers/archive.go @@ -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) { + 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) if err != nil { return nil, err } - params := url.Values{} + + params, err := options.ToParams() + if err != nil { + return nil, err + } + params.Set("path", path) return func() error { diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index 0d22c32f8..db710d3da 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -251,3 +251,11 @@ type ExistsOptions struct { // External checks for containers created outside of Podman 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"` +} diff --git a/pkg/bindings/containers/types_copy_options.go b/pkg/bindings/containers/types_copy_options.go new file mode 100644 index 000000000..12ad085fd --- /dev/null +++ b/pkg/bindings/containers/types_copy_options.go @@ -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 +} diff --git a/pkg/bindings/internal/util/util.go b/pkg/bindings/internal/util/util.go index ef93d6e25..bcf6959f2 100644 --- a/pkg/bindings/internal/util/util.go +++ b/pkg/bindings/internal/util/util.go @@ -72,14 +72,18 @@ func ToParams(o interface{}) (url.Values, error) { if reflect.Ptr == f.Kind() { f = f.Elem() } + paramName := fieldName + if pn, ok := sType.Field(i).Tag.Lookup("schema"); ok { + paramName = pn + } switch { case IsSimpleType(f): - params.Set(fieldName, SimpleTypeToParam(f)) + params.Set(paramName, SimpleTypeToParam(f)) case f.Kind() == reflect.Slice: for i := 0; i < f.Len(); i++ { elem := f.Index(i) if IsSimpleType(elem) { - params.Add(fieldName, SimpleTypeToParam(elem)) + params.Add(paramName, SimpleTypeToParam(elem)) } else { return nil, errors.New("slices must contain only simple types") } @@ -95,7 +99,7 @@ func ToParams(o interface{}) (url.Values, error) { return nil, err } - params.Set(fieldName, s) + params.Set(paramName, s) } } return params, nil diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 8ed9b9b61..302b35a47 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -160,6 +160,13 @@ type CommitOptions struct { 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 { Id string //nolint } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 4cd3cfd2a..1b35135d0 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -20,7 +20,7 @@ type ContainerEngine interface { ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, 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) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error) ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error) diff --git a/pkg/domain/infra/abi/archive.go b/pkg/domain/infra/abi/archive.go index 2ea63aa5e..1a5bb6dc4 100644 --- a/pkg/domain/infra/abi/archive.go +++ b/pkg/domain/infra/abi/archive.go @@ -7,12 +7,12 @@ import ( "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) if err != nil { 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) { diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index ccebfffa4..c02e36804 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -833,8 +833,8 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o return reports, nil } -func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) { - return containers.CopyFromArchive(ic.ClientCtx, nameOrID, path, reader) +func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID, path string, reader io.Reader, options entities.CopyOptions) (entities.ContainerCopyFunc, error) { + 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) { diff --git a/test/apiv2/23-containersArchive.at b/test/apiv2/23-containersArchive.at index 688ca9f06..c55164780 100644 --- a/test/apiv2/23-containersArchive.at +++ b/test/apiv2/23-containersArchive.at @@ -16,7 +16,7 @@ CTR="ArchiveTestingCtr" TMPD=$(mktemp -d podman-apiv2-test.archive.XXXXXXXX) HELLO_TAR="${TMPD}/hello.tar" 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 @@ -72,6 +72,20 @@ if [ "$(tar -xf "${TMPD}/body.tar" hello.txt --to-stdout)" != "Hello" ]; then ARCHIVE_TEST_ERROR="1" 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 if [[ "${ARCHIVE_TEST_ERROR}" ]] ; then exit 1; diff --git a/test/python/docker/compat/test_containers.py b/test/python/docker/compat/test_containers.py index be70efa67..511ab1451 100644 --- a/test/python/docker/compat/test_containers.py +++ b/test/python/docker/compat/test_containers.py @@ -1,13 +1,18 @@ +import io import subprocess import sys import time import unittest +from typing import IO, Optional from docker import DockerClient, errors +from docker.models.containers import Container from test.python.docker import Podman from test.python.docker.compat import common, constant +import tarfile + class TestContainers(unittest.TestCase): podman = None # initialized podman configuration for tests @@ -198,3 +203,37 @@ class TestContainers(unittest.TestCase): filters = {"name": "top"} ctnrs = self.client.containers.list(all=True, filters=filters) 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() diff --git a/test/system/065-cp.bats b/test/system/065-cp.bats index eda04611f..5778eb46e 100644 --- a/test/system/065-cp.bats +++ b/test/system/065-cp.bats @@ -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 mkdir -p $srcdir content=cp-user-test-$(random_string 10) @@ -129,6 +129,25 @@ load helpers 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" { if is_rootless && ! is_cgroupsv2; then |