summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorValentin Rothberg <rothberg@redhat.com>2020-12-11 11:00:25 +0100
committerValentin Rothberg <rothberg@redhat.com>2020-12-18 12:08:49 +0100
commitadcb3a7a609ba756f8b9de17521f0e3dce3b778e (patch)
tree4572b8ed243ffa33648a9190dcf4c2ac9a419099 /pkg
parentf56865879ccffeddce3b9e36f585fe67c37591d5 (diff)
downloadpodman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.tar.gz
podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.tar.bz2
podman-adcb3a7a609ba756f8b9de17521f0e3dce3b778e.zip
remote copy
Implement `podman-remote cp` and break out the logic from the previously added `pkg/copy` into it's basic building blocks and move them up into the `ContainerEngine` interface and `cmd/podman`. The `--pause` and `--extract` flags are now deprecated and turned into nops. Note that this commit is vendoring a non-release version of Buildah to pull in updates to the copier package. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
Diffstat (limited to 'pkg')
-rw-r--r--pkg/api/handlers/compat/containers_archive.go97
-rw-r--r--pkg/api/server/register_archive.go8
-rw-r--r--pkg/bindings/containers/archive.go92
-rw-r--r--pkg/copy/copy.go220
-rw-r--r--pkg/copy/fileinfo.go57
-rw-r--r--pkg/copy/item.go588
-rw-r--r--pkg/domain/entities/containers.go5
-rw-r--r--pkg/domain/entities/engine_container.go7
-rw-r--r--pkg/domain/infra/abi/archive.go172
-rw-r--r--pkg/domain/infra/abi/containers_stat.go251
-rw-r--r--pkg/domain/infra/abi/cp.go70
-rw-r--r--pkg/domain/infra/tunnel/containers.go13
-rw-r--r--pkg/errorhandling/errorhandling.go7
13 files changed, 638 insertions, 949 deletions
diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go
index d8197415c..083c72ce8 100644
--- a/pkg/api/handlers/compat/containers_archive.go
+++ b/pkg/api/handlers/compat/containers_archive.go
@@ -3,11 +3,13 @@ package compat
import (
"fmt"
"net/http"
+ "os"
"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/pkg/api/handlers/utils"
"github.com/containers/podman/v2/pkg/copy"
+ "github.com/containers/podman/v2/pkg/domain/infra/abi"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -44,58 +46,47 @@ func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.De
}
containerName := utils.GetName(r)
-
- ctr, err := runtime.LookupContainer(containerName)
- if errors.Cause(err) == define.ErrNoSuchCtr {
- utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists"))
+ containerEngine := abi.ContainerEngine{Libpod: runtime}
+ statReport, err := containerEngine.ContainerStat(r.Context(), containerName, query.Path)
+
+ // NOTE
+ // The statReport may actually be set even in case of an error. That's
+ // the case when we're looking at a symlink pointing to nirvana. In
+ // such cases, we really need the FileInfo but we also need the error.
+ if statReport != nil {
+ statHeader, err := copy.EncodeFileInfo(&statReport.FileInfo)
+ if err != nil {
+ utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
+ return
+ }
+ w.Header().Add(copy.XDockerContainerPathStatHeader, statHeader)
+ }
+
+ if errors.Cause(err) == define.ErrNoSuchCtr || errors.Cause(err) == copy.ENOENT {
+ // 404 is returned for an absent container and path. The
+ // clients must deal with it accordingly.
+ utils.Error(w, "Not found.", http.StatusNotFound, err)
return
} else if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return
}
- source, err := copy.CopyItemForContainer(ctr, query.Path, true, true)
- defer source.CleanUp()
- if err != nil {
- utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path))
- return
- }
-
- // NOTE: Docker always sets the header.
- info, err := source.Stat()
- if err != nil {
- utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path))
- return
- }
- statHeader, err := copy.EncodeFileInfo(info)
- if err != nil {
- utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
- return
- }
- w.Header().Add(copy.XDockerContainerPathStatHeader, statHeader)
-
// Our work is done when the user is interested in the header only.
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
- // Alright, the users wants data from the container.
- destination, err := copy.CopyItemForWriter(w)
- if err != nil {
- utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
- return
- }
-
- copier, err := copy.GetCopier(&source, &destination, false)
+ copyFunc, err := containerEngine.ContainerCopyToArchive(r.Context(), containerName, query.Path, w)
if err != nil {
utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
return
}
+ w.Header().Set("Content-Type", "application/x-tar")
w.WriteHeader(http.StatusOK)
- if err := copier.Copy(); err != nil {
- logrus.Errorf("Error during copy: %v", err)
- return
+ if err := copyFunc(); err != nil {
+ logrus.Error(err.Error())
}
}
@@ -113,36 +104,22 @@ func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder,
return
}
- ctrName := utils.GetName(r)
-
- ctr, err := runtime.LookupContainer(ctrName)
- if err != nil {
- utils.Error(w, "Not found", http.StatusNotFound, errors.Wrapf(err, "the %s container doesn't exists", ctrName))
- return
- }
+ containerName := utils.GetName(r)
+ containerEngine := abi.ContainerEngine{Libpod: runtime}
- destination, err := copy.CopyItemForContainer(ctr, query.Path, true, false)
- defer destination.CleanUp()
- if err != nil {
- utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
+ copyFunc, err := containerEngine.ContainerCopyFromArchive(r.Context(), containerName, query.Path, r.Body)
+ if errors.Cause(err) == define.ErrNoSuchCtr || os.IsNotExist(err) {
+ // 404 is returned for an absent container and path. The
+ // clients must deal with it accordingly.
+ utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists"))
return
- }
-
- source, err := copy.CopyItemForReader(r.Body)
- defer source.CleanUp()
- if err != nil {
- utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
+ } else if err != nil {
+ utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return
}
- copier, err := copy.GetCopier(&source, &destination, false)
- if err != nil {
- utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
- return
- }
w.WriteHeader(http.StatusOK)
- if err := copier.Copy(); err != nil {
- logrus.Errorf("Error during copy: %v", err)
- return
+ if err := copyFunc(); err != nil {
+ logrus.Error(err.Error())
}
}
diff --git a/pkg/api/server/register_archive.go b/pkg/api/server/register_archive.go
index b2d2543c4..9ff0ad0eb 100644
--- a/pkg/api/server/register_archive.go
+++ b/pkg/api/server/register_archive.go
@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/containers/podman/v2/pkg/api/handlers/compat"
- "github.com/containers/podman/v2/pkg/api/handlers/libpod"
"github.com/gorilla/mux"
)
@@ -92,7 +91,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
Libpod
*/
- // swagger:operation POST /libpod/containers/{name}/copy libpod libpodPutArchive
+ // swagger:operation POST /libpod/containers/{name}/archive libpod libpodPutArchive
// ---
// summary: Copy files into a container
// description: Copy a tar archive of files into a container
@@ -133,7 +132,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
// 500:
// $ref: "#/responses/InternalError"
- // swagger:operation GET /libpod/containers/{name}/copy libpod libpodGetArchive
+ // swagger:operation GET /libpod/containers/{name}/archive libpod libpodGetArchive
// ---
// summary: Copy files from a container
// description: Copy a tar archive of files from a container
@@ -164,8 +163,7 @@ func (s *APIServer) registerAchiveHandlers(r *mux.Router) error {
// $ref: "#/responses/NoSuchContainer"
// 500:
// $ref: "#/responses/InternalError"
- r.HandleFunc(VersionedPath("/libpod/containers/{name}/copy"), s.APIHandler(libpod.Archive)).Methods(http.MethodGet, http.MethodPost)
- r.HandleFunc(VersionedPath("/libpod/containers/{name}/archive"), s.APIHandler(libpod.Archive)).Methods(http.MethodGet, http.MethodPost)
+ r.HandleFunc(VersionedPath("/libpod/containers/{name}/archive"), s.APIHandler(compat.Archive)).Methods(http.MethodGet, http.MethodPut, http.MethodHead)
return nil
}
diff --git a/pkg/bindings/containers/archive.go b/pkg/bindings/containers/archive.go
new file mode 100644
index 000000000..d1bbc0b95
--- /dev/null
+++ b/pkg/bindings/containers/archive.go
@@ -0,0 +1,92 @@
+package containers
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "net/url"
+
+ "github.com/containers/podman/v2/pkg/bindings"
+ "github.com/containers/podman/v2/pkg/copy"
+ "github.com/containers/podman/v2/pkg/domain/entities"
+ "github.com/pkg/errors"
+)
+
+// Stat checks if the specified path is on the container. Note that the stat
+// report may be set even in case of an error. This happens when the path
+// resolves to symlink pointing to a non-existent path.
+func Stat(ctx context.Context, nameOrID string, path string) (*entities.ContainerStatReport, error) {
+ conn, err := bindings.GetClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+ params := url.Values{}
+ params.Set("path", path)
+
+ response, err := conn.DoRequest(nil, http.MethodHead, "/containers/%s/archive", params, nil, nameOrID)
+ if err != nil {
+ return nil, err
+ }
+
+ var finalErr error
+ if response.StatusCode == http.StatusNotFound {
+ finalErr = copy.ENOENT
+ } else if response.StatusCode != http.StatusOK {
+ finalErr = errors.New(response.Status)
+ }
+
+ var statReport *entities.ContainerStatReport
+
+ fileInfo, err := copy.ExtractFileInfoFromHeader(&response.Header)
+ if err != nil && finalErr == nil {
+ return nil, err
+ }
+
+ if fileInfo != nil {
+ statReport = &entities.ContainerStatReport{FileInfo: *fileInfo}
+ }
+
+ return statReport, finalErr
+}
+
+func CopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
+ conn, err := bindings.GetClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+ params := url.Values{}
+ params.Set("path", path)
+
+ return func() error {
+ response, err := conn.DoRequest(reader, http.MethodPut, "/containers/%s/archive", params, nil, nameOrID)
+ if err != nil {
+ return err
+ }
+ if response.StatusCode != http.StatusOK {
+ return errors.New(response.Status)
+ }
+ return response.Process(nil)
+ }, nil
+}
+
+func CopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) {
+ conn, err := bindings.GetClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+ params := url.Values{}
+ params.Set("path", path)
+
+ response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/archive", params, nil, nameOrID)
+ if err != nil {
+ return nil, err
+ }
+ if response.StatusCode != http.StatusOK {
+ return nil, response.Process(nil)
+ }
+
+ return func() error {
+ _, err := io.Copy(writer, response.Body)
+ return err
+ }, nil
+}
diff --git a/pkg/copy/copy.go b/pkg/copy/copy.go
deleted file mode 100644
index 13893deb2..000000000
--- a/pkg/copy/copy.go
+++ /dev/null
@@ -1,220 +0,0 @@
-package copy
-
-import (
- "io"
- "os"
- "path/filepath"
- "strings"
-
- buildahCopiah "github.com/containers/buildah/copier"
- "github.com/containers/storage/pkg/archive"
- securejoin "github.com/cyphar/filepath-securejoin"
- "github.com/pkg/errors"
-)
-
-// ********************************* NOTE *************************************
-//
-// Most security bugs are caused by attackers playing around with symlinks
-// trying to escape from the container onto the host and/or trick into data
-// corruption on the host. Hence, file operations on containers (including
-// *stat) should always be handled by `github.com/containers/buildah/copier`
-// which makes sure to evaluate files in a chroot'ed environment.
-//
-// Please make sure to add verbose comments when changing code to make the
-// lives of future readers easier.
-//
-// ****************************************************************************
-
-// Copier copies data from a source to a destination CopyItem.
-type Copier struct {
- copyFunc func() error
- cleanUpFuncs []deferFunc
-}
-
-// cleanUp releases resources the Copier may hold open.
-func (c *Copier) cleanUp() {
- for _, f := range c.cleanUpFuncs {
- f()
- }
-}
-
-// Copy data from a source to a destination CopyItem.
-func (c *Copier) Copy() error {
- defer c.cleanUp()
- return c.copyFunc()
-}
-
-// GetCopiers returns a Copier to copy the source item to destination. Use
-// extract to untar the source if it's a tar archive.
-func GetCopier(source *CopyItem, destination *CopyItem, extract bool) (*Copier, error) {
- copier := &Copier{}
-
- // First, do the man-page dance. See podman-cp(1) for details.
- if err := enforceCopyRules(source, destination); err != nil {
- return nil, err
- }
-
- // Destination is a stream (e.g., stdout or an http body).
- if destination.info.IsStream {
- // Source is a stream (e.g., stdin or an http body).
- if source.info.IsStream {
- copier.copyFunc = func() error {
- _, err := io.Copy(destination.writer, source.reader)
- return err
- }
- return copier, nil
- }
- root, glob, err := source.buildahGlobs()
- if err != nil {
- return nil, err
- }
- copier.copyFunc = func() error {
- return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer)
- }
- return copier, nil
- }
-
- // Destination is either a file or a directory.
- if source.info.IsStream {
- copier.copyFunc = func() error {
- return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader)
- }
- return copier, nil
- }
-
- tarOptions := &archive.TarOptions{
- Compression: archive.Uncompressed,
- CopyPass: true,
- }
-
- root := destination.root
- dir := destination.resolved
- if !source.info.IsDir {
- // When copying a file, make sure to rename the
- // destination base path.
- nameMap := make(map[string]string)
- nameMap[filepath.Base(source.resolved)] = filepath.Base(destination.resolved)
- tarOptions.RebaseNames = nameMap
- dir = filepath.Dir(dir)
- }
-
- var tarReader io.ReadCloser
- if extract && archive.IsArchivePath(source.resolved) {
- if !destination.info.IsDir {
- return nil, errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original)
- }
-
- reader, err := os.Open(source.resolved)
- if err != nil {
- return nil, err
- }
- copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() })
-
- // The stream from stdin may be compressed (e.g., via gzip).
- decompressedStream, err := archive.DecompressStream(reader)
- if err != nil {
- return nil, err
- }
-
- copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { decompressedStream.Close() })
- tarReader = decompressedStream
- } else {
- reader, err := archive.TarWithOptions(source.resolved, tarOptions)
- if err != nil {
- return nil, err
- }
- copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() })
- tarReader = reader
- }
-
- copier.copyFunc = func() error {
- return buildahCopiah.Put(root, dir, source.putOptions(), tarReader)
- }
- return copier, nil
-}
-
-// enforceCopyRules enforces the rules for copying from a source to a
-// destination as mentioned in the podman-cp(1) man page. Please refer to the
-// man page and/or the inline comments for further details. Note that source
-// and destination are passed by reference and the their data may be changed.
-func enforceCopyRules(source, destination *CopyItem) error {
- if source.statError != nil {
- return source.statError
- }
-
- // We can copy everything to a stream.
- if destination.info.IsStream {
- return nil
- }
-
- if source.info.IsStream {
- if !(destination.info.IsDir || destination.info.IsStream) {
- return errors.New("destination must be a directory or stream when copying from a stream")
- }
- return nil
- }
-
- // Source is a *directory*.
- if source.info.IsDir {
- if destination.statError != nil {
- // It's okay if the destination does not exist. We
- // made sure before that it's parent exists, so it
- // would be created while copying.
- if os.IsNotExist(destination.statError) {
- return nil
- }
- // Could be a permission error.
- return destination.statError
- }
-
- // If the destination exists and is not a directory, we have a
- // problem.
- if !destination.info.IsDir {
- return errors.Errorf("cannot copy directory %q to file %q", source.original, destination.original)
- }
-
- // If the destination exists and is a directory, we need to
- // append the source base directory to it. This makes sure
- // that copying "/foo/bar" "/tmp" will copy to "/tmp/bar" (and
- // not "/tmp").
- newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
- if err != nil {
- return err
- }
- destination.resolved = newDestination
- return nil
- }
-
- // Source is a *file*.
- if destination.statError != nil {
- // It's okay if the destination does not exist, unless it ends
- // with "/".
- if !os.IsNotExist(destination.statError) {
- return destination.statError
- } else if strings.HasSuffix(destination.resolved, "/") {
- // Note: this is practically unreachable code as the
- // existence of parent directories is enforced early
- // on. It's left here as an extra security net.
- return errors.Errorf("destination directory %q must exist (trailing %q)", destination.original, "/")
- }
- // Does not exist and does not end with "/".
- return nil
- }
-
- // If the destination is a file, we're good. We will overwrite the
- // contents while copying.
- if !destination.info.IsDir {
- return nil
- }
-
- // If the destination exists and is a directory, we need to append the
- // source base directory to it. This makes sure that copying
- // "/foo/bar" "/tmp" will copy to "/tmp/bar" (and not "/tmp").
- newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved))
- if err != nil {
- return err
- }
-
- destination.resolved = newDestination
- return nil
-}
diff --git a/pkg/copy/fileinfo.go b/pkg/copy/fileinfo.go
index 08b4eb377..50b0ca9a1 100644
--- a/pkg/copy/fileinfo.go
+++ b/pkg/copy/fileinfo.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"os"
+ "path/filepath"
"strings"
"time"
@@ -15,6 +16,10 @@ import (
// base64 encoded JSON payload of stating a path in a container.
const XDockerContainerPathStatHeader = "X-Docker-Container-Path-Stat"
+// ENOENT mimics the stdlib's ENONENT and can be used to implement custom logic
+// while preserving the user-visible error message.
+var ENOENT = errors.New("No such file or directory")
+
// FileInfo describes a file or directory and is returned by
// (*CopyItem).Stat().
type FileInfo struct {
@@ -23,7 +28,6 @@ type FileInfo struct {
Mode os.FileMode `json:"mode"`
ModTime time.Time `json:"mtime"`
IsDir bool `json:"isDir"`
- IsStream bool `json:"isStream"`
LinkTarget string `json:"linkTarget"`
}
@@ -54,3 +58,54 @@ func ExtractFileInfoFromHeader(header *http.Header) (*FileInfo, error) {
return &info, nil
}
+
+// ResolveHostPath resolves the specified, possibly relative, path on the host.
+func ResolveHostPath(path string) (*FileInfo, error) {
+ resolvedHostPath, err := filepath.Abs(path)
+ if err != nil {
+ return nil, err
+ }
+ resolvedHostPath = PreserveBasePath(path, resolvedHostPath)
+
+ statInfo, err := os.Stat(resolvedHostPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, ENOENT
+ }
+ return nil, err
+ }
+
+ return &FileInfo{
+ Name: statInfo.Name(),
+ Size: statInfo.Size(),
+ Mode: statInfo.Mode(),
+ ModTime: statInfo.ModTime(),
+ IsDir: statInfo.IsDir(),
+ LinkTarget: resolvedHostPath,
+ }, nil
+}
+
+// PreserveBasePath makes sure that the original base path (e.g., "/" or "./")
+// is preserved. The filepath API among tends to clean up a bit too much but
+// we *must* preserve this data by all means.
+func PreserveBasePath(original, resolved string) string {
+ // Handle "/"
+ if strings.HasSuffix(original, "/") {
+ if !strings.HasSuffix(resolved, "/") {
+ resolved += "/"
+ }
+ return resolved
+ }
+
+ // Handle "/."
+ if strings.HasSuffix(original, "/.") {
+ if strings.HasSuffix(resolved, "/") { // could be root!
+ resolved += "."
+ } else if !strings.HasSuffix(resolved, "/.") {
+ resolved += "/."
+ }
+ return resolved
+ }
+
+ return resolved
+}
diff --git a/pkg/copy/item.go b/pkg/copy/item.go
deleted file mode 100644
index df8bf30b9..000000000
--- a/pkg/copy/item.go
+++ /dev/null
@@ -1,588 +0,0 @@
-package copy
-
-import (
- "io"
- "os"
- "path/filepath"
- "strings"
-
- buildahCopiah "github.com/containers/buildah/copier"
- "github.com/containers/buildah/pkg/chrootuser"
- "github.com/containers/buildah/util"
- "github.com/containers/podman/v2/libpod"
- "github.com/containers/podman/v2/libpod/define"
- "github.com/containers/podman/v2/pkg/cgroups"
- "github.com/containers/podman/v2/pkg/rootless"
- "github.com/containers/storage"
- "github.com/containers/storage/pkg/archive"
- "github.com/containers/storage/pkg/idtools"
- securejoin "github.com/cyphar/filepath-securejoin"
- "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/pkg/errors"
- "github.com/sirupsen/logrus"
-)
-
-// ********************************* NOTE *************************************
-//
-// Most security bugs are caused by attackers playing around with symlinks
-// trying to escape from the container onto the host and/or trick into data
-// corruption on the host. Hence, file operations on containers (including
-// *stat) should always be handled by `github.com/containers/buildah/copier`
-// which makes sure to evaluate files in a chroot'ed environment.
-//
-// Please make sure to add verbose comments when changing code to make the
-// lives of future readers easier.
-//
-// ****************************************************************************
-
-var (
- _stdin = os.Stdin.Name()
- _stdout = os.Stdout.Name()
-)
-
-// CopyItem is the source or destination of a copy operation. Use the
-// CopyItemFrom* functions to create one for the specific source/destination
-// item.
-type CopyItem struct {
- // The original path provided by the caller. Useful in error messages.
- original string
- // The resolved path on the host or container. Maybe altered at
- // multiple stages when copying.
- resolved string
- // The root for copying data in a chroot'ed environment.
- root string
-
- // IDPair of the resolved path.
- idPair *idtools.IDPair
- // Storage ID mappings.
- idMappings *storage.IDMappingOptions
-
- // Internal FileInfo. We really don't want users to mess with a
- // CopyItem but only plug and play with it.
- info FileInfo
- // Error when creating the upper FileInfo. Some errors are non-fatal,
- // for instance, when a destination *base* path does not exist.
- statError error
-
- writer io.Writer
- reader io.Reader
-
- // Needed to clean up resources (e.g., unmount a container).
- cleanUpFuncs []deferFunc
-}
-
-// deferFunc allows for returning functions that must be deferred at call sites.
-type deferFunc func()
-
-// Stat returns the FileInfo.
-func (item *CopyItem) Stat() (*FileInfo, error) {
- return &item.info, item.statError
-}
-
-// CleanUp releases resources such as the container mounts. It *must* be
-// called even in case of errors.
-func (item *CopyItem) CleanUp() {
- for _, f := range item.cleanUpFuncs {
- f()
- }
-}
-
-// CopyItemForWriter returns a CopyItem for the specified io.WriteCloser. Note
-// that the returned item can only act as a copy destination.
-func CopyItemForWriter(writer io.Writer) (item CopyItem, _ error) {
- item.writer = writer
- item.info.IsStream = true
- return item, nil
-}
-
-// CopyItemForReader returns a CopyItem for the specified io.ReaderCloser. Note
-// that the returned item can only act as a copy source.
-//
-// Note that the specified reader will be auto-decompressed if needed.
-func CopyItemForReader(reader io.Reader) (item CopyItem, _ error) {
- item.info.IsStream = true
- decompressed, err := archive.DecompressStream(reader)
- if err != nil {
- return item, err
- }
- item.reader = decompressed
- item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
- if err := decompressed.Close(); err != nil {
- logrus.Errorf("Error closing decompressed reader of copy item: %v", err)
- }
- })
- return item, nil
-}
-
-// CopyItemForHost creates a CopyItem for the specified host path. It's a
-// destination by default. Use isSource to set it as a destination.
-//
-// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors.
-func CopyItemForHost(hostPath string, isSource bool) (item CopyItem, _ error) {
- if hostPath == "-" {
- if isSource {
- hostPath = _stdin
- } else {
- hostPath = _stdout
- }
- }
-
- if hostPath == _stdin {
- return CopyItemForReader(os.Stdin)
- }
-
- if hostPath == _stdout {
- return CopyItemForWriter(os.Stdout)
- }
-
- // Now do the dance for the host data.
- resolvedHostPath, err := filepath.Abs(hostPath)
- if err != nil {
- return item, err
- }
-
- resolvedHostPath = preserveBasePath(hostPath, resolvedHostPath)
- item.original = hostPath
- item.resolved = resolvedHostPath
- item.root = "/"
-
- statInfo, statError := os.Stat(resolvedHostPath)
- item.statError = statError
-
- // It exists, we're done.
- if statError == nil {
- item.info.Name = statInfo.Name()
- item.info.Size = statInfo.Size()
- item.info.Mode = statInfo.Mode()
- item.info.ModTime = statInfo.ModTime()
- item.info.IsDir = statInfo.IsDir()
- item.info.LinkTarget = resolvedHostPath
- return item, nil
- }
-
- // The source must exist, but let's try to give some human-friendly
- // errors.
- if isSource {
- if os.IsNotExist(item.statError) {
- return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", hostPath)
- }
- return item, item.statError // could be a permission error
- }
-
- // If we're a destination, we need to make sure that the parent
- // directory exists.
- parent := filepath.Dir(resolvedHostPath)
- if _, err := os.Stat(parent); err != nil {
- if os.IsNotExist(err) {
- return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", parent)
- }
- return item, err
- }
-
- return item, nil
-}
-
-// CopyItemForContainer creates a CopyItem for the specified path on the
-// container. It's a destination by default. Use isSource to set it as a
-// destination. Note that the container path may resolve to a path outside of
-// the container's mount point if the path hits a volume or mount on the
-// container.
-//
-// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors.
-func CopyItemForContainer(container *libpod.Container, containerPath string, pause bool, isSource bool) (item CopyItem, _ error) {
- // Mount and pause the container.
- containerMountPoint, err := item.mountAndPauseContainer(container, pause)
- if err != nil {
- return item, err
- }
-
- // Make sure that "/" copies the *contents* of the mount point and not
- // the directory.
- if containerPath == "/" {
- containerPath += "/."
- }
-
- // Now resolve the container's path. It may hit a volume, it may hit a
- // bind mount, it may be relative.
- resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath)
- if err != nil {
- return item, err
- }
- resolvedContainerPath = preserveBasePath(containerPath, resolvedContainerPath)
-
- idMappings, idPair, err := getIDMappingsAndPair(container, containerMountPoint)
- if err != nil {
- return item, err
- }
-
- item.original = containerPath
- item.resolved = resolvedContainerPath
- item.root = resolvedRoot
- item.idMappings = idMappings
- item.idPair = idPair
-
- statInfo, statError := secureStat(resolvedRoot, resolvedContainerPath)
- item.statError = statError
-
- // It exists, we're done.
- if statError == nil {
- item.info.IsDir = statInfo.IsDir
- item.info.Name = filepath.Base(statInfo.Name)
- item.info.Size = statInfo.Size
- item.info.Mode = statInfo.Mode
- item.info.ModTime = statInfo.ModTime
- item.info.IsDir = statInfo.IsDir
- item.info.LinkTarget = resolvedContainerPath
- return item, nil
- }
-
- // The source must exist, but let's try to give some human-friendly
- // errors.
- if isSource {
- if os.IsNotExist(statError) {
- return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath)
- }
- return item, item.statError // could be a permission error
- }
-
- // If we're a destination, we need to make sure that the parent
- // directory exists.
- parent := filepath.Dir(resolvedContainerPath)
- if _, err := secureStat(resolvedRoot, parent); err != nil {
- if os.IsNotExist(err) {
- return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath)
- }
- return item, err
- }
-
- return item, nil
-}
-
-// putOptions returns PUT options for buildah's copier package.
-func (item *CopyItem) putOptions() buildahCopiah.PutOptions {
- options := buildahCopiah.PutOptions{}
- if item.idMappings != nil {
- options.UIDMap = item.idMappings.UIDMap
- options.GIDMap = item.idMappings.GIDMap
- }
- if item.idPair != nil {
- options.ChownDirs = item.idPair
- options.ChownFiles = item.idPair
- }
- return options
-}
-
-// getOptions returns GET options for buildah's copier package.
-func (item *CopyItem) getOptions() buildahCopiah.GetOptions {
- options := buildahCopiah.GetOptions{}
- if item.idMappings != nil {
- options.UIDMap = item.idMappings.UIDMap
- options.GIDMap = item.idMappings.GIDMap
- }
- if item.idPair != nil {
- options.ChownDirs = item.idPair
- options.ChownFiles = item.idPair
- }
- return options
-
-}
-
-// mount and pause the container. Also set the item's cleanUpFuncs. Those
-// *must* be invoked by callers, even in case of errors.
-func (item *CopyItem) mountAndPauseContainer(container *libpod.Container, pause bool) (string, error) {
- // Make sure to pause and unpause the container. We cannot pause on
- // cgroupsv1 as rootless user, in which case we turn off pausing.
- if pause && rootless.IsRootless() {
- cgroupv2, _ := cgroups.IsCgroup2UnifiedMode()
- if !cgroupv2 {
- logrus.Debugf("Cannot pause container for copying as a rootless user on cgroupsv1: default to not pause")
- pause = false
- }
- }
-
- // Mount and unmount the container.
- mountPoint, err := container.Mount()
- if err != nil {
- return "", err
- }
-
- item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
- if err := container.Unmount(false); err != nil {
- logrus.Errorf("Error unmounting container after copy operation: %v", err)
- }
- })
-
- // Pause and unpause the container.
- if pause {
- if err := container.Pause(); err != nil {
- // Ignore errors when the container isn't running. No
- // need to pause.
- if errors.Cause(err) != define.ErrCtrStateInvalid {
- return "", err
- }
- } else {
- item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
- if err := container.Unpause(); err != nil {
- logrus.Errorf("Error unpausing container after copy operation: %v", err)
- }
- })
- }
- }
-
- return mountPoint, nil
-}
-
-// buildahGlobs returns the root, dir and glob used in buildah's copier
-// package.
-//
-// Note that dir is always empty.
-func (item *CopyItem) buildahGlobs() (root string, glob string, err error) {
- root = item.root
-
- // If the root and the resolved path are equal, then dir must be empty
- // and the glob must be ".".
- if filepath.Clean(root) == filepath.Clean(item.resolved) {
- glob = "."
- return
- }
-
- glob, err = filepath.Rel(root, item.resolved)
- return
-}
-
-// preserveBasePath makes sure that the original base path (e.g., "/" or "./")
-// is preserved. The filepath API among tends to clean up a bit too much but
-// we *must* preserve this data by all means.
-func preserveBasePath(original, resolved string) string {
- // Handle "/"
- if strings.HasSuffix(original, "/") {
- if !strings.HasSuffix(resolved, "/") {
- resolved += "/"
- }
- return resolved
- }
-
- // Handle "/."
- if strings.HasSuffix(original, "/.") {
- if strings.HasSuffix(resolved, "/") { // could be root!
- resolved += "."
- } else if !strings.HasSuffix(resolved, "/.") {
- resolved += "/."
- }
- return resolved
- }
-
- return resolved
-}
-
-// secureStat extracts file info for path in a chroot'ed environment in root.
-func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) {
- var glob string
- var err error
-
- // If root and path are equal, then dir must be empty and the glob must
- // be ".".
- if filepath.Clean(root) == filepath.Clean(path) {
- glob = "."
- } else {
- glob, err = filepath.Rel(root, path)
- if err != nil {
- return nil, err
- }
- }
-
- globStats, err := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob})
- if err != nil {
- return nil, err
- }
-
- if len(globStats) != 1 {
- return nil, errors.Errorf("internal libpod error: secureStat: expected 1 item but got %d", len(globStats))
- }
-
- stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay
- if !exists {
- return stat, os.ErrNotExist
- }
-
- var statErr error
- if stat.Error != "" {
- statErr = errors.New(stat.Error)
- }
- return stat, statErr
-}
-
-// resolveContainerPaths resolves the container's mount point and the container
-// path as specified by the user. Both may resolve to paths outside of the
-// container's mount point when the container path hits a volume or bind mount.
-//
-// NOTE: We must take volumes and bind mounts into account as, regrettably, we
-// can copy to/from stopped containers. In that case, the volumes and bind
-// mounts are not present. For running containers, the runtime (e.g., runc or
-// crun) takes care of these mounts. For stopped ones, we need to do quite
-// some dance, as done below.
-func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) {
- // Let's first make sure we have a path relative to the mount point.
- pathRelativeToContainerMountPoint := containerPath
- if !filepath.IsAbs(containerPath) {
- // If the containerPath is not absolute, it's relative to the
- // container's working dir. To be extra careful, let's first
- // join the working dir with "/", and the add the containerPath
- // to it.
- pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath)
- }
- // NOTE: the secure join makes sure that we follow symlinks. This way,
- // we catch scenarios where the container path symlinks to a volume or
- // bind mount.
- resolvedPathOnTheContainerMountPoint, err := securejoin.SecureJoin(mountPoint, pathRelativeToContainerMountPoint)
- if err != nil {
- return "", "", err
- }
- pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint)
- pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint)
-
- // Now we have an "absolute container Path" but not yet resolved on the
- // host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to
- // check if "/foo/bar/file.txt" is on a volume or bind mount. To do
- // that, we need to walk *down* the paths to the root. Assuming
- // volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar",
- // we must select "/foo/bar". Once selected, we need to rebase the
- // remainder (i.e, "/file.txt") on the volume's mount point on the
- // host. Same applies to bind mounts.
-
- searchPath := pathRelativeToContainerMountPoint
- for {
- volume, err := findVolume(container, searchPath)
- if err != nil {
- return "", "", err
- }
- if volume != nil {
- logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)
- // We found a matching volume for searchPath. We now
- // need to first find the relative path of our input
- // path to the searchPath, and then join it with the
- // volume's mount point.
- pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
- absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(volume.MountPoint(), pathRelativeToVolume)
- if err != nil {
- return "", "", err
- }
- return volume.MountPoint(), absolutePathOnTheVolumeMount, nil
- }
-
- if mount := findBindMount(container, searchPath); mount != nil {
- logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath)
- // We found a matching bind mount for searchPath. We
- // now need to first find the relative path of our
- // input path to the searchPath, and then join it with
- // the source of the bind mount.
- pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
- absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
- if err != nil {
- return "", "", err
- }
- return mount.Source, absolutePathOnTheBindMount, nil
-
- }
-
- if searchPath == "/" {
- // Cannot go beyond "/", so we're done.
- break
- }
-
- // Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar").
- searchPath = filepath.Dir(searchPath)
- }
-
- // No volume, no bind mount but just a normal path on the container.
- return mountPoint, resolvedPathOnTheContainerMountPoint, nil
-}
-
-// findVolume checks if the specified container path matches a volume inside
-// the container. It returns a matching volume or nil.
-func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) {
- runtime := c.Runtime()
- cleanedContainerPath := filepath.Clean(containerPath)
- for _, vol := range c.Config().NamedVolumes {
- if cleanedContainerPath == filepath.Clean(vol.Dest) {
- return runtime.GetVolume(vol.Name)
- }
- }
- return nil, nil
-}
-
-// findBindMount checks if the specified container path matches a bind mount
-// inside the container. It returns a matching mount or nil.
-func findBindMount(c *libpod.Container, containerPath string) *specs.Mount {
- cleanedPath := filepath.Clean(containerPath)
- for _, m := range c.Config().Spec.Mounts {
- if m.Type != "bind" {
- continue
- }
- if cleanedPath == filepath.Clean(m.Destination) {
- mount := m
- return &mount
- }
- }
- return nil
-}
-
-// getIDMappingsAndPair returns the ID mappings for the container and the host
-// ID pair.
-func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) {
- user, err := getContainerUser(container, containerMount)
- if err != nil {
- return nil, nil, err
- }
-
- idMappingOpts, err := container.IDMappings()
- if err != nil {
- return nil, nil, err
- }
-
- hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID)
- if err != nil {
- return nil, nil, err
- }
-
- idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
- return &idMappingOpts, &idPair, nil
-}
-
-// getContainerUser returns the specs.User of the container.
-func getContainerUser(container *libpod.Container, mountPoint string) (specs.User, error) {
- userspec := container.Config().User
-
- uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec)
- u := specs.User{
- UID: uid,
- GID: gid,
- Username: userspec,
- }
-
- if !strings.Contains(userspec, ":") {
- groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
- if err2 != nil {
- if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
- err = err2
- }
- } else {
- u.AdditionalGids = groups
- }
- }
-
- return u, err
-}
-
-// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
-func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
- for _, idmap := range idMaps {
- tempIDMap := specs.LinuxIDMapping{
- ContainerID: uint32(idmap.ContainerID),
- HostID: uint32(idmap.HostID),
- Size: uint32(idmap.Size),
- }
- convertedIDMap = append(convertedIDMap, tempIDMap)
- }
- return convertedIDMap
-}
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index 01086a2b3..4442c0030 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -8,6 +8,7 @@ import (
"github.com/containers/image/v5/types"
"github.com/containers/podman/v2/libpod/define"
+ "github.com/containers/podman/v2/pkg/copy"
"github.com/containers/podman/v2/pkg/specgen"
"github.com/cri-o/ocicni/pkg/ocicni"
)
@@ -143,6 +144,10 @@ type ContainerInspectReport struct {
*define.InspectContainerData
}
+type ContainerStatReport struct {
+ copy.FileInfo
+}
+
type CommitOptions struct {
Author string
Changes []string
diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go
index ac9073402..80127ea45 100644
--- a/pkg/domain/entities/engine_container.go
+++ b/pkg/domain/entities/engine_container.go
@@ -2,6 +2,7 @@ package entities
import (
"context"
+ "io"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v2/libpod/define"
@@ -9,6 +10,8 @@ import (
"github.com/spf13/cobra"
)
+type ContainerCopyFunc func() error
+
type ContainerEngine interface {
AutoUpdate(ctx context.Context, options AutoUpdateOptions) (*AutoUpdateReport, []error)
Config(ctx context.Context) (*config.Config, error)
@@ -16,7 +19,8 @@ type ContainerEngine interface {
ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error)
ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error)
ContainerCommit(ctx context.Context, nameOrID string, options CommitOptions) (*CommitReport, error)
- ContainerCp(ctx context.Context, source, dest string, options ContainerCpOptions) error
+ ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (ContainerCopyFunc, error)
+ ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (ContainerCopyFunc, error)
ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error)
ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error)
ContainerExec(ctx context.Context, nameOrID string, options ExecOptions, streams define.AttachStreams) (int, error)
@@ -38,6 +42,7 @@ type ContainerEngine interface {
ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error)
ContainerRunlabel(ctx context.Context, label string, image string, args []string, opts ContainerRunlabelOptions) error
ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error)
+ ContainerStat(ctx context.Context, nameOrDir string, path string) (*ContainerStatReport, error)
ContainerStats(ctx context.Context, namesOrIds []string, options ContainerStatsOptions) (chan ContainerStatsReport, error)
ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error)
ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error)
diff --git a/pkg/domain/infra/abi/archive.go b/pkg/domain/infra/abi/archive.go
new file mode 100644
index 000000000..809813756
--- /dev/null
+++ b/pkg/domain/infra/abi/archive.go
@@ -0,0 +1,172 @@
+package abi
+
+import (
+ "context"
+ "io"
+ "strings"
+
+ buildahCopiah "github.com/containers/buildah/copier"
+ "github.com/containers/buildah/pkg/chrootuser"
+ "github.com/containers/buildah/util"
+ "github.com/containers/podman/v2/libpod"
+ "github.com/containers/podman/v2/pkg/domain/entities"
+ "github.com/containers/storage"
+ "github.com/containers/storage/pkg/archive"
+ "github.com/containers/storage/pkg/idtools"
+ "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// NOTE: Only the parent directory of the container path must exist. The path
+// itself may be created while copying.
+func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, containerPath string, reader io.Reader) (entities.ContainerCopyFunc, error) {
+ container, err := ic.Libpod.LookupContainer(nameOrID)
+ if err != nil {
+ return nil, err
+ }
+
+ unmount := func() {
+ if err := container.Unmount(false); err != nil {
+ logrus.Errorf("Error unmounting container: %v", err)
+ }
+ }
+
+ _, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath)
+ if err != nil {
+ unmount()
+ return nil, err
+ }
+
+ decompressed, err := archive.DecompressStream(reader)
+ if err != nil {
+ unmount()
+ return nil, err
+ }
+
+ idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot)
+ if err != nil {
+ unmount()
+ return nil, err
+ }
+
+ logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID())
+
+ return func() error {
+ defer unmount()
+ defer decompressed.Close()
+ putOptions := buildahCopiah.PutOptions{
+ UIDMap: idMappings.UIDMap,
+ GIDMap: idMappings.GIDMap,
+ ChownDirs: idPair,
+ ChownFiles: idPair,
+ }
+ return buildahCopiah.Put(resolvedRoot, resolvedContainerPath, putOptions, decompressed)
+ }, nil
+}
+
+func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, containerPath string, writer io.Writer) (entities.ContainerCopyFunc, error) {
+ container, err := ic.Libpod.LookupContainer(nameOrID)
+ if err != nil {
+ return nil, err
+ }
+
+ unmount := func() {
+ if err := container.Unmount(false); err != nil {
+ logrus.Errorf("Error unmounting container: %v", err)
+ }
+ }
+
+ // Make sure that "/" copies the *contents* of the mount point and not
+ // the directory.
+ if containerPath == "/" {
+ containerPath = "/."
+ }
+
+ _, resolvedRoot, resolvedContainerPath, err := ic.containerStat(container, containerPath)
+ if err != nil {
+ unmount()
+ return nil, err
+ }
+
+ idMappings, idPair, err := getIDMappingsAndPair(container, resolvedRoot)
+ if err != nil {
+ unmount()
+ return nil, err
+ }
+
+ logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", containerPath, resolvedContainerPath, container.Name(), container.ID())
+
+ return func() error {
+ defer container.Unmount(false)
+ getOptions := buildahCopiah.GetOptions{
+ // Unless the specified path ends with ".", we want to copy the base directory.
+ KeepDirectoryNames: !strings.HasSuffix(resolvedContainerPath, "."),
+ UIDMap: idMappings.UIDMap,
+ GIDMap: idMappings.GIDMap,
+ ChownDirs: idPair,
+ ChownFiles: idPair,
+ }
+ return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedContainerPath}, writer)
+ }, nil
+}
+
+// getIDMappingsAndPair returns the ID mappings for the container and the host
+// ID pair.
+func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) {
+ user, err := getContainerUser(container, containerMount)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ idMappingOpts, err := container.IDMappings()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
+ return &idMappingOpts, &idPair, nil
+}
+
+// getContainerUser returns the specs.User of the container.
+func getContainerUser(container *libpod.Container, mountPoint string) (specs.User, error) {
+ userspec := container.Config().User
+
+ uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec)
+ u := specs.User{
+ UID: uid,
+ GID: gid,
+ Username: userspec,
+ }
+
+ if !strings.Contains(userspec, ":") {
+ groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
+ if err2 != nil {
+ if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
+ err = err2
+ }
+ } else {
+ u.AdditionalGids = groups
+ }
+ }
+
+ return u, err
+}
+
+// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
+func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
+ for _, idmap := range idMaps {
+ tempIDMap := specs.LinuxIDMapping{
+ ContainerID: uint32(idmap.ContainerID),
+ HostID: uint32(idmap.HostID),
+ Size: uint32(idmap.Size),
+ }
+ convertedIDMap = append(convertedIDMap, tempIDMap)
+ }
+ return convertedIDMap
+}
diff --git a/pkg/domain/infra/abi/containers_stat.go b/pkg/domain/infra/abi/containers_stat.go
new file mode 100644
index 000000000..c9610d1b9
--- /dev/null
+++ b/pkg/domain/infra/abi/containers_stat.go
@@ -0,0 +1,251 @@
+package abi
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+
+ buildahCopiah "github.com/containers/buildah/copier"
+ "github.com/containers/podman/v2/libpod"
+ "github.com/containers/podman/v2/pkg/copy"
+ "github.com/containers/podman/v2/pkg/domain/entities"
+ securejoin "github.com/cyphar/filepath-securejoin"
+ "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+func (ic *ContainerEngine) containerStat(container *libpod.Container, containerPath string) (*entities.ContainerStatReport, string, string, error) {
+ containerMountPoint, err := container.Mount()
+ if err != nil {
+ return nil, "", "", err
+ }
+
+ // Make sure that "/" copies the *contents* of the mount point and not
+ // the directory.
+ if containerPath == "/" {
+ containerPath += "/."
+ }
+
+ // Now resolve the container's path. It may hit a volume, it may hit a
+ // bind mount, it may be relative.
+ resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath)
+ if err != nil {
+ return nil, "", "", err
+ }
+
+ statInfo, statInfoErr := secureStat(resolvedRoot, resolvedContainerPath)
+ if statInfoErr != nil {
+ // Not all errors from secureStat map to ErrNotExist, so we
+ // have to look into the error string. Turning it into an
+ // ENOENT let's the API handlers return the correct status code
+ // which is crucuial for the remote client.
+ if os.IsNotExist(err) || strings.Contains(statInfoErr.Error(), "o such file or directory") {
+ statInfoErr = copy.ENOENT
+ }
+ // If statInfo is nil, there's nothing we can do anymore. A
+ // non-nil statInfo may indicate a symlink where we must have
+ // a closer look.
+ if statInfo == nil {
+ return nil, "", "", statInfoErr
+ }
+ }
+
+ // Now make sure that the info's LinkTarget is relative to the
+ // container's mount.
+ var absContainerPath string
+
+ if statInfo.IsSymlink {
+ // Evaluated symlinks are always relative to the container's mount point.
+ absContainerPath = statInfo.ImmediateTarget
+ } else if strings.HasPrefix(resolvedContainerPath, containerMountPoint) {
+ // If the path is on the container's mount point, strip it off.
+ absContainerPath = strings.TrimPrefix(resolvedContainerPath, containerMountPoint)
+ absContainerPath = filepath.Join("/", absContainerPath)
+ } else {
+ // No symlink and not on the container's mount point, so let's
+ // move it back to the original input. It must have evaluated
+ // to a volume or bind mount but we cannot return host paths.
+ absContainerPath = containerPath
+ }
+
+ // Now we need to make sure to preseve the base path as specified by
+ // the user. The `filepath` packages likes to remove trailing slashes
+ // and dots that are crucial to the copy logic.
+ absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath)
+ resolvedContainerPath = copy.PreserveBasePath(containerPath, resolvedContainerPath)
+
+ info := copy.FileInfo{
+ IsDir: statInfo.IsDir,
+ Name: filepath.Base(absContainerPath),
+ Size: statInfo.Size,
+ Mode: statInfo.Mode,
+ ModTime: statInfo.ModTime,
+ LinkTarget: absContainerPath,
+ }
+
+ return &entities.ContainerStatReport{FileInfo: info}, resolvedRoot, resolvedContainerPath, statInfoErr
+}
+
+func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, containerPath string) (*entities.ContainerStatReport, error) {
+ container, err := ic.Libpod.LookupContainer(nameOrID)
+ if err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ if err := container.Unmount(false); err != nil {
+ logrus.Errorf("Error unmounting container: %v", err)
+ }
+ }()
+
+ statReport, _, _, err := ic.containerStat(container, containerPath)
+ return statReport, err
+}
+
+// resolveContainerPaths resolves the container's mount point and the container
+// path as specified by the user. Both may resolve to paths outside of the
+// container's mount point when the container path hits a volume or bind mount.
+//
+// NOTE: We must take volumes and bind mounts into account as, regrettably, we
+// can copy to/from stopped containers. In that case, the volumes and bind
+// mounts are not present. For running containers, the runtime (e.g., runc or
+// crun) takes care of these mounts. For stopped ones, we need to do quite
+// some dance, as done below.
+func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) {
+ // Let's first make sure we have a path relative to the mount point.
+ pathRelativeToContainerMountPoint := containerPath
+ if !filepath.IsAbs(containerPath) {
+ // If the containerPath is not absolute, it's relative to the
+ // container's working dir. To be extra careful, let's first
+ // join the working dir with "/", and the add the containerPath
+ // to it.
+ pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath)
+ }
+ resolvedPathOnTheContainerMountPoint := filepath.Join(mountPoint, pathRelativeToContainerMountPoint)
+ pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint)
+ pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint)
+
+ // Now we have an "absolute container Path" but not yet resolved on the
+ // host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to
+ // check if "/foo/bar/file.txt" is on a volume or bind mount. To do
+ // that, we need to walk *down* the paths to the root. Assuming
+ // volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar",
+ // we must select "/foo/bar". Once selected, we need to rebase the
+ // remainder (i.e, "/file.txt") on the volume's mount point on the
+ // host. Same applies to bind mounts.
+
+ searchPath := pathRelativeToContainerMountPoint
+ for {
+ volume, err := findVolume(container, searchPath)
+ if err != nil {
+ return "", "", err
+ }
+ if volume != nil {
+ logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)
+ // We found a matching volume for searchPath. We now
+ // need to first find the relative path of our input
+ // path to the searchPath, and then join it with the
+ // volume's mount point.
+ pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
+ absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(volume.MountPoint(), pathRelativeToVolume)
+ if err != nil {
+ return "", "", err
+ }
+ return volume.MountPoint(), absolutePathOnTheVolumeMount, nil
+ }
+
+ if mount := findBindMount(container, searchPath); mount != nil {
+ logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath)
+ // We found a matching bind mount for searchPath. We
+ // now need to first find the relative path of our
+ // input path to the searchPath, and then join it with
+ // the source of the bind mount.
+ pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
+ absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
+ if err != nil {
+ return "", "", err
+ }
+ return mount.Source, absolutePathOnTheBindMount, nil
+
+ }
+
+ if searchPath == "/" {
+ // Cannot go beyond "/", so we're done.
+ break
+ }
+
+ // Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar").
+ searchPath = filepath.Dir(searchPath)
+ }
+
+ // No volume, no bind mount but just a normal path on the container.
+ return mountPoint, resolvedPathOnTheContainerMountPoint, nil
+}
+
+// findVolume checks if the specified container path matches a volume inside
+// the container. It returns a matching volume or nil.
+func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) {
+ runtime := c.Runtime()
+ cleanedContainerPath := filepath.Clean(containerPath)
+ for _, vol := range c.Config().NamedVolumes {
+ if cleanedContainerPath == filepath.Clean(vol.Dest) {
+ return runtime.GetVolume(vol.Name)
+ }
+ }
+ return nil, nil
+}
+
+// findBindMount checks if the specified container path matches a bind mount
+// inside the container. It returns a matching mount or nil.
+func findBindMount(c *libpod.Container, containerPath string) *specs.Mount {
+ cleanedPath := filepath.Clean(containerPath)
+ for _, m := range c.Config().Spec.Mounts {
+ if m.Type != "bind" {
+ continue
+ }
+ if cleanedPath == filepath.Clean(m.Destination) {
+ mount := m
+ return &mount
+ }
+ }
+ return nil
+}
+
+// secureStat extracts file info for path in a chroot'ed environment in root.
+func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) {
+ var glob string
+ var err error
+
+ // If root and path are equal, then dir must be empty and the glob must
+ // be ".".
+ if filepath.Clean(root) == filepath.Clean(path) {
+ glob = "."
+ } else {
+ glob, err = filepath.Rel(root, path)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ globStats, err := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob})
+ if err != nil {
+ return nil, err
+ }
+
+ if len(globStats) != 1 {
+ return nil, errors.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats))
+ }
+
+ stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay
+ if !exists {
+ return nil, copy.ENOENT
+ }
+
+ var statErr error
+ if stat.Error != "" {
+ statErr = errors.New(stat.Error)
+ }
+ return stat, statErr
+}
diff --git a/pkg/domain/infra/abi/cp.go b/pkg/domain/infra/abi/cp.go
deleted file mode 100644
index 362053cce..000000000
--- a/pkg/domain/infra/abi/cp.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package abi
-
-import (
- "context"
-
- "github.com/containers/podman/v2/libpod"
- "github.com/containers/podman/v2/pkg/copy"
- "github.com/containers/podman/v2/pkg/domain/entities"
-)
-
-func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error {
- // Parse user input.
- sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(source, dest)
- if err != nil {
- return err
- }
-
- // Look up containers.
- var sourceContainer, destContainer *libpod.Container
- if len(sourceContainerStr) > 0 {
- sourceContainer, err = ic.Libpod.LookupContainer(sourceContainerStr)
- if err != nil {
- return err
- }
- }
- if len(destContainerStr) > 0 {
- destContainer, err = ic.Libpod.LookupContainer(destContainerStr)
- if err != nil {
- return err
- }
- }
-
- var sourceItem, destinationItem copy.CopyItem
-
- // Source ... container OR host.
- if sourceContainer != nil {
- sourceItem, err = copy.CopyItemForContainer(sourceContainer, sourcePath, options.Pause, true)
- defer sourceItem.CleanUp()
- if err != nil {
- return err
- }
- } else {
- sourceItem, err = copy.CopyItemForHost(sourcePath, true)
- if err != nil {
- return err
- }
- }
-
- // Destination ... container OR host.
- if destContainer != nil {
- destinationItem, err = copy.CopyItemForContainer(destContainer, destPath, options.Pause, false)
- defer destinationItem.CleanUp()
- if err != nil {
- return err
- }
- } else {
- destinationItem, err = copy.CopyItemForHost(destPath, false)
- defer destinationItem.CleanUp()
- if err != nil {
- return err
- }
- }
-
- // Copy from the host to the container.
- copier, err := copy.GetCopier(&sourceItem, &destinationItem, options.Extract)
- if err != nil {
- return err
- }
- return copier.Copy()
-}
diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go
index ad7688f62..855339fef 100644
--- a/pkg/domain/infra/tunnel/containers.go
+++ b/pkg/domain/infra/tunnel/containers.go
@@ -772,9 +772,16 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o
return reports, nil
}
-func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error {
- return nil
- // return containers.Copy(ic.ClientCxt, source, dest, options)
+func (ic *ContainerEngine) ContainerCopyFromArchive(ctx context.Context, nameOrID string, path string, reader io.Reader) (entities.ContainerCopyFunc, error) {
+ return containers.CopyFromArchive(ic.ClientCxt, nameOrID, path, reader)
+}
+
+func (ic *ContainerEngine) ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (entities.ContainerCopyFunc, error) {
+ return containers.CopyToArchive(ic.ClientCxt, nameOrID, path, writer)
+}
+
+func (ic *ContainerEngine) ContainerStat(ctx context.Context, nameOrID string, path string) (*entities.ContainerStatReport, error) {
+ return containers.Stat(ic.ClientCxt, nameOrID, path)
}
// Shutdown Libpod engine
diff --git a/pkg/errorhandling/errorhandling.go b/pkg/errorhandling/errorhandling.go
index ca6b60bc5..21df261fb 100644
--- a/pkg/errorhandling/errorhandling.go
+++ b/pkg/errorhandling/errorhandling.go
@@ -19,7 +19,12 @@ func JoinErrors(errs []error) error {
// blank lines when printing the error.
var multiE *multierror.Error
multiE = multierror.Append(multiE, errs...)
- return errors.New(strings.TrimSpace(multiE.ErrorOrNil().Error()))
+
+ finalErr := multiE.ErrorOrNil()
+ if finalErr == nil {
+ return finalErr
+ }
+ return errors.New(strings.TrimSpace(finalErr.Error()))
}
// ErrorsToString converts the slice of errors into a slice of corresponding