Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make sure the workdir exists on container mount #9054

Merged
merged 2 commits into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
63 changes: 49 additions & 14 deletions libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,25 +174,60 @@ func (c *Container) prepare() error {
return err
}

// Ensure container entrypoint is created (if required)
if c.config.CreateWorkingDir {
workdir, err := securejoin.SecureJoin(c.state.Mountpoint, c.WorkingDir())
if err != nil {
return errors.Wrapf(err, "error creating path to container %s working dir", c.ID())
}
rootUID := c.RootUID()
rootGID := c.RootGID()
// Make sure the workdir exists
if err := c.resolveWorkDir(); err != nil {
return err
}

if err := os.MkdirAll(workdir, 0755); err != nil {
if os.IsExist(err) {
return nil
return nil
}

// resolveWorkDir resolves the container's workdir and, depending on the
// configuration, will create it, or error out if it does not exist.
// Note that the container must be mounted before.
func (c *Container) resolveWorkDir() error {
workdir := c.WorkingDir()

// If the specified workdir is a subdir of a volume or mount,
// we don't need to do anything. The runtime is taking care of
// that.
if isPathOnVolume(c, workdir) || isPathOnBindMount(c, workdir) {
logrus.Debugf("Workdir %q resolved to a volume or mount", workdir)
return nil
}

_, resolvedWorkdir, err := c.resolvePath(c.state.Mountpoint, workdir)
if err != nil {
return err
}
logrus.Debugf("Workdir %q resolved to host path %q", workdir, resolvedWorkdir)

// No need to create it (e.g., `--workdir=/foo`), so let's make sure
// the path exists on the container.
if !c.config.CreateWorkingDir {
if _, err := os.Stat(resolvedWorkdir); err != nil {
if os.IsNotExist(err) {
return errors.Errorf("workdir %q does not exist on container %s", workdir, c.ID())
vrothberg marked this conversation as resolved.
Show resolved Hide resolved
}
return errors.Wrapf(err, "error creating container %s working dir", c.ID())
// This might be a serious error (e.g., permission), so
// we need to return the full error.
return errors.Wrapf(err, "error detecting workdir %q on container %s", workdir, c.ID())
vrothberg marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Ensure container entrypoint is created (if required).
rootUID := c.RootUID()
rootGID := c.RootGID()

if err := os.Chown(workdir, rootUID, rootGID); err != nil {
return errors.Wrapf(err, "error chowning container %s working directory to container root", c.ID())
if err := os.MkdirAll(resolvedWorkdir, 0755); err != nil {
if os.IsExist(err) {
return nil
}
return errors.Wrapf(err, "error creating container %s workdir", c.ID())
}

if err := os.Chown(resolvedWorkdir, rootUID, rootGID); err != nil {
return errors.Wrapf(err, "error chowning container %s workdir to container root", c.ID())
}

return nil
Expand Down
166 changes: 166 additions & 0 deletions libpod/container_path_resolution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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.
//
// It returns a bool, indicating whether containerPath resolves outside of
// mountPoint (e.g., via a mount or volume), the resolved root (e.g., container
// mount, bind mount or volume) and the resolved path on the root (absolute to
// the host).
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 containerPath matches the destination
// path of a Volume. 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
}

// isPathOnVolume returns true if the specified containerPath is a subdir of any
// Volume's destination.
func isPathOnVolume(c *Container, containerPath string) bool {
cleanedContainerPath := filepath.Clean(containerPath)
for _, vol := range c.Config().NamedVolumes {
if cleanedContainerPath == filepath.Clean(vol.Dest) {
return true
}
for dest := vol.Dest; dest != "/"; dest = filepath.Dir(dest) {
if cleanedContainerPath == dest {
return true
}
}
}
return false
}

// findBindMounts checks if the specified containerPath matches the destination
// path of a Mount. 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
}
Comment on lines +140 to +142
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to check for a bind mount, we'd need to look for bind or rbind in the options, the type is ignored by the runtime

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we require that Type also be set for Libpod - at the very least, all of our existing code is guaranteed to do so.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, i's detecting correctly when using --mount=type=bind etc. It's pretty the code from cp.

If there's something missing, let's fix that in a separate PR.

if cleanedPath == filepath.Clean(m.Destination) {
mount := m
return &mount
}
}
return nil
}

/// isPathOnBindMount returns true if the specified containerPath is a subdir of any
// Mount's destination.
func isPathOnBindMount(c *Container, containerPath string) bool {
cleanedContainerPath := filepath.Clean(containerPath)
for _, m := range c.Config().Spec.Mounts {
if cleanedContainerPath == filepath.Clean(m.Destination) {
return true
}
for dest := m.Destination; dest != "/"; dest = filepath.Dir(dest) {
if cleanedContainerPath == dest {
return true
}
}
}
return false
}
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
Loading