From ccbca0b4abfe5daec59b3193a6bff077d817fd18 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Thu, 19 Nov 2020 10:06:19 +0100 Subject: rewrite podman-cp * Add a new `pkg/copy` to centralize all container-copy related code. * The new code is based on Buildah's `copier` package. * The compat `/archive` endpoints use the new `copy` package. * Update docs and an several new tests. * Includes many fixes, most notably, the look-up of volumes and mounts. Breaking changes: * Podman is now expecting that container-destination paths exist. Before, Podman created the paths if needed. Docker does not do that and I believe Podman should not either as it's a recipe for masking errors. These errors may be user induced (e.g., a path typo), or internal typos (e.g., when the destination may be a mistakenly unmounted volume). Let's keep the magic low for such a security sensitive feature. Signed-off-by: Valentin Rothberg --- pkg/api/handlers/compat/containers_archive.go | 319 +++++--------------------- 1 file changed, 59 insertions(+), 260 deletions(-) (limited to 'pkg/api') diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go index 1dd563393..223eb2cd5 100644 --- a/pkg/api/handlers/compat/containers_archive.go +++ b/pkg/api/handlers/compat/containers_archive.go @@ -5,24 +5,16 @@ import ( "encoding/base64" "encoding/json" "fmt" - "path/filepath" - "strings" - - "github.com/containers/buildah/copier" - "github.com/containers/buildah/pkg/chrootuser" - "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/storage/pkg/idtools" - "github.com/opencontainers/runtime-spec/specs-go" - "net/http" "os" "time" + "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/gorilla/schema" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func Archive(w http.ResponseWriter, r *http.Request) { @@ -32,14 +24,14 @@ func Archive(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: handlePut(w, r, decoder, runtime) - case http.MethodGet, http.MethodHead: - handleHeadOrGet(w, r, decoder, runtime) + case http.MethodHead, http.MethodGet: + handleHeadAndGet(w, r, decoder, runtime) default: - utils.Error(w, fmt.Sprintf("not implemented, method: %v", r.Method), http.StatusNotImplemented, errors.New(fmt.Sprintf("not implemented, method: %v", r.Method))) + utils.Error(w, fmt.Sprintf("unsupported method: %v", r.Method), http.StatusNotImplemented, errors.New(fmt.Sprintf("unsupported method: %v", r.Method))) } } -func handleHeadOrGet(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { +func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { query := struct { Path string `schema:"path"` }{} @@ -66,170 +58,62 @@ func handleHeadOrGet(w http.ResponseWriter, r *http.Request, decoder *schema.Dec return } - mountPoint, err := ctr.Mount() + source, err := copy.CopyItemForContainer(ctr, query.Path, true, true) + defer source.CleanUp() if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to mount the container")) + utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path)) return } - defer func() { - if err := ctr.Unmount(false); err != nil { - logrus.Warnf("failed to unmount container %s: %q", containerName, err) - } - }() - - opts := copier.StatOptions{} - - mountPoint, path, err := fixUpMountPointAndPath(runtime, ctr, mountPoint, query.Path) + // NOTE: Docker always sets the header. + info, err := source.Stat() if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path)) return } - - stats, err := copier.Stat(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)}) + statHeader, err := fileInfoToDockerStats(info) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to get stats about file")) - return - } - - if len(stats) <= 0 || len(stats[0].Globbed) <= 0 { - errs := make([]string, 0, len(stats)) - - for _, stat := range stats { - if stat.Error != "" { - errs = append(errs, stat.Error) - } - } - - utils.Error(w, "Not found.", http.StatusNotFound, fmt.Errorf("file doesn't exist (errs: %q)", strings.Join(errs, ";"))) - - return - } - - statHeader, err := statsToHeader(stats[0].Results[stats[0].Globbed[0]]) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - w.Header().Add("X-Docker-Container-Path-Stat", statHeader) - if r.Method == http.MethodGet { - idMappingOpts, err := ctr.IDMappings() - if err != nil { - utils.Error(w, "Not found.", http.StatusInternalServerError, - errors.Wrapf(err, "error getting IDMappingOptions")) - return - } - - destOwner := idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()} - - opts := copier.GetOptions{ - UIDMap: idMappingOpts.UIDMap, - GIDMap: idMappingOpts.GIDMap, - ChownDirs: &destOwner, - ChownFiles: &destOwner, - KeepDirectoryNames: true, - } - - w.WriteHeader(http.StatusOK) - - err = copier.Get(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)}, w) - if err != nil { - logrus.Error(errors.Wrapf(err, "failed to copy from the %s container path %s", containerName, query.Path)) - return - } - } else { + // Our work is done when the user is interested in the header only. + if r.Method == http.MethodHead { w.WriteHeader(http.StatusOK) - } -} - -func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { - query := struct { - Path string `schema:"path"` - // TODO handle params below - NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` - CopyUIDGID bool `schema:"copyUIDGID"` - }{} - - err := decoder.Decode(&query, r.URL.Query()) - if err != nil { - utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) 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 - } - - mountPoint, err := ctr.Mount() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, errors.Wrapf(err, "failed to mount the %s container", ctrName)) - return - } - - defer func() { - if err := ctr.Unmount(false); err != nil { - logrus.Warnf("failed to unmount container %s", ctrName) - } - }() - - user, err := getUser(mountPoint, ctr.User()) - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - - idMappingOpts, err := ctr.IDMappings() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, errors.Wrapf(err, "error getting IDMappingOptions")) - return - } - - destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} - - opts := copier.PutOptions{ - UIDMap: idMappingOpts.UIDMap, - GIDMap: idMappingOpts.GIDMap, - ChownDirs: &destOwner, - ChownFiles: &destOwner, - } - - mountPoint, path, err := fixUpMountPointAndPath(runtime, ctr, mountPoint, query.Path) + // 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 } w.WriteHeader(http.StatusOK) - - err = copier.Put(mountPoint, filepath.Join(mountPoint, path), opts, r.Body) - if err != nil { - logrus.Error(errors.Wrapf(err, "failed to copy to the %s container path %s", ctrName, query.Path)) + if err := copy.Copy(&source, &destination, false); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } } -func statsToHeader(stats *copier.StatForItem) (string, error) { - statsDTO := struct { +func fileInfoToDockerStats(info *copy.FileInfo) (string, error) { + dockerStats := struct { Name string `json:"name"` Size int64 `json:"size"` Mode os.FileMode `json:"mode"` ModTime time.Time `json:"mtime"` LinkTarget string `json:"linkTarget"` }{ - Name: filepath.Base(stats.Name), - Size: stats.Size, - Mode: stats.Mode, - ModTime: stats.ModTime, - LinkTarget: stats.ImmediateTarget, + Name: info.Name, + Size: info.Size, + Mode: info.Mode, + ModTime: info.ModTime, + LinkTarget: info.LinkTarget, } - jsonBytes, err := json.Marshal(&statsDTO) + jsonBytes, err := json.Marshal(&dockerStats) if err != nil { return "", errors.Wrap(err, "failed to serialize file stats") } @@ -250,130 +134,45 @@ func statsToHeader(stats *copier.StatForItem) (string, error) { return buff.String(), nil } -// the utility functions below are copied from abi/cp.go - -func getUser(mountPoint string, userspec string) (specs.User, error) { - 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 -} - -func fixUpMountPointAndPath(runtime *libpod.Runtime, ctr *libpod.Container, mountPoint, ctrPath string) (string, string, error) { - if !filepath.IsAbs(ctrPath) { - endsWithSep := strings.HasSuffix(ctrPath, string(filepath.Separator)) - ctrPath = filepath.Join(ctr.WorkingDir(), ctrPath) - - if endsWithSep { - ctrPath = ctrPath + string(filepath.Separator) - } - } - if isVol, volDestName, volName := isVolumeDestName(ctrPath, ctr); isVol { //nolint(gocritic) - newMountPoint, path, err := pathWithVolumeMount(runtime, volDestName, volName, ctrPath) - if err != nil { - return "", "", errors.Wrapf(err, "error getting source path from volume %s", volDestName) - } - - mountPoint = newMountPoint - ctrPath = path - } else if isBindMount, mount := isBindMountDestName(ctrPath, ctr); isBindMount { //nolint(gocritic) - newMountPoint, path := pathWithBindMountSource(mount, ctrPath) - mountPoint = newMountPoint - ctrPath = path - } - - return mountPoint, ctrPath, nil -} - -func isVolumeDestName(path string, ctr *libpod.Container) (bool, string, string) { - separator := string(os.PathSeparator) - - if filepath.IsAbs(path) { - path = strings.TrimPrefix(path, separator) - } - - if path == "" { - return false, "", "" - } - - for _, vol := range ctr.Config().NamedVolumes { - volNamePath := strings.TrimPrefix(vol.Dest, separator) - if matchVolumePath(path, volNamePath) { - return true, vol.Dest, vol.Name - } - } - - return false, "", "" -} +func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { + query := struct { + Path string `schema:"path"` + // TODO handle params below + NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` + CopyUIDGID bool `schema:"copyUIDGID"` + }{} -func pathWithVolumeMount(runtime *libpod.Runtime, volDestName, volName, path string) (string, string, error) { - destVolume, err := runtime.GetVolume(volName) + err := decoder.Decode(&query, r.URL.Query()) if err != nil { - return "", "", errors.Wrapf(err, "error getting volume destination %s", volName) - } - - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) + utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) + return } - return destVolume.MountPoint(), strings.TrimPrefix(path, volDestName), err -} - -func isBindMountDestName(path string, ctr *libpod.Container) (bool, specs.Mount) { - separator := string(os.PathSeparator) - - if filepath.IsAbs(path) { - path = strings.TrimPrefix(path, string(os.PathSeparator)) - } + ctrName := utils.GetName(r) - if path == "" { - return false, specs.Mount{} + 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 } - for _, m := range ctr.Config().Spec.Mounts { - if m.Type != "bind" { - continue - } - - mDest := strings.TrimPrefix(m.Destination, separator) - if matchVolumePath(path, mDest) { - return true, m - } + 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 } - return false, specs.Mount{} -} - -func matchVolumePath(path, target string) bool { - pathStr := filepath.Clean(path) - target = filepath.Clean(target) - - for len(pathStr) > len(target) && strings.Contains(pathStr, string(os.PathSeparator)) { - pathStr = pathStr[:strings.LastIndex(pathStr, string(os.PathSeparator))] + source, err := copy.CopyItemForReader(r.Body) + defer source.CleanUp() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return } - return pathStr == target -} - -func pathWithBindMountSource(m specs.Mount, path string) (string, string) { - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) + w.WriteHeader(http.StatusOK) + if err := copy.Copy(&source, &destination, false); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return } - - return m.Source, strings.TrimPrefix(path, m.Destination) } -- cgit v1.2.3-54-g00ecf