diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/api/handlers/compat/containers.go | 7 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_archive.go | 319 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_pause.go | 2 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_restart.go | 2 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_start.go | 4 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_stop.go | 4 | ||||
-rw-r--r-- | pkg/api/handlers/compat/containers_unpause.go | 2 | ||||
-rw-r--r-- | pkg/api/handlers/compat/images_push.go | 1 | ||||
-rw-r--r-- | pkg/api/handlers/types.go | 16 | ||||
-rw-r--r-- | pkg/copy/copy.go | 188 | ||||
-rw-r--r-- | pkg/copy/item.go | 601 | ||||
-rw-r--r-- | pkg/domain/entities/containers.go | 10 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/entities/images.go | 22 | ||||
-rw-r--r-- | pkg/domain/infra/abi/cp.go | 439 | ||||
-rw-r--r-- | pkg/domain/infra/abi/images_list.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/containers.go | 4 | ||||
-rw-r--r-- | pkg/util/utils.go | 5 |
18 files changed, 927 insertions, 703 deletions
diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 5886455e7..7a3e5dd84 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -73,7 +74,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } func ListContainers(w http.ResponseWriter, r *http.Request) { @@ -207,7 +208,7 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { } } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } func WaitContainer(w http.ResponseWriter, r *http.Request) { @@ -215,8 +216,10 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/wait exitCode, err := utils.WaitContainer(w, r) if err != nil { + logrus.Warnf("failed to wait on container %q: %v", mux.Vars(r)["name"], err) return } + utils.WriteResponse(w, http.StatusOK, handlers.ContainerWaitOKBody{ StatusCode: int(exitCode), Error: struct { 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) } diff --git a/pkg/api/handlers/compat/containers_pause.go b/pkg/api/handlers/compat/containers_pause.go index 8712969c0..a7e0a66f1 100644 --- a/pkg/api/handlers/compat/containers_pause.go +++ b/pkg/api/handlers/compat/containers_pause.go @@ -24,5 +24,5 @@ func PauseContainer(w http.ResponseWriter, r *http.Request) { return } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_restart.go b/pkg/api/handlers/compat/containers_restart.go index f4d8f06a1..e8928596a 100644 --- a/pkg/api/handlers/compat/containers_restart.go +++ b/pkg/api/handlers/compat/containers_restart.go @@ -41,5 +41,5 @@ func RestartContainer(w http.ResponseWriter, r *http.Request) { } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_start.go b/pkg/api/handlers/compat/containers_start.go index 6236b1357..726da6f99 100644 --- a/pkg/api/handlers/compat/containers_start.go +++ b/pkg/api/handlers/compat/containers_start.go @@ -39,12 +39,12 @@ func StartContainer(w http.ResponseWriter, r *http.Request) { return } if state == define.ContainerStateRunning { - utils.WriteResponse(w, http.StatusNotModified, "") + utils.WriteResponse(w, http.StatusNotModified, nil) return } if err := con.Start(r.Context(), len(con.PodID()) > 0); err != nil { utils.InternalServerError(w, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_stop.go b/pkg/api/handlers/compat/containers_stop.go index 13fe25338..8bc58cf59 100644 --- a/pkg/api/handlers/compat/containers_stop.go +++ b/pkg/api/handlers/compat/containers_stop.go @@ -40,7 +40,7 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { } // If the Container is stopped already, send a 304 if state == define.ContainerStateStopped || state == define.ContainerStateExited { - utils.WriteResponse(w, http.StatusNotModified, "") + utils.WriteResponse(w, http.StatusNotModified, nil) return } @@ -56,5 +56,5 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_unpause.go b/pkg/api/handlers/compat/containers_unpause.go index f87b95b64..760e85814 100644 --- a/pkg/api/handlers/compat/containers_unpause.go +++ b/pkg/api/handlers/compat/containers_unpause.go @@ -24,5 +24,5 @@ func UnpauseContainer(w http.ResponseWriter, r *http.Request) { } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/images_push.go b/pkg/api/handlers/compat/images_push.go index 12593a68c..0f3da53e8 100644 --- a/pkg/api/handlers/compat/images_push.go +++ b/pkg/api/handlers/compat/images_push.go @@ -81,5 +81,4 @@ func PushImage(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, "") - } diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 40cf16807..25e7640b6 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -145,13 +145,14 @@ type PodCreateConfig struct { Share string `json:"share"` } +// HistoryResponse provides details on image layers type HistoryResponse struct { - ID string `json:"Id"` - Created int64 `json:"Created"` - CreatedBy string `json:"CreatedBy"` - Tags []string `json:"Tags"` - Size int64 `json:"Size"` - Comment string `json:"Comment"` + ID string `json:"Id"` + Created int64 + CreatedBy string + Tags []string + Size int64 + Comment string } type ImageLayer struct{} @@ -213,6 +214,7 @@ func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { ID: l.ID(), ParentId: l.Parent, RepoTags: repoTags, + RepoDigests: digests, Created: l.Created().Unix(), Size: int64(*size), SharedSize: 0, @@ -223,7 +225,6 @@ func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { Dangling: l.Dangling(), Names: l.Names(), Digest: string(l.Digest()), - Digests: digests, ConfigDigest: string(l.ConfigDigest), History: l.NamesHistory(), } @@ -329,7 +330,6 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI dockerImageInspect.Parent = d.Parent.String() } return &ImageInspect{dockerImageInspect}, nil - } // portsToPortSet converts libpods exposed ports to dockers structs diff --git a/pkg/copy/copy.go b/pkg/copy/copy.go new file mode 100644 index 000000000..0e68eb450 --- /dev/null +++ b/pkg/copy/copy.go @@ -0,0 +1,188 @@ +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. +// +// **************************************************************************** + +// Copy the source item to destination. Use extract to untar the source if +// it's a tar archive. +func Copy(source *CopyItem, destination *CopyItem, extract bool) error { + // First, do the man-page dance. See podman-cp(1) for details. + if err := enforceCopyRules(source, destination); err != nil { + return 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 { + _, err := io.Copy(destination.writer, source.reader) + return err + } + root, glob, err := source.buildahGlobs() + if err != nil { + return err + } + return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer) + } + + // Destination is either a file or a directory. + if source.info.IsStream { + return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader) + } + + 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 errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original) + } + + reader, err := os.Open(source.resolved) + if err != nil { + return err + } + defer reader.Close() + + // The stream from stdin may be compressed (e.g., via gzip). + decompressedStream, err := archive.DecompressStream(reader) + if err != nil { + return err + } + + defer decompressedStream.Close() + tarReader = decompressedStream + } else { + reader, err := archive.TarWithOptions(source.resolved, tarOptions) + if err != nil { + return err + } + defer reader.Close() + tarReader = reader + } + + return buildahCopiah.Put(root, dir, source.putOptions(), tarReader) +} + +// 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 + } + + // Source is a *stream*. + 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/item.go b/pkg/copy/item.go new file mode 100644 index 000000000..db6bca610 --- /dev/null +++ b/pkg/copy/item.go @@ -0,0 +1,601 @@ +package copy + +import ( + "io" + "os" + "path/filepath" + "strings" + "time" + + 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() + +// FileInfo describes a file or directory and is returned by +// (*CopyItem).Stat(). +type FileInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + ModTime time.Time `json:"mtime"` + IsDir bool `json:"isDir"` + IsStream bool `json:"isStream"` + LinkTarget string `json:"linkTarget"` +} + +// 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 3fd7c79f4..39d679eaf 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -403,16 +403,14 @@ type ContainerPortReport struct { Ports []ocicni.PortMapping } -// ContainerCpOptions describes input options for cp +// ContainerCpOptions describes input options for cp. type ContainerCpOptions struct { - Pause bool + // Pause the container while copying. + Pause bool + // Extract the tarfile into the destination directory. Extract bool } -// ContainerCpReport describes the output from a cp operation -type ContainerCpReport struct { -} - // ContainerStatsOptions describes input options for getting // stats on containers type ContainerStatsOptions struct { diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index df7da616a..e1f40e307 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -16,7 +16,7 @@ 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) (*ContainerCpReport, error) + ContainerCp(ctx context.Context, source, dest string, options ContainerCpOptions) 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) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index ab545d882..81f12bff7 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -51,22 +51,22 @@ func (i *Image) Id() string { // nolint } type ImageSummary struct { - ID string `json:"Id"` - ParentId string // nolint - RepoTags []string `json:",omitempty"` + ID string `json:"Id"` + ParentId string // nolint + RepoTags []string + RepoDigests []string Created int64 - Size int64 `json:",omitempty"` - SharedSize int `json:",omitempty"` - VirtualSize int64 `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Containers int `json:",omitempty"` - ReadOnly bool `json:",omitempty"` - Dangling bool `json:",omitempty"` + Size int64 + SharedSize int + VirtualSize int64 + Labels map[string]string + Containers int + ReadOnly bool `json:",omitempty"` + Dangling bool `json:",omitempty"` // Podman extensions Names []string `json:",omitempty"` Digest string `json:",omitempty"` - Digests []string `json:",omitempty"` ConfigDigest string `json:",omitempty"` History []string `json:",omitempty"` } diff --git a/pkg/domain/infra/abi/cp.go b/pkg/domain/infra/abi/cp.go index 8f4f5d3d7..9409df743 100644 --- a/pkg/domain/infra/abi/cp.go +++ b/pkg/domain/infra/abi/cp.go @@ -1,195 +1,70 @@ package abi import ( - "archive/tar" "context" - "fmt" - "io" - "os" - "path/filepath" "strings" - "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/copy" "github.com/containers/podman/v2/pkg/domain/entities" - "github.com/containers/storage" - "github.com/containers/storage/pkg/chrootarchive" - "github.com/containers/storage/pkg/idtools" - securejoin "github.com/cyphar/filepath-securejoin" - "github.com/docker/docker/pkg/archive" - "github.com/opencontainers/go-digest" - "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) -func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) (*entities.ContainerCpReport, error) { - extract := options.Extract - +func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error { srcCtr, srcPath := parsePath(ic.Libpod, source) destCtr, destPath := parsePath(ic.Libpod, dest) - if (srcCtr == nil && destCtr == nil) || (srcCtr != nil && destCtr != nil) { - return nil, errors.Errorf("invalid arguments %s, %s you must use just one container", source, dest) + if srcCtr != nil && destCtr != nil { + return errors.Errorf("invalid arguments %q, %q: you must use just one container", source, dest) } - - if len(srcPath) == 0 || len(destPath) == 0 { - return nil, errors.Errorf("invalid arguments %s, %s you must specify paths", source, dest) + if srcCtr == nil && destCtr == nil { + return errors.Errorf("invalid arguments %q, %q: you must specify one container", source, dest) } - ctr := srcCtr - isFromHostToCtr := ctr == nil - if isFromHostToCtr { - ctr = destCtr + if len(srcPath) == 0 || len(destPath) == 0 { + return errors.Errorf("invalid arguments %q, %q: you must specify paths", source, dest) } - mountPoint, err := ctr.Mount() - if err != nil { - return nil, err - } - defer func() { - if err := ctr.Unmount(false); err != nil { - logrus.Errorf("unable to umount container '%s': %q", ctr.ID(), err) + var sourceItem, destinationItem copy.CopyItem + var err error + // Copy from the container to the host. + if srcCtr != nil { + sourceItem, err = copy.CopyItemForContainer(srcCtr, srcPath, options.Pause, true) + defer sourceItem.CleanUp() + if err != nil { + return err } - }() - - if options.Pause { - if err := ctr.Pause(); err != nil { - // An invalid state error is fine. - // The container isn't running or is already paused. - // TODO: We can potentially start the container while - // the copy is running, which still allows a race where - // malicious code could mess with the symlink. - if errors.Cause(err) != define.ErrCtrStateInvalid { - return nil, err - } - } else { - // Only add the defer if we actually paused - defer func() { - if err := ctr.Unpause(); err != nil { - logrus.Errorf("Error unpausing container after copying: %v", err) - } - }() + } else { + sourceItem, err = copy.CopyItemForHost(srcPath, true) + if err != nil { + return err } } - user, err := getUser(mountPoint, ctr.User()) - if err != nil { - return nil, err - } - idMappingOpts, err := ctr.IDMappings() - if err != nil { - return nil, errors.Wrapf(err, "error getting IDMappingOptions") - } - destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} - hostUID, hostGID, err := util.GetHostIDs(convertIDMap(idMappingOpts.UIDMap), convertIDMap(idMappingOpts.GIDMap), user.UID, user.GID) - if err != nil { - return nil, err - } - - hostOwner := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} - - if isFromHostToCtr { - if isVol, volDestName, volName := isVolumeDestName(destPath, ctr); isVol { //nolint(gocritic) - path, err := pathWithVolumeMount(ic.Libpod, volDestName, volName, destPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting destination path from volume %s", volDestName) - } - destPath = path - } else if isBindMount, mount := isBindMountDestName(destPath, ctr); isBindMount { //nolint(gocritic) - path, err := pathWithBindMountSource(mount, destPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting destination path from bind mount %s", mount.Destination) - } - destPath = path - } else if filepath.IsAbs(destPath) { //nolint(gocritic) - cleanedPath, err := securejoin.SecureJoin(mountPoint, destPath) - if err != nil { - return nil, err - } - destPath = cleanedPath - } else { //nolint(gocritic) - ctrWorkDir, err := securejoin.SecureJoin(mountPoint, ctr.WorkingDir()) - if err != nil { - return nil, err - } - if err = idtools.MkdirAllAndChownNew(ctrWorkDir, 0755, hostOwner); err != nil { - return nil, err - } - cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), destPath)) - if err != nil { - return nil, err - } - destPath = cleanedPath + if destCtr != nil { + destinationItem, err = copy.CopyItemForContainer(destCtr, destPath, options.Pause, false) + defer destinationItem.CleanUp() + if err != nil { + return err } } else { - destOwner = idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()} - if isVol, volDestName, volName := isVolumeDestName(srcPath, ctr); isVol { //nolint(gocritic) - path, err := pathWithVolumeMount(ic.Libpod, volDestName, volName, srcPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting source path from volume %s", volDestName) - } - srcPath = path - } else if isBindMount, mount := isBindMountDestName(srcPath, ctr); isBindMount { //nolint(gocritic) - path, err := pathWithBindMountSource(mount, srcPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting source path from bind mount %s", mount.Destination) - } - srcPath = path - } else if filepath.IsAbs(srcPath) { //nolint(gocritic) - cleanedPath, err := securejoin.SecureJoin(mountPoint, srcPath) - if err != nil { - return nil, err - } - srcPath = cleanedPath - } else { //nolint(gocritic) - cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), srcPath)) - if err != nil { - return nil, err - } - srcPath = cleanedPath - } - } - - if !filepath.IsAbs(destPath) { - dir, err := os.Getwd() + destinationItem, err = copy.CopyItemForHost(destPath, false) + defer destinationItem.CleanUp() if err != nil { - return nil, errors.Wrapf(err, "err getting current working directory") + return err } - destPath = filepath.Join(dir, destPath) } - if source == "-" { - srcPath = os.Stdin.Name() - extract = true - } - err = containerCopy(srcPath, destPath, source, dest, idMappingOpts, &destOwner, extract, isFromHostToCtr) - return &entities.ContainerCpReport{}, err + // Copy from the host to the container. + return copy.Copy(&sourceItem, &destinationItem, options.Extract) } -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, +func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) { + if len(path) == 0 { + return nil, "" } - 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 - } - + if path[0] == '.' || path[0] == '/' { // A path cannot point to a container. + return nil, path } - return u, err -} - -func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) { pathArr := strings.SplitN(path, ":", 2) if len(pathArr) == 2 { ctr, err := runtime.LookupContainer(pathArr[0]) @@ -199,247 +74,3 @@ func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) } return nil, path } - -func evalSymlinks(path string) (string, error) { - if path == os.Stdin.Name() { - return path, nil - } - return filepath.EvalSymlinks(path) -} - -func getPathInfo(path string) (string, os.FileInfo, error) { - path, err := evalSymlinks(path) - if err != nil { - return "", nil, errors.Wrapf(err, "error evaluating symlinks %q", path) - } - srcfi, err := os.Stat(path) - if err != nil { - return "", nil, err - } - return path, srcfi, nil -} - -func containerCopy(srcPath, destPath, src, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair, extract, isFromHostToCtr bool) error { - srcPath, err := evalSymlinks(srcPath) - if err != nil { - return errors.Wrapf(err, "error evaluating symlinks %q", srcPath) - } - - srcPath, srcfi, err := getPathInfo(srcPath) - if err != nil { - return err - } - - filename := filepath.Base(destPath) - if filename == "-" && !isFromHostToCtr { - err := streamFileToStdout(srcPath, srcfi) - if err != nil { - return errors.Wrapf(err, "error streaming source file %s to Stdout", srcPath) - } - return nil - } - - destdir := destPath - if !srcfi.IsDir() { - destdir = filepath.Dir(destPath) - } - _, err = os.Stat(destdir) - if err != nil && !os.IsNotExist(err) { - return err - } - destDirIsExist := err == nil - if err = os.MkdirAll(destdir, 0755); err != nil { - return err - } - - // return functions for copying items - copyFileWithTar := chrootarchive.CopyFileWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) - copyWithTar := chrootarchive.CopyWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) - untarPath := chrootarchive.UntarPathAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) - - if srcfi.IsDir() { - logrus.Debugf("copying %q to %q", srcPath+string(os.PathSeparator)+"*", dest+string(os.PathSeparator)+"*") - if destDirIsExist && !strings.HasSuffix(src, fmt.Sprintf("%s.", string(os.PathSeparator))) { - srcPathBase := filepath.Base(srcPath) - if !isFromHostToCtr { - pathArr := strings.SplitN(src, ":", 2) - if len(pathArr) != 2 { - return errors.Errorf("invalid arguments %s, you must specify source path", src) - } - if pathArr[1] == "/" { - // If `srcPath` is the root directory of the container, - // `srcPath` will be `.../${sha256_ID}/merged/`, so do not join it - srcPathBase = "" - } - } - destPath = filepath.Join(destPath, srcPathBase) - } - if err = copyWithTar(srcPath, destPath); err != nil { - return errors.Wrapf(err, "error copying %q to %q", srcPath, dest) - } - return nil - } - - if extract { - // We're extracting an archive into the destination directory. - logrus.Debugf("extracting contents of %q into %q", srcPath, destPath) - if err = untarPath(srcPath, destPath); err != nil { - return errors.Wrapf(err, "error extracting %q into %q", srcPath, destPath) - } - return nil - } - - destfi, err := os.Stat(destPath) - if err != nil { - if !os.IsNotExist(err) || strings.HasSuffix(dest, string(os.PathSeparator)) { - return err - } - } - if destfi != nil && destfi.IsDir() { - destPath = filepath.Join(destPath, filepath.Base(srcPath)) - } - - // Copy the file, preserving attributes. - logrus.Debugf("copying %q to %q", srcPath, destPath) - if err = copyFileWithTar(srcPath, destPath); err != nil { - return errors.Wrapf(err, "error copying %q to %q", srcPath, destPath) - } - return nil -} - -func convertIDMap(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 -} - -func streamFileToStdout(srcPath string, srcfi os.FileInfo) error { - if srcfi.IsDir() { - tw := tar.NewWriter(os.Stdout) - err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { - if err != nil || !info.Mode().IsRegular() || path == srcPath { - return err - } - hdr, err := tar.FileInfoHeader(info, "") - if err != nil { - return err - } - - if err = tw.WriteHeader(hdr); err != nil { - return err - } - fh, err := os.Open(path) - if err != nil { - return err - } - defer fh.Close() - - _, err = io.Copy(tw, fh) - return err - }) - if err != nil { - return errors.Wrapf(err, "error streaming directory %s to Stdout", srcPath) - } - return nil - } - - file, err := os.Open(srcPath) - if err != nil { - return err - } - defer file.Close() - if !archive.IsArchivePath(srcPath) { - tw := tar.NewWriter(os.Stdout) - hdr, err := tar.FileInfoHeader(srcfi, "") - if err != nil { - return err - } - err = tw.WriteHeader(hdr) - if err != nil { - return err - } - _, err = io.Copy(tw, file) - if err != nil { - return errors.Wrapf(err, "error streaming archive %s to Stdout", srcPath) - } - return nil - } - - _, err = io.Copy(os.Stdout, file) - if err != nil { - return errors.Wrapf(err, "error streaming file to Stdout") - } - return 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, "", "" -} - -// if SRCPATH or DESTPATH is from volume mount's destination -v or --mount type=volume, generates the path with volume mount point -func pathWithVolumeMount(runtime *libpod.Runtime, volDestName, volName, path string) (string, error) { - destVolume, err := runtime.GetVolume(volName) - if err != nil { - return "", errors.Wrapf(err, "error getting volume destination %s", volName) - } - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) - } - path, err = securejoin.SecureJoin(destVolume.MountPoint(), strings.TrimPrefix(path, volDestName)) - return path, 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)) - } - if path == "" { - return false, specs.Mount{} - } - 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 - } - } - 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))] - } - return pathStr == target -} - -func pathWithBindMountSource(m specs.Mount, path string) (string, error) { - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) - } - return securejoin.SecureJoin(m.Source, strings.TrimPrefix(path, m.Destination)) -} diff --git a/pkg/domain/infra/abi/images_list.go b/pkg/domain/infra/abi/images_list.go index 281b04294..20ae0d0f6 100644 --- a/pkg/domain/infra/abi/images_list.go +++ b/pkg/domain/infra/abi/images_list.go @@ -35,7 +35,7 @@ func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) Created: img.Created().Unix(), Dangling: img.Dangling(), Digest: string(img.Digest()), - Digests: digests, + RepoDigests: digests, History: img.NamesHistory(), Names: img.Names(), ParentId: img.Parent, diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 63677719b..3584668c7 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -731,8 +731,8 @@ 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) (*entities.ContainerCpReport, error) { - return nil, errors.New("not implemented") +func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error { + return errors.New("not implemented") } // Shutdown Libpod engine diff --git a/pkg/util/utils.go b/pkg/util/utils.go index f6a084c00..e0f631eb4 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -530,6 +530,11 @@ func ParseInputTime(inputTime string) (time.Time, error) { } } + unix_timestamp, err := strconv.ParseInt(inputTime, 10, 64) + if err == nil { + return time.Unix(unix_timestamp, 0), nil + } + // input might be a duration duration, err := time.ParseDuration(inputTime) if err != nil { |