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>
This commit is contained in:
Valentin Rothberg
2020-12-11 11:00:25 +01:00
parent f56865879c
commit adcb3a7a60
32 changed files with 1247 additions and 1160 deletions

View File

@ -1,19 +1,34 @@
package containers
import (
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
buildahCopiah "github.com/containers/buildah/copier"
"github.com/containers/podman/v2/cmd/podman/common"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/pkg/copy"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/errorhandling"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
cpDescription = `Command copies the contents of SRC_PATH to the DEST_PATH.
cpDescription = `Copy the contents of SRC_PATH to the DEST_PATH.
You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. If "-" is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container. The SRC_PATH or DEST_PATH can be a file or directory.
You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. If "-" is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container. The SRC_PATH or DEST_PATH can be a file or a directory.
`
cpCommand = &cobra.Command{
Use: "cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
Use: "cp [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
Short: "Copy files/folders between a container and the local filesystem",
Long: cpDescription,
Args: cobra.ExactArgs(2),
@ -39,19 +54,21 @@ var (
func cpFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVar(&cpOpts.Extract, "extract", false, "Extract the tar file into the destination directory.")
flags.BoolVar(&cpOpts.Pause, "pause", true, "Pause the container while copying")
flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...")
flags.BoolVar(&cpOpts.Pause, "pause", true, "Deorecated")
_ = flags.MarkHidden("extract")
_ = flags.MarkHidden("pause")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode},
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: cpCommand,
})
cpFlags(cpCommand)
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode},
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: containerCpCommand,
Parent: containerCmd,
})
@ -59,5 +76,290 @@ func init() {
}
func cp(cmd *cobra.Command, args []string) error {
return registry.ContainerEngine().ContainerCp(registry.GetContext(), args[0], args[1], cpOpts)
// Parse user input.
sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1])
if err != nil {
return err
}
if len(sourceContainerStr) > 0 {
return copyFromContainer(sourceContainerStr, sourcePath, destPath)
}
return copyToContainer(destContainerStr, destPath, sourcePath)
}
// containerMustExist returns an error if the specified container does not
// exist.
func containerMustExist(container string) error {
exists, err := registry.ContainerEngine().ContainerExists(registry.GetContext(), container, entities.ContainerExistsOptions{})
if err != nil {
return err
}
if !exists.Value {
return errors.Errorf("container %q does not exist", container)
}
return nil
}
// doCopy executes the two functions in parallel to copy data from A to B and
// joins the errors if any.
func doCopy(funcA func() error, funcB func() error) error {
errChan := make(chan error)
go func() {
errChan <- funcA()
}()
var copyErrors []error
copyErrors = append(copyErrors, funcB())
copyErrors = append(copyErrors, <-errChan)
return errorhandling.JoinErrors(copyErrors)
}
// copyFromContainer copies from the containerPath on the container to hostPath.
func copyFromContainer(container string, containerPath string, hostPath string) error {
if err := containerMustExist(container); err != nil {
return err
}
if hostPath == "-" {
hostPath = os.Stdout.Name()
}
containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath)
if err != nil {
return errors.Wrapf(err, "%q could not be found on container %s", containerPath, container)
}
var hostBaseName string
hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath)
if hostInfoErr != nil {
if strings.HasSuffix(hostPath, "/") {
return errors.Wrapf(hostInfoErr, "%q could not be found on the host", hostPath)
}
// If it doesn't exist, then let's have a look at the parent dir.
parentDir := filepath.Dir(hostPath)
hostInfo, err = copy.ResolveHostPath(parentDir)
if err != nil {
return errors.Wrapf(hostInfoErr, "%q could not be found on the host", hostPath)
}
// If the specified path does not exist, we need to assume that
// it'll be created while copying. Hence, we use it as the
// base path.
hostBaseName = filepath.Base(hostPath)
} else {
// If the specified path exists on the host, we must use its
// base path as it may have changed due to symlink evaluations.
hostBaseName = filepath.Base(hostInfo.LinkTarget)
}
reader, writer := io.Pipe()
hostCopy := func() error {
defer reader.Close()
if hostInfo.LinkTarget == os.Stdout.Name() {
_, err := io.Copy(os.Stdout, reader)
return err
}
groot, err := user.Current()
if err != nil {
return err
}
// Set the {G,U}ID. Let's be tolerant towards the different
// operating systems and only log the errors, so we can debug
// if necessary.
idPair := idtools.IDPair{}
if i, err := strconv.Atoi(groot.Uid); err == nil {
idPair.UID = i
} else {
logrus.Debugf("Error converting UID %q to int: %v", groot.Uid, err)
}
if i, err := strconv.Atoi(groot.Gid); err == nil {
idPair.GID = i
} else {
logrus.Debugf("Error converting GID %q to int: %v", groot.Gid, err)
}
putOptions := buildahCopiah.PutOptions{
ChownDirs: &idPair,
ChownFiles: &idPair,
}
if !containerInfo.IsDir && (!hostInfo.IsDir || hostInfoErr != nil) {
// If we're having a file-to-file copy, make sure to
// rename accordingly.
putOptions.Rename = map[string]string{filepath.Base(containerInfo.LinkTarget): hostBaseName}
}
dir := hostInfo.LinkTarget
if !hostInfo.IsDir {
dir = filepath.Dir(dir)
}
if err := buildahCopiah.Put(dir, "", putOptions, reader); err != nil {
return errors.Wrap(err, "error copying to host")
}
return nil
}
containerCopy := func() error {
defer writer.Close()
copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), container, containerInfo.LinkTarget, writer)
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return errors.Wrap(err, "error copying from container")
}
return nil
}
return doCopy(containerCopy, hostCopy)
}
// copyToContainer copies the hostPath to containerPath on the container.
func copyToContainer(container string, containerPath string, hostPath string) error {
if err := containerMustExist(container); err != nil {
return err
}
isStdin := false
if hostPath == "-" {
hostPath = os.Stdin.Name()
isStdin = true
} else if hostPath == os.Stdin.Name() {
isStdin = true
}
// Make sure that host path exists.
hostInfo, err := copy.ResolveHostPath(hostPath)
if err != nil {
return errors.Wrapf(err, "%q could not be found on the host", hostPath)
}
// If the path on the container does not exist. We need to make sure
// that it's parent directory exists. The destination may be created
// while copying.
var containerBaseName string
containerInfo, containerInfoErr := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath)
if containerInfoErr != nil {
if strings.HasSuffix(containerPath, "/") {
return errors.Wrapf(containerInfoErr, "%q could not be found on container %s", containerPath, container)
}
if isStdin {
return errors.New("destination must be a directory when copying from stdin")
}
// NOTE: containerInfo may actually be set. That happens when
// the container path is a symlink into nirvana. In that case,
// we must use the symlinked path instead.
path := containerPath
if containerInfo != nil {
containerBaseName = filepath.Base(containerInfo.LinkTarget)
path = containerInfo.LinkTarget
} else {
containerBaseName = filepath.Base(containerPath)
}
parentDir, err := containerParentDir(container, path)
if err != nil {
return errors.Wrapf(err, "could not determine parent dir of %q on container %s", path, container)
}
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir)
if err != nil {
return errors.Wrapf(err, "%q could not be found on container %s", containerPath, container)
}
} else {
// If the specified path exists on the container, we must use
// its base path as it may have changed due to symlink
// evaluations.
containerBaseName = filepath.Base(containerInfo.LinkTarget)
}
var stdinFile string
if isStdin {
if !containerInfo.IsDir {
return errors.New("destination must be a directory when copying from stdin")
}
// Copy from stdin to a temporary file *before* throwing it
// over the wire. This allows for proper client-side error
// reporting.
tmpFile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
_, err = io.Copy(tmpFile, os.Stdin)
if err != nil {
return err
}
if err = tmpFile.Close(); err != nil {
return err
}
if !archive.IsArchivePath(tmpFile.Name()) {
return errors.New("source must be a (compressed) tar archive when copying from stdin")
}
stdinFile = tmpFile.Name()
}
reader, writer := io.Pipe()
hostCopy := func() error {
defer writer.Close()
if isStdin {
stream, err := os.Open(stdinFile)
if err != nil {
return err
}
defer stream.Close()
_, err = io.Copy(writer, stream)
return err
}
getOptions := buildahCopiah.GetOptions{
// Unless the specified path ends with ".", we want to copy the base directory.
KeepDirectoryNames: !strings.HasSuffix(hostPath, "."),
}
if !hostInfo.IsDir && (!containerInfo.IsDir || containerInfoErr != nil) {
// If we're having a file-to-file copy, make sure to
// rename accordingly.
getOptions.Rename = map[string]string{filepath.Base(hostInfo.LinkTarget): containerBaseName}
}
if err := buildahCopiah.Get("/", "", getOptions, []string{hostInfo.LinkTarget}, writer); err != nil {
return errors.Wrap(err, "error copying from host")
}
return nil
}
containerCopy := func() error {
defer reader.Close()
target := containerInfo.FileInfo.LinkTarget
if !containerInfo.IsDir {
target = filepath.Dir(target)
}
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader)
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return errors.Wrap(err, "error copying to container")
}
return nil
}
return doCopy(hostCopy, containerCopy)
}
// containerParentDir returns the parent directory of the specified path on the
// container. If the path is relative, it will be resolved relative to the
// container's working directory (or "/" if the work dir isn't set).
func containerParentDir(container string, containerPath string) (string, error) {
if filepath.IsAbs(containerPath) {
return filepath.Dir(containerPath), nil
}
inspectData, _, err := registry.ContainerEngine().ContainerInspect(registry.GetContext(), []string{container}, entities.InspectOptions{})
if err != nil {
return "", err
}
if len(inspectData) != 1 {
return "", errors.Errorf("inspecting container %q: expected 1 data item but got %d", container, len(inspectData))
}
workDir := filepath.Join("/", inspectData[0].Config.WorkingDir)
workDir = filepath.Join(workDir, containerPath)
return filepath.Dir(workDir), nil
}

View File

@ -4,9 +4,9 @@
podman\-cp - Copy files/folders between a container and the local filesystem
## SYNOPSIS
**podman cp** [*options*] [*container*:]*src_path* [*container*:]*dest_path*
**podman cp** [*container*:]*src_path* [*container*:]*dest_path*
**podman container cp** [*options*] [*container*:]*src_path* [*container*:]*dest_path*
**podman container cp** [*container*:]*src_path* [*container*:]*dest_path*
## DESCRIPTION
Copy the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container.
@ -59,14 +59,6 @@ Using `-` as the *src_path* streams the contents of STDIN as a tar archive. The
## OPTIONS
#### **--extract**
If the source is a tar archive, extract it to the provided destination (must be a directory). If the source is not a tar archive, follow the above rules.
#### **--pause**
Pause the container while copying into it to avoid potential security issues around symlinks. Defaults to *true*. On rootless containers with cgroups V1, defaults to false.
## ALTERNATIVES
Podman has much stronger capabilities than just `podman cp` to achieve copy files between host and container.
@ -112,8 +104,6 @@ podman cp containerID:/myapp/ /myapp/
podman cp containerID:/home/myuser/. /home/myuser/
podman cp --extract /home/myuser/myfiles.tar.gz containerID:/myfiles
podman cp - containerID:/myfiles.tar.gz < myfiles.tar.gz
## SEE ALSO

2
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
github.com/containernetworking/cni v0.8.0
github.com/containernetworking/plugins v0.9.0
github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c
github.com/containers/buildah v1.18.1-0.20201217112226-67470615779c
github.com/containers/common v0.31.1
github.com/containers/conmon v2.0.20+incompatible
github.com/containers/image/v5 v5.9.0

12
go.sum
View File

@ -93,9 +93,9 @@ github.com/containernetworking/plugins v0.8.7 h1:bU7QieuAp+sACI2vCzESJ3FoT860urY
github.com/containernetworking/plugins v0.8.7/go.mod h1:R7lXeZaBzpfqapcAbHRW8/CYwm0dHzbz0XEjofx0uB0=
github.com/containernetworking/plugins v0.9.0 h1:c+1gegKhR7+d0Caum9pEHugZlyhXPOG6v3V6xJgIGCI=
github.com/containernetworking/plugins v0.9.0/go.mod h1:dbWv4dI0QrBGuVgj+TuVQ6wJRZVOhrCQj91YyC92sxg=
github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c h1:vyc2iYz9b2vfDiigpLyhiXNqXITt/dmDk74HpHzlQow=
github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c/go.mod h1:B+0OkXUogxdwsEy4ax3a5/vDtJjL6vCisiV6frQZJ4A=
github.com/containers/common v0.29.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA=
github.com/containers/buildah v1.18.1-0.20201217112226-67470615779c h1:DnJiPjBKeoZbzjkUA6YMf/r5ShYpNacK+EcQ/ui1Mxo=
github.com/containers/buildah v1.18.1-0.20201217112226-67470615779c/go.mod h1:hvIoL3urgYPL0zX8XlK05aWP6qfUnBNqTrsedsYw6OY=
github.com/containers/common v0.31.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA=
github.com/containers/common v0.31.1 h1:oBINnZpYZ2u90HPMnVCXOhm/TsTaTB7wU/56l05hq44=
github.com/containers/common v0.31.1/go.mod h1:Fehe82hQfJQvDspnRrV9rcdAWG3IalNHEt0F6QWNBHQ=
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
@ -110,7 +110,6 @@ github.com/containers/ocicrypt v1.0.3/go.mod h1:CUBa+8MRNL/VkpxYIpaMtgn1WgXGyvPQ
github.com/containers/psgo v1.5.1 h1:MQNb7FLbXqBdqz6u4lI2QWizVz4RSTzs1+Nk9XT1iVA=
github.com/containers/psgo v1.5.1/go.mod h1:2ubh0SsreMZjSXW1Hif58JrEcFudQyIy9EzPUWfawVU=
github.com/containers/storage v1.23.7/go.mod h1:cUT2zHjtx+WlVri30obWmM2gpqpi8jfPsmIzP1TVpEI=
github.com/containers/storage v1.24.1 h1:1+f8fy6ly35c8SLet5jzZ8t0WJJs5+xSpfMAYw0R3kc=
github.com/containers/storage v1.24.1/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
github.com/containers/storage v1.24.3 h1:8UB4S62l4hrU6Yw3dbsLCJtLg7Ofo39IN2HdckBIX4E=
github.com/containers/storage v1.24.3/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
@ -441,7 +440,6 @@ github.com/opencontainers/runtime-spec v1.0.3-0.20200817204227-f9c09b4ea1df/go.m
github.com/opencontainers/runtime-tools v0.9.0 h1:FYgwVsKRI/H9hU32MJ/4MLOzXWodKK5zsQavY8NPMkU=
github.com/opencontainers/runtime-tools v0.9.0/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
github.com/opencontainers/selinux v1.5.1/go.mod h1:yTcKuYAh6R95iDpefGLQaPaRwJFwyzAJufJyiTt7s0g=
github.com/opencontainers/selinux v1.6.0 h1:+bIAS/Za3q5FTwWym4fTB0vObnfCf3G/NC7K6Jx62mY=
github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
github.com/opencontainers/selinux v1.8.0 h1:+77ba4ar4jsCbL1GLbFL8fFM57w6suPfSS9PDLDY7KM=
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
@ -557,16 +555,13 @@ github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpc
github.com/vbauerster/mpb/v5 v5.3.0 h1:vgrEJjUzHaSZKDRRxul5Oh4C72Yy/5VEMb0em+9M0mQ=
github.com/vbauerster/mpb/v5 v5.3.0/go.mod h1:4yTkvAb8Cm4eylAp6t0JRq6pXDkFJ4krUlDqWYkakAs=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 h1:cPXZWzzG0NllBLdjWoD1nDfaqu98YMv+OneaKc8sPOA=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243 h1:R43TdZy32XXSXjJn7M/HhALJ9imq6ztLnChfYJpVDnM=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
@ -706,7 +701,6 @@ golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637 h1:O5hKNaGxIT4A8OTMnuh6UpmBdI3SAPxlZ3g0olDrJVM=
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -3,11 +3,13 @@ package compat
import (
"fmt"
"net/http"
"os"
"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/pkg/api/handlers/utils"
"github.com/containers/podman/v2/pkg/copy"
"github.com/containers/podman/v2/pkg/domain/infra/abi"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -44,58 +46,47 @@ func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.De
}
containerName := utils.GetName(r)
containerEngine := abi.ContainerEngine{Libpod: runtime}
statReport, err := containerEngine.ContainerStat(r.Context(), containerName, query.Path)
ctr, err := runtime.LookupContainer(containerName)
if errors.Cause(err) == define.ErrNoSuchCtr {
utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists"))
// NOTE
// The statReport may actually be set even in case of an error. That's
// the case when we're looking at a symlink pointing to nirvana. In
// such cases, we really need the FileInfo but we also need the error.
if statReport != nil {
statHeader, err := copy.EncodeFileInfo(&statReport.FileInfo)
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
w.Header().Add(copy.XDockerContainerPathStatHeader, statHeader)
}
if errors.Cause(err) == define.ErrNoSuchCtr || errors.Cause(err) == copy.ENOENT {
// 404 is returned for an absent container and path. The
// clients must deal with it accordingly.
utils.Error(w, "Not found.", http.StatusNotFound, err)
return
} else if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return
}
source, err := copy.CopyItemForContainer(ctr, query.Path, true, true)
defer source.CleanUp()
if err != nil {
utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path))
return
}
// NOTE: Docker always sets the header.
info, err := source.Stat()
if err != nil {
utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path))
return
}
statHeader, err := copy.EncodeFileInfo(info)
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
w.Header().Add(copy.XDockerContainerPathStatHeader, statHeader)
// Our work is done when the user is interested in the header only.
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
// Alright, the users wants data from the container.
destination, err := copy.CopyItemForWriter(w)
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
copier, err := copy.GetCopier(&source, &destination, false)
copyFunc, err := containerEngine.ContainerCopyToArchive(r.Context(), containerName, query.Path, w)
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
w.Header().Set("Content-Type", "application/x-tar")
w.WriteHeader(http.StatusOK)
if err := copier.Copy(); err != nil {
logrus.Errorf("Error during copy: %v", err)
return
if err := copyFunc(); err != nil {
logrus.Error(err.Error())
}
}
@ -113,36 +104,22 @@ func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder,
return
}
ctrName := utils.GetName(r)
containerName := utils.GetName(r)
containerEngine := abi.ContainerEngine{Libpod: runtime}
ctr, err := runtime.LookupContainer(ctrName)
if err != nil {
utils.Error(w, "Not found", http.StatusNotFound, errors.Wrapf(err, "the %s container doesn't exists", ctrName))
copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body)
if errors.Cause(err) == define.ErrNoSuchCtr || os.IsNotExist(err) {
// 404 is returned for an absent container and path. The
// clients must deal with it accordingly.
utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists"))
return
} else if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return
}
destination, err := copy.CopyItemForContainer(ctr, query.Path, true, false)
defer destination.CleanUp()
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
source, err := copy.CopyItemForReader(r.Body)
defer source.CleanUp()
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
copier, err := copy.GetCopier(&source, &destination, false)
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
w.WriteHeader(http.StatusOK)
if err := copier.Copy(); err != nil {
logrus.Errorf("Error during copy: %v", err)
return
if err := copyFunc(); err != nil {
logrus.Error(err.Error())
}
}

View File

@ -4,7 +4,6 @@ import (
"net/http"
"github.com/containers/podman/v2/pkg/api/handlers/compat"
"github.com/containers/podman/v2/pkg/api/handlers/libpod"
"github.com/gorilla/mux"
)
@ -92,7 +91,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
Libpod
*/
// swagger:operation POST /libpod/containers/{name}/copy libpod libpodPutArchive
// swagger:operation POST /libpod/containers/{name}/archive libpod libpodPutArchive
// ---
// summary: Copy files into a container
// description: Copy a tar archive of files into a container
@ -133,7 +132,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
// 500:
// $ref: "#/responses/InternalError"
// swagger:operation GET /libpod/containers/{name}/copy libpod libpodGetArchive
// swagger:operation GET /libpod/containers/{name}/archive libpod libpodGetArchive
// ---
// summary: Copy files from a container
// description: Copy a tar archive of files from a container
@ -164,8 +163,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
// $ref: "#/responses/NoSuchContainer"
// 500:
// $ref: "#/responses/InternalError"
r.HandleFunc(VersionedPath("/libpod/containers/{name}/copy"), s.APIHandler(libpod.Archive)).Methods(http.MethodGet, http.MethodPost)
r.HandleFunc(VersionedPath("/libpod/containers/{name}/archive"), s.APIHandler(libpod.Archive)).Methods(http.MethodGet, http.MethodPost)
r.HandleFunc(VersionedPath("/libpod/containers/{name}/archive"), s.APIHandler(compat.Archive)).Methods(http.MethodGet, http.MethodPut, http.MethodHead)
return nil
}

View File

@ -0,0 +1,92 @@
package containers
import (
"context"
"io"
"net/http"
"net/url"
"github.com/containers/podman/v2/pkg/bindings"
"github.com/containers/podman/v2/pkg/copy"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/pkg/errors"
)
// Stat checks if the specified path is on the container. Note that the stat
// report may be set even in case of an error. This happens when the path
// resolves to symlink pointing to a non-existent path.
func Stat(ctx context.Context, nameOrID string, path string) (*entities.ContainerStatReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("path", path)
response, err := conn.DoRequest(nil, http.MethodHead, "/containers/%s/archive", params, nil, nameOrID)
if err != nil {
return nil, err
}
var finalErr error
if response.StatusCode == http.StatusNotFound {
finalErr = copy.ENOENT
} else if response.StatusCode != http.StatusOK {
finalErr = errors.New(response.Status)
}
var statReport *entities.ContainerStatReport
fileInfo, err := copy.ExtractFileInfoFromHeader(&response.Header)
if err != nil && finalErr == nil {
return nil, err
}
if fileInfo != nil {
statReport = &entities.ContainerStatReport{FileInfo: *fileInfo}
}
return statReport, finalErr
}
func CopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("path", path)
return func() error {
response, err := conn.DoRequest(reader, http.MethodPut, "/containers/%s/archive", params, nil, nameOrID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK {
return errors.New(response.Status)
}
return response.Process(nil)
}, nil
}
func CopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("path", path)
response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/archive", params, nil, nameOrID)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, response.Process(nil)
}
return func() error {
_, err := io.Copy(writer, response.Body)
return err
}, nil
}

View File

@ -1,220 +0,0 @@
package copy
import (
"io"
"os"
"path/filepath"
"strings"
buildahCopiah "github.com/containers/buildah/copier"
"github.com/containers/storage/pkg/archive"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/pkg/errors"
)
// ********************************* NOTE *************************************
//
// Most security bugs are caused by attackers playing around with symlinks
// trying to escape from the container onto the host and/or trick into data
// corruption on the host. Hence, file operations on containers (including
// *stat) should always be handled by `github.com/containers/buildah/copier`
// which makes sure to evaluate files in a chroot'ed environment.
//
// Please make sure to add verbose comments when changing code to make the
// lives of future readers easier.
//
// ****************************************************************************
// Copier copies data from a source to a destination CopyItem.
type Copier struct {
copyFunc func() error
cleanUpFuncs []deferFunc
}
// cleanUp releases resources the Copier may hold open.
func (c *Copier) cleanUp() {
for _, f := range c.cleanUpFuncs {
f()
}
}
// Copy data from a source to a destination CopyItem.
func (c *Copier) Copy() error {
defer c.cleanUp()
return c.copyFunc()
}
// GetCopiers returns a Copier to copy the source item to destination. Use
// extract to untar the source if it's a tar archive.
func GetCopier(source *CopyItem, destination *CopyItem, extract bool) (*Copier, error) {
copier := &Copier{}
// First, do the man-page dance. See podman-cp(1) for details.
if err := enforceCopyRules(source, destination); err != nil {
return nil, err
}
// Destination is a stream (e.g., stdout or an http body).
if destination.info.IsStream {
// Source is a stream (e.g., stdin or an http body).
if source.info.IsStream {
copier.copyFunc = func() error {
_, err := io.Copy(destination.writer, source.reader)
return err
}
return copier, nil
}
root, glob, err := source.buildahGlobs()
if err != nil {
return nil, err
}
copier.copyFunc = func() error {
return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer)
}
return copier, nil
}
// Destination is either a file or a directory.
if source.info.IsStream {
copier.copyFunc = func() error {
return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader)
}
return copier, nil
}
tarOptions := &archive.TarOptions{
Compression: archive.Uncompressed,
CopyPass: true,
}
root := destination.root
dir := destination.resolved
if !source.info.IsDir {
// When copying a file, make sure to rename the
// destination base path.
nameMap := make(map[string]string)
nameMap[filepath.Base(source.resolved)] = filepath.Base(destination.resolved)
tarOptions.RebaseNames = nameMap
dir = filepath.Dir(dir)
}
var tarReader io.ReadCloser
if extract && archive.IsArchivePath(source.resolved) {
if !destination.info.IsDir {
return nil, errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original)
}
reader, err := os.Open(source.resolved)
if err != nil {
return nil, err
}
copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() })
// The stream from stdin may be compressed (e.g., via gzip).
decompressedStream, err := archive.DecompressStream(reader)
if err != nil {
return nil, err
}
copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { decompressedStream.Close() })
tarReader = decompressedStream
} else {
reader, err := archive.TarWithOptions(source.resolved, tarOptions)
if err != nil {
return nil, err
}
copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() })
tarReader = reader
}
copier.copyFunc = func() error {
return buildahCopiah.Put(root, dir, source.putOptions(), tarReader)
}
return copier, nil
}
// enforceCopyRules enforces the rules for copying from a source to a
// destination as mentioned in the podman-cp(1) man page. Please refer to the
// man page and/or the inline comments for further details. Note that source
// and destination are passed by reference and the their data may be changed.
func enforceCopyRules(source, destination *CopyItem) error {
if source.statError != nil {
return source.statError
}
// We can copy everything to a stream.
if destination.info.IsStream {
return nil
}
if source.info.IsStream {
if !(destination.info.IsDir || destination.info.IsStream) {
return errors.New("destination must be a directory or stream when copying from a stream")
}
return nil
}
// Source is a *directory*.
if source.info.IsDir {
if destination.statError != nil {
// It's okay if the destination does not exist. We
// made sure before that it's parent exists, so it
// would be created while copying.
if os.IsNotExist(destination.statError) {
return nil
}
// Could be a permission error.
return destination.statError
}
// If the destination exists and is not a directory, we have a
// problem.
if !destination.info.IsDir {
return errors.Errorf("cannot copy directory %q to file %q", source.original, destination.original)
}
// If the destination exists and is a directory, we need to
// append the source base directory to it. This makes sure
// that copying "/foo/bar" "/tmp" will copy to "/tmp/bar" (and
// not "/tmp").
newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
if err != nil {
return err
}
destination.resolved = newDestination
return nil
}
// Source is a *file*.
if destination.statError != nil {
// It's okay if the destination does not exist, unless it ends
// with "/".
if !os.IsNotExist(destination.statError) {
return destination.statError
} else if strings.HasSuffix(destination.resolved, "/") {
// Note: this is practically unreachable code as the
// existence of parent directories is enforced early
// on. It's left here as an extra security net.
return errors.Errorf("destination directory %q must exist (trailing %q)", destination.original, "/")
}
// Does not exist and does not end with "/".
return nil
}
// If the destination is a file, we're good. We will overwrite the
// contents while copying.
if !destination.info.IsDir {
return nil
}
// If the destination exists and is a directory, we need to append the
// source base directory to it. This makes sure that copying
// "/foo/bar" "/tmp" will copy to "/tmp/bar" (and not "/tmp").
newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
if err != nil {
return err
}
destination.resolved = newDestination
return nil
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"time"
@ -15,6 +16,10 @@ import (
// base64 encoded JSON payload of stating a path in a container.
const XDockerContainerPathStatHeader = "X-Docker-Container-Path-Stat"
// ENOENT mimics the stdlib's ENONENT and can be used to implement custom logic
// while preserving the user-visible error message.
var ENOENT = errors.New("No such file or directory")
// FileInfo describes a file or directory and is returned by
// (*CopyItem).Stat().
type FileInfo struct {
@ -23,7 +28,6 @@ type FileInfo struct {
Mode os.FileMode `json:"mode"`
ModTime time.Time `json:"mtime"`
IsDir bool `json:"isDir"`
IsStream bool `json:"isStream"`
LinkTarget string `json:"linkTarget"`
}
@ -54,3 +58,54 @@ func ExtractFileInfoFromHeader(header *http.Header) (*FileInfo, error) {
return &info, nil
}
// ResolveHostPath resolves the specified, possibly relative, path on the host.
func ResolveHostPath(path string) (*FileInfo, error) {
resolvedHostPath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
resolvedHostPath = PreserveBasePath(path, resolvedHostPath)
statInfo, err := os.Stat(resolvedHostPath)
if err != nil {
if os.IsNotExist(err) {
return nil, ENOENT
}
return nil, err
}
return &FileInfo{
Name: statInfo.Name(),
Size: statInfo.Size(),
Mode: statInfo.Mode(),
ModTime: statInfo.ModTime(),
IsDir: statInfo.IsDir(),
LinkTarget: resolvedHostPath,
}, nil
}
// PreserveBasePath makes sure that the original base path (e.g., "/" or "./")
// is preserved. The filepath API among tends to clean up a bit too much but
// we *must* preserve this data by all means.
func PreserveBasePath(original, resolved string) string {
// Handle "/"
if strings.HasSuffix(original, "/") {
if !strings.HasSuffix(resolved, "/") {
resolved += "/"
}
return resolved
}
// Handle "/."
if strings.HasSuffix(original, "/.") {
if strings.HasSuffix(resolved, "/") { // could be root!
resolved += "."
} else if !strings.HasSuffix(resolved, "/.") {
resolved += "/."
}
return resolved
}
return resolved
}

View File

@ -1,588 +0,0 @@
package copy
import (
"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/v2/libpod"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/pkg/cgroups"
"github.com/containers/podman/v2/pkg/rootless"
"github.com/containers/storage"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// ********************************* NOTE *************************************
//
// Most security bugs are caused by attackers playing around with symlinks
// trying to escape from the container onto the host and/or trick into data
// corruption on the host. Hence, file operations on containers (including
// *stat) should always be handled by `github.com/containers/buildah/copier`
// which makes sure to evaluate files in a chroot'ed environment.
//
// Please make sure to add verbose comments when changing code to make the
// lives of future readers easier.
//
// ****************************************************************************
var (
_stdin = os.Stdin.Name()
_stdout = os.Stdout.Name()
)
// CopyItem is the source or destination of a copy operation. Use the
// CopyItemFrom* functions to create one for the specific source/destination
// item.
type CopyItem struct {
// The original path provided by the caller. Useful in error messages.
original string
// The resolved path on the host or container. Maybe altered at
// multiple stages when copying.
resolved string
// The root for copying data in a chroot'ed environment.
root string
// IDPair of the resolved path.
idPair *idtools.IDPair
// Storage ID mappings.
idMappings *storage.IDMappingOptions
// Internal FileInfo. We really don't want users to mess with a
// CopyItem but only plug and play with it.
info FileInfo
// Error when creating the upper FileInfo. Some errors are non-fatal,
// for instance, when a destination *base* path does not exist.
statError error
writer io.Writer
reader io.Reader
// Needed to clean up resources (e.g., unmount a container).
cleanUpFuncs []deferFunc
}
// deferFunc allows for returning functions that must be deferred at call sites.
type deferFunc func()
// Stat returns the FileInfo.
func (item *CopyItem) Stat() (*FileInfo, error) {
return &item.info, item.statError
}
// CleanUp releases resources such as the container mounts. It *must* be
// called even in case of errors.
func (item *CopyItem) CleanUp() {
for _, f := range item.cleanUpFuncs {
f()
}
}
// CopyItemForWriter returns a CopyItem for the specified io.WriteCloser. Note
// that the returned item can only act as a copy destination.
func CopyItemForWriter(writer io.Writer) (item CopyItem, _ error) {
item.writer = writer
item.info.IsStream = true
return item, nil
}
// CopyItemForReader returns a CopyItem for the specified io.ReaderCloser. Note
// that the returned item can only act as a copy source.
//
// Note that the specified reader will be auto-decompressed if needed.
func CopyItemForReader(reader io.Reader) (item CopyItem, _ error) {
item.info.IsStream = true
decompressed, err := archive.DecompressStream(reader)
if err != nil {
return item, err
}
item.reader = decompressed
item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
if err := decompressed.Close(); err != nil {
logrus.Errorf("Error closing decompressed reader of copy item: %v", err)
}
})
return item, nil
}
// CopyItemForHost creates a CopyItem for the specified host path. It's a
// destination by default. Use isSource to set it as a destination.
//
// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors.
func CopyItemForHost(hostPath string, isSource bool) (item CopyItem, _ error) {
if hostPath == "-" {
if isSource {
hostPath = _stdin
} else {
hostPath = _stdout
}
}
if hostPath == _stdin {
return CopyItemForReader(os.Stdin)
}
if hostPath == _stdout {
return CopyItemForWriter(os.Stdout)
}
// Now do the dance for the host data.
resolvedHostPath, err := filepath.Abs(hostPath)
if err != nil {
return item, err
}
resolvedHostPath = preserveBasePath(hostPath, resolvedHostPath)
item.original = hostPath
item.resolved = resolvedHostPath
item.root = "/"
statInfo, statError := os.Stat(resolvedHostPath)
item.statError = statError
// It exists, we're done.
if statError == nil {
item.info.Name = statInfo.Name()
item.info.Size = statInfo.Size()
item.info.Mode = statInfo.Mode()
item.info.ModTime = statInfo.ModTime()
item.info.IsDir = statInfo.IsDir()
item.info.LinkTarget = resolvedHostPath
return item, nil
}
// The source must exist, but let's try to give some human-friendly
// errors.
if isSource {
if os.IsNotExist(item.statError) {
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", hostPath)
}
return item, item.statError // could be a permission error
}
// If we're a destination, we need to make sure that the parent
// directory exists.
parent := filepath.Dir(resolvedHostPath)
if _, err := os.Stat(parent); err != nil {
if os.IsNotExist(err) {
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", parent)
}
return item, err
}
return item, nil
}
// CopyItemForContainer creates a CopyItem for the specified path on the
// container. It's a destination by default. Use isSource to set it as a
// destination. Note that the container path may resolve to a path outside of
// the container's mount point if the path hits a volume or mount on the
// container.
//
// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors.
func CopyItemForContainer(container *libpod.Container, containerPath string, pause bool, isSource bool) (item CopyItem, _ error) {
// Mount and pause the container.
containerMountPoint, err := item.mountAndPauseContainer(container, pause)
if err != nil {
return item, 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 item, err
}
resolvedContainerPath = preserveBasePath(containerPath, resolvedContainerPath)
idMappings, idPair, err := getIDMappingsAndPair(container, containerMountPoint)
if err != nil {
return item, err
}
item.original = containerPath
item.resolved = resolvedContainerPath
item.root = resolvedRoot
item.idMappings = idMappings
item.idPair = idPair
statInfo, statError := secureStat(resolvedRoot, resolvedContainerPath)
item.statError = statError
// It exists, we're done.
if statError == nil {
item.info.IsDir = statInfo.IsDir
item.info.Name = filepath.Base(statInfo.Name)
item.info.Size = statInfo.Size
item.info.Mode = statInfo.Mode
item.info.ModTime = statInfo.ModTime
item.info.IsDir = statInfo.IsDir
item.info.LinkTarget = resolvedContainerPath
return item, nil
}
// The source must exist, but let's try to give some human-friendly
// errors.
if isSource {
if os.IsNotExist(statError) {
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath)
}
return item, item.statError // could be a permission error
}
// If we're a destination, we need to make sure that the parent
// directory exists.
parent := filepath.Dir(resolvedContainerPath)
if _, err := secureStat(resolvedRoot, parent); err != nil {
if os.IsNotExist(err) {
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath)
}
return item, err
}
return item, nil
}
// putOptions returns PUT options for buildah's copier package.
func (item *CopyItem) putOptions() buildahCopiah.PutOptions {
options := buildahCopiah.PutOptions{}
if item.idMappings != nil {
options.UIDMap = item.idMappings.UIDMap
options.GIDMap = item.idMappings.GIDMap
}
if item.idPair != nil {
options.ChownDirs = item.idPair
options.ChownFiles = item.idPair
}
return options
}
// getOptions returns GET options for buildah's copier package.
func (item *CopyItem) getOptions() buildahCopiah.GetOptions {
options := buildahCopiah.GetOptions{}
if item.idMappings != nil {
options.UIDMap = item.idMappings.UIDMap
options.GIDMap = item.idMappings.GIDMap
}
if item.idPair != nil {
options.ChownDirs = item.idPair
options.ChownFiles = item.idPair
}
return options
}
// mount and pause the container. Also set the item's cleanUpFuncs. Those
// *must* be invoked by callers, even in case of errors.
func (item *CopyItem) mountAndPauseContainer(container *libpod.Container, pause bool) (string, error) {
// Make sure to pause and unpause the container. We cannot pause on
// cgroupsv1 as rootless user, in which case we turn off pausing.
if pause && rootless.IsRootless() {
cgroupv2, _ := cgroups.IsCgroup2UnifiedMode()
if !cgroupv2 {
logrus.Debugf("Cannot pause container for copying as a rootless user on cgroupsv1: default to not pause")
pause = false
}
}
// Mount and unmount the container.
mountPoint, err := container.Mount()
if err != nil {
return "", err
}
item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
if err := container.Unmount(false); err != nil {
logrus.Errorf("Error unmounting container after copy operation: %v", err)
}
})
// Pause and unpause the container.
if pause {
if err := container.Pause(); err != nil {
// Ignore errors when the container isn't running. No
// need to pause.
if errors.Cause(err) != define.ErrCtrStateInvalid {
return "", err
}
} else {
item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
if err := container.Unpause(); err != nil {
logrus.Errorf("Error unpausing container after copy operation: %v", err)
}
})
}
}
return mountPoint, nil
}
// buildahGlobs returns the root, dir and glob used in buildah's copier
// package.
//
// Note that dir is always empty.
func (item *CopyItem) buildahGlobs() (root string, glob string, err error) {
root = item.root
// If the root and the resolved path are equal, then dir must be empty
// and the glob must be ".".
if filepath.Clean(root) == filepath.Clean(item.resolved) {
glob = "."
return
}
glob, err = filepath.Rel(root, item.resolved)
return
}
// preserveBasePath makes sure that the original base path (e.g., "/" or "./")
// is preserved. The filepath API among tends to clean up a bit too much but
// we *must* preserve this data by all means.
func preserveBasePath(original, resolved string) string {
// Handle "/"
if strings.HasSuffix(original, "/") {
if !strings.HasSuffix(resolved, "/") {
resolved += "/"
}
return resolved
}
// Handle "/."
if strings.HasSuffix(original, "/.") {
if strings.HasSuffix(resolved, "/") { // could be root!
resolved += "."
} else if !strings.HasSuffix(resolved, "/.") {
resolved += "/."
}
return resolved
}
return resolved
}
// 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 libpod 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 stat, os.ErrNotExist
}
var statErr error
if stat.Error != "" {
statErr = errors.New(stat.Error)
}
return stat, statErr
}
// 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)
}
// NOTE: the secure join makes sure that we follow symlinks. This way,
// we catch scenarios where the container path symlinks to a volume or
// bind mount.
resolvedPathOnTheContainerMountPoint, err := securejoin.SecureJoin(mountPoint, pathRelativeToContainerMountPoint)
if err != nil {
return "", "", err
}
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
}
// 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
}

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/image/v5/types"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/pkg/copy"
"github.com/containers/podman/v2/pkg/specgen"
"github.com/cri-o/ocicni/pkg/ocicni"
)
@ -143,6 +144,10 @@ type ContainerInspectReport struct {
*define.InspectContainerData
}
type ContainerStatReport struct {
copy.FileInfo
}
type CommitOptions struct {
Author string
Changes []string

View File

@ -2,6 +2,7 @@ package entities
import (
"context"
"io"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v2/libpod/define"
@ -9,6 +10,8 @@ import (
"github.com/spf13/cobra"
)
type ContainerCopyFunc func() error
type ContainerEngine interface {
AutoUpdate(ctx context.Context, options AutoUpdateOptions) (*AutoUpdateReport, []error)
Config(ctx context.Context) (*config.Config, error)
@ -16,7 +19,8 @@ type ContainerEngine interface {
ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error)
ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error)
ContainerCommit(ctx context.Context, nameOrID string, options CommitOptions) (*CommitReport, error)
ContainerCp(ctx context.Context, source, dest string, options ContainerCpOptions) error
ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (ContainerCopyFunc, error)
ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (ContainerCopyFunc, error)
ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error)
ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error)
ContainerExec(ctx context.Context, nameOrID string, options ExecOptions, streams define.AttachStreams) (int, error)
@ -38,6 +42,7 @@ type ContainerEngine interface {
ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error)
ContainerRunlabel(ctx context.Context, label string, image string, args []string, opts ContainerRunlabelOptions) error
ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error)
ContainerStat(ctx context.Context, nameOrDir string, path string) (*ContainerStatReport, error)
ContainerStats(ctx context.Context, namesOrIds []string, options ContainerStatsOptions) (chan ContainerStatsReport, error)
ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error)
ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error)

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -772,9 +772,16 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o
return reports, nil
}
func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error {
return nil
// return containers.Copy(ic.ClientCxt, source, dest, options)
func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
return containers.CopyFromArchive(ic.ClientCxt, nameOrID, path, reader)
}
func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) {
return containers.CopyToArchive(ic.ClientCxt, nameOrID, path, writer)
}
func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, path string) (*entities.ContainerStatReport, error) {
return containers.Stat(ic.ClientCxt, nameOrID, path)
}
// Shutdown Libpod engine

View File

@ -19,7 +19,12 @@ func JoinErrors(errs []error) error {
// blank lines when printing the error.
var multiE *multierror.Error
multiE = multierror.Append(multiE, errs...)
return errors.New(strings.TrimSpace(multiE.ErrorOrNil().Error()))
finalErr := multiE.ErrorOrNil()
if finalErr == nil {
return finalErr
}
return errors.New(strings.TrimSpace(finalErr.Error()))
}
// ErrorsToString converts the slice of errors into a slice of corresponding

View File

@ -24,7 +24,6 @@ var _ = Describe("Podman cp", func() {
)
BeforeEach(func() {
SkipIfRemote("FIXME: Podman-remote cp needs to work")
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)

View File

@ -8,8 +8,6 @@
load helpers
@test "podman cp file from host to container" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-file-host-to-ctr
mkdir -p $srcdir
local -a randomcontent=(
@ -57,60 +55,16 @@ load helpers
is "$output" 'Error: ".*/IdoNotExist" could not be found on the host' \
"copy nonexistent host path"
# Container path does not exist. Notice that the error message shows how
# the specified container is resolved.
# Container (parent) path does not exist.
run_podman 125 cp $srcdir/hostfile0 cpcontainer:/IdoNotExist/
is "$output" 'Error: "/IdoNotExist/" could not be found on container.*(resolved to .*/IdoNotExist.*' \
is "$output" 'Error: "/IdoNotExist/" could not be found on container cpcontainer: No such file or directory' \
"copy into nonexistent path in container"
run_podman rm -f cpcontainer
}
@test "podman cp --extract=true tar archive to container" {
skip_if_remote "podman-remote does not yet handle cp"
# Create tempfile with random name and content
dirname=cp-test-extract
srcdir=$PODMAN_TMPDIR/$dirname
mkdir -p $srcdir
rand_filename=$(random_string 20)
rand_content=$(random_string 50)
echo $rand_content > $srcdir/$rand_filename
chmod 644 $srcdir/$rand_filename
# Now tar it up!
tar_file=$PODMAN_TMPDIR/archive.tar.gz
tar -C $PODMAN_TMPDIR -zvcf $tar_file $dirname
run_podman run -d --name cpcontainer $IMAGE sleep infinity
# First just copy without extracting the archive.
run_podman cp $tar_file cpcontainer:/tmp
# Now remove the archive which will also test if it exists and is a file.
# To save expensive exec'ing, create a file for the next tests.
run_podman exec cpcontainer sh -c "rm /tmp/archive.tar.gz; touch /tmp/file.txt"
# Now copy with extracting the archive. NOTE that Podman should
# auto-decompress the file if needed.
run_podman cp --extract=true $tar_file cpcontainer:/tmp
run_podman exec cpcontainer cat /tmp/$dirname/$rand_filename
is "$output" "$rand_content"
# Test extract on non archive.
run_podman cp --extract=true $srcdir/$rand_filename cpcontainer:/foo.txt
# Cannot extract an archive to a file!
run_podman 125 cp --extract=true $tar_file cpcontainer:/tmp/file.txt
is "$output" 'Error: cannot extract archive .* to file "/tmp/file.txt"'
run_podman rm -f cpcontainer
}
@test "podman cp file from container to host" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-file-ctr-to-host
mkdir -p $srcdir
@ -153,8 +107,6 @@ load helpers
@test "podman cp dir from host to container" {
skip_if_remote "podman-remote does not yet handle cp"
dirname=dir-test
srcdir=$PODMAN_TMPDIR/$dirname
mkdir -p $srcdir
@ -195,8 +147,6 @@ load helpers
@test "podman cp dir from container to host" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/dir-test
mkdir -p $srcdir
@ -230,8 +180,6 @@ load helpers
@test "podman cp file from host to container volume" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-volume
mkdir -p $srcdir
echo "This file should be in volume2" > $srcdir/hostfile
@ -268,8 +216,6 @@ load helpers
@test "podman cp file from host to container mount" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-mount-src
mountdir=$PODMAN_TMPDIR/cp-test-mount
mkdir -p $srcdir $mountdir
@ -296,8 +242,6 @@ load helpers
# perform wildcard expansion in the container. We should get both
# files copied into the host.
@test "podman cp * - wildcard copy multiple files from container to host" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-in
dstdir=$PODMAN_TMPDIR/cp-test-out
mkdir -p $srcdir $dstdir
@ -321,8 +265,6 @@ load helpers
# Create a file on the host; make a symlink in the container pointing
# into host-only space. Try to podman-cp that symlink. It should fail.
@test "podman cp - will not recognize symlink pointing into host space" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-in
dstdir=$PODMAN_TMPDIR/cp-test-out
mkdir -p $srcdir $dstdir
@ -350,8 +292,6 @@ load helpers
# in the container pointing to 'file*' (file star). Try to podman-cp
# this invalid double symlink. It must fail.
@test "podman cp - will not expand globs in host space (#3829)" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-in
dstdir=$PODMAN_TMPDIR/cp-test-out
mkdir -p $srcdir $dstdir
@ -372,8 +312,6 @@ load helpers
# Another symlink into host space, this one named '*' (star). cp should fail.
@test "podman cp - will not expand wildcard" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-in
dstdir=$PODMAN_TMPDIR/cp-test-out
mkdir -p $srcdir $dstdir
@ -394,8 +332,6 @@ load helpers
# THIS IS EXTREMELY WEIRD. Podman expands symlinks in weird ways.
@test "podman cp into container: weird symlink expansion" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-in
dstdir=$PODMAN_TMPDIR/cp-test-out
mkdir -p $srcdir $dstdir
@ -427,7 +363,7 @@ load helpers
is "$output" "" "output from podman cp 1"
run_podman 125 cp --pause=false $srcdir/$rand_filename2 cpcontainer:/tmp/d2/x/
is "$output" 'Error: "/tmp/d2/x/" could not be found on container.*' "cp will not create nonexistent destination directory"
is "$output" 'Error: "/tmp/d2/x/" could not be found on container cpcontainer: No such file or directory' "cp will not create nonexistent destination directory"
run_podman cp --pause=false $srcdir/$rand_filename3 cpcontainer:/tmp/d3/x
is "$output" "" "output from podman cp 3"
@ -454,8 +390,6 @@ load helpers
# rhbz1741718 : file copied into container:/var/lib/foo appears as /foo
# (docker only, never seems to have affected podman. Make sure it never does).
@test "podman cp into a subdirectory matching GraphRoot" {
skip_if_remote "podman-remote does not yet handle cp"
# Create tempfile with random name and content
srcdir=$PODMAN_TMPDIR/cp-test-in
mkdir -p $srcdir
@ -491,8 +425,6 @@ load helpers
@test "podman cp from stdin to container" {
skip_if_remote "podman-remote does not yet handle cp"
# Create tempfile with random name and content
srcdir=$PODMAN_TMPDIR/cp-test-stdin
mkdir -p $srcdir
@ -525,24 +457,22 @@ load helpers
# Input stream must be a (compressed) tar archive.
run_podman 125 cp - cpcontainer:/tmp < $srcdir/$rand_filename
is "$output" "Error:.*: error reading tar stream.*" "input stream must be a (compressed) tar archive"
is "$output" "Error: source must be a (compressed) tar archive when copying from stdin"
# Destination must be a directory (on an existing file).
run_podman exec cpcontainer touch /tmp/file.txt
run_podman 125 cp /dev/stdin cpcontainer:/tmp/file.txt < $tar_file
is "$output" 'Error: destination must be a directory or stream when copying from a stream'
is "$output" 'Error: destination must be a directory when copying from stdin'
# Destination must be a directory (on an absent path).
run_podman 125 cp /dev/stdin cpcontainer:/tmp/IdoNotExist < $tar_file
is "$output" 'Error: destination must be a directory or stream when copying from a stream'
is "$output" 'Error: destination must be a directory when copying from stdin'
run_podman rm -f cpcontainer
}
@test "podman cp from container to stdout" {
skip_if_remote "podman-remote does not yet handle cp"
srcdir=$PODMAN_TMPDIR/cp-test-stdout
mkdir -p $srcdir
rand_content=$(random_string 50)
@ -579,9 +509,9 @@ load helpers
fi
tar xvf $srcdir/stdout.tar -C $srcdir
run cat $srcdir/file.txt
run cat $srcdir/tmp/file.txt
is "$output" "$rand_content"
run cat $srcdir/empty.txt
run cat $srcdir/tmp/empty.txt
is "$output" ""
run_podman rm -f cpcontainer

View File

@ -178,7 +178,7 @@ gce_instance:
image_name: "${FEDORA_CACHE_IMAGE_NAME}"
image_name: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
image_name: "${UBUNTU_CACHE_IMAGE_NAME}"
image_name: "${PRIOR_UBUNTU_CACHE_IMAGE_NAME}"
# image_name: "${PRIOR_UBUNTU_CACHE_IMAGE_NAME}"
# Separate scripts for separate outputs, makes debugging easier.
setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}'

View File

@ -39,6 +39,14 @@ SOURCES=*.go imagebuildah/*.go bind/*.go chroot/*.go cmd/buildah/*.go copier/*.g
LINTFLAGS ?=
ifeq ($(DEBUG), 1)
override GOGCFLAGS += -N -l
endif
# make all DEBUG=1
# Note: Uses the -N -l go compiler options to disable compiler optimizations
# and inlining. Using these build options allows you to subsequently
# use source debugging tools like delve.
all: bin/buildah bin/imgtype docs
# Update nix/nixpkgs.json its latest stable commit
@ -56,7 +64,7 @@ static:
.PHONY: bin/buildah
bin/buildah: $(SOURCES)
$(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./cmd/buildah
$(GO_BUILD) $(BUILDAH_LDFLAGS) -gcflags "$(GOGCFLAGS)" -o $@ $(BUILDFLAGS) ./cmd/buildah
.PHONY: buildah
buildah: bin/buildah

View File

@ -10,6 +10,7 @@ import (
"net"
"os"
"os/user"
"path"
"path/filepath"
"strconv"
"strings"
@ -202,11 +203,11 @@ type StatOptions struct {
// If root and directory are both not specified, the current root directory is
// used, and relative names in the globs list are treated as being relative to
// the current working directory.
// If root is specified and the current OS supports it, the stat() is performed
// in a chrooted context. If the directory is specified as an absolute path,
// it should either be the root directory or a subdirectory of the root
// directory. Otherwise, the directory is treated as a path relative to the
// root directory.
// If root is specified and the current OS supports it, and the calling process
// has the necessary privileges, the stat() is performed in a chrooted context.
// If the directory is specified as an absolute path, it should either be the
// root directory or a subdirectory of the root directory. Otherwise, the
// directory is treated as a path relative to the root directory.
// Relative names in the glob list are treated as being relative to the
// directory.
func Stat(root string, directory string, options StatOptions, globs []string) ([]*StatsForGlob, error) {
@ -229,18 +230,19 @@ func Stat(root string, directory string, options StatOptions, globs []string) ([
// GetOptions controls parts of Get()'s behavior.
type GetOptions struct {
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive
Excludes []string // contents to pretend don't exist, using the OS-specific path separator
ExpandArchives bool // extract the contents of named items that are archives
ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted
ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted
ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted
ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
StripXattrs bool // don't record extended attributes of items being copied. no effect on archives being extracted
KeepDirectoryNames bool // don't strip the top directory's basename from the paths of items in subdirectories
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive
Excludes []string // contents to pretend don't exist, using the OS-specific path separator
ExpandArchives bool // extract the contents of named items that are archives
ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted
ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted
ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted
ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
StripXattrs bool // don't record extended attributes of items being copied. no effect on archives being extracted
KeepDirectoryNames bool // don't strip the top directory's basename from the paths of items in subdirectories
Rename map[string]string // rename items with the specified names, or under the specified names
}
// Get produces an archive containing items that match the specified glob
@ -248,11 +250,11 @@ type GetOptions struct {
// If root and directory are both not specified, the current root directory is
// used, and relative names in the globs list are treated as being relative to
// the current working directory.
// If root is specified and the current OS supports it, the contents are read
// in a chrooted context. If the directory is specified as an absolute path,
// it should either be the root directory or a subdirectory of the root
// directory. Otherwise, the directory is treated as a path relative to the
// root directory.
// If root is specified and the current OS supports it, and the calling process
// has the necessary privileges, the contents are read in a chrooted context.
// If the directory is specified as an absolute path, it should either be the
// root directory or a subdirectory of the root directory. Otherwise, the
// directory is treated as a path relative to the root directory.
// Relative names in the glob list are treated as being relative to the
// directory.
func Get(root string, directory string, options GetOptions, globs []string, bulkWriter io.Writer) error {
@ -278,25 +280,28 @@ func Get(root string, directory string, options GetOptions, globs []string, bulk
// PutOptions controls parts of Put()'s behavior.
type PutOptions struct {
UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs when writing contents to disk
DefaultDirOwner *idtools.IDPair // set ownership of implicitly-created directories, default is ChownDirs, or 0:0 if ChownDirs not set
DefaultDirMode *os.FileMode // set permissions on implicitly-created directories, default is ChmodDirs, or 0755 if ChmodDirs not set
ChownDirs *idtools.IDPair // set ownership of newly-created directories
ChmodDirs *os.FileMode // set permissions on newly-created directories
ChownFiles *idtools.IDPair // set ownership of newly-created files
ChmodFiles *os.FileMode // set permissions on newly-created files
StripXattrs bool // don't bother trying to set extended attributes of items being copied
IgnoreXattrErrors bool // ignore any errors encountered when attempting to set extended attributes
UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs when writing contents to disk
DefaultDirOwner *idtools.IDPair // set ownership of implicitly-created directories, default is ChownDirs, or 0:0 if ChownDirs not set
DefaultDirMode *os.FileMode // set permissions on implicitly-created directories, default is ChmodDirs, or 0755 if ChmodDirs not set
ChownDirs *idtools.IDPair // set ownership of newly-created directories
ChmodDirs *os.FileMode // set permissions on newly-created directories
ChownFiles *idtools.IDPair // set ownership of newly-created files
ChmodFiles *os.FileMode // set permissions on newly-created files
StripXattrs bool // don't bother trying to set extended attributes of items being copied
IgnoreXattrErrors bool // ignore any errors encountered when attempting to set extended attributes
NoOverwriteDirNonDir bool // instead of quietly overwriting directories with non-directories, return an error
Rename map[string]string // rename items with the specified names, or under the specified names
}
// Put extracts an archive from the bulkReader at the specified directory.
// If root and directory are both not specified, the current root directory is
// used.
// If root is specified and the current OS supports it, the contents are written
// in a chrooted context. If the directory is specified as an absolute path,
// it should either be the root directory or a subdirectory of the root
// directory. Otherwise, the directory is treated as a path relative to the
// root directory.
// If root is specified and the current OS supports it, and the calling process
// has the necessary privileges, the contents are written in a chrooted
// context. If the directory is specified as an absolute path, it should
// either be the root directory or a subdirectory of the root directory.
// Otherwise, the directory is treated as a path relative to the root
// directory.
func Put(root string, directory string, options PutOptions, bulkReader io.Reader) error {
req := request{
Request: requestPut,
@ -325,11 +330,12 @@ type MkdirOptions struct {
// need to be created will be given the specified ownership and permissions.
// If root and directory are both not specified, the current root directory is
// used.
// If root is specified and the current OS supports it, the directory is
// created in a chrooted context. If the directory is specified as an absolute
// path, it should either be the root directory or a subdirectory of the root
// directory. Otherwise, the directory is treated as a path relative to the
// root directory.
// If root is specified and the current OS supports it, and the calling process
// has the necessary privileges, the directory is created in a chrooted
// context. If the directory is specified as an absolute path, it should
// either be the root directory or a subdirectory of the root directory.
// Otherwise, the directory is treated as a path relative to the root
// directory.
func Mkdir(root string, directory string, options MkdirOptions) error {
req := request{
Request: requestMkdir,
@ -547,13 +553,13 @@ func copierWithSubprocess(bulkReader io.Reader, bulkWriter io.Writer, req reques
return nil, errors.Wrap(err, step)
}
if err = encoder.Encode(req); err != nil {
return killAndReturn(err, "error encoding request")
return killAndReturn(err, "error encoding request for copier subprocess")
}
if err = decoder.Decode(&resp); err != nil {
return killAndReturn(err, "error decoding response")
return killAndReturn(err, "error decoding response from copier subprocess")
}
if err = encoder.Encode(&request{Request: requestQuit}); err != nil {
return killAndReturn(err, "error encoding request")
return killAndReturn(err, "error encoding request for copier subprocess")
}
stdinWrite.Close()
stdinWrite = nil
@ -626,7 +632,7 @@ func copierMain() {
// Read a request.
req := new(request)
if err := decoder.Decode(req); err != nil {
fmt.Fprintf(os.Stderr, "error decoding request: %v", err)
fmt.Fprintf(os.Stderr, "error decoding request from copier parent process: %v", err)
os.Exit(1)
}
if req.Request == requestQuit {
@ -717,12 +723,12 @@ func copierMain() {
}
resp, cb, err := copierHandler(bulkReader, bulkWriter, *req)
if err != nil {
fmt.Fprintf(os.Stderr, "error handling request %#v: %v", *req, err)
fmt.Fprintf(os.Stderr, "error handling request %#v from copier parent process: %v", *req, err)
os.Exit(1)
}
// Encode the response.
if err := encoder.Encode(resp); err != nil {
fmt.Fprintf(os.Stderr, "error encoding response %#v: %v", *req, err)
fmt.Fprintf(os.Stderr, "error encoding response %#v for copier parent process: %v", *req, err)
os.Exit(1)
}
// If there's bulk data to transfer, run the callback to either
@ -1118,6 +1124,34 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
return &response{Stat: statResponse.Stat, Get: getResponse{}}, cb, nil
}
func handleRename(rename map[string]string, name string) string {
if rename == nil {
return name
}
// header names always use '/', so use path instead of filepath to manipulate it
if directMapping, ok := rename[name]; ok {
return directMapping
}
prefix, remainder := path.Split(name)
for prefix != "" {
if mappedPrefix, ok := rename[prefix]; ok {
return path.Join(mappedPrefix, remainder)
}
if prefix[len(prefix)-1] == '/' {
if mappedPrefix, ok := rename[prefix[:len(prefix)-1]]; ok {
return path.Join(mappedPrefix, remainder)
}
}
newPrefix, middlePart := path.Split(prefix)
if newPrefix == prefix {
return name
}
prefix = newPrefix
remainder = path.Join(middlePart, remainder)
}
return name
}
func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath string, options GetOptions, tw *tar.Writer, hardlinkChecker *util.HardlinkChecker, idMappings *idtools.IDMappings) error {
// build the header using the name provided
hdr, err := tar.FileInfoHeader(srcfi, symlinkTarget)
@ -1127,6 +1161,9 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str
if name != "" {
hdr.Name = filepath.ToSlash(name)
}
if options.Rename != nil {
hdr.Name = handleRename(options.Rename, hdr.Name)
}
if options.StripSetuidBit {
hdr.Mode &^= cISUID
}
@ -1164,6 +1201,9 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str
tr := tar.NewReader(rc)
hdr, err := tr.Next()
for err == nil {
if options.Rename != nil {
hdr.Name = handleRename(options.Rename, hdr.Name)
}
if err = tw.WriteHeader(hdr); err != nil {
return errors.Wrapf(err, "error writing tar header from %q to pipe", contentPath)
}
@ -1311,8 +1351,13 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM
createFile := func(path string, tr *tar.Reader) (int64, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_EXCL, 0600)
if err != nil && os.IsExist(err) {
if err = os.Remove(path); err != nil {
return 0, errors.Wrapf(err, "copier: put: error removing file to be overwritten %q", path)
if req.PutOptions.NoOverwriteDirNonDir {
if st, err2 := os.Lstat(path); err2 == nil && st.IsDir() {
return 0, errors.Wrapf(err, "copier: put: error creating file at %q", path)
}
}
if err = os.RemoveAll(path); err != nil {
return 0, errors.Wrapf(err, "copier: put: error removing item to be overwritten %q", path)
}
f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_EXCL, 0600)
}
@ -1360,6 +1405,14 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM
tr := tar.NewReader(bulkReader)
hdr, err := tr.Next()
for err == nil {
if len(hdr.Name) == 0 {
// no name -> ignore the entry
hdr, err = tr.Next()
continue
}
if req.PutOptions.Rename != nil {
hdr.Name = handleRename(req.PutOptions.Rename, hdr.Name)
}
// figure out who should own this new item
if idMappings != nil && !idMappings.Empty() {
containerPair := idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}
@ -1412,35 +1465,70 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM
}
case tar.TypeLink:
var linkTarget string
if req.PutOptions.Rename != nil {
hdr.Linkname = handleRename(req.PutOptions.Rename, hdr.Linkname)
}
if linkTarget, err = resolvePath(targetDirectory, filepath.Join(req.Root, filepath.FromSlash(hdr.Linkname)), nil); err != nil {
return errors.Errorf("error resolving hardlink target path %q under root %q", hdr.Linkname, req.Root)
}
if err = os.Link(linkTarget, path); err != nil && os.IsExist(err) {
if req.PutOptions.NoOverwriteDirNonDir {
if st, err := os.Lstat(path); err == nil && st.IsDir() {
break
}
}
if err = os.Remove(path); err == nil {
err = os.Link(linkTarget, path)
}
}
case tar.TypeSymlink:
// if req.PutOptions.Rename != nil {
// todo: the general solution requires resolving to an absolute path, handling
// renaming, and then possibly converting back to a relative symlink
// }
if err = os.Symlink(filepath.FromSlash(hdr.Linkname), filepath.FromSlash(path)); err != nil && os.IsExist(err) {
if req.PutOptions.NoOverwriteDirNonDir {
if st, err := os.Lstat(path); err == nil && st.IsDir() {
break
}
}
if err = os.Remove(path); err == nil {
err = os.Symlink(filepath.FromSlash(hdr.Linkname), filepath.FromSlash(path))
}
}
case tar.TypeChar:
if err = mknod(path, chrMode(0600), int(mkdev(devMajor, devMinor))); err != nil && os.IsExist(err) {
if req.PutOptions.NoOverwriteDirNonDir {
if st, err := os.Lstat(path); err == nil && st.IsDir() {
break
}
}
if err = os.Remove(path); err == nil {
err = mknod(path, chrMode(0600), int(mkdev(devMajor, devMinor)))
}
}
case tar.TypeBlock:
if err = mknod(path, blkMode(0600), int(mkdev(devMajor, devMinor))); err != nil && os.IsExist(err) {
if req.PutOptions.NoOverwriteDirNonDir {
if st, err := os.Lstat(path); err == nil && st.IsDir() {
break
}
}
if err = os.Remove(path); err == nil {
err = mknod(path, blkMode(0600), int(mkdev(devMajor, devMinor)))
}
}
case tar.TypeDir:
if err = os.Mkdir(path, 0700); err != nil && os.IsExist(err) {
err = nil
var st os.FileInfo
if st, err = os.Stat(path); err == nil && !st.IsDir() {
// it's not a directory, so remove it and mkdir
if err = os.Remove(path); err == nil {
err = os.Mkdir(path, 0700)
}
}
// either we removed it and retried, or it was a directory,
// in which case we want to just add the new stuff under it
}
// make a note of the directory's times. we
// might create items under it, which will
@ -1453,6 +1541,11 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM
})
case tar.TypeFifo:
if err = mkfifo(path, 0600); err != nil && os.IsExist(err) {
if req.PutOptions.NoOverwriteDirNonDir {
if st, err := os.Lstat(path); err == nil && st.IsDir() {
break
}
}
if err = os.Remove(path); err == nil {
err = mkfifo(path, 0600)
}

View File

@ -10,7 +10,7 @@ import (
"golang.org/x/sys/unix"
)
var canChroot = true
var canChroot = os.Getuid() == 0
func chroot(root string) (bool, error) {
if canChroot {

View File

@ -5,10 +5,10 @@ go 1.12
require (
github.com/containerd/containerd v1.4.1 // indirect
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784
github.com/containers/common v0.29.0
github.com/containers/common v0.31.0
github.com/containers/image/v5 v5.8.1
github.com/containers/ocicrypt v1.0.3
github.com/containers/storage v1.24.1
github.com/containers/storage v1.24.3
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/go-units v0.4.0
github.com/docker/libnetwork v0.8.0-dev.2.0.20190625141545-5a177b73e316
@ -21,7 +21,7 @@ require (
github.com/moby/sys/mount v0.1.1 // indirect
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 // indirect
github.com/onsi/ginkgo v1.14.2
github.com/onsi/gomega v1.10.3
github.com/onsi/gomega v1.10.4
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
github.com/opencontainers/runc v1.0.0-rc91

View File

@ -73,8 +73,8 @@ github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDG
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784 h1:rqUVLD8I859xRgUx/WMC3v7QAFqbLKZbs+0kqYboRJc=
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
github.com/containers/common v0.29.0 h1:hTMC+urdkk5bKfhL/OgCixIX5xjJgQ2l2jPG745ECFQ=
github.com/containers/common v0.29.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA=
github.com/containers/common v0.31.0 h1:SRnjfoqbjfaojpY9YJq9JBPEslwB5hoXJbaE+5zMFwM=
github.com/containers/common v0.31.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA=
github.com/containers/image/v5 v5.8.1 h1:aHW8a/Kd0dTJ7PTL/fc6y12sJqHxWgqilu+XyHfjD8Q=
github.com/containers/image/v5 v5.8.1/go.mod h1:blOEFd/iFdeyh891ByhCVUc+xAcaI3gBegXECwz9UbQ=
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
@ -84,6 +84,8 @@ github.com/containers/ocicrypt v1.0.3/go.mod h1:CUBa+8MRNL/VkpxYIpaMtgn1WgXGyvPQ
github.com/containers/storage v1.23.7/go.mod h1:cUT2zHjtx+WlVri30obWmM2gpqpi8jfPsmIzP1TVpEI=
github.com/containers/storage v1.24.1 h1:1+f8fy6ly35c8SLet5jzZ8t0WJJs5+xSpfMAYw0R3kc=
github.com/containers/storage v1.24.1/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
github.com/containers/storage v1.24.3 h1:8UB4S62l4hrU6Yw3dbsLCJtLg7Ofo39IN2HdckBIX4E=
github.com/containers/storage v1.24.3/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -303,6 +305,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -482,6 +486,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View File

@ -69,15 +69,35 @@ sudo apt-get update
sudo apt-get -y install buildah
```
The [Kubic project](https://build.opensuse.org/project/show/devel:kubic:libcontainers:stable)
provides packages for Debian 10. The Kubic packages for Debian Testing/Bullseye and Debian Unstable/Sid
have been discontinued to avoid
[conflicts](https://github.com/containers/buildah/issues/2797) with the official packages.
If you would prefer newer (though not as well-tested) packages,
the [Kubic project](https://build.opensuse.org/package/show/devel:kubic:libcontainers:stable/buildah)
provides packages for Debian 10 and newer. The packages in Kubic project repos are more frequently
updated than the one in Debian's official repositories, due to how Debian works.
The build sources for the Kubic packages can be found [here](https://gitlab.com/rhcontainerbot/buildah/-/tree/debian/debian).
CAUTION: On Debian 11 and newer, including Testing and Sid/Unstable, we highly recommend you use Buildah, Podman and Skopeo ONLY from EITHER the Kubic repo
OR the official Debian repos. Mixing and matching may lead to unpredictable situations including installation conflicts.
```bash
# Debian 10
echo 'deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_10/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_10/Release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get -y install buildah
# Debian Testing
echo 'deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_Testing/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_Testing/Release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get -y install buildah
# Debian Sid/Unstable
echo 'deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_Unstable/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_Unstable/Release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get -y install buildah
```
Caution: If you upgrade from Debian 10 to Testing/Bullseye or
Unstable/Sid you would likely end up downgrading Buildah because the version in
OBS is more frequently updated than the one in Debian's official repositories,
due to how Debian works.
### [Fedora](https://www.fedoraproject.org)
@ -125,7 +145,8 @@ sudo yum -y install buildah
#### [Raspberry Pi OS armhf (ex Raspbian)](https://www.raspberrypi.org/downloads/raspberry-pi-os/)
The Kubic project provides packages for Raspbian 10.
The [Kubic project](https://build.opensuse.org/package/show/devel:kubic:libcontainers:stable/buildah) provides
packages for Raspbian 10.
```bash
# Raspbian 10
@ -135,6 +156,8 @@ sudo apt-get update -qq
sudo apt-get -qq -y install buildah
```
The build sources for the Kubic packages can be found [here](https://gitlab.com/rhcontainerbot/buildah/-/tree/debian/debian).
#### [Raspberry Pi OS arm64 (beta)](https://downloads.raspberrypi.org/raspios_arm64/images/)
Raspberry Pi OS use the standard Debian's repositories,
@ -160,7 +183,16 @@ sudo apt-get -y update
sudo apt-get -y install buildah
```
The [Kubic project](https://build.opensuse.org/package/show/devel:kubic:libcontainers:stable/buildah) provides packages for some older but supported Ubuntu versions (it should also work with direct derivatives like Pop!\_OS).
If you would prefer newer (though not as well-tested) packages,
the [Kubic project](https://build.opensuse.org/package/show/devel:kubic:libcontainers:stable/buildah)
provides packages for active Ubuntu releases 18.04 and newer (it should also work with direct derivatives like Pop!\_OS).
The packages in Kubic project repos are more frequently updated than the one in Ubuntu's official repositories, due to how Debian/Ubuntu works.
Checkout the Kubic project page for a list of supported Ubuntu version and architecture combinations.
The build sources for the Kubic packages can be found [here](https://gitlab.com/rhcontainerbot/buildah/-/tree/debian/debian).
CAUTION: On Ubuntu 20.10 and newer, we highly recommend you use Buildah, Podman and Skopeo ONLY from EITHER the Kubic repo
OR the official Ubuntu repos. Mixing and matching may lead to unpredictable situations including installation conflicts.
```bash
. /etc/os-release
@ -473,6 +505,13 @@ cat /etc/containers/policy.json
}
```
## Debug with Delve and the like
To make a source debug build without optimizations use `DEBUG=1`, like:
```
make all DEBUG=1
```
## Vendoring
Buildah uses Go Modules for vendoring purposes. If you need to update or add a vendored package into Buildah, please follow this procedure:

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"math/rand"
"os"
"strings"
"github.com/containers/buildah/util"
@ -127,27 +126,10 @@ func resolveLocalImage(systemContext *types.SystemContext, store storage.Store,
return nil, "", nil, nil
}
// getShortNameMode looks up the `CONTAINERS_SHORT_NAME_ALIASING` environment
// variable. If it's "on", return `nil` to use the defaults from
// containers/image and the registries.conf files on the system. If it's
// "off", empty or unset, return types.ShortNameModeDisabled to turn off
// short-name aliasing by default.
//
// TODO: remove this function once we want to default to short-name aliasing.
func getShortNameMode() *types.ShortNameMode {
env := os.Getenv("CONTAINERS_SHORT_NAME_ALIASING")
if strings.ToLower(env) == "on" {
return nil // default to whatever registries.conf and c/image decide
}
mode := types.ShortNameModeDisabled
return &mode
}
func resolveImage(ctx context.Context, systemContext *types.SystemContext, store storage.Store, options BuilderOptions) (types.ImageReference, string, *storage.Image, error) {
if systemContext == nil {
systemContext = &types.SystemContext{}
}
systemContext.ShortNameMode = getShortNameMode()
fromImage := options.FromImage
// If the image name includes a transport we can use it as it. Special

View File

@ -17,6 +17,7 @@ import (
"github.com/containers/common/pkg/auth"
commonComp "github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/config"
"github.com/containers/storage/pkg/unshare"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/spf13/pflag"
@ -366,6 +367,9 @@ func DefaultIsolation() string {
if isolation != "" {
return isolation
}
if unshare.IsRootless() {
return "rootless"
}
return buildah.OCI
}

View File

@ -486,7 +486,7 @@ func ValidateVolumeCtrDir(ctrDir string) error {
// ValidateVolumeOpts validates a volume's options
func ValidateVolumeOpts(options []string) ([]string, error) {
var foundRootPropagation, foundRWRO, foundLabelChange, bindType, foundExec, foundDev, foundSuid int
var foundRootPropagation, foundRWRO, foundLabelChange, bindType, foundExec, foundDev, foundSuid, foundChown int
finalOpts := make([]string, 0, len(options))
for _, opt := range options {
switch opt {
@ -515,6 +515,11 @@ func ValidateVolumeOpts(options []string) ([]string, error) {
if foundLabelChange > 1 {
return nil, errors.Errorf("invalid options %q, can only specify 1 'z', 'Z', or 'O' option", strings.Join(options, ", "))
}
case "U":
foundChown++
if foundChown > 1 {
return nil, errors.Errorf("invalid options %q, can only specify 1 'U' option", strings.Join(options, ", "))
}
case "private", "rprivate", "shared", "rshared", "slave", "rslave", "unbindable", "runbindable":
foundRootPropagation++
if foundRootPropagation > 1 {
@ -878,20 +883,12 @@ func NamespaceOptions(c *cobra.Command) (namespaceOptions buildah.NamespaceOptio
logrus.Debugf("setting network to disabled")
break
}
if !filepath.IsAbs(how) {
options.AddOrReplace(buildah.NamespaceOption{
Name: what,
Path: how,
})
policy = buildah.NetworkEnabled
logrus.Debugf("setting network configuration to %q", how)
break
}
}
how = strings.TrimPrefix(how, "ns:")
if _, err := os.Stat(how); err != nil {
return nil, buildah.NetworkDefault, errors.Wrapf(err, "error checking for %s namespace at %q", what, how)
return nil, buildah.NetworkDefault, errors.Wrapf(err, "error checking for %s namespace", what)
}
policy = buildah.NetworkEnabled
logrus.Debugf("setting %q namespace to %q", what, how)
options.AddOrReplace(buildah.NamespaceOption{
Name: what,

View File

@ -506,8 +506,14 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
return err
}
// Get host UID and GID of the container process.
processUID, processGID, err := util.GetHostIDs(spec.Linux.UIDMappings, spec.Linux.GIDMappings, spec.Process.User.UID, spec.Process.User.GID)
if err != nil {
return err
}
// Get the list of explicitly-specified volume mounts.
volumes, err := b.runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts, int(rootUID), int(rootGID))
volumes, err := b.runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts, int(rootUID), int(rootGID), int(processUID), int(processGID))
if err != nil {
return err
}
@ -1687,7 +1693,7 @@ func (b *Builder) cleanupTempVolumes() {
}
}
func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, rootUID, rootGID int) (mounts []specs.Mount, Err error) {
func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, rootUID, rootGID, processUID, processGID int) (mounts []specs.Mount, Err error) {
// Make sure the overlay directory is clean before running
containerDir, err := b.store.ContainerDirectory(b.ContainerID)
@ -1699,7 +1705,7 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
}
parseMount := func(mountType, host, container string, options []string) (specs.Mount, error) {
var foundrw, foundro, foundz, foundZ, foundO bool
var foundrw, foundro, foundz, foundZ, foundO, foundU bool
var rootProp string
for _, opt := range options {
switch opt {
@ -1713,6 +1719,8 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
foundZ = true
case "O":
foundO = true
case "U":
foundU = true
case "private", "rprivate", "slave", "rslave", "shared", "rshared":
rootProp = opt
}
@ -1730,6 +1738,11 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
return specs.Mount{}, err
}
}
if foundU {
if err := chownSourceVolume(host, processUID, processGID); err != nil {
return specs.Mount{}, err
}
}
if foundO {
containerDir, err := b.store.ContainerDirectory(b.ContainerID)
if err != nil {
@ -1746,6 +1759,14 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
b.TempVolumes[contentDir] = true
}
// If chown true, add correct ownership to the overlay temp directories.
if foundU {
if err := chownSourceVolume(contentDir, processUID, processGID); err != nil {
return specs.Mount{}, err
}
}
return overlayMount, err
}
if rootProp == "" {
@ -1789,6 +1810,39 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
return mounts, nil
}
// chownSourceVolume changes the ownership of a volume source directory or file within the host.
func chownSourceVolume(path string, UID, GID int) error {
fi, err := os.Lstat(path)
if err != nil {
// Skip if path does not exist
if os.IsNotExist(err) {
logrus.Debugf("error returning file info of %q: %v", path, err)
return nil
}
return err
}
currentUID := int(fi.Sys().(*syscall.Stat_t).Uid)
currentGID := int(fi.Sys().(*syscall.Stat_t).Gid)
if UID != currentUID || GID != currentGID {
err := filepath.Walk(path, func(filePath string, f os.FileInfo, err error) error {
return os.Lchown(filePath, UID, GID)
})
if err != nil {
// Skip if path does not exist
if os.IsNotExist(err) {
logrus.Debugf("error changing the uid and gid of %q: %v", path, err)
return nil
}
return err
}
}
return nil
}
func setupMaskedPaths(g *generate.Generator) {
for _, mp := range []string{
"/proc/acpi",

View File

@ -154,5 +154,5 @@ Choose one of the following:
* Complete the build operation as a privileged user.
* Install and configure fuse-overlayfs.
* Install the fuse-overlayfs package for your Linux Distribution.
* Add `mount_program = "/usr/bin/fuse-overlayfs` under `[storage.options]` in your `~/.config/containers/storage.conf` file.
* Add `mount_program = "/usr/bin/fuse-overlayfs"` under `[storage.options]` in your `~/.config/containers/storage.conf` file.
---

2
vendor/modules.txt vendored
View File

@ -67,7 +67,7 @@ github.com/containernetworking/plugins/pkg/utils/hwaddr
github.com/containernetworking/plugins/pkg/utils/sysctl
github.com/containernetworking/plugins/plugins/ipam/host-local/backend
github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator
# github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c
# github.com/containers/buildah v1.18.1-0.20201217112226-67470615779c
github.com/containers/buildah
github.com/containers/buildah/bind
github.com/containers/buildah/chroot