Merge pull request #15856 from dfr/freebsd-copy

Add support for 'podman cp' on FreeBSD
This commit is contained in:
OpenShift Merge Robot
2022-09-21 14:32:13 +02:00
committed by GitHub
8 changed files with 415 additions and 368 deletions

View File

@ -0,0 +1,213 @@
//go:build linux || freebsd
// +build linux freebsd
package libpod
import (
"errors"
"io"
"path/filepath"
"strings"
buildahCopiah "github.com/containers/buildah/copier"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/util"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir bool, rename map[string]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() {
if err := c.unmount(false); err != nil {
logrus.Errorf("Failed to unmount container: %v", err)
}
}
}
resolvedRoot, resolvedPath, err = c.resolveCopyTarget(mountPoint, path)
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)}
}
decompressed, err := archive.DecompressStream(reader)
if err != nil {
unmount()
return nil, err
}
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: c.config.IDMappings.UIDMap,
GIDMap: c.config.IDMappings.GIDMap,
ChownDirs: idPair,
ChownFiles: idPair,
NoOverwriteDirNonDir: noOverwriteDirNonDir,
NoOverwriteNonDirDir: noOverwriteDirNonDir,
Rename: rename,
}
return c.joinMountAndExec(
func() error {
return buildahCopiah.Put(resolvedRoot, resolvedPath, putOptions, decompressed)
},
)
}, nil
}
func (c *Container) copyToArchive(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() {
if err := c.unmount(false); err != nil {
logrus.Errorf("Failed to unmount container: %v", err)
}
}
}
statInfo, resolvedRoot, resolvedPath, err := c.stat(mountPoint, path)
if err != nil {
unmount()
return nil, err
}
// We optimistically chown to the host user. In case of a hypothetical
// container-to-container copy, the reading side will chown back to the
// container user.
user, err := getContainerUser(c, mountPoint)
if err != nil {
unmount()
return nil, err
}
hostUID, hostGID, err := util.GetHostIDs(
idtoolsToRuntimeSpec(c.config.IDMappings.UIDMap),
idtoolsToRuntimeSpec(c.config.IDMappings.GIDMap),
user.UID,
user.GID,
)
if err != nil {
unmount()
return nil, err
}
idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
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: c.config.IDMappings.UIDMap,
GIDMap: c.config.IDMappings.GIDMap,
ChownDirs: &idPair,
ChownFiles: &idPair,
Excludes: []string{"dev", "proc", "sys"},
// Ignore EPERMs when copying from rootless containers
// since we cannot read TTY devices. Those are owned
// by the host's root and hence "nobody" inside the
// container's user namespace.
IgnoreUnreadable: rootless.IsRootless() && c.state.State == define.ContainerStateRunning,
}
return c.joinMountAndExec(
func() error {
return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedPath}, writer)
},
)
}, nil
}
// getContainerUser returns the specs.User and ID mappings 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.Is(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
}

View File

@ -0,0 +1,13 @@
package libpod
// On FreeBSD, the container's mounts are in the global mount
// namespace so we can just execute the function directly.
func (c *Container) joinMountAndExec(f func() error) error {
return f()
}
// Similarly, we can just use resolvePath for both running and stopped
// containers.
func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, error) {
return c.resolvePath(mountPoint, containerPath)
}

View File

