Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

podman cp: support copying on tmpfs mounts #9593

Merged
merged 1 commit into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/podman/containers/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ func copyFromContainer(container string, containerPath string, hostPath string)
}

putOptions := buildahCopiah.PutOptions{
ChownDirs: &idPair,
ChownFiles: &idPair,
ChownDirs: &idPair,
ChownFiles: &idPair,
IgnoreDevices: true,
}
if !containerInfo.IsDir && (!hostInfo.IsDir || hostInfoErr != nil) {
// If we're having a file-to-file copy, make sure to
Expand Down
6 changes: 6 additions & 0 deletions libpod/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,12 @@ func (c *Container) NamespacePath(linuxNS LinuxNS) (string, error) { //nolint:in
}
}

return c.namespacePath(linuxNS)
}

// namespacePath returns the path of one of the container's namespaces
// If the container is not running, an error will be returned
func (c *Container) namespacePath(linuxNS LinuxNS) (string, error) { //nolint:interfacer
if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused {
return "", errors.Wrapf(define.ErrCtrStopped, "cannot get namespace path unless container %s is running", c.ID())
}
Expand Down
64 changes: 44 additions & 20 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package libpod

import (
"context"
"io"
"io/ioutil"
"net/http"
"os"
Expand Down Expand Up @@ -349,10 +350,6 @@ func (c *Container) Mount() (string, error) {
}
}

if c.state.State == define.ContainerStateRemoving {
return "", errors.Wrapf(define.ErrCtrStateInvalid, "cannot mount container %s as it is being removed", c.ID())
}

defer c.newContainerEvent(events.Mount)
return c.mount()
}
Expand All @@ -367,7 +364,6 @@ func (c *Container) Unmount(force bool) error {
return err
}
}

if c.state.Mounted {
mounted, err := c.runtime.storageService.MountedContainerImage(c.ID())
if err != nil {
Expand Down Expand Up @@ -847,31 +843,59 @@ func (c *Container) ShouldRestart(ctx context.Context) bool {
return c.shouldRestart()
}

// ResolvePath resolves the specified path on the root for the container. The
// root must either be the mounted image of the container or the already
// mounted container storage.
//
// It returns the resolved root and the resolved path. Note that the path may
// resolve to the container's mount point or to a volume or bind mount.
func (c *Container) ResolvePath(ctx context.Context, root string, path string) (string, string, error) {
logrus.Debugf("Resolving path %q (root %q) on container %s", path, root, c.ID())
// 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) {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()

// Minimal sanity checks.
if len(root)*len(path) == 0 {
return "", "", errors.Wrapf(define.ErrInternal, "ResolvePath: root (%q) and path (%q) must be non empty", root, path)
if err := c.syncContainer(); err != nil {
return nil, err
}
}
if _, err := os.Stat(root); err != nil {
return "", "", errors.Wrapf(err, "cannot locate root to resolve path on container %s", c.ID())

return c.copyFromArchive(ctx, containerPath, tarStream)
}

// CopyToArchive copies the contents from the specified path *inside* the
// container to the tarStream.
func (c *Container) CopyToArchive(ctx context.Context, containerPath string, tarStream io.Writer) (func() error, error) {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()

if err := c.syncContainer(); err != nil {
return nil, err
}
}

return c.copyToArchive(ctx, containerPath, tarStream)
}

// Stat the specified path *inside* the container and return a file info.
func (c *Container) Stat(ctx context.Context, containerPath string) (*define.FileInfo, error) {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
vrothberg marked this conversation as resolved.
Show resolved Hide resolved

if err := c.syncContainer(); err != nil {
return "", "", err
return nil, err
}
}

var mountPoint string
var err error
if c.state.Mounted {
mountPoint = c.state.Mountpoint
} else {
mountPoint, err = c.mount()
if err != nil {
return nil, err
}
defer c.unmount(false)
}

return c.resolvePath(root, path)
info, _, _, err := c.stat(ctx, mountPoint, containerPath)
return info, err
}
266 changes: 266 additions & 0 deletions libpod/container_copy_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// +build linux

package libpod

import (
"context"
"io"
"os"
"path/filepath"
"runtime"
"strings"

buildahCopiah "github.com/containers/buildah/copier"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/util"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/storage"
"github.com/containers/storage/pkg/idtools"
"github.com/docker/docker/pkg/archive"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

func (c *Container) copyFromArchive(ctx context.Context, path string, reader io.Reader) (func() error, error) {
var (
mountPoint string
resolvedRoot string
resolvedPath string
unmount func()
err error
)

// Make sure that "/" copies the *contents* of the mount point and not
// the directory.
if path == "/" {
path = "/."
}

// Optimization: only mount if the container is not already.
if c.state.Mounted {
mountPoint = c.state.Mountpoint
unmount = func() {}
} else {
// NOTE: make sure to unmount in error paths.
mountPoint, err = c.mount()
if err != nil {
return nil, err
}
unmount = func() { c.unmount(false) }
}

if c.state.State == define.ContainerStateRunning {
resolvedRoot = "/"
resolvedPath = c.pathAbs(path)
} else {
resolvedRoot, resolvedPath, err = c.resolvePath(mountPoint, path)
if err != nil {
unmount()
return nil, err
}
}

decompressed, err := archive.DecompressStream(reader)
if err != nil {
unmount()
return nil, err
}

idMappings, idPair, err := getIDMappingsAndPair(c, mountPoint)
if err != nil {
decompressed.Close()
unmount()
return nil, err
vrothberg marked this conversation as resolved.
Show resolved Hide resolved
}

logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID())

return func() error {
defer unmount()
defer decompressed.Close()
putOptions := buildahCopiah.PutOptions{
UIDMap: idMappings.UIDMap,
GIDMap: idMappings.GIDMap,
ChownDirs: idPair,
ChownFiles: idPair,
}

return c.joinMountAndExec(ctx,
func() error {
return buildahCopiah.Put(resolvedRoot, resolvedPath, putOptions, decompressed)
},
)
}, nil
}

func (c *Container) copyToArchive(ctx context.Context, path string, writer io.Writer) (func() error, error) {
var (
mountPoint string
unmount func()
err error
)

// Optimization: only mount if the container is not already.
if c.state.Mounted {
mountPoint = c.state.Mountpoint
unmount = func() {}
} else {
// NOTE: make sure to unmount in error paths.
mountPoint, err = c.mount()
if err != nil {
return nil, err
}
unmount = func() { c.unmount(false) }
}

statInfo, resolvedRoot, resolvedPath, err := c.stat(ctx, mountPoint, path)
if err != nil {
unmount()
return nil, err
}

idMappings, idPair, err := getIDMappingsAndPair(c, mountPoint)
if err != nil {
unmount()
return nil, err
}

logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID())

return func() error {
defer unmount()
getOptions := buildahCopiah.GetOptions{
// Unless the specified points to ".", we want to copy the base directory.
KeepDirectoryNames: statInfo.IsDir && filepath.Base(path) != ".",
UIDMap: idMappings.UIDMap,
GIDMap: idMappings.GIDMap,
ChownDirs: idPair,
ChownFiles: idPair,
Excludes: []string{"dev", "proc", "sys"},
}
return c.joinMountAndExec(ctx,
func() error {
return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedPath}, writer)
},
)
}, nil
}

