mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00

This solves several problems with copying into volumes on a container that is not running. The first, and most obvious, is that we were previously entirely unable to copy into a volume that required mounting - like image volumes, volume plugins, and volumes that specified mount options. The second is that this fixed several permissions and content issues with a fresh volume and a container that has not been run before. A copy-up will not have occurred, so permissions on the volume root will not have been set and content will not have been copied into the volume. If the container is running, this is very low cost - we maintain a mount counter for named volumes, so it's just an increment in the DB if the volume actually needs mounting, and a no-op if it doesn't. Unfortunately, we also have to fix permissions, and that is rather more complicated. This involves an ugly set of manual edits to the volume state to ensure that the permissions fixes actually worked, as the code was never meant to be used in this way. It's really ugly, but necessary to reach full Docker compatibility. Fixes #24405 Signed-off-by: Matthew Heon <matthew.heon@pm.me>
348 lines
9.9 KiB
Go
348 lines
9.9 KiB
Go
//go:build !remote && (linux || freebsd)
|
|
|
|
package libpod
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
buildahCopiah "github.com/containers/buildah/copier"
|
|
"github.com/containers/buildah/pkg/chrootuser"
|
|
"github.com/containers/buildah/util"
|
|
"github.com/containers/podman/v5/libpod/define"
|
|
"github.com/containers/podman/v5/libpod/shutdown"
|
|
"github.com/containers/podman/v5/pkg/rootless"
|
|
"github.com/containers/storage/pkg/archive"
|
|
"github.com/containers/storage/pkg/idtools"
|
|
"github.com/containers/storage/pkg/stringid"
|
|
"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()
|
|
cleanupFuncs []func()
|
|
err error
|
|
locked bool = true
|
|
)
|
|
|
|
// 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
|
|
}
|
|
c.state.Mountpoint = mountPoint
|
|
if err := c.save(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unmount = func() {
|
|
if !locked {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
}
|
|
|
|
if err := c.syncContainer(); err != nil {
|
|
logrus.Errorf("Unable to sync container %s state: %v", c.ID(), err)
|
|
return
|
|
}
|
|
|
|
// These have to be first, some of them rely on container rootfs still being mounted.
|
|
for _, cleanupFunc := range cleanupFuncs {
|
|
cleanupFunc()
|
|
}
|
|
if err := c.unmount(false); err != nil {
|
|
logrus.Errorf("Failed to unmount container: %v", err)
|
|
}
|
|
|
|
if c.ensureState(define.ContainerStateConfigured, define.ContainerStateExited) {
|
|
c.state.Mountpoint = ""
|
|
if err := c.save(); err != nil {
|
|
logrus.Errorf("Writing container %s state: %v", c.ID(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Before we proceed, mount all named volumes associated with the
|
|
// container.
|
|
// This solves two issues:
|
|
// Firstly, it ensures that if the volume actually requires a mount, we
|
|
// will mount it for safe use.
|
|
// (For example, image volumes, volume plugins).
|
|
// Secondly, it copies up into the volume if necessary.
|
|
// This ensures that permissions are correct for copies into volumes on
|
|
// containers that have never started.
|
|
if len(c.config.NamedVolumes) > 0 {
|
|
for _, v := range c.config.NamedVolumes {
|
|
vol, err := c.mountNamedVolume(v, mountPoint)
|
|
if err != nil {
|
|
unmount()
|
|
return nil, err
|
|
}
|
|
|
|
volUnmountName := fmt.Sprintf("volume unmount %s %s", vol.Name(), stringid.GenerateNonCryptoID()[0:12])
|
|
|
|
// The unmount function can be called in two places:
|
|
// First, from unmount(), our generic cleanup function that gets
|
|
// called on success or on failure by error.
|
|
// Second, from the shutdown handler on receipt of a SIGTERM
|
|
// or similar.
|
|
volUnmountFunc := func() error {
|
|
vol.lock.Lock()
|
|
defer vol.lock.Unlock()
|
|
|
|
if err := vol.unmount(false); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
cleanupFuncs = append(cleanupFuncs, func() {
|
|
_ = shutdown.Unregister(volUnmountName)
|
|
|
|
if err := volUnmountFunc(); err != nil {
|
|
logrus.Errorf("Unmounting container %s volume %s: %v", c.ID(), vol.Name(), err)
|
|
}
|
|
})
|
|
|
|
if err := shutdown.Register(volUnmountName, func(_ os.Signal) error {
|
|
return volUnmountFunc()
|
|
}); err != nil && !errors.Is(err, shutdown.ErrHandlerExists) {
|
|
return nil, fmt.Errorf("adding shutdown handler for volume %s unmount: %w", vol.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resolvedRoot, resolvedPath, volume, err := c.resolveCopyTarget(mountPoint, path)
|
|
if err != nil {
|
|
unmount()
|
|
return nil, err
|
|
}
|
|
|
|
if volume != nil {
|
|
// This must be the first cleanup function so it fires before volume unmounts happen.
|
|
cleanupFuncs = append([]func(){func() {
|
|
// This is a gross hack to ensure correct permissions
|
|
// on a volume that was copied into that needed, but did
|
|
// not receive, a copy-up.
|
|
// Why do we need this?
|
|
// Basically: fixVolumePermissions is needed to ensure
|
|
// the volume has the right permissions.
|
|
// However, fixVolumePermissions only fires on a volume
|
|
// that is not empty iff a copy-up occurred.
|
|
// In this case, the volume is not empty as we just
|
|
// copied into it, so in order to get
|
|
// fixVolumePermissions to actually run, we must
|
|
// convince it that a copy-up occurred - even if it did
|
|
// not.
|
|
// At the same time, clear NeedsCopyUp as we just
|
|
// populated the volume and that will block a future
|
|
// copy-up.
|
|
volume.lock.Lock()
|
|
|
|
if err := volume.update(); err != nil {
|
|
logrus.Errorf("Unable to update volume %s status: %v", volume.Name(), err)
|
|
volume.lock.Unlock()
|
|
return
|
|
}
|
|
|
|
if volume.state.NeedsCopyUp && volume.state.NeedsChown {
|
|
volume.state.NeedsCopyUp = false
|
|
volume.state.CopiedUp = true
|
|
if err := volume.save(); err != nil {
|
|
logrus.Errorf("Unable to save volume %s state: %v", volume.Name(), err)
|
|
volume.lock.Unlock()
|
|
return
|
|
}
|
|
|
|
volume.lock.Unlock()
|
|
|
|
for _, namedVol := range c.config.NamedVolumes {
|
|
if namedVol.Name == volume.Name() {
|
|
if err := c.fixVolumePermissions(namedVol); err != nil {
|
|
logrus.Errorf("Unable to fix volume %s permissions: %v", volume.Name(), err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}}, cleanupFuncs...)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
locked = false
|
|
|
|
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
|
|
}
|