@ -1,226 +1,14 @@
//go:build linux
// +build linux
package libpod
import (
"errors"
"fmt"
"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/v4/libpod/define"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir bool, rename map[string]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() {
if err := c.unmount(false); err != nil {
logrus.Errorf("Failed to unmount container: %v", err)
}
}
}
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
}
}
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)}
}
decompressed, err := archive.DecompressStream(reader)
if err != nil {
unmount()
return nil, err
}
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: c.config.IDMappings.UIDMap,
GIDMap: c.config.IDMappings.GIDMap,
ChownDirs: idPair,
ChownFiles: idPair,
NoOverwriteDirNonDir: noOverwriteDirNonDir,
NoOverwriteNonDirDir: noOverwriteDirNonDir,
Rename: rename,
}
return c.joinMountAndExec(
func() error {
return buildahCopiah.Put(resolvedRoot, resolvedPath, putOptions, decompressed)
},
)
}, nil
}
func (c *Container) copyToArchive(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() {
if err := c.unmount(false); err != nil {
logrus.Errorf("Failed to unmount container: %v", err)
}
}
}
statInfo, resolvedRoot, resolvedPath, err := c.stat(mountPoint, path)
if err != nil {
unmount()
return nil, err
}
// We optimistically chown to the host user. In case of a hypothetical
// container-to-container copy, the reading side will chown back to the
// container user.
user, err := getContainerUser(c, mountPoint)
if err != nil {
unmount()
return nil, err
}
hostUID, hostGID, err := util.GetHostIDs(
idtoolsToRuntimeSpec(c.config.IDMappings.UIDMap),
idtoolsToRuntimeSpec(c.config.IDMappings.GIDMap),
user.UID,
user.GID,
)
if err != nil {
unmount()
return nil, err
}
idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
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: c.config.IDMappings.UIDMap,
GIDMap: c.config.IDMappings.GIDMap,
ChownDirs: &idPair,
ChownFiles: &idPair,
Excludes: []string{"dev", "proc", "sys"},
// Ignore EPERMs when copying from rootless containers
// since we cannot read TTY devices. Those are owned
// by the host's root and hence "nobody" inside the
// container's user namespace.
IgnoreUnreadable: rootless.IsRootless() && c.state.State == define.ContainerStateRunning,
}
return c.joinMountAndExec(
func() error {
return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedPath}, writer)
},
)
}, nil
}
// getContainerUser returns the specs.User and ID mappings 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.Is(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.
@ -288,3 +76,13 @@ func (c *Container) joinMountAndExec(f func() error) error {
}()
return <-errChan
}
func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, error) {
// If the container is running, we will execute the copy
// inside the container's mount namespace so we return a path
// relative to the container's root.
if c.state.State == define.ContainerStateRunning {
return "/", c.pathAbs(containerPath), nil
}
return c.resolvePath(mountPoint, containerPath)
}

View File

@ -1,5 +1,5 @@
//go:build !linux
// +build !linux
//go:build !linux && !freebsd
// +build !linux,!freebsd
package libpod

View File

