mirror of
https://github.com/containers/podman.git
synced 2025-06-28 22:53:21 +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
|
package libpod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"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/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"
|
"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
|
// joinMountAndExec executes the specified function `f` inside the container's
|
||||||
// mount and PID namespace. That allows for having the exact view on the
|
// mount and PID namespace. That allows for having the exact view on the
|
||||||
// container's file system.
|
// container's file system.
|
||||||
@ -288,3 +76,13 @@ func (c *Container) joinMountAndExec(f func() error) error {
|
|||||||
}()
|
}()
|
||||||
return <-errChan
|
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
|
//go:build !linux && !freebsd
|
||||||
// +build !linux
|
// +build !linux,!freebsd
|
||||||
|
|
||||||
package libpod
|
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
|
package libpod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/containers/buildah/copier"
|
"github.com/containers/buildah/copier"
|
||||||
"github.com/containers/podman/v4/libpod/define"
|
"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
|
// 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
|
return statInfo, resolvedRoot, resolvedPath, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// statOnHost stats the specified path *on the host*. It returns the file info
|
// Calls either statOnHost or statInsideMount depending on whether the
|
||||||
// along with the resolved root and the resolved path. Both paths are absolute
|
// container is running
|
||||||
// to the host's root. Note that the paths may resolved outside the
|
func (c *Container) statInContainer(mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) {
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.state.State == define.ContainerStateRunning {
|
if c.state.State == define.ContainerStateRunning {
|
||||||
// If the container is running, we need to join it's mount namespace
|
// If the container is running, we need to join it's mount namespace
|
||||||
// and stat there.
|
// and stat there.
|
||||||
statInfo, resolvedRoot, resolvedPath, statErr = c.statInsideMount(containerPath)
|
return c.statInsideMount(containerPath)
|
||||||
} else {
|
}
|
||||||
// If the container is NOT running, we need to resolve the path
|
// If the container is NOT running, we need to resolve the path
|
||||||
// on the host.
|
// on the host.
|
||||||
statInfo, resolvedRoot, resolvedPath, statErr = c.statOnHost(containerMountPoint, containerPath)
|
return c.statOnHost(mountPoint, 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
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//go:build !linux
|
//go:build !linux && !freebsd
|
||||||
// +build !linux
|
// +build !linux,!freebsd
|
||||||
|
|
||||||
package libpod
|
package libpod
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user