mirror of
https://github.com/containers/podman.git
synced 2025-06-27 21:50:18 +08:00
Merge pull request #15856 from dfr/freebsd-copy
Add support for 'podman cp' on FreeBSD
This commit is contained in:
213
libpod/container_copy_common.go
Normal file
213
libpod/container_copy_common.go
Normal 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
|
||||
}
|
13
libpod/container_copy_freebsd.go
Normal file
13
libpod/container_copy_freebsd.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
//go:build !linux && !freebsd
|
||||
// +build !linux,!freebsd
|
||||
|
||||
package libpod
|
||||
|
||||
|
155
libpod/container_stat_common.go
Normal file
155
libpod/container_stat_common.go
Normal 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
|
||||
}
|
13
libpod/container_stat_freebsd.go
Normal file
13
libpod/container_stat_freebsd.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
//go:build !linux && !freebsd
|
||||
// +build !linux,!freebsd
|
||||
|
||||
package libpod
|
||||
|
||||
|
Reference in New Issue
Block a user