@ -0,0 +1,155 @@
//go:build linux || freebsd
// +build linux freebsd
package libpod
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/buildah/copier"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/copy"
)
// statOnHost stats the specified path *on the host*. It returns the file info
// along with the resolved root and the resolved path. Both paths are absolute
// to the host's root. Note that the paths may resolved outside the
// container's mount point (e.g., to a volume or bind mount).
func (c *Container) statOnHost(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) {
// Now resolve the container's path. It may hit a volume, it may hit a
// bind mount, it may be relative.
resolvedRoot, resolvedPath, err := c.resolvePath(mountPoint, containerPath)
if err != nil {
return nil, "", "", err
}
statInfo, err := secureStat(resolvedRoot, resolvedPath)
return statInfo, resolvedRoot, resolvedPath, err
}
func (c *Container) stat(containerMountPoint string, containerPath string) (*define.FileInfo, string, string, error) {
var (
resolvedRoot string
resolvedPath string
absContainerPath string
statInfo *copier.StatForItem
statErr error
)
// Make sure that "/" copies the *contents* of the mount point and not
// the directory.
if containerPath == "/" {
containerPath = "/."
}
// Wildcards are not allowed.
// TODO: it's now technically possible wildcards.
// We may consider enabling support in the future.
if strings.Contains(containerPath, "*") {
return nil, "", "", copy.ErrENOENT
}
statInfo, resolvedRoot, resolvedPath, statErr = c.statInContainer(containerMountPoint, containerPath)
if statErr != nil {
if statInfo == nil {
return nil, "", "", statErr
}
// Not all errors from secureStat map to ErrNotExist, so we
// have to look into the error string. Turning it into an
// ENOENT let's the API handlers return the correct status code
// which is crucial for the remote client.
if os.IsNotExist(statErr) || strings.Contains(statErr.Error(), "o such file or directory") {
statErr = copy.ErrENOENT
}
}
switch {
case statInfo.IsSymlink:
// Symlinks are already evaluated and always relative to the
// container's mount point.
absContainerPath = statInfo.ImmediateTarget
case strings.HasPrefix(resolvedPath, containerMountPoint):
// If the path is on the container's mount point, strip it off.
absContainerPath = strings.TrimPrefix(resolvedPath, containerMountPoint)
absContainerPath = filepath.Join("/", absContainerPath)
default:
// No symlink and not on the container's mount point, so let's
// move it back to the original input. It must have evaluated
// to a volume or bind mount but we cannot return host paths.
absContainerPath = containerPath
}
// Preserve the base path as specified by the user. The `filepath`
// packages likes to remove trailing slashes and dots that are crucial
// to the copy logic.
absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath)
resolvedPath = copy.PreserveBasePath(containerPath, resolvedPath)
info := &define.FileInfo{
IsDir: statInfo.IsDir,
Name: filepath.Base(absContainerPath),
Size: statInfo.Size,
Mode: statInfo.Mode,
ModTime: statInfo.ModTime,
LinkTarget: absContainerPath,
}
return info, resolvedRoot, resolvedPath, statErr
}
// secureStat extracts file info for path in a chroot'ed environment in root.
func secureStat(root string, path string) (*copier.StatForItem, error) {
var glob string
var err error
// If root and path are equal, then dir must be empty and the glob must
// be ".".
if filepath.Clean(root) == filepath.Clean(path) {
glob = "."
} else {
glob, err = filepath.Rel(root, path)
if err != nil {
return nil, err
}
}
globStats, err := copier.Stat(root, "", copier.StatOptions{}, []string{glob})
if err != nil {
return nil, err
}
if len(globStats) != 1 {
return nil, fmt.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats))
}
if len(globStats) != 1 {
return nil, fmt.Errorf("internal error: secureStat: expected 1 result but got %d", len(globStats[0].Results))
}
// NOTE: the key in the map differ from `glob` when hitting symlink.
// Hence, we just take the first (and only) key/value pair.
for _, stat := range globStats[0].Results {
var statErr error
if stat.Error != "" {
statErr = errors.New(stat.Error)
}
// If necessary evaluate the symlink
if stat.IsSymlink {
target, err := copier.Eval(root, path, copier.EvalOptions{})
if err != nil {
return nil, fmt.Errorf("evaluating symlink in container: %w", err)
}
// Need to make sure the symlink is relative to the root!
target = strings.TrimPrefix(target, root)
target = filepath.Join("/", target)
stat.ImmediateTarget = target
}
return stat, statErr
}
// Nothing found!
return nil, copy.ErrENOENT
}

View File

@ -0,0 +1,13 @@
package libpod
import (
"github.com/containers/buildah/copier"
)
// On FreeBSD, jails use the global mount namespace, filtered to only
// the mounts the jail should see. This means that we can use
// statOnHost whether the container is running or not.
// container is running
func (c *Container) statInContainer(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) {
return c.statOnHost(mountPoint, containerPath)
}

View File

