diff options
32 files changed, 1247 insertions, 1160 deletions
diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index 9b0a01a2f..e0161824f 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -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 } diff --git a/docs/source/markdown/podman-cp.1.md b/docs/source/markdown/podman-cp.1.md index 241a74c42..56511c244 100644 --- a/docs/source/markdown/podman-cp.1.md +++ b/docs/source/markdown/podman-cp.1.md @@ -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 @@ -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 @@ -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= diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go index d8197415c..083c72ce8 100644 --- a/pkg/api/handlers/compat/containers_archive.go +++ b/pkg/api/handlers/compat/containers_archive.go @@ -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) - - 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")) + containerEngine := abi.ContainerEngine{Libpod: runtime} + statReport, err := containerEngine.ContainerStat(r.Context(), containerName, query.Path) + + // 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) - - 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)) - return - } + containerName := utils.GetName(r) + containerEngine := abi.ContainerEngine{Libpod: runtime} - destination, err := copy.CopyItemForContainer(ctr, query.Path, true, false) - defer destination.CleanUp() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + 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 - } - - source, err := copy.CopyItemForReader(r.Body) - defer source.CleanUp() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + } else 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()) } } diff --git a/pkg/api/server/register_archive.go b/pkg/api/server/register_archive.go index b2d2543c4..9ff0ad0eb 100644 --- a/pkg/api/server/register_archive.go +++ b/pkg/api/server/register_archive.go @@ -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 } diff --git a/pkg/bindings/containers/archive.go b/pkg/bindings/containers/archive.go new file mode 100644 index 000000000..d1bbc0b95 --- /dev/null +++ b/pkg/bindings/containers/archive.go @@ -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 +} diff --git a/pkg/copy/copy.go b/pkg/copy/copy.go deleted file mode 100644 index 13893deb2..000000000 --- a/pkg/copy/copy.go +++ /dev/null @@ -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 -} diff --git a/pkg/copy/fileinfo.go b/pkg/copy/fileinfo.go index 08b4eb377..50b0ca9a1 100644 --- a/pkg/copy/fileinfo.go +++ b/pkg/copy/fileinfo.go @@ -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 +} diff --git a/pkg/copy/item.go b/pkg/copy/item.go deleted file mode 100644 index df8bf30b9..000000000 --- a/pkg/copy/item.go +++ /dev/null @@ -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 -} diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 01086a2b3..4442c0030 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -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 diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index ac9073402..80127ea45 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -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) diff --git a/pkg/domain/infra/abi/archive.go b/pkg/domain/infra/abi/archive.go new file mode 100644 index 000000000..809813756 --- /dev/null +++ b/pkg/domain/infra/abi/archive.go @@ -0,0 +1,172 @@ +package abi + +import ( + "context" + "io" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/buildah/pkg/chrootuser" + "github.com/containers/buildah/util" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// NOTE: Only the parent directory of the container path must exist. The path +// itself may be created while copying. +func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, containerPath string, reader io.Reader) (entities.ContainerCopyFunc, error) { + container, err := ic.Libpod.LookupContainer(nameOrID) + if err != nil { + return nil, err + } + + unmount := func() { + if err := container.Unmount(false); err != nil { + logrus.Errorf("Error unmounting container: %v", err) + } + } + + _, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath) + if err != nil { + unmount() + return nil, err + } + + decompressed, err := archive.DecompressStream(reader) + if err != nil { + unmount() + return nil, err + } + + idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot) + if err != nil { + unmount() + return nil, err + } + + logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID()) + + return func() error { + defer unmount() + defer decompressed.Close() + putOptions := buildahCopiah.PutOptions{ + UIDMap: idMappings.UIDMap, + GIDMap: idMappings.GIDMap, + ChownDirs: idPair, + ChownFiles: idPair, + } + return buildahCopiah.Put(resolvedRoot, resolvedContainerPath, putOptions, decompressed) + }, nil +} + +func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) { + container, err := ic.Libpod.LookupContainer(nameOrID) + if err != nil { + return nil, err + } + + unmount := func() { + if err := container.Unmount(false); err != nil { + logrus.Errorf("Error unmounting container: %v", err) + } + } + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if containerPath == "/" { + containerPath = "/." + } + + _, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath) + if err != nil { + unmount() + return nil, err + } + + idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot) + if err != nil { + unmount() + return nil, err + } + + logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID()) + + return func() error { + defer container.Unmount(false) + getOptions := buildahCopiah.GetOptions{ + // Unless the specified path ends with ".", we want to copy the base directory. + KeepDirectoryNames: !strings.HasSuffix(resolvedContainerPath, "."), + UIDMap: idMappings.UIDMap, + GIDMap: idMappings.GIDMap, + ChownDirs: idPair, + ChownFiles: idPair, + } + return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedContainerPath}, writer) + }, nil +} + +// getIDMappingsAndPair returns the ID mappings for the container and the host +// ID pair. +func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) { + user, err := getContainerUser(container, containerMount) + if err != nil { + return nil, nil, err + } + + idMappingOpts, err := container.IDMappings() + if err != nil { + return nil, nil, err + } + + hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID) + if err != nil { + return nil, nil, err + } + + idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} + return &idMappingOpts, &idPair, nil +} + +// getContainerUser returns the specs.User of the container. +func getContainerUser(container *libpod.Container, mountPoint string) (specs.User, error) { + userspec := container.Config().User + + uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) + u := specs.User{ + UID: uid, + GID: gid, + Username: userspec, + } + + if !strings.Contains(userspec, ":") { + groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) + if err2 != nil { + if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil { + err = err2 + } + } else { + u.AdditionalGids = groups + } + } + + return u, err +} + +// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec. +func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) { + for _, idmap := range idMaps { + tempIDMap := specs.LinuxIDMapping{ + ContainerID: uint32(idmap.ContainerID), + HostID: uint32(idmap.HostID), + Size: uint32(idmap.Size), + } + convertedIDMap = append(convertedIDMap, tempIDMap) + } + return convertedIDMap +} diff --git a/pkg/domain/infra/abi/containers_stat.go b/pkg/domain/infra/abi/containers_stat.go new file mode 100644 index 000000000..c9610d1b9 --- /dev/null +++ b/pkg/domain/infra/abi/containers_stat.go @@ -0,0 +1,251 @@ +package abi + +import ( + "context" + "os" + "path/filepath" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/copy" + "github.com/containers/podman/v2/pkg/domain/entities" + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +func (ic *ContainerEngine) containerStat(container *libpod.Container, containerPath string) (*entities.ContainerStatReport, string, string, error) { + containerMountPoint, err := container.Mount() + if err != nil { + return nil, "", "", err + } + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if containerPath == "/" { + containerPath += "/." + } + + // Now resolve the container's path. It may hit a volume, it may hit a + // bind mount, it may be relative. + resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath) + if err != nil { + return nil, "", "", err + } + + statInfo, statInfoErr := secureStat(resolvedRoot, resolvedContainerPath) + if statInfoErr != nil { + // Not all errors from secureStat map to ErrNotExist, so we + // have to look into the error string. Turning it into an + // ENOENT let's the API handlers return the correct status code + // which is crucuial for the remote client. + if os.IsNotExist(err) || strings.Contains(statInfoErr.Error(), "o such file or directory") { + statInfoErr = copy.ENOENT + } + // If statInfo is nil, there's nothing we can do anymore. A + // non-nil statInfo may indicate a symlink where we must have + // a closer look. + if statInfo == nil { + return nil, "", "", statInfoErr + } + } + + // Now make sure that the info's LinkTarget is relative to the + // container's mount. + var absContainerPath string + + if statInfo.IsSymlink { + // Evaluated symlinks are always relative to the container's mount point. + absContainerPath = statInfo.ImmediateTarget + } else if strings.HasPrefix(resolvedContainerPath, containerMountPoint) { + // If the path is on the container's mount point, strip it off. + absContainerPath = strings.TrimPrefix(resolvedContainerPath, containerMountPoint) + absContainerPath = filepath.Join("/", absContainerPath) + } else { + // No symlink and not on the container's mount point, so let's + // move it back to the original input. It must have evaluated + // to a volume or bind mount but we cannot return host paths. + absContainerPath = containerPath + } + + // Now we need to make sure to preseve the base path as specified by + // the user. The `filepath` packages likes to remove trailing slashes + // and dots that are crucial to the copy logic. + absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath) + resolvedContainerPath = copy.PreserveBasePath(containerPath, resolvedContainerPath) + + info := copy.FileInfo{ + IsDir: statInfo.IsDir, + Name: filepath.Base(absContainerPath), + Size: statInfo.Size, + Mode: statInfo.Mode, + ModTime: statInfo.ModTime, + LinkTarget: absContainerPath, + } + + return &entities.ContainerStatReport{FileInfo: info}, resolvedRoot, resolvedContainerPath, statInfoErr +} + +func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, containerPath string) (*entities.ContainerStatReport, error) { + container, err := ic.Libpod.LookupContainer(nameOrID) + if err != nil { + return nil, err + } + + defer func() { + if err := container.Unmount(false); err != nil { + logrus.Errorf("Error unmounting container: %v", err) + } + }() + + statReport, _, _, err := ic.containerStat(container, containerPath) + return statReport, err +} + +// resolveContainerPaths resolves the container's mount point and the container +// path as specified by the user. Both may resolve to paths outside of the +// container's mount point when the container path hits a volume or bind mount. +// +// NOTE: We must take volumes and bind mounts into account as, regrettably, we +// can copy to/from stopped containers. In that case, the volumes and bind +// mounts are not present. For running containers, the runtime (e.g., runc or +// crun) takes care of these mounts. For stopped ones, we need to do quite +// some dance, as done below. +func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) { + // Let's first make sure we have a path relative to the mount point. + pathRelativeToContainerMountPoint := containerPath + if !filepath.IsAbs(containerPath) { + // If the containerPath is not absolute, it's relative to the + // container's working dir. To be extra careful, let's first + // join the working dir with "/", and the add the containerPath + // to it. + pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath) + } + resolvedPathOnTheContainerMountPoint := filepath.Join(mountPoint, pathRelativeToContainerMountPoint) + pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint) + pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint) + + // Now we have an "absolute container Path" but not yet resolved on the + // host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to + // check if "/foo/bar/file.txt" is on a volume or bind mount. To do + // that, we need to walk *down* the paths to the root. Assuming + // volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar", + // we must select "/foo/bar". Once selected, we need to rebase the + // remainder (i.e, "/file.txt") on the volume's mount point on the + // host. Same applies to bind mounts. + + searchPath := pathRelativeToContainerMountPoint + for { + volume, err := findVolume(container, searchPath) + if err != nil { + return "", "", err + } + if volume != nil { + logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath) + // We found a matching volume for searchPath. We now + // need to first find the relative path of our input + // path to the searchPath, and then join it with the + // volume's mount point. + pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath) + absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(volume.MountPoint(), pathRelativeToVolume) + if err != nil { + return "", "", err + } + return volume.MountPoint(), absolutePathOnTheVolumeMount, nil + } + + if mount := findBindMount(container, searchPath); mount != nil { + logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath) + // We found a matching bind mount for searchPath. We + // now need to first find the relative path of our + // input path to the searchPath, and then join it with + // the source of the bind mount. + pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath) + absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount) + if err != nil { + return "", "", err + } + return mount.Source, absolutePathOnTheBindMount, nil + + } + + if searchPath == "/" { + // Cannot go beyond "/", so we're done. + break + } + + // Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar"). + searchPath = filepath.Dir(searchPath) + } + + // No volume, no bind mount but just a normal path on the container. + return mountPoint, resolvedPathOnTheContainerMountPoint, nil +} + +// findVolume checks if the specified container path matches a volume inside +// the container. It returns a matching volume or nil. +func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) { + runtime := c.Runtime() + cleanedContainerPath := filepath.Clean(containerPath) + for _, vol := range c.Config().NamedVolumes { + if cleanedContainerPath == filepath.Clean(vol.Dest) { + return runtime.GetVolume(vol.Name) + } + } + return nil, nil +} + +// findBindMount checks if the specified container path matches a bind mount +// inside the container. It returns a matching mount or nil. +func findBindMount(c *libpod.Container, containerPath string) *specs.Mount { + cleanedPath := filepath.Clean(containerPath) + for _, m := range c.Config().Spec.Mounts { + if m.Type != "bind" { + continue + } + if cleanedPath == filepath.Clean(m.Destination) { + mount := m + return &mount + } + } + return nil +} + +// secureStat extracts file info for path in a chroot'ed environment in root. +func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) { + var glob string + var err error + + // If root and path are equal, then dir must be empty and the glob must + // be ".". + if filepath.Clean(root) == filepath.Clean(path) { + glob = "." + } else { + glob, err = filepath.Rel(root, path) + if err != nil { + return nil, err + } + } + + globStats, err := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob}) + if err != nil { + return nil, err + } + + if len(globStats) != 1 { + return nil, errors.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats)) + } + + stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay + if !exists { + return nil, copy.ENOENT + } + + var statErr error + if stat.Error != "" { + statErr = errors.New(stat.Error) + } + return stat, statErr +} diff --git a/pkg/domain/infra/abi/cp.go b/pkg/domain/infra/abi/cp.go deleted file mode 100644 index 362053cce..000000000 --- a/pkg/domain/infra/abi/cp.go +++ /dev/null @@ -1,70 +0,0 @@ -package abi - -import ( - "context" - - "github.com/containers/podman/v2/libpod" - "github.com/containers/podman/v2/pkg/copy" - "github.com/containers/podman/v2/pkg/domain/entities" -) - -func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error { - // Parse user input. - sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(source, dest) - if err != nil { - return err - } - - // Look up containers. - var sourceContainer, destContainer *libpod.Container - if len(sourceContainerStr) > 0 { - sourceContainer, err = ic.Libpod.LookupContainer(sourceContainerStr) - if err != nil { - return err - } - } - if len(destContainerStr) > 0 { - destContainer, err = ic.Libpod.LookupContainer(destContainerStr) - if err != nil { - return err - } - } - - var sourceItem, destinationItem copy.CopyItem - - // Source ... container OR host. - if sourceContainer != nil { - sourceItem, err = copy.CopyItemForContainer(sourceContainer, sourcePath, options.Pause, true) - defer sourceItem.CleanUp() - if err != nil { - return err - } - } else { - sourceItem, err = copy.CopyItemForHost(sourcePath, true) - if err != nil { - return err - } - } - - // Destination ... container OR host. - if destContainer != nil { - destinationItem, err = copy.CopyItemForContainer(destContainer, destPath, options.Pause, false) - defer destinationItem.CleanUp() - if err != nil { - return err - } - } else { - destinationItem, err = copy.CopyItemForHost(destPath, false) - defer destinationItem.CleanUp() - if err != nil { - return err - } - } - - // Copy from the host to the container. - copier, err := copy.GetCopier(&sourceItem, &destinationItem, options.Extract) - if err != nil { - return err - } - return copier.Copy() -} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index ad7688f62..855339fef 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -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 diff --git a/pkg/errorhandling/errorhandling.go b/pkg/errorhandling/errorhandling.go index ca6b60bc5..21df261fb 100644 --- a/pkg/errorhandling/errorhandling.go +++ b/pkg/errorhandling/errorhandling.go @@ -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 diff --git a/test/e2e/cp_test.go b/test/e2e/cp_test.go index 33908b60e..6fe26c444 100644 --- a/test/e2e/cp_test.go +++ b/test/e2e/cp_test.go @@ -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) diff --git a/test/system/065-cp.bats b/test/system/065-cp.bats index 43bdf217d..73b07ea45 100644 --- a/test/system/065-cp.bats +++ b/test/system/065-cp.bats @@ -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 diff --git a/vendor/github.com/containers/buildah/.cirrus.yml b/vendor/github.com/containers/buildah/.cirrus.yml index 65ce26080..589de9c61 100644 --- a/vendor/github.com/containers/buildah/.cirrus.yml +++ b/vendor/github.com/containers/buildah/.cirrus.yml @@ -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}' diff --git a/vendor/github.com/containers/buildah/Makefile b/vendor/github.com/containers/buildah/Makefile index e70dd161d..45f8a8ec8 100644 --- a/vendor/github.com/containers/buildah/Makefile +++ b/vendor/github.com/containers/buildah/Makefile @@ -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 diff --git a/vendor/github.com/containers/buildah/copier/copier.go b/vendor/github.com/containers/buildah/copier/copier.go index 84b636202..ef0e4778d 100644 --- a/vendor/github.com/containers/buildah/copier/copier.go +++ b/vendor/github.com/containers/buildah/copier/copier.go @@ -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) } diff --git a/vendor/github.com/containers/buildah/copier/syscall_unix.go b/vendor/github.com/containers/buildah/copier/syscall_unix.go index 55f2f368a..2c2806d0a 100644 --- a/vendor/github.com/containers/buildah/copier/syscall_unix.go +++ b/vendor/github.com/containers/buildah/copier/syscall_unix.go @@ -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 { diff --git a/vendor/github.com/containers/buildah/go.mod b/vendor/github.com/containers/buildah/go.mod index 0d795f6b6..ea9a956be 100644 --- a/vendor/github.com/containers/buildah/go.mod +++ b/vendor/github.com/containers/buildah/go.mod @@ -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 diff --git a/vendor/github.com/containers/buildah/go.sum b/vendor/github.com/containers/buildah/go.sum index e3413bc68..c2082c5ef 100644 --- a/vendor/github.com/containers/buildah/go.sum +++ b/vendor/github.com/containers/buildah/go.sum @@ -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= diff --git a/vendor/github.com/containers/buildah/install.md b/vendor/github.com/containers/buildah/install.md index 119315d1f..90e844c3e 100644 --- a/vendor/github.com/containers/buildah/install.md +++ b/vendor/github.com/containers/buildah/install.md @@ -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: diff --git a/vendor/github.com/containers/buildah/new.go b/vendor/github.com/containers/buildah/new.go index c1abb1cdb..aab17fea2 100644 --- a/vendor/github.com/containers/buildah/new.go +++ b/vendor/github.com/containers/buildah/new.go @@ -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 diff --git a/vendor/github.com/containers/buildah/pkg/cli/common.go b/vendor/github.com/containers/buildah/pkg/cli/common.go index 123548d97..1e2db58c4 100644 --- a/vendor/github.com/containers/buildah/pkg/cli/common.go +++ b/vendor/github.com/containers/buildah/pkg/cli/common.go @@ -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 } diff --git a/vendor/github.com/containers/buildah/pkg/parse/parse.go b/vendor/github.com/containers/buildah/pkg/parse/parse.go index fb348b252..f256e6c2a 100644 --- a/vendor/github.com/containers/buildah/pkg/parse/parse.go +++ b/vendor/github.com/containers/buildah/pkg/parse/parse.go @@ -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, diff --git a/vendor/github.com/containers/buildah/run_linux.go b/vendor/github.com/containers/buildah/run_linux.go index d20d39423..dc2f5c5ad 100644 --- a/vendor/github.com/containers/buildah/run_linux.go +++ b/vendor/github.com/containers/buildah/run_linux.go @@ -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", diff --git a/vendor/github.com/containers/buildah/troubleshooting.md b/vendor/github.com/containers/buildah/troubleshooting.md index afd9c640a..02631ae13 100644 --- a/vendor/github.com/containers/buildah/troubleshooting.md +++ b/vendor/github.com/containers/buildah/troubleshooting.md @@ -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. --- diff --git a/vendor/modules.txt b/vendor/modules.txt index 26b782b85..4ef7fccd1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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 |