Skip to content

Commit

Permalink
libpod: add (*Container).ResolvePath()
Browse files Browse the repository at this point in the history
Add an API to libpod to resolve a path on the container.  We can
refactor the code that was originally written for copy.  Other
functions are requiring a proper path resolution, so libpod seems
like a reasonable home for sharing that code.

Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
vrothberg authored and mheon committed Jan 29, 2021
1 parent 2393dd3 commit 0b50051
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 134 deletions.
29 changes: 29 additions & 0 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -776,3 +776,32 @@ func (c *Container) ShouldRestart(ctx context.Context) bool {
}
return c.shouldRestart()
}

// ResolvePath resolves the specified path on the root for the container. The
// root must either be the mounted image of the container or the already
// mounted container storage.
//
// It returns the resolved root and the resolved path. Note that the path may
// resolve to the container's mount point or to a volume or bind mount.
func (c *Container) ResolvePath(ctx context.Context, root string, path string) (string, string, error) {
logrus.Debugf("Resolving path %q (root %q) on container %s", path, root, c.ID())

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

if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()

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

return c.resolvePath(root, path)
}
127 changes: 127 additions & 0 deletions libpod/container_path_resolution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package libpod

import (
"path/filepath"
"strings"

securejoin "github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

// resolveContainerPaths resolves the container's mount point and the container
// path as specified by the user. Both may resolve to paths outside of the
// container's mount point when the container path hits a volume or bind mount.
func (container *Container) resolvePath(mountPoint string, containerPath string) (string, string, error) {
// Let's first make sure we have a path relative to the mount point.
pathRelativeToContainerMountPoint := containerPath
if !filepath.IsAbs(containerPath) {
// If the containerPath is not absolute, it's relative to the
// container's working dir. To be extra careful, let's first
// join the working dir with "/", and the add the containerPath
// to it.
pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath)
}
resolvedPathOnTheContainerMountPoint := filepath.Join(mountPoint, pathRelativeToContainerMountPoint)
pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint)
pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint)

// Now we have an "absolute container Path" but not yet resolved on the
// host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to
// check if "/foo/bar/file.txt" is on a volume or bind mount. To do
// that, we need to walk *down* the paths to the root. Assuming
// volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar",
// we must select "/foo/bar". Once selected, we need to rebase the
// remainder (i.e, "/file.txt") on the volume's mount point on the
// host. Same applies to bind mounts.

searchPath := pathRelativeToContainerMountPoint
for {
volume, err := findVolume(container, searchPath)
if err != nil {
return "", "", err
}
if volume != nil {
logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)

// TODO: We really need to force the volume to mount
// before doing this, but that API is not exposed
// externally right now and doing so is beyond the scope
// of this commit.
mountPoint, err := volume.MountPoint()
if err != nil {
return "", "", err
}
if mountPoint == "" {
return "", "", errors.Errorf("volume %s is not mounted, cannot copy into it", volume.Name())
}

// We found a matching volume for searchPath. We now
// need to first find the relative path of our input
// path to the searchPath, and then join it with the
// volume's mount point.
pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(mountPoint, pathRelativeToVolume)
if err != nil {
return "", "", err
}
return mountPoint, absolutePathOnTheVolumeMount, nil
}

if mount := findBindMount(container, searchPath); mount != nil {
logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath)
// We found a matching bind mount for searchPath. We
// now need to first find the relative path of our
// input path to the searchPath, and then join it with
// the source of the bind mount.
pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
if err != nil {
return "", "", err
}
return mount.Source, absolutePathOnTheBindMount, nil

}

if searchPath == "/" {
// Cannot go beyond "/", so we're done.
break
}

// Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar").
searchPath = filepath.Dir(searchPath)
}

// No volume, no bind mount but just a normal path on the container.
return mountPoint, resolvedPathOnTheContainerMountPoint, nil
}

// findVolume checks if the specified container path matches a volume inside
// the container. It returns a matching volume or nil.
func findVolume(c *Container, containerPath string) (*Volume, error) {
runtime := c.Runtime()
cleanedContainerPath := filepath.Clean(containerPath)
for _, vol := range c.Config().NamedVolumes {
if cleanedContainerPath == filepath.Clean(vol.Dest) {
return runtime.GetVolume(vol.Name)
}
}
return nil, nil
}

// findBindMount checks if the specified container path matches a bind mount
// inside the container. It returns a matching mount or nil.
func findBindMount(c *Container, containerPath string) *specs.Mount {
cleanedPath := filepath.Clean(containerPath)
for _, m := range c.Config().Spec.Mounts {
if m.Type != "bind" {
continue
}
if cleanedPath == filepath.Clean(m.Destination) {
mount := m
return &mount
}
}
return nil
}
14 changes: 12 additions & 2 deletions pkg/domain/infra/abi/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrI
return nil, err
}

containerMountPoint, err := container.Mount()
if err != nil {
return nil, err
}

unmount := func() {
if err := container.Unmount(false); err != nil {
logrus.Errorf("Error unmounting container: %v", err)
}
}

_, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath)
_, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerMountPoint, containerPath)
if err != nil {
unmount()
return nil, err
Expand Down Expand Up @@ -71,6 +76,11 @@ func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID
return nil, err
}

containerMountPoint, err := container.Mount()
if err != nil {
return nil, err
}