@ -1,18 +1,8 @@
//go:build linux
// +build linux
package libpod
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/buildah/copier"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/copy"
)
// statInsideMount stats the specified path *inside* the container's mount and PID
@ -34,150 +24,15 @@ func (c *Container) statInsideMount(containerPath string) (*copier.StatForItem,
return statInfo, resolvedRoot, resolvedPath, err
}
// statOnHost stats the specified path *on the host*. It returns the file info
// along with the resolved root and the resolved path. Both paths are absolute
// to the host's root. Note that the paths may resolved outside the
// container's mount point (e.g., to a volume or bind mount).
func (c *Container) statOnHost(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) {
// Now resolve the container's path. It may hit a volume, it may hit a
// bind mount, it may be relative.
resolvedRoot, resolvedPath, err := c.resolvePath(mountPoint, containerPath)
if err != nil {
return nil, "", "", err
}
statInfo, err := secureStat(resolvedRoot, resolvedPath)
return statInfo, resolvedRoot, resolvedPath, err
}
func (c *Container) stat(containerMountPoint string, containerPath string) (*define.FileInfo, string, string, error) {
var (
resolvedRoot string
resolvedPath string
absContainerPath string
statInfo *copier.StatForItem
statErr error
)
// Make sure that "/" copies the *contents* of the mount point and not
// the directory.
if containerPath == "/" {
containerPath = "/."
}
// Wildcards are not allowed.
// TODO: it's now technically possible wildcards.
// We may consider enabling support in the future.
if strings.Contains(containerPath, "*") {
return nil, "", "", copy.ErrENOENT
}
// Calls either statOnHost or statInsideMount depending on whether the
// container is running
func (c *Container) statInContainer(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) {
if c.state.State == define.ContainerStateRunning {
// If the container is running, we need to join it's mount namespace
// and stat there.
statInfo, resolvedRoot, resolvedPath, statErr = c.statInsideMount(containerPath)
} else {
// If the container is NOT running, we need to resolve the path
// on the host.
statInfo, resolvedRoot, resolvedPath, statErr = c.statOnHost(containerMountPoint, containerPath)
return c.statInsideMount(containerPath)
}
if statErr != nil {
if statInfo == nil {
return nil, "", "", statErr
}
// Not all errors from secureStat map to ErrNotExist, so we
// have to look into the error string. Turning it into an
// ENOENT let's the API handlers return the correct status code
// which is crucial for the remote client.
if os.IsNotExist(statErr) || strings.Contains(statErr.Error(), "o such file or directory") {
statErr = copy.ErrENOENT
}
}
switch {
case statInfo.IsSymlink:
// Symlinks are already evaluated and always relative to the
// container's mount point.
absContainerPath = statInfo.ImmediateTarget
case strings.HasPrefix(resolvedPath, containerMountPoint):
// If the path is on the container's mount point, strip it off.
absContainerPath = strings.TrimPrefix(resolvedPath, containerMountPoint)
absContainerPath = filepath.Join("/", absContainerPath)
default:
// No symlink and not on the container's mount point, so let's
// move it back to the original input. It must have evaluated
// to a volume or bind mount but we cannot return host paths.
absContainerPath = containerPath
}
// Preserve the base path as specified by the user. The `filepath`
// packages likes to remove trailing slashes and dots that are crucial
// to the copy logic.
absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath)
resolvedPath = copy.PreserveBasePath(containerPath, resolvedPath)
info := &define.FileInfo{
IsDir: statInfo.IsDir,
Name: filepath.Base(absContainerPath),
Size: statInfo.Size,
Mode: statInfo.Mode,
ModTime: statInfo.ModTime,
LinkTarget: absContainerPath,
}
return info, resolvedRoot, resolvedPath, statErr
}
// secureStat extracts file info for path in a chroot'ed environment in root.
func secureStat(root string, path string) (*copier.StatForItem, error) {
var glob string
var err error
// If root and path are equal, then dir must be empty and the glob must
// be ".".
if filepath.Clean(root) == filepath.Clean(path) {
glob = "."
} else {
glob, err = filepath.Rel(root, path)
if err != nil {
return nil, err
}
}
globStats, err := copier.Stat(root, "", copier.StatOptions{}, []string{glob})
if err != nil {
return nil, err
}
if len(globStats) != 1 {
return nil, fmt.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats))
}
if len(globStats) != 1 {
return nil, fmt.Errorf("internal error: secureStat: expected 1 result but got %d", len(globStats[0].Results))
}
// NOTE: the key in the map differ from `glob` when hitting symlink.
// Hence, we just take the first (and only) key/value pair.
for _, stat := range globStats[0].Results {
var statErr error
if stat.Error != "" {
statErr = errors.New(stat.Error)
}
// If necessary evaluate the symlink
if stat.IsSymlink {
target, err := copier.Eval(root, path, copier.EvalOptions{})
if err != nil {
return nil, fmt.Errorf("evaluating symlink in container: %w", err)
}
// Need to make sure the symlink is relative to the root!
target = strings.TrimPrefix(target, root)
target = filepath.Join("/", target)
stat.ImmediateTarget = target
}
return stat, statErr
}
// Nothing found!
return nil, copy.ErrENOENT
// If the container is NOT running, we need to resolve the path
// on the host.
return c.statOnHost(mountPoint, containerPath)
}

View File

@ -1,5 +1,5 @@
//go:build !linux
// +build !linux
//go:build !linux && !freebsd
// +build !linux,!freebsd
package libpod