diff options
author | Valentin Rothberg <rothberg@redhat.com> | 2020-12-11 11:00:25 +0100 |
---|---|---|
committer | Valentin Rothberg <rothberg@redhat.com> | 2020-12-18 12:08:49 +0100 |
commit | adcb3a7a609ba756f8b9de17521f0e3dce3b778e (patch) | |
tree | 4572b8ed243ffa33648a9190dcf4c2ac9a419099 /pkg/domain/infra/abi | |
parent | f56865879ccffeddce3b9e36f585fe67c37591d5 (diff) | |
download | podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.tar.gz podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.tar.bz2 podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.zip |
remote copy
Implement `podman-remote cp` and break out the logic from the previously
added `pkg/copy` into it's basic building blocks and move them up into
the `ContainerEngine` interface and `cmd/podman`.
The `--pause` and `--extract` flags are now deprecated and turned into
nops.
Note that this commit is vendoring a non-release version of Buildah to
pull in updates to the copier package.
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
Diffstat (limited to 'pkg/domain/infra/abi')
-rw-r--r-- | pkg/domain/infra/abi/archive.go | 172 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers_stat.go | 251 | ||||
-rw-r--r-- | pkg/domain/infra/abi/cp.go | 70 |
3 files changed, 423 insertions, 70 deletions
diff --git a/pkg/domain/infra/abi/archive.go b/pkg/domain/infra/abi/archive.go new file mode 100644 index 000000000..809813756 --- /dev/null +++ b/pkg/domain/infra/abi/archive.go @@ -0,0 +1,172 @@ +package abi + +import ( + "context" + "io" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/buildah/pkg/chrootuser" + "github.com/containers/buildah/util" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// NOTE: Only the parent directory of the container path must exist. The path +// itself may be created while copying. +func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, containerPath string, reader io.Reader) (entities.ContainerCopyFunc, error) { + container, err := ic.Libpod.LookupContainer(nameOrID) + 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) + if err != nil { + unmount() + return nil, err + } + + decompressed, err := archive.DecompressStream(reader) + if err != nil { + unmount() + return nil, err + } + + idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot) + if err != nil { + unmount() + return nil, err + } + + logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID()) + + return func() error { + defer unmount() + defer decompressed.Close() + putOptions := buildahCopiah.PutOptions{ + UIDMap: idMappings.UIDMap, + GIDMap: idMappings.GIDMap, + ChownDirs: idPair, + ChownFiles: idPair, + } + return buildahCopiah.Put(resolvedRoot, resolvedContainerPath, putOptions, decompressed) + }, nil +} + +func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) { + container, err := ic.Libpod.LookupContainer(nameOrID) + if err != nil { + return nil, err + } + + unmount := func() { + if err := container.Unmount(false); err != nil { + logrus.Errorf("Error unmounting container: %v", err) + } + } + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if containerPath == "/" { + containerPath = "/." + } + + _, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath) + if err != nil { + unmount() + return nil, err + } + + idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot) + if err != nil { + unmount() + return nil, err + } + + logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID()) + + return func() error { + defer container.Unmount(false) + getOptions := buildahCopiah.GetOptions{ + // Unless the specified path ends with ".", we want to copy the base directory. + KeepDirectoryNames: !strings.HasSuffix(resolvedContainerPath, "."), + UIDMap: idMappings.UIDMap, + GIDMap: idMappings.GIDMap, + ChownDirs: idPair, + ChownFiles: idPair, + } + return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedContainerPath}, writer) + }, nil +} + +// getIDMappingsAndPair returns the ID mappings for the container and the host +// ID pair. +func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) { + user, err := getContainerUser(container, containerMount) + if err != nil { + return nil, nil, err + } + + idMappingOpts, err := container.IDMappings() + if err != nil { + return nil, nil, err + } + + hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID) + if err != nil { + return nil, nil, err + } + + idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} + return &idMappingOpts, &idPair, nil +} + +// getContainerUser returns the specs.User of the container. +func getContainerUser(container *libpod.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.Cause(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 +} diff --git a/pkg/domain/infra/abi/containers_stat.go b/pkg/domain/infra/abi/containers_stat.go new file mode 100644 index 000000000..c9610d1b9 --- /dev/null +++ b/pkg/domain/infra/abi/containers_stat.go @@ -0,0 +1,251 @@ +package abi + +import ( + "context" + "os" + "path/filepath" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" + "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 + } + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if containerPath == "/" { + containerPath += "/." + } + + // 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) + if err != nil { + return nil, "", "", err + } + + statInfo, statInfoErr := secureStat(resolvedRoot, resolvedContainerPath) + if statInfoErr != nil { + // 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 crucuial for the remote client. + if os.IsNotExist(err) || strings.Contains(statInfoErr.Error(), "o such file or directory") { + statInfoErr = copy.ENOENT + } + // If statInfo is nil, there's nothing we can do anymore. A + // non-nil statInfo may indicate a symlink where we must have + // a closer look. + if statInfo == nil { + return nil, "", "", statInfoErr + } + } + + // Now make sure that the info's LinkTarget is relative to the + // container's mount. + var absContainerPath string + + if statInfo.IsSymlink { + // Evaluated symlinks are always relative to the container's mount point. + absContainerPath = statInfo.ImmediateTarget + } else if strings.HasPrefix(resolvedContainerPath, containerMountPoint) { + // If the path is on the container's mount point, strip it off. + absContainerPath = strings.TrimPrefix(resolvedContainerPath, containerMountPoint) + absContainerPath = filepath.Join("/", absContainerPath) + } else { + // 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 + } + + // Now we need to make sure to preseve 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) + resolvedContainerPath = copy.PreserveBasePath(containerPath, resolvedContainerPath) + + info := copy.FileInfo{ + IsDir: statInfo.IsDir, + Name: filepath.Base(absContainerPath), + Size: statInfo.Size, + Mode: statInfo.Mode, + ModTime: statInfo.ModTime, + LinkTarget: absContainerPath, + } + + return &entities.ContainerStatReport{FileInfo: info}, resolvedRoot, resolvedContainerPath, statInfoErr +} + +func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, containerPath string) (*entities.ContainerStatReport, error) { + container, err := ic.Libpod.LookupContainer(nameOrID) + 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) + 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) + // 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(volume.MountPoint(), pathRelativeToVolume) + if err != nil { + return "", "", err + } + return volume.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 + 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 := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob}) + if err != nil { + return nil, err + } + + if len(globStats) != 1 { + return nil, errors.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats)) + } + + stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay + if !exists { + return nil, copy.ENOENT + } + + var statErr error + if stat.Error != "" { + statErr = errors.New(stat.Error) + } + return stat, statErr +} diff --git a/pkg/domain/infra/abi/cp.go b/pkg/domain/infra/abi/cp.go deleted file mode 100644 index 362053cce..000000000 --- a/pkg/domain/infra/abi/cp.go +++ /dev/null @@ -1,70 +0,0 @@ -package abi - -import ( - "context" - - "github.com/containers/podman/v2/libpod" - "github.com/containers/podman/v2/pkg/copy" - "github.com/containers/podman/v2/pkg/domain/entities" -) - -func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error { - // Parse user input. - sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(source, dest) - if err != nil { - return err - } - - // Look up containers. - var sourceContainer, destContainer *libpod.Container - if len(sourceContainerStr) > 0 { - sourceContainer, err = ic.Libpod.LookupContainer(sourceContainerStr) - if err != nil { - return err - } - } - if len(destContainerStr) > 0 { - destContainer, err = ic.Libpod.LookupContainer(destContainerStr) - if err != nil { - return err - } - } - - var sourceItem, destinationItem copy.CopyItem - - // Source ... container OR host. - if sourceContainer != nil { - sourceItem, err = copy.CopyItemForContainer(sourceContainer, sourcePath, options.Pause, true) - defer sourceItem.CleanUp() - if err != nil { - return err - } - } else { - sourceItem, err = copy.CopyItemForHost(sourcePath, true) - if err != nil { - return err - } - } - - // Destination ... container OR host. - if destContainer != nil { - destinationItem, err = copy.CopyItemForContainer(destContainer, destPath, options.Pause, false) - defer destinationItem.CleanUp() - if err != nil { - return err - } - } else { - destinationItem, err = copy.CopyItemForHost(destPath, false) - defer destinationItem.CleanUp() - if err != nil { - return err - } - } - - // Copy from the host to the container. - copier, err := copy.GetCopier(&sourceItem, &destinationItem, options.Extract) - if err != nil { - return err - } - return copier.Copy() -} |