unmount := func() {
if err := container.Unmount(false); err != nil {
logrus.Errorf("Error unmounting container: %v", err)
Expand All @@ -83,7 +93,7 @@ func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID
containerPath = "/."
}

_, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath)
_, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerMountPoint, containerPath)
if err != nil {
unmount()
return nil, err
Expand Down
140 changes: 8 additions & 132 deletions pkg/domain/infra/abi/containers_stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,11 @@ import (
"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/pkg/copy"
"github.com/containers/podman/v2/pkg/domain/entities"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

func (ic *ContainerEngine) containerStat(container *libpod.Container, containerPath string) (*entities.ContainerStatReport, string, string, error) {
containerMountPoint, err := container.Mount()
if err != nil {
return nil, "", "", err
}

func (ic *ContainerEngine) containerStat(container *libpod.Container, containerMountPoint string, containerPath string) (*entities.ContainerStatReport, string, string, error) {
// Make sure that "/" copies the *contents* of the mount point and not
// the directory.
if containerPath == "/" {
Expand All @@ -30,7 +23,7 @@ func (ic *ContainerEngine) containerStat(container *libpod.Container, containerP

// Now resolve the container's path. It may hit a volume, it may hit a
// bind mount, it may be relative.
resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath)
resolvedRoot, resolvedContainerPath, err := container.ResolvePath(context.Background(), containerMountPoint, containerPath)
if err != nil {
return nil, "", "", err
}
Expand Down Expand Up @@ -94,138 +87,21 @@ func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, c
return nil, err
}

containerMountPoint, err := container.Mount()
if err != nil {
return nil, err
}

defer func() {
if err := container.Unmount(false); err != nil {
logrus.Errorf("Error unmounting container: %v", err)
}
}()

statReport, _, _, err := ic.containerStat(container, containerPath)
statReport, _, _, err := ic.containerStat(container, containerMountPoint, containerPath)
return statReport, err
}

// resolveContainerPaths resolves the container's mount point and the container
// path as specified by the user. Both may resolve to paths outside of the
// container's mount point when the container path hits a volume or bind mount.
//
// NOTE: We must take volumes and bind mounts into account as, regrettably, we
// can copy to/from stopped containers. In that case, the volumes and bind
// mounts are not present. For running containers, the runtime (e.g., runc or
// crun) takes care of these mounts. For stopped ones, we need to do quite
// some dance, as done below.
func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) {
// Let's first make sure we have a path relative to the mount point.
pathRelativeToContainerMountPoint := containerPath
if !filepath.IsAbs(containerPath) {
// If the containerPath is not absolute, it's relative to the
// container's working dir. To be extra careful, let's first
// join the working dir with "/", and the add the containerPath
// to it.
pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath)
}
resolvedPathOnTheContainerMountPoint := filepath.Join(mountPoint, pathRelativeToContainerMountPoint)
pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint)
pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint)

// Now we have an "absolute container Path" but not yet resolved on the
// host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to
// check if "/foo/bar/file.txt" is on a volume or bind mount. To do
// that, we need to walk *down* the paths to the root. Assuming
// volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar",
// we must select "/foo/bar". Once selected, we need to rebase the
// remainder (i.e, "/file.txt") on the volume's mount point on the
// host. Same applies to bind mounts.

searchPath := pathRelativeToContainerMountPoint
for {
volume, err := findVolume(container, searchPath)
if err != nil {
return "", "", err
}
if volume != nil {
logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)

// TODO: We really need to force the volume to mount
// before doing this, but that API is not exposed
// externally right now and doing so is beyond the scope
// of this commit.
mountPoint, err := volume.MountPoint()
if err != nil {
return "", "", err
}
if mountPoint == "" {
return "", "", errors.Errorf("volume %s is not mounted, cannot copy into it", volume.Name())
}

// We found a matching volume for searchPath. We now
// need to first find the relative path of our input
// path to the searchPath, and then join it with the
// volume's mount point.
pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(mountPoint, pathRelativeToVolume)
if err != nil {
return "", "", err
}
return mountPoint, absolutePathOnTheVolumeMount, nil
}

if mount := findBindMount(container, searchPath); mount != nil {
logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath)
// We found a matching bind mount for searchPath. We
// now need to first find the relative path of our
// input path to the searchPath, and then join it with
// the source of the bind mount.
pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
if err != nil {
return "", "", err
}
return mount.Source, absolutePathOnTheBindMount, nil

}

if searchPath == "/" {
// Cannot go beyond "/", so we're done.
break
}

// Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar").
searchPath = filepath.Dir(searchPath)
}

// No volume, no bind mount but just a normal path on the container.
return mountPoint, resolvedPathOnTheContainerMountPoint, nil
}

// findVolume checks if the specified container path matches a volume inside
// the container. It returns a matching volume or nil.
func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) {
runtime := c.Runtime()
cleanedContainerPath := filepath.Clean(containerPath)
for _, vol := range c.Config().NamedVolumes {
if cleanedContainerPath == filepath.Clean(vol.Dest) {
return runtime.GetVolume(vol.Name)
}
}
return nil, nil
}

// findBindMount checks if the specified container path matches a bind mount
// inside the container. It returns a matching mount or nil.
func findBindMount(c *libpod.Container, containerPath string) *specs.Mount {
cleanedPath := filepath.Clean(containerPath)
for _, m := range c.Config().Spec.Mounts {
if m.Type != "bind" {
continue
}
if cleanedPath == filepath.Clean(m.Destination) {
mount := m
return &mount
}
}
return nil
}

// secureStat extracts file info for path in a chroot'ed environment in root.
func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) {
var glob string
Expand Down

0 comments on commit 0b50051

Please sign in to comment.