// getIDMappingsAndPair returns the ID mappings for the container and the host
// ID pair.
func getIDMappingsAndPair(container *Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) {
user, err := getContainerUser(container, containerMount)
if err != nil {
return nil, nil, err
}

idMappingOpts, err := container.IDMappings()
if err != nil {
return nil, nil, err
}

hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID)
if err != nil {
return nil, nil, err
}

idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
return &idMappingOpts, &idPair, nil
}

// getContainerUser returns the specs.User of the container.
func getContainerUser(container *Container, mountPoint string) (specs.User, error) {
userspec := container.Config().User

uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec)
u := specs.User{
UID: uid,
GID: gid,
Username: userspec,
}

if !strings.Contains(userspec, ":") {
groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
if err2 != nil {
if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
err = err2
}
} else {
u.AdditionalGids = groups
}
}

return u, err
}

// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
for _, idmap := range idMaps {
tempIDMap := specs.LinuxIDMapping{
ContainerID: uint32(idmap.ContainerID),
HostID: uint32(idmap.HostID),
Size: uint32(idmap.Size),
}
convertedIDMap = append(convertedIDMap, tempIDMap)
}
return convertedIDMap
}

// joinMountAndExec executes the specified function `f` inside the container's
// mount and PID namespace. That allows for having the exact view on the
// container's file system.
//
// Note, if the container is not running `f()` will be executed as is.
func (c *Container) joinMountAndExec(ctx context.Context, f func() error) error {
if c.state.State != define.ContainerStateRunning {
return f()
}

// Container's running, so we need to execute `f()` inside its mount NS.
errChan := make(chan error)
go func() {
runtime.LockOSThread()

// Join the mount and PID NS of the container.
getFD := func(ns LinuxNS) (*os.File, error) {
nsPath, err := c.namespacePath(ns)
if err != nil {
return nil, err
}
return os.Open(nsPath)
}

mountFD, err := getFD(MountNS)
if err != nil {
errChan <- err
return
}
defer mountFD.Close()

pidFD, err := getFD(PIDNS)
if err != nil {
errChan <- err
return
}
defer pidFD.Close()
if err := unix.Unshare(unix.CLONE_NEWNS); err != nil {
errChan <- err
return
}
if err := unix.Setns(int(pidFD.Fd()), unix.CLONE_NEWPID); err != nil {
errChan <- err
return
}

if err := unix.Setns(int(mountFD.Fd()), unix.CLONE_NEWNS); err != nil {
errChan <- err
return
}

// Last but not least, execute the workload.
errChan <- f()
}()
return <-errChan
}
Loading