mirror of
https://github.com/containers/podman.git
synced 2025-07-15 03:02:52 +08:00
remote copy
Implement `podman-remote cp` and break out the logic from the previously added `pkg/copy` into it's basic building blocks and move them up into the `ContainerEngine` interface and `cmd/podman`. The `--pause` and `--extract` flags are now deprecated and turned into nops. Note that this commit is vendoring a non-release version of Buildah to pull in updates to the copier package. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
@ -1,19 +1,34 @@
|
|||||||
package containers
|
package containers
|
||||||
|
|
||||||
import (
|
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/common"
|
||||||
"github.com/containers/podman/v2/cmd/podman/registry"
|
"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/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"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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{
|
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",
|
Short: "Copy files/folders between a container and the local filesystem",
|
||||||
Long: cpDescription,
|
Long: cpDescription,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
@ -39,19 +54,21 @@ var (
|
|||||||
|
|
||||||
func cpFlags(cmd *cobra.Command) {
|
func cpFlags(cmd *cobra.Command) {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVar(&cpOpts.Extract, "extract", false, "Extract the tar file into the destination directory.")
|
flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...")
|
||||||
flags.BoolVar(&cpOpts.Pause, "pause", true, "Pause the container while copying")
|
flags.BoolVar(&cpOpts.Pause, "pause", true, "Deorecated")
|
||||||
|
_ = flags.MarkHidden("extract")
|
||||||
|
_ = flags.MarkHidden("pause")
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registry.Commands = append(registry.Commands, registry.CliCommand{
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
Mode: []entities.EngineMode{entities.ABIMode},
|
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
|
||||||
Command: cpCommand,
|
Command: cpCommand,
|
||||||
})
|
})
|
||||||
cpFlags(cpCommand)
|
cpFlags(cpCommand)
|
||||||
|
|
||||||
registry.Commands = append(registry.Commands, registry.CliCommand{
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
Mode: []entities.EngineMode{entities.ABIMode},
|
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
|
||||||
Command: containerCpCommand,
|
Command: containerCpCommand,
|
||||||
Parent: containerCmd,
|
Parent: containerCmd,
|
||||||
})
|
})
|
||||||
@ -59,5 +76,290 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cp(cmd *cobra.Command, args []string) error {
|
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
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
podman\-cp - Copy files/folders between a container and the local filesystem
|
podman\-cp - Copy files/folders between a container and the local filesystem
|
||||||
|
|
||||||
## SYNOPSIS
|
## 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
|
## 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.
|
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
|
## 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
|
## ALTERNATIVES
|
||||||
|
|
||||||
Podman has much stronger capabilities than just `podman cp` to achieve copy files between host and container.
|
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 containerID:/home/myuser/. /home/myuser/
|
||||||
|
|
||||||
podman cp --extract /home/myuser/myfiles.tar.gz containerID:/myfiles
|
|
||||||
|
|
||||||
podman cp - containerID:/myfiles.tar.gz < myfiles.tar.gz
|
podman cp - containerID:/myfiles.tar.gz < myfiles.tar.gz
|
||||||
|
|
||||||
## SEE ALSO
|
## SEE ALSO
|
||||||
|
2
go.mod
2
go.mod
@ -10,7 +10,7 @@ require (
|
|||||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
|
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
|
||||||
github.com/containernetworking/cni v0.8.0
|
github.com/containernetworking/cni v0.8.0
|
||||||
github.com/containernetworking/plugins v0.9.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/common v0.31.1
|
||||||
github.com/containers/conmon v2.0.20+incompatible
|
github.com/containers/conmon v2.0.20+incompatible
|
||||||
github.com/containers/image/v5 v5.9.0
|
github.com/containers/image/v5 v5.9.0
|
||||||
|
12
go.sum
12
go.sum
@ -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.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 h1:c+1gegKhR7+d0Caum9pEHugZlyhXPOG6v3V6xJgIGCI=
|
||||||
github.com/containernetworking/plugins v0.9.0/go.mod h1:dbWv4dI0QrBGuVgj+TuVQ6wJRZVOhrCQj91YyC92sxg=
|
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.20201217112226-67470615779c h1:DnJiPjBKeoZbzjkUA6YMf/r5ShYpNacK+EcQ/ui1Mxo=
|
||||||
github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c/go.mod h1:B+0OkXUogxdwsEy4ax3a5/vDtJjL6vCisiV6frQZJ4A=
|
github.com/containers/buildah v1.18.1-0.20201217112226-67470615779c/go.mod h1:hvIoL3urgYPL0zX8XlK05aWP6qfUnBNqTrsedsYw6OY=
|
||||||
github.com/containers/common v0.29.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA=
|
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 h1:oBINnZpYZ2u90HPMnVCXOhm/TsTaTB7wU/56l05hq44=
|
||||||
github.com/containers/common v0.31.1/go.mod h1:Fehe82hQfJQvDspnRrV9rcdAWG3IalNHEt0F6QWNBHQ=
|
github.com/containers/common v0.31.1/go.mod h1:Fehe82hQfJQvDspnRrV9rcdAWG3IalNHEt0F6QWNBHQ=
|
||||||
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
|
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 h1:MQNb7FLbXqBdqz6u4lI2QWizVz4RSTzs1+Nk9XT1iVA=
|
||||||
github.com/containers/psgo v1.5.1/go.mod h1:2ubh0SsreMZjSXW1Hif58JrEcFudQyIy9EzPUWfawVU=
|
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.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.1/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
|
||||||
github.com/containers/storage v1.24.3 h1:8UB4S62l4hrU6Yw3dbsLCJtLg7Ofo39IN2HdckBIX4E=
|
github.com/containers/storage v1.24.3 h1:8UB4S62l4hrU6Yw3dbsLCJtLg7Ofo39IN2HdckBIX4E=
|
||||||
github.com/containers/storage v1.24.3/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
|
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 h1:FYgwVsKRI/H9hU32MJ/4MLOzXWodKK5zsQavY8NPMkU=
|
||||||
github.com/opencontainers/runtime-tools v0.9.0/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
|
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.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.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
|
||||||
github.com/opencontainers/selinux v1.8.0 h1:+77ba4ar4jsCbL1GLbFL8fFM57w6suPfSS9PDLDY7KM=
|
github.com/opencontainers/selinux v1.8.0 h1:+77ba4ar4jsCbL1GLbFL8fFM57w6suPfSS9PDLDY7KM=
|
||||||
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
|
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 h1:vgrEJjUzHaSZKDRRxul5Oh4C72Yy/5VEMb0em+9M0mQ=
|
||||||
github.com/vbauerster/mpb/v5 v5.3.0/go.mod h1:4yTkvAb8Cm4eylAp6t0JRq6pXDkFJ4krUlDqWYkakAs=
|
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 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.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 h1:cPXZWzzG0NllBLdjWoD1nDfaqu98YMv+OneaKc8sPOA=
|
||||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
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-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-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 h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
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-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 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
|
||||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
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-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-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-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-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 h1:O5hKNaGxIT4A8OTMnuh6UpmBdI3SAPxlZ3g0olDrJVM=
|
||||||
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -3,11 +3,13 @@ package compat
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/containers/podman/v2/libpod"
|
"github.com/containers/podman/v2/libpod"
|
||||||
"github.com/containers/podman/v2/libpod/define"
|
"github.com/containers/podman/v2/libpod/define"
|
||||||
"github.com/containers/podman/v2/pkg/api/handlers/utils"
|
"github.com/containers/podman/v2/pkg/api/handlers/utils"
|
||||||
"github.com/containers/podman/v2/pkg/copy"
|
"github.com/containers/podman/v2/pkg/copy"
|
||||||
|
"github.com/containers/podman/v2/pkg/domain/infra/abi"
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -44,58 +46,47 @@ func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.De
|
|||||||
}
|
}
|
||||||
|
|
||||||
containerName := utils.GetName(r)
|
containerName := utils.GetName(r)
|
||||||
|
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||||
|
statReport, err := containerEngine.ContainerStat(r.Context(), containerName, query.Path)
|
||||||
|
|
||||||
ctr, err := runtime.LookupContainer(containerName)
|
// NOTE
|
||||||
if errors.Cause(err) == define.ErrNoSuchCtr {
|
// The statReport may actually be set even in case of an error. That's
|
||||||
utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists"))
|
// 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
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
|
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
|
||||||
return
|
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.
|
// Our work is done when the user is interested in the header only.
|
||||||
if r.Method == http.MethodHead {
|
if r.Method == http.MethodHead {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alright, the users wants data from the container.
|
copyFunc, err := containerEngine.ContainerCopyToArchive(r.Context(), containerName, query.Path, w)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
|
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/x-tar")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
if err := copier.Copy(); err != nil {
|
if err := copyFunc(); err != nil {
|
||||||
logrus.Errorf("Error during copy: %v", err)
|
logrus.Error(err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,36 +104,22 @@ func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrName := utils.GetName(r)
|
containerName := utils.GetName(r)
|
||||||
|
containerEngine := abi.ContainerEngine{Libpod: runtime}
|
||||||
|
|
||||||
ctr, err := runtime.LookupContainer(ctrName)
|
copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body)
|
||||||
if err != nil {
|
if errors.Cause(err) == define.ErrNoSuchCtr || os.IsNotExist(err) {
|
||||||
utils.Error(w, "Not found", http.StatusNotFound, errors.Wrapf(err, "the %s container doesn't exists", ctrName))
|
// 404 is returned for an absent container and path. The
|
||||||
|
// clients must deal with it accordingly.
|
||||||
|
utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists"))
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
destination, err := copy.CopyItemForContainer(ctr, query.Path, true, false)
|
|
||||||
defer destination.CleanUp()
|
|
||||||
if err != nil {
|
|
||||||
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
source, err := copy.CopyItemForReader(r.Body)
|
|
||||||
defer source.CleanUp()
|
|
||||||
if err != nil {
|
|
||||||
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
copier, err := copy.GetCopier(&source, &destination, false)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
if err := copier.Copy(); err != nil {
|
if err := copyFunc(); err != nil {
|
||||||
logrus.Errorf("Error during copy: %v", err)
|
logrus.Error(err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/containers/podman/v2/pkg/api/handlers/compat"
|
"github.com/containers/podman/v2/pkg/api/handlers/compat"
|
||||||
"github.com/containers/podman/v2/pkg/api/handlers/libpod"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
|
|||||||
Libpod
|
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
|
// summary: Copy files into a container
|
||||||
// description: Copy a tar archive of 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:
|
// 500:
|
||||||
// $ref: "#/responses/InternalError"
|
// $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
|
// summary: Copy files from a container
|
||||||
// description: Copy a tar archive of 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"
|
// $ref: "#/responses/NoSuchContainer"
|
||||||
// 500:
|
// 500:
|
||||||
// $ref: "#/responses/InternalError"
|
// $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(compat.Archive)).Methods(http.MethodGet, http.MethodPut, http.MethodHead)
|
||||||
r.HandleFunc(VersionedPath("/libpod/containers/{name}/archive"), s.APIHandler(libpod.Archive)).Methods(http.MethodGet, http.MethodPost)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
92
pkg/bindings/containers/archive.go
Normal file
92
pkg/bindings/containers/archive.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package containers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v2/pkg/bindings"
|
||||||
|
"github.com/containers/podman/v2/pkg/copy"
|
||||||
|
"github.com/containers/podman/v2/pkg/domain/entities"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stat checks if the specified path is on the container. Note that the stat
|
||||||
|
// report may be set even in case of an error. This happens when the path
|
||||||
|
// resolves to symlink pointing to a non-existent path.
|
||||||
|
func Stat(ctx context.Context, nameOrID string, path string) (*entities.ContainerStatReport, error) {
|
||||||
|
conn, err := bindings.GetClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("path", path)
|
||||||
|
|
||||||
|
response, err := conn.DoRequest(nil, http.MethodHead, "/containers/%s/archive", params, nil, nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalErr error
|
||||||
|
if response.StatusCode == http.StatusNotFound {
|
||||||
|
finalErr = copy.ENOENT
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
finalErr = errors.New(response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statReport *entities.ContainerStatReport
|
||||||
|
|
||||||
|
fileInfo, err := copy.ExtractFileInfoFromHeader(&response.Header)
|
||||||
|
if err != nil && finalErr == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo != nil {
|
||||||
|
statReport = &entities.ContainerStatReport{FileInfo: *fileInfo}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statReport, finalErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
|
||||||
|
conn, err := bindings.GetClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("path", path)
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
response, err := conn.DoRequest(reader, http.MethodPut, "/containers/%s/archive", params, nil, nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return errors.New(response.Status)
|
||||||
|
}
|
||||||
|
return response.Process(nil)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) {
|
||||||
|
conn, err := bindings.GetClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("path", path)
|
||||||
|
|
||||||
|
response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/archive", params, nil, nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return nil, response.Process(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
_, err := io.Copy(writer, response.Body)
|
||||||
|
return err
|
||||||
|
}, nil
|
||||||
|
}
|
220
pkg/copy/copy.go
220
pkg/copy/copy.go
@ -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
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -15,6 +16,10 @@ import (
|
|||||||
// base64 encoded JSON payload of stating a path in a container.
|
// base64 encoded JSON payload of stating a path in a container.
|
||||||
const XDockerContainerPathStatHeader = "X-Docker-Container-Path-Stat"
|
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
|
// FileInfo describes a file or directory and is returned by
|
||||||
// (*CopyItem).Stat().
|
// (*CopyItem).Stat().
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
@ -23,7 +28,6 @@ type FileInfo struct {
|
|||||||
Mode os.FileMode `json:"mode"`
|
Mode os.FileMode `json:"mode"`
|
||||||
ModTime time.Time `json:"mtime"`
|
ModTime time.Time `json:"mtime"`
|
||||||
IsDir bool `json:"isDir"`
|
IsDir bool `json:"isDir"`
|
||||||
IsStream bool `json:"isStream"`
|
|
||||||
LinkTarget string `json:"linkTarget"`
|
LinkTarget string `json:"linkTarget"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,3 +58,54 @@ func ExtractFileInfoFromHeader(header *http.Header) (*FileInfo, error) {
|
|||||||
|
|
||||||
return &info, nil
|
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
|
||||||
|
}
|
||||||
|
588
pkg/copy/item.go
588
pkg/copy/item.go
@ -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
|
|
||||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/containers/image/v5/types"
|
"github.com/containers/image/v5/types"
|
||||||
"github.com/containers/podman/v2/libpod/define"
|
"github.com/containers/podman/v2/libpod/define"
|
||||||
|
"github.com/containers/podman/v2/pkg/copy"
|
||||||
"github.com/containers/podman/v2/pkg/specgen"
|
"github.com/containers/podman/v2/pkg/specgen"
|
||||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||||
)
|
)
|
||||||
@ -143,6 +144,10 @@ type ContainerInspectReport struct {
|
|||||||
*define.InspectContainerData
|
*define.InspectContainerData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContainerStatReport struct {
|
||||||
|
copy.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
type CommitOptions struct {
|
type CommitOptions struct {
|
||||||
Author string
|
Author string
|
||||||
Changes []string
|
Changes []string
|
||||||
|
@ -2,6 +2,7 @@ package entities
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/containers/common/pkg/config"
|
"github.com/containers/common/pkg/config"
|
||||||
"github.com/containers/podman/v2/libpod/define"
|
"github.com/containers/podman/v2/libpod/define"
|
||||||
@ -9,6 +10,8 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ContainerCopyFunc func() error
|
||||||
|
|
||||||
type ContainerEngine interface {
|
type ContainerEngine interface {
|
||||||
AutoUpdate(ctx context.Context, options AutoUpdateOptions) (*AutoUpdateReport, []error)
|
AutoUpdate(ctx context.Context, options AutoUpdateOptions) (*AutoUpdateReport, []error)
|
||||||
Config(ctx context.Context) (*config.Config, 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)
|
ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error)
|
||||||
ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error)
|
ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error)
|
||||||
ContainerCommit(ctx context.Context, nameOrID string, options CommitOptions) (*CommitReport, 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)
|
ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error)
|
||||||
ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error)
|
ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error)
|
||||||
ContainerExec(ctx context.Context, nameOrID string, options ExecOptions, streams define.AttachStreams) (int, 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)
|
ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error)
|
||||||
ContainerRunlabel(ctx context.Context, label string, image string, args []string, opts ContainerRunlabelOptions) error
|
ContainerRunlabel(ctx context.Context, label string, image string, args []string, opts ContainerRunlabelOptions) error
|
||||||
ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, 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)
|
ContainerStats(ctx context.Context, namesOrIds []string, options ContainerStatsOptions) (chan ContainerStatsReport, error)
|
||||||
ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error)
|
ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error)
|
||||||
ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error)
|
ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error)
|
||||||
|
172
pkg/domain/infra/abi/archive.go
Normal file
172
pkg/domain/infra/abi/archive.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package abi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
buildahCopiah "github.com/containers/buildah/copier"
|
||||||
|
"github.com/containers/buildah/pkg/chrootuser"
|
||||||
|
"github.com/containers/buildah/util"
|
||||||
|
"github.com/containers/podman/v2/libpod"
|
||||||
|
"github.com/containers/podman/v2/pkg/domain/entities"
|
||||||
|
"github.com/containers/storage"
|
||||||
|
"github.com/containers/storage/pkg/archive"
|
||||||
|
"github.com/containers/storage/pkg/idtools"
|
||||||
|
"github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: Only the parent directory of the container path must exist. The path
|
||||||
|
// itself may be created while copying.
|
||||||
|
func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, containerPath string, reader io.Reader) (entities.ContainerCopyFunc, error) {
|
||||||
|
container, err := ic.Libpod.LookupContainer(nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount := func() {
|
||||||
|
if err := container.Unmount(false); err != nil {
|
||||||
|
logrus.Errorf("Error unmounting container: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath)
|
||||||
|
if err != nil {
|
||||||
|
unmount()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decompressed, err := archive.DecompressStream(reader)
|
||||||
|
if err != nil {
|
||||||
|
unmount()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot)
|
||||||
|
if err != nil {
|
||||||
|
unmount()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID())
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
defer unmount()
|
||||||
|
defer decompressed.Close()
|
||||||
|
putOptions := buildahCopiah.PutOptions{
|
||||||
|
UIDMap: idMappings.UIDMap,
|
||||||
|
GIDMap: idMappings.GIDMap,
|
||||||
|
ChownDirs: idPair,
|
||||||
|
ChownFiles: idPair,
|
||||||
|
}
|
||||||
|
return buildahCopiah.Put(resolvedRoot, resolvedContainerPath, putOptions, decompressed)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) {
|
||||||
|
container, err := ic.Libpod.LookupContainer(nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount := func() {
|
||||||
|
if err := container.Unmount(false); err != nil {
|
||||||
|
logrus.Errorf("Error unmounting container: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that "/" copies the *contents* of the mount point and not
|
||||||
|
// the directory.
|
||||||
|
if containerPath == "/" {
|
||||||
|
containerPath = "/."
|
||||||
|
}
|
||||||
|
|
||||||
|
_, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath)
|
||||||
|
if err != nil {
|
||||||
|
unmount()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot)
|
||||||
|
if err != nil {
|
||||||
|
unmount()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID())
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
defer container.Unmount(false)
|
||||||
|
getOptions := buildahCopiah.GetOptions{
|
||||||
|
// Unless the specified path ends with ".", we want to copy the base directory.
|
||||||
|
KeepDirectoryNames: !strings.HasSuffix(resolvedContainerPath, "."),
|
||||||
|
UIDMap: idMappings.UIDMap,
|
||||||
|
GIDMap: idMappings.GIDMap,
|
||||||
|
ChownDirs: idPair,
|
||||||
|
ChownFiles: idPair,
|
||||||
|
}
|
||||||
|
return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedContainerPath}, writer)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIDMappingsAndPair returns the ID mappings for the container and the host
|
||||||
|
// ID pair.
|
||||||
|
func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) {
|
||||||
|
user, err := getContainerUser(container, containerMount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idMappingOpts, err := container.IDMappings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
|
||||||
|
return &idMappingOpts, &idPair, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContainerUser returns the specs.User of the container.
|
||||||
|
func getContainerUser(container *libpod.Container, mountPoint string) (specs.User, error) {
|
||||||
|
userspec := container.Config().User
|
||||||
|
|
||||||
|
uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec)
|
||||||
|
u := specs.User{
|
||||||
|
UID: uid,
|
||||||
|
GID: gid,
|
||||||
|
Username: userspec,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(userspec, ":") {
|
||||||
|
groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
|
||||||
|
if err2 != nil {
|
||||||
|
if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
u.AdditionalGids = groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
|
||||||
|
func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
|
||||||
|
for _, idmap := range idMaps {
|
||||||
|
tempIDMap := specs.LinuxIDMapping{
|
||||||
|
ContainerID: uint32(idmap.ContainerID),
|
||||||
|
HostID: uint32(idmap.HostID),
|
||||||
|
Size: uint32(idmap.Size),
|
||||||
|
}
|
||||||
|
convertedIDMap = append(convertedIDMap, tempIDMap)
|
||||||
|
}
|
||||||
|
return convertedIDMap
|
||||||
|
}
|
251
pkg/domain/infra/abi/containers_stat.go
Normal file
251
pkg/domain/infra/abi/containers_stat.go
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
package abi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
buildahCopiah "github.com/containers/buildah/copier"
|
||||||
|
"github.com/containers/podman/v2/libpod"
|
||||||
|
"github.com/containers/podman/v2/pkg/copy"
|
||||||
|
"github.com/containers/podman/v2/pkg/domain/entities"
|
||||||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
|
"github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) containerStat(container *libpod.Container, containerPath string) (*entities.ContainerStatReport, string, string, error) {
|
||||||
|
containerMountPoint, err := container.Mount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that "/" copies the *contents* of the mount point and not
|
||||||
|
// the directory.
|
||||||
|
if containerPath == "/" {
|
||||||
|
containerPath += "/."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now resolve the container's path. It may hit a volume, it may hit a
|
||||||
|
// bind mount, it may be relative.
|
||||||
|
resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
statInfo, statInfoErr := secureStat(resolvedRoot, resolvedContainerPath)
|
||||||
|
if statInfoErr != nil {
|
||||||
|
// Not all errors from secureStat map to ErrNotExist, so we
|
||||||
|
// have to look into the error string. Turning it into an
|
||||||
|
// ENOENT let's the API handlers return the correct status code
|
||||||
|
// which is crucuial for the remote client.
|
||||||
|
if os.IsNotExist(err) || strings.Contains(statInfoErr.Error(), "o such file or directory") {
|
||||||
|
statInfoErr = copy.ENOENT
|
||||||
|
}
|
||||||
|
// If statInfo is nil, there's nothing we can do anymore. A
|
||||||
|
// non-nil statInfo may indicate a symlink where we must have
|
||||||
|
// a closer look.
|
||||||
|
if statInfo == nil {
|
||||||
|
return nil, "", "", statInfoErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now make sure that the info's LinkTarget is relative to the
|
||||||
|
// container's mount.
|
||||||
|
var absContainerPath string
|
||||||
|
|
||||||
|
if statInfo.IsSymlink {
|
||||||
|
// Evaluated symlinks are always relative to the container's mount point.
|
||||||
|
absContainerPath = statInfo.ImmediateTarget
|
||||||
|
} else if strings.HasPrefix(resolvedContainerPath, containerMountPoint) {
|
||||||
|
// If the path is on the container's mount point, strip it off.
|
||||||
|
absContainerPath = strings.TrimPrefix(resolvedContainerPath, containerMountPoint)
|
||||||
|
absContainerPath = filepath.Join("/", absContainerPath)
|
||||||
|
} else {
|
||||||
|
// No symlink and not on the container's mount point, so let's
|
||||||
|
// move it back to the original input. It must have evaluated
|
||||||
|
// to a volume or bind mount but we cannot return host paths.
|
||||||
|
absContainerPath = containerPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to make sure to preseve the base path as specified by
|
||||||
|
// the user. The `filepath` packages likes to remove trailing slashes
|
||||||
|
// and dots that are crucial to the copy logic.
|
||||||
|
absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath)
|
||||||
|
resolvedContainerPath = copy.PreserveBasePath(containerPath, resolvedContainerPath)
|
||||||
|
|
||||||
|
info := copy.FileInfo{
|
||||||
|
IsDir: statInfo.IsDir,
|
||||||
|
Name: filepath.Base(absContainerPath),
|
||||||
|
Size: statInfo.Size,
|
||||||
|
Mode: statInfo.Mode,
|
||||||
|
ModTime: statInfo.ModTime,
|
||||||
|
LinkTarget: absContainerPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.ContainerStatReport{FileInfo: info}, resolvedRoot, resolvedContainerPath, statInfoErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, containerPath string) (*entities.ContainerStatReport, error) {
|
||||||
|
container, err := ic.Libpod.LookupContainer(nameOrID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := container.Unmount(false); err != nil {
|
||||||
|
logrus.Errorf("Error unmounting container: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
statReport, _, _, err := ic.containerStat(container, containerPath)
|
||||||
|
return statReport, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveContainerPaths resolves the container's mount point and the container
|
||||||
|
// path as specified by the user. Both may resolve to paths outside of the
|
||||||
|
// container's mount point when the container path hits a volume or bind mount.
|
||||||
|
//
|
||||||
|
// NOTE: We must take volumes and bind mounts into account as, regrettably, we
|
||||||
|
// can copy to/from stopped containers. In that case, the volumes and bind
|
||||||
|
// mounts are not present. For running containers, the runtime (e.g., runc or
|
||||||
|
// crun) takes care of these mounts. For stopped ones, we need to do quite
|
||||||
|
// some dance, as done below.
|
||||||
|
func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) {
|
||||||
|
// Let's first make sure we have a path relative to the mount point.
|
||||||
|
pathRelativeToContainerMountPoint := containerPath
|
||||||
|
if !filepath.IsAbs(containerPath) {
|
||||||
|
// If the containerPath is not absolute, it's relative to the
|
||||||
|
// container's working dir. To be extra careful, let's first
|
||||||
|
// join the working dir with "/", and the add the containerPath
|
||||||
|
// to it.
|
||||||
|
pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath)
|
||||||
|
}
|
||||||
|
resolvedPathOnTheContainerMountPoint := filepath.Join(mountPoint, pathRelativeToContainerMountPoint)
|
||||||
|
pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint)
|
||||||
|
pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint)
|
||||||
|
|
||||||
|
// Now we have an "absolute container Path" but not yet resolved on the
|
||||||
|
// host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to
|
||||||
|
// check if "/foo/bar/file.txt" is on a volume or bind mount. To do
|
||||||
|
// that, we need to walk *down* the paths to the root. Assuming
|
||||||
|
// volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar",
|
||||||
|
// we must select "/foo/bar". Once selected, we need to rebase the
|
||||||
|
// remainder (i.e, "/file.txt") on the volume's mount point on the
|
||||||
|
// host. Same applies to bind mounts.
|
||||||
|
|
||||||
|
searchPath := pathRelativeToContainerMountPoint
|
||||||
|
for {
|
||||||
|
volume, err := findVolume(container, searchPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if volume != nil {
|
||||||
|
logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)
|
||||||
|
// We found a matching volume for searchPath. We now
|
||||||
|
// need to first find the relative path of our input
|
||||||
|
// path to the searchPath, and then join it with the
|
||||||
|
// volume's mount point.
|
||||||
|
pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
|
||||||
|
absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(volume.MountPoint(), pathRelativeToVolume)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return volume.MountPoint(), absolutePathOnTheVolumeMount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount := findBindMount(container, searchPath); mount != nil {
|
||||||
|
logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath)
|
||||||
|
// We found a matching bind mount for searchPath. We
|
||||||
|
// now need to first find the relative path of our
|
||||||
|
// input path to the searchPath, and then join it with
|
||||||
|
// the source of the bind mount.
|
||||||
|
pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
|
||||||
|
absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return mount.Source, absolutePathOnTheBindMount, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if searchPath == "/" {
|
||||||
|
// Cannot go beyond "/", so we're done.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar").
|
||||||
|
searchPath = filepath.Dir(searchPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No volume, no bind mount but just a normal path on the container.
|
||||||
|
return mountPoint, resolvedPathOnTheContainerMountPoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findVolume checks if the specified container path matches a volume inside
|
||||||
|
// the container. It returns a matching volume or nil.
|
||||||
|
func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) {
|
||||||
|
runtime := c.Runtime()
|
||||||
|
cleanedContainerPath := filepath.Clean(containerPath)
|
||||||
|
for _, vol := range c.Config().NamedVolumes {
|
||||||
|
if cleanedContainerPath == filepath.Clean(vol.Dest) {
|
||||||
|
return runtime.GetVolume(vol.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBindMount checks if the specified container path matches a bind mount
|
||||||
|
// inside the container. It returns a matching mount or nil.
|
||||||
|
func findBindMount(c *libpod.Container, containerPath string) *specs.Mount {
|
||||||
|
cleanedPath := filepath.Clean(containerPath)
|
||||||
|
for _, m := range c.Config().Spec.Mounts {
|
||||||
|
if m.Type != "bind" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cleanedPath == filepath.Clean(m.Destination) {
|
||||||
|
mount := m
|
||||||
|
return &mount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// secureStat extracts file info for path in a chroot'ed environment in root.
|
||||||
|
func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) {
|
||||||
|
var glob string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// If root and path are equal, then dir must be empty and the glob must
|
||||||
|
// be ".".
|
||||||
|
if filepath.Clean(root) == filepath.Clean(path) {
|
||||||
|
glob = "."
|
||||||
|
} else {
|
||||||
|
glob, err = filepath.Rel(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globStats, err := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(globStats) != 1 {
|
||||||
|
return nil, errors.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats))
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay
|
||||||
|
if !exists {
|
||||||
|
return nil, copy.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
var statErr error
|
||||||
|
if stat.Error != "" {
|
||||||
|
statErr = errors.New(stat.Error)
|
||||||
|
}
|
||||||
|
return stat, statErr
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -772,9 +772,16 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o
|
|||||||
return reports, nil
|
return reports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error {
|
func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
|
||||||
return nil
|
return containers.CopyFromArchive(ic.ClientCxt, nameOrID, path, reader)
|
||||||
// return containers.Copy(ic.ClientCxt, source, dest, options)
|
}
|
||||||
|
|
||||||
|
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
|
// Shutdown Libpod engine
|
||||||
|
@ -19,7 +19,12 @@ func JoinErrors(errs []error) error {
|
|||||||
// blank lines when printing the error.
|
// blank lines when printing the error.
|
||||||
var multiE *multierror.Error
|
var multiE *multierror.Error
|
||||||
multiE = multierror.Append(multiE, errs...)
|
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
|
// ErrorsToString converts the slice of errors into a slice of corresponding
|
||||||
|
@ -24,7 +24,6 @@ var _ = Describe("Podman cp", func() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
SkipIfRemote("FIXME: Podman-remote cp needs to work")
|
|
||||||
tempdir, err = CreateTempDirInTempDir()
|
tempdir, err = CreateTempDirInTempDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -8,8 +8,6 @@
|
|||||||
load helpers
|
load helpers
|
||||||
|
|
||||||
@test "podman cp file from host to container" {
|
@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
|
srcdir=$PODMAN_TMPDIR/cp-test-file-host-to-ctr
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
local -a randomcontent=(
|
local -a randomcontent=(
|
||||||
@ -57,60 +55,16 @@ load helpers
|
|||||||
is "$output" 'Error: ".*/IdoNotExist" could not be found on the host' \
|
is "$output" 'Error: ".*/IdoNotExist" could not be found on the host' \
|
||||||
"copy nonexistent host path"
|
"copy nonexistent host path"
|
||||||
|
|
||||||
# Container path does not exist. Notice that the error message shows how
|
# Container (parent) path does not exist.
|
||||||
# the specified container is resolved.
|
|
||||||
run_podman 125 cp $srcdir/hostfile0 cpcontainer:/IdoNotExist/
|
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"
|
"copy into nonexistent path in container"
|
||||||
|
|
||||||
run_podman rm -f cpcontainer
|
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" {
|
@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
|
srcdir=$PODMAN_TMPDIR/cp-test-file-ctr-to-host
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
|
|
||||||
@ -153,8 +107,6 @@ load helpers
|
|||||||
|
|
||||||
|
|
||||||
@test "podman cp dir from host to container" {
|
@test "podman cp dir from host to container" {
|
||||||
skip_if_remote "podman-remote does not yet handle cp"
|
|
||||||
|
|
||||||
dirname=dir-test
|
dirname=dir-test
|
||||||
srcdir=$PODMAN_TMPDIR/$dirname
|
srcdir=$PODMAN_TMPDIR/$dirname
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
@ -195,8 +147,6 @@ load helpers
|
|||||||
|
|
||||||
|
|
||||||
@test "podman cp dir from container to host" {
|
@test "podman cp dir from container to host" {
|
||||||
skip_if_remote "podman-remote does not yet handle cp"
|
|
||||||
|
|
||||||
srcdir=$PODMAN_TMPDIR/dir-test
|
srcdir=$PODMAN_TMPDIR/dir-test
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
|
|
||||||
@ -230,8 +180,6 @@ load helpers
|
|||||||
|
|
||||||
|
|
||||||
@test "podman cp file from host to container volume" {
|
@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
|
srcdir=$PODMAN_TMPDIR/cp-test-volume
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
echo "This file should be in volume2" > $srcdir/hostfile
|
echo "This file should be in volume2" > $srcdir/hostfile
|
||||||
@ -268,8 +216,6 @@ load helpers
|
|||||||
|
|
||||||
|
|
||||||
@test "podman cp file from host to container mount" {
|
@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
|
srcdir=$PODMAN_TMPDIR/cp-test-mount-src
|
||||||
mountdir=$PODMAN_TMPDIR/cp-test-mount
|
mountdir=$PODMAN_TMPDIR/cp-test-mount
|
||||||
mkdir -p $srcdir $mountdir
|
mkdir -p $srcdir $mountdir
|
||||||
@ -296,8 +242,6 @@ load helpers
|
|||||||
# perform wildcard expansion in the container. We should get both
|
# perform wildcard expansion in the container. We should get both
|
||||||
# files copied into the host.
|
# files copied into the host.
|
||||||
@test "podman cp * - wildcard copy multiple files from container to 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
|
srcdir=$PODMAN_TMPDIR/cp-test-in
|
||||||
dstdir=$PODMAN_TMPDIR/cp-test-out
|
dstdir=$PODMAN_TMPDIR/cp-test-out
|
||||||
mkdir -p $srcdir $dstdir
|
mkdir -p $srcdir $dstdir
|
||||||
@ -321,8 +265,6 @@ load helpers
|
|||||||
# Create a file on the host; make a symlink in the container pointing
|
# 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.
|
# into host-only space. Try to podman-cp that symlink. It should fail.
|
||||||
@test "podman cp - will not recognize symlink pointing into host space" {
|
@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
|
srcdir=$PODMAN_TMPDIR/cp-test-in
|
||||||
dstdir=$PODMAN_TMPDIR/cp-test-out
|
dstdir=$PODMAN_TMPDIR/cp-test-out
|
||||||
mkdir -p $srcdir $dstdir
|
mkdir -p $srcdir $dstdir
|
||||||
@ -350,8 +292,6 @@ load helpers
|
|||||||
# in the container pointing to 'file*' (file star). Try to podman-cp
|
# in the container pointing to 'file*' (file star). Try to podman-cp
|
||||||
# this invalid double symlink. It must fail.
|
# this invalid double symlink. It must fail.
|
||||||
@test "podman cp - will not expand globs in host space (#3829)" {
|
@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
|
srcdir=$PODMAN_TMPDIR/cp-test-in
|
||||||
dstdir=$PODMAN_TMPDIR/cp-test-out
|
dstdir=$PODMAN_TMPDIR/cp-test-out
|
||||||
mkdir -p $srcdir $dstdir
|
mkdir -p $srcdir $dstdir
|
||||||
@ -372,8 +312,6 @@ load helpers
|
|||||||
|
|
||||||
# Another symlink into host space, this one named '*' (star). cp should fail.
|
# Another symlink into host space, this one named '*' (star). cp should fail.
|
||||||
@test "podman cp - will not expand wildcard" {
|
@test "podman cp - will not expand wildcard" {
|
||||||
skip_if_remote "podman-remote does not yet handle cp"
|
|
||||||
|
|
||||||
srcdir=$PODMAN_TMPDIR/cp-test-in
|
srcdir=$PODMAN_TMPDIR/cp-test-in
|
||||||
dstdir=$PODMAN_TMPDIR/cp-test-out
|
dstdir=$PODMAN_TMPDIR/cp-test-out
|
||||||
mkdir -p $srcdir $dstdir
|
mkdir -p $srcdir $dstdir
|
||||||
@ -394,8 +332,6 @@ load helpers
|
|||||||
|
|
||||||
# THIS IS EXTREMELY WEIRD. Podman expands symlinks in weird ways.
|
# THIS IS EXTREMELY WEIRD. Podman expands symlinks in weird ways.
|
||||||
@test "podman cp into container: weird symlink expansion" {
|
@test "podman cp into container: weird symlink expansion" {
|
||||||
skip_if_remote "podman-remote does not yet handle cp"
|
|
||||||
|
|
||||||
srcdir=$PODMAN_TMPDIR/cp-test-in
|
srcdir=$PODMAN_TMPDIR/cp-test-in
|
||||||
dstdir=$PODMAN_TMPDIR/cp-test-out
|
dstdir=$PODMAN_TMPDIR/cp-test-out
|
||||||
mkdir -p $srcdir $dstdir
|
mkdir -p $srcdir $dstdir
|
||||||
@ -427,7 +363,7 @@ load helpers
|
|||||||
is "$output" "" "output from podman cp 1"
|
is "$output" "" "output from podman cp 1"
|
||||||
|
|
||||||
run_podman 125 cp --pause=false $srcdir/$rand_filename2 cpcontainer:/tmp/d2/x/
|
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
|
run_podman cp --pause=false $srcdir/$rand_filename3 cpcontainer:/tmp/d3/x
|
||||||
is "$output" "" "output from podman cp 3"
|
is "$output" "" "output from podman cp 3"
|
||||||
@ -454,8 +390,6 @@ load helpers
|
|||||||
# rhbz1741718 : file copied into container:/var/lib/foo appears as /foo
|
# rhbz1741718 : file copied into container:/var/lib/foo appears as /foo
|
||||||
# (docker only, never seems to have affected podman. Make sure it never does).
|
# (docker only, never seems to have affected podman. Make sure it never does).
|
||||||
@test "podman cp into a subdirectory matching GraphRoot" {
|
@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
|
# Create tempfile with random name and content
|
||||||
srcdir=$PODMAN_TMPDIR/cp-test-in
|
srcdir=$PODMAN_TMPDIR/cp-test-in
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
@ -491,8 +425,6 @@ load helpers
|
|||||||
|
|
||||||
|
|
||||||
@test "podman cp from stdin to container" {
|
@test "podman cp from stdin to container" {
|
||||||
skip_if_remote "podman-remote does not yet handle cp"
|
|
||||||
|
|
||||||
# Create tempfile with random name and content
|
# Create tempfile with random name and content
|
||||||
srcdir=$PODMAN_TMPDIR/cp-test-stdin
|
srcdir=$PODMAN_TMPDIR/cp-test-stdin
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
@ -525,24 +457,22 @@ load helpers
|
|||||||
|
|
||||||
# Input stream must be a (compressed) tar archive.
|
# Input stream must be a (compressed) tar archive.
|
||||||
run_podman 125 cp - cpcontainer:/tmp < $srcdir/$rand_filename
|
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).
|
# Destination must be a directory (on an existing file).
|
||||||
run_podman exec cpcontainer touch /tmp/file.txt
|
run_podman exec cpcontainer touch /tmp/file.txt
|
||||||
run_podman 125 cp /dev/stdin cpcontainer:/tmp/file.txt < $tar_file
|
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).
|
# Destination must be a directory (on an absent path).
|
||||||
run_podman 125 cp /dev/stdin cpcontainer:/tmp/IdoNotExist < $tar_file
|
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
|
run_podman rm -f cpcontainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@test "podman cp from container to stdout" {
|
@test "podman cp from container to stdout" {
|
||||||
skip_if_remote "podman-remote does not yet handle cp"
|
|
||||||
|
|
||||||
srcdir=$PODMAN_TMPDIR/cp-test-stdout
|
srcdir=$PODMAN_TMPDIR/cp-test-stdout
|
||||||
mkdir -p $srcdir
|
mkdir -p $srcdir
|
||||||
rand_content=$(random_string 50)
|
rand_content=$(random_string 50)
|
||||||
@ -579,9 +509,9 @@ load helpers
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
tar xvf $srcdir/stdout.tar -C $srcdir
|
tar xvf $srcdir/stdout.tar -C $srcdir
|
||||||
run cat $srcdir/file.txt
|
run cat $srcdir/tmp/file.txt
|
||||||
is "$output" "$rand_content"
|
is "$output" "$rand_content"
|
||||||
run cat $srcdir/empty.txt
|
run cat $srcdir/tmp/empty.txt
|
||||||
is "$output" ""
|
is "$output" ""
|
||||||
|
|
||||||
run_podman rm -f cpcontainer
|
run_podman rm -f cpcontainer
|
||||||
|
2
vendor/github.com/containers/buildah/.cirrus.yml
generated
vendored
2
vendor/github.com/containers/buildah/.cirrus.yml
generated
vendored
@ -178,7 +178,7 @@ gce_instance:
|
|||||||
image_name: "${FEDORA_CACHE_IMAGE_NAME}"
|
image_name: "${FEDORA_CACHE_IMAGE_NAME}"
|
||||||
image_name: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
|
image_name: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
|
||||||
image_name: "${UBUNTU_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.
|
# Separate scripts for separate outputs, makes debugging easier.
|
||||||
setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}'
|
setup_script: '${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}'
|
||||||
|
10
vendor/github.com/containers/buildah/Makefile
generated
vendored
10
vendor/github.com/containers/buildah/Makefile
generated
vendored
@ -39,6 +39,14 @@ SOURCES=*.go imagebuildah/*.go bind/*.go chroot/*.go cmd/buildah/*.go copier/*.g
|
|||||||
|
|
||||||
LINTFLAGS ?=
|
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
|
all: bin/buildah bin/imgtype docs
|
||||||
|
|
||||||
# Update nix/nixpkgs.json its latest stable commit
|
# Update nix/nixpkgs.json its latest stable commit
|
||||||
@ -56,7 +64,7 @@ static:
|
|||||||
|
|
||||||
.PHONY: bin/buildah
|
.PHONY: bin/buildah
|
||||||
bin/buildah: $(SOURCES)
|
bin/buildah: $(SOURCES)
|
||||||
$(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./cmd/buildah
|
$(GO_BUILD) $(BUILDAH_LDFLAGS) -gcflags "$(GOGCFLAGS)" -o $@ $(BUILDFLAGS) ./cmd/buildah
|
||||||
|
|
||||||
.PHONY: buildah
|
.PHONY: buildah
|
||||||
buildah: bin/buildah
|
buildah: bin/buildah
|
||||||
|
193
vendor/github.com/containers/buildah/copier/copier.go
generated
vendored
193
vendor/github.com/containers/buildah/copier/copier.go
generated
vendored
@ -10,6 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -202,11 +203,11 @@ type StatOptions struct {
|
|||||||
// If root and directory are both not specified, the current root directory is
|
// 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
|
// used, and relative names in the globs list are treated as being relative to
|
||||||
// the current working directory.
|
// the current working directory.
|
||||||
// If root is specified and the current OS supports it, the stat() is performed
|
// If root is specified and the current OS supports it, and the calling process
|
||||||
// in a chrooted context. If the directory is specified as an absolute path,
|
// has the necessary privileges, the stat() is performed in a chrooted context.
|
||||||
// it should either be the root directory or a subdirectory of the root
|
// If the directory is specified as an absolute path, it should either be the
|
||||||
// directory. Otherwise, the directory is treated as a path relative to the
|
// root directory or a subdirectory of the root directory. Otherwise, the
|
||||||
// root directory.
|
// directory is treated as a path relative to the root directory.
|
||||||
// Relative names in the glob list are treated as being relative to the
|
// Relative names in the glob list are treated as being relative to the
|
||||||
// directory.
|
// directory.
|
||||||
func Stat(root string, directory string, options StatOptions, globs []string) ([]*StatsForGlob, error) {
|
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.
|
// GetOptions controls parts of Get()'s behavior.
|
||||||
type GetOptions struct {
|
type GetOptions struct {
|
||||||
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive
|
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
|
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
|
ExpandArchives bool // extract the contents of named items that are archives
|
||||||
ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
// 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
|
// 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
|
// used, and relative names in the globs list are treated as being relative to
|
||||||
// the current working directory.
|
// the current working directory.
|
||||||
// If root is specified and the current OS supports it, the contents are read
|
// If root is specified and the current OS supports it, and the calling process
|
||||||
// in a chrooted context. If the directory is specified as an absolute path,
|
// has the necessary privileges, the contents are read in a chrooted context.
|
||||||
// it should either be the root directory or a subdirectory of the root
|
// If the directory is specified as an absolute path, it should either be the
|
||||||
// directory. Otherwise, the directory is treated as a path relative to the
|
// root directory or a subdirectory of the root directory. Otherwise, the
|
||||||
// root directory.
|
// directory is treated as a path relative to the root directory.
|
||||||
// Relative names in the glob list are treated as being relative to the
|
// Relative names in the glob list are treated as being relative to the
|
||||||
// directory.
|
// directory.
|
||||||
func Get(root string, directory string, options GetOptions, globs []string, bulkWriter io.Writer) error {
|
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.
|
// PutOptions controls parts of Put()'s behavior.
|
||||||
type PutOptions struct {
|
type PutOptions struct {
|
||||||
UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs when writing contents to disk
|
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
|
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
|
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
|
ChownDirs *idtools.IDPair // set ownership of newly-created directories
|
||||||
ChmodDirs *os.FileMode // set permissions on newly-created directories
|
ChmodDirs *os.FileMode // set permissions on newly-created directories
|
||||||
ChownFiles *idtools.IDPair // set ownership of newly-created files
|
ChownFiles *idtools.IDPair // set ownership of newly-created files
|
||||||
ChmodFiles *os.FileMode // set permissions on 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
|
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
|
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.
|
// Put extracts an archive from the bulkReader at the specified directory.
|
||||||
// If root and directory are both not specified, the current root directory is
|
// If root and directory are both not specified, the current root directory is
|
||||||
// used.
|
// used.
|
||||||
// If root is specified and the current OS supports it, the contents are written
|
// If root is specified and the current OS supports it, and the calling process
|
||||||
// in a chrooted context. If the directory is specified as an absolute path,
|
// has the necessary privileges, the contents are written in a chrooted
|
||||||
// it should either be the root directory or a subdirectory of the root
|
// context. If the directory is specified as an absolute path, it should
|
||||||
// directory. Otherwise, the directory is treated as a path relative to the
|
// either be the root directory or a subdirectory of the root directory.
|
||||||
// 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 {
|
func Put(root string, directory string, options PutOptions, bulkReader io.Reader) error {
|
||||||
req := request{
|
req := request{
|
||||||
Request: requestPut,
|
Request: requestPut,
|
||||||
@ -325,11 +330,12 @@ type MkdirOptions struct {
|
|||||||
// need to be created will be given the specified ownership and permissions.
|
// 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
|
// If root and directory are both not specified, the current root directory is
|
||||||
// used.
|
// used.
|
||||||
// If root is specified and the current OS supports it, the directory is
|
// If root is specified and the current OS supports it, and the calling process
|
||||||
// created in a chrooted context. If the directory is specified as an absolute
|
// has the necessary privileges, the directory is created in a chrooted
|
||||||
// path, it should either be the root directory or a subdirectory of the root
|
// context. If the directory is specified as an absolute path, it should
|
||||||
// directory. Otherwise, the directory is treated as a path relative to the
|
// either be the root directory or a subdirectory of the root directory.
|
||||||
// root directory.
|
// Otherwise, the directory is treated as a path relative to the root
|
||||||
|
// directory.
|
||||||
func Mkdir(root string, directory string, options MkdirOptions) error {
|
func Mkdir(root string, directory string, options MkdirOptions) error {
|
||||||
req := request{
|
req := request{
|
||||||
Request: requestMkdir,
|
Request: requestMkdir,
|
||||||
@ -547,13 +553,13 @@ func copierWithSubprocess(bulkReader io.Reader, bulkWriter io.Writer, req reques
|
|||||||
return nil, errors.Wrap(err, step)
|
return nil, errors.Wrap(err, step)
|
||||||
}
|
}
|
||||||
if err = encoder.Encode(req); err != nil {
|
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 {
|
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 {
|
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.Close()
|
||||||
stdinWrite = nil
|
stdinWrite = nil
|
||||||
@ -626,7 +632,7 @@ func copierMain() {
|
|||||||
// Read a request.
|
// Read a request.
|
||||||
req := new(request)
|
req := new(request)
|
||||||
if err := decoder.Decode(req); err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if req.Request == requestQuit {
|
if req.Request == requestQuit {
|
||||||
@ -717,12 +723,12 @@ func copierMain() {
|
|||||||
}
|
}
|
||||||
resp, cb, err := copierHandler(bulkReader, bulkWriter, *req)
|
resp, cb, err := copierHandler(bulkReader, bulkWriter, *req)
|
||||||
if err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// Encode the response.
|
// Encode the response.
|
||||||
if err := encoder.Encode(resp); err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// If there's bulk data to transfer, run the callback to either
|
// 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
|
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 {
|
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
|
// build the header using the name provided
|
||||||
hdr, err := tar.FileInfoHeader(srcfi, symlinkTarget)
|
hdr, err := tar.FileInfoHeader(srcfi, symlinkTarget)
|
||||||
@ -1127,6 +1161,9 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str
|
|||||||
if name != "" {
|
if name != "" {
|
||||||
hdr.Name = filepath.ToSlash(name)
|
hdr.Name = filepath.ToSlash(name)
|
||||||
}
|
}
|
||||||
|
if options.Rename != nil {
|
||||||
|
hdr.Name = handleRename(options.Rename, hdr.Name)
|
||||||
|
}
|
||||||
if options.StripSetuidBit {
|
if options.StripSetuidBit {
|
||||||
hdr.Mode &^= cISUID
|
hdr.Mode &^= cISUID
|
||||||
}
|
}
|
||||||
@ -1164,6 +1201,9 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str
|
|||||||
tr := tar.NewReader(rc)
|
tr := tar.NewReader(rc)
|
||||||
hdr, err := tr.Next()
|
hdr, err := tr.Next()
|
||||||
for err == nil {
|
for err == nil {
|
||||||
|
if options.Rename != nil {
|
||||||
|
hdr.Name = handleRename(options.Rename, hdr.Name)
|
||||||
|
}
|
||||||
if err = tw.WriteHeader(hdr); err != nil {
|
if err = tw.WriteHeader(hdr); err != nil {
|
||||||
return errors.Wrapf(err, "error writing tar header from %q to pipe", contentPath)
|
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) {
|
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)
|
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 != nil && os.IsExist(err) {
|
||||||
if err = os.Remove(path); err != nil {
|
if req.PutOptions.NoOverwriteDirNonDir {
|
||||||
return 0, errors.Wrapf(err, "copier: put: error removing file to be overwritten %q", path)
|
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)
|
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)
|
tr := tar.NewReader(bulkReader)
|
||||||
hdr, err := tr.Next()
|
hdr, err := tr.Next()
|
||||||
for err == nil {
|
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
|
// figure out who should own this new item
|
||||||
if idMappings != nil && !idMappings.Empty() {
|
if idMappings != nil && !idMappings.Empty() {
|
||||||
containerPair := idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}
|
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:
|
case tar.TypeLink:
|
||||||
var linkTarget string
|
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 {
|
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)
|
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 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 {
|
if err = os.Remove(path); err == nil {
|
||||||
err = os.Link(linkTarget, path)
|
err = os.Link(linkTarget, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tar.TypeSymlink:
|
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 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 {
|
if err = os.Remove(path); err == nil {
|
||||||
err = os.Symlink(filepath.FromSlash(hdr.Linkname), filepath.FromSlash(path))
|
err = os.Symlink(filepath.FromSlash(hdr.Linkname), filepath.FromSlash(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tar.TypeChar:
|
case tar.TypeChar:
|
||||||
if err = mknod(path, chrMode(0600), int(mkdev(devMajor, devMinor))); err != nil && os.IsExist(err) {
|
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 {
|
if err = os.Remove(path); err == nil {
|
||||||
err = mknod(path, chrMode(0600), int(mkdev(devMajor, devMinor)))
|
err = mknod(path, chrMode(0600), int(mkdev(devMajor, devMinor)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tar.TypeBlock:
|
case tar.TypeBlock:
|
||||||
if err = mknod(path, blkMode(0600), int(mkdev(devMajor, devMinor))); err != nil && os.IsExist(err) {
|
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 {
|
if err = os.Remove(path); err == nil {
|
||||||
err = mknod(path, blkMode(0600), int(mkdev(devMajor, devMinor)))
|
err = mknod(path, blkMode(0600), int(mkdev(devMajor, devMinor)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
if err = os.Mkdir(path, 0700); err != nil && os.IsExist(err) {
|
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
|
// make a note of the directory's times. we
|
||||||
// might create items under it, which will
|
// might create items under it, which will
|
||||||
@ -1453,6 +1541,11 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM
|
|||||||
})
|
})
|
||||||
case tar.TypeFifo:
|
case tar.TypeFifo:
|
||||||
if err = mkfifo(path, 0600); err != nil && os.IsExist(err) {
|
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 {
|
if err = os.Remove(path); err == nil {
|
||||||
err = mkfifo(path, 0600)
|
err = mkfifo(path, 0600)
|
||||||
}
|
}
|
||||||
|
2
vendor/github.com/containers/buildah/copier/syscall_unix.go
generated
vendored
2
vendor/github.com/containers/buildah/copier/syscall_unix.go
generated
vendored
@ -10,7 +10,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
var canChroot = true
|
var canChroot = os.Getuid() == 0
|
||||||
|
|
||||||
func chroot(root string) (bool, error) {
|
func chroot(root string) (bool, error) {
|
||||||
if canChroot {
|
if canChroot {
|
||||||
|
6
vendor/github.com/containers/buildah/go.mod
generated
vendored
6
vendor/github.com/containers/buildah/go.mod
generated
vendored
@ -5,10 +5,10 @@ go 1.12
|
|||||||
require (
|
require (
|
||||||
github.com/containerd/containerd v1.4.1 // indirect
|
github.com/containerd/containerd v1.4.1 // indirect
|
||||||
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784
|
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/image/v5 v5.8.1
|
||||||
github.com/containers/ocicrypt v1.0.3
|
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/distribution v2.7.1+incompatible
|
||||||
github.com/docker/go-units v0.4.0
|
github.com/docker/go-units v0.4.0
|
||||||
github.com/docker/libnetwork v0.8.0-dev.2.0.20190625141545-5a177b73e316
|
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/sys/mount v0.1.1 // indirect
|
||||||
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 // indirect
|
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 // indirect
|
||||||
github.com/onsi/ginkgo v1.14.2
|
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/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
|
github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
|
||||||
github.com/opencontainers/runc v1.0.0-rc91
|
github.com/opencontainers/runc v1.0.0-rc91
|
||||||
|
10
vendor/github.com/containers/buildah/go.sum
generated
vendored
10
vendor/github.com/containers/buildah/go.sum
generated
vendored
@ -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/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 h1:rqUVLD8I859xRgUx/WMC3v7QAFqbLKZbs+0kqYboRJc=
|
||||||
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
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.31.0 h1:SRnjfoqbjfaojpY9YJq9JBPEslwB5hoXJbaE+5zMFwM=
|
||||||
github.com/containers/common v0.29.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA=
|
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 h1:aHW8a/Kd0dTJ7PTL/fc6y12sJqHxWgqilu+XyHfjD8Q=
|
||||||
github.com/containers/image/v5 v5.8.1/go.mod h1:blOEFd/iFdeyh891ByhCVUc+xAcaI3gBegXECwz9UbQ=
|
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=
|
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.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 h1:1+f8fy6ly35c8SLet5jzZ8t0WJJs5+xSpfMAYw0R3kc=
|
||||||
github.com/containers/storage v1.24.1/go.mod h1:0xJL06Dmd+ZYXIUdnBUPN0JnhHGgwMkLvnnAonJfWJU=
|
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/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/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
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.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 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
|
||||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
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 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-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
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-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 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
|
||||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
59
vendor/github.com/containers/buildah/install.md
generated
vendored
59
vendor/github.com/containers/buildah/install.md
generated
vendored
@ -69,15 +69,35 @@ sudo apt-get update
|
|||||||
sudo apt-get -y install buildah
|
sudo apt-get -y install buildah
|
||||||
```
|
```
|
||||||
|
|
||||||
The [Kubic project](https://build.opensuse.org/project/show/devel:kubic:libcontainers:stable)
|
If you would prefer newer (though not as well-tested) packages,
|
||||||
provides packages for Debian 10. The Kubic packages for Debian Testing/Bullseye and Debian Unstable/Sid
|
the [Kubic project](https://build.opensuse.org/package/show/devel:kubic:libcontainers:stable/buildah)
|
||||||
have been discontinued to avoid
|
provides packages for Debian 10 and newer. The packages in Kubic project repos are more frequently
|
||||||
[conflicts](https://github.com/containers/buildah/issues/2797) with the official packages.
|
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)
|
### [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/)
|
#### [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
|
```bash
|
||||||
# Raspbian 10
|
# Raspbian 10
|
||||||
@ -135,6 +156,8 @@ sudo apt-get update -qq
|
|||||||
sudo apt-get -qq -y install buildah
|
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 arm64 (beta)](https://downloads.raspberrypi.org/raspios_arm64/images/)
|
||||||
|
|
||||||
Raspberry Pi OS use the standard Debian's repositories,
|
Raspberry Pi OS use the standard Debian's repositories,
|
||||||
@ -160,7 +183,16 @@ sudo apt-get -y update
|
|||||||
sudo apt-get -y install buildah
|
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
|
```bash
|
||||||
. /etc/os-release
|
. /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
|
## Vendoring
|
||||||
|
|
||||||
Buildah uses Go Modules for vendoring purposes. If you need to update or add a vendored package into Buildah, please follow this procedure:
|
Buildah uses Go Modules for vendoring purposes. If you need to update or add a vendored package into Buildah, please follow this procedure:
|
||||||
|
18
vendor/github.com/containers/buildah/new.go
generated
vendored
18
vendor/github.com/containers/buildah/new.go
generated
vendored
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/buildah/util"
|
"github.com/containers/buildah/util"
|
||||||
@ -127,27 +126,10 @@ func resolveLocalImage(systemContext *types.SystemContext, store storage.Store,
|
|||||||
return nil, "", nil, nil
|
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) {
|
func resolveImage(ctx context.Context, systemContext *types.SystemContext, store storage.Store, options BuilderOptions) (types.ImageReference, string, *storage.Image, error) {
|
||||||
if systemContext == nil {
|
if systemContext == nil {
|
||||||
systemContext = &types.SystemContext{}
|
systemContext = &types.SystemContext{}
|
||||||
}
|
}
|
||||||
systemContext.ShortNameMode = getShortNameMode()
|
|
||||||
|
|
||||||
fromImage := options.FromImage
|
fromImage := options.FromImage
|
||||||
// If the image name includes a transport we can use it as it. Special
|
// If the image name includes a transport we can use it as it. Special
|
||||||
|
4
vendor/github.com/containers/buildah/pkg/cli/common.go
generated
vendored
4
vendor/github.com/containers/buildah/pkg/cli/common.go
generated
vendored
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/containers/common/pkg/auth"
|
"github.com/containers/common/pkg/auth"
|
||||||
commonComp "github.com/containers/common/pkg/completion"
|
commonComp "github.com/containers/common/pkg/completion"
|
||||||
"github.com/containers/common/pkg/config"
|
"github.com/containers/common/pkg/config"
|
||||||
|
"github.com/containers/storage/pkg/unshare"
|
||||||
"github.com/opencontainers/runtime-spec/specs-go"
|
"github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@ -366,6 +367,9 @@ func DefaultIsolation() string {
|
|||||||
if isolation != "" {
|
if isolation != "" {
|
||||||
return isolation
|
return isolation
|
||||||
}
|
}
|
||||||
|
if unshare.IsRootless() {
|
||||||
|
return "rootless"
|
||||||
|
}
|
||||||
return buildah.OCI
|
return buildah.OCI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
vendor/github.com/containers/buildah/pkg/parse/parse.go
generated
vendored
19
vendor/github.com/containers/buildah/pkg/parse/parse.go
generated
vendored
@ -486,7 +486,7 @@ func ValidateVolumeCtrDir(ctrDir string) error {
|
|||||||
|
|
||||||
// ValidateVolumeOpts validates a volume's options
|
// ValidateVolumeOpts validates a volume's options
|
||||||
func ValidateVolumeOpts(options []string) ([]string, error) {
|
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))
|
finalOpts := make([]string, 0, len(options))
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
switch opt {
|
switch opt {
|
||||||
@ -515,6 +515,11 @@ func ValidateVolumeOpts(options []string) ([]string, error) {
|
|||||||
if foundLabelChange > 1 {
|
if foundLabelChange > 1 {
|
||||||
return nil, errors.Errorf("invalid options %q, can only specify 1 'z', 'Z', or 'O' option", strings.Join(options, ", "))
|
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":
|
case "private", "rprivate", "shared", "rshared", "slave", "rslave", "unbindable", "runbindable":
|
||||||
foundRootPropagation++
|
foundRootPropagation++
|
||||||
if foundRootPropagation > 1 {
|
if foundRootPropagation > 1 {
|
||||||
@ -878,20 +883,12 @@ func NamespaceOptions(c *cobra.Command) (namespaceOptions buildah.NamespaceOptio
|
|||||||
logrus.Debugf("setting network to disabled")
|
logrus.Debugf("setting network to disabled")
|
||||||
break
|
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:")
|
how = strings.TrimPrefix(how, "ns:")
|
||||||
if _, err := os.Stat(how); err != nil {
|
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)
|
logrus.Debugf("setting %q namespace to %q", what, how)
|
||||||
options.AddOrReplace(buildah.NamespaceOption{
|
options.AddOrReplace(buildah.NamespaceOption{
|
||||||
Name: what,
|
Name: what,
|
||||||
|
60
vendor/github.com/containers/buildah/run_linux.go
generated
vendored
60
vendor/github.com/containers/buildah/run_linux.go
generated
vendored
@ -506,8 +506,14 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
|
|||||||
return err
|
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.
|
// 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 {
|
if err != nil {
|
||||||
return err
|
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
|
// Make sure the overlay directory is clean before running
|
||||||
containerDir, err := b.store.ContainerDirectory(b.ContainerID)
|
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) {
|
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
|
var rootProp string
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
switch opt {
|
switch opt {
|
||||||
@ -1713,6 +1719,8 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
|
|||||||
foundZ = true
|
foundZ = true
|
||||||
case "O":
|
case "O":
|
||||||
foundO = true
|
foundO = true
|
||||||
|
case "U":
|
||||||
|
foundU = true
|
||||||
case "private", "rprivate", "slave", "rslave", "shared", "rshared":
|
case "private", "rprivate", "slave", "rslave", "shared", "rshared":
|
||||||
rootProp = opt
|
rootProp = opt
|
||||||
}
|
}
|
||||||
@ -1730,6 +1738,11 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
|
|||||||
return specs.Mount{}, err
|
return specs.Mount{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if foundU {
|
||||||
|
if err := chownSourceVolume(host, processUID, processGID); err != nil {
|
||||||
|
return specs.Mount{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if foundO {
|
if foundO {
|
||||||
containerDir, err := b.store.ContainerDirectory(b.ContainerID)
|
containerDir, err := b.store.ContainerDirectory(b.ContainerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1746,6 +1759,14 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
|
|||||||
|
|
||||||
b.TempVolumes[contentDir] = true
|
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
|
return overlayMount, err
|
||||||
}
|
}
|
||||||
if rootProp == "" {
|
if rootProp == "" {
|
||||||
@ -1789,6 +1810,39 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
|
|||||||
return mounts, nil
|
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) {
|
func setupMaskedPaths(g *generate.Generator) {
|
||||||
for _, mp := range []string{
|
for _, mp := range []string{
|
||||||
"/proc/acpi",
|
"/proc/acpi",
|
||||||
|
2
vendor/github.com/containers/buildah/troubleshooting.md
generated
vendored
2
vendor/github.com/containers/buildah/troubleshooting.md
generated
vendored
@ -154,5 +154,5 @@ Choose one of the following:
|
|||||||
* Complete the build operation as a privileged user.
|
* Complete the build operation as a privileged user.
|
||||||
* Install and configure fuse-overlayfs.
|
* Install and configure fuse-overlayfs.
|
||||||
* Install the fuse-overlayfs package for your Linux Distribution.
|
* Install the fuse-overlayfs package for your Linux Distribution.
|
||||||
* Add `mount_program = "/usr/bin/fuse-overlayfs` under `[storage.options]` in your `~/.config/containers/storage.conf` file.
|
* Add `mount_program = "/usr/bin/fuse-overlayfs"` under `[storage.options]` in your `~/.config/containers/storage.conf` file.
|
||||||
---
|
---
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -67,7 +67,7 @@ github.com/containernetworking/plugins/pkg/utils/hwaddr
|
|||||||
github.com/containernetworking/plugins/pkg/utils/sysctl
|
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
|
||||||
github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator
|
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
|
||||||
github.com/containers/buildah/bind
|
github.com/containers/buildah/bind
|
||||||
github.com/containers/buildah/chroot
|
github.com/containers/buildah/chroot
|
||||||
|
Reference in New Issue
Block a user