Skip to content

Commit

Permalink
Merge pull request #8126 from matejvasek/impl-apiv2-archive
Browse files Browse the repository at this point in the history
Implement containers/{id or name}/archive api
  • Loading branch information
openshift-merge-robot authored Dec 1, 2020
2 parents b735aa8 + 34931ac commit ce45b71
Show file tree
Hide file tree
Showing 2 changed files with 451 additions and 3 deletions.
373 changes: 370 additions & 3 deletions pkg/api/handlers/compat/containers_archive.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,379 @@
package compat

import (
"errors"
"net/http"
"bytes"
"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/gorilla/schema"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

func Archive(w http.ResponseWriter, r *http.Request) {
utils.Error(w, "not implemented", http.StatusNotImplemented, errors.New("not implemented"))
decoder := r.Context().Value("decoder").(*schema.Decoder)
runtime := r.Context().Value("runtime").(*libpod.Runtime)

switch r.Method {
case http.MethodPut:
handlePut(w, r, decoder, runtime)
case http.MethodGet, http.MethodHead:
handleHeadOrGet(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)))
}
}

func handleHeadOrGet(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) {
query := struct {
Path string `schema:"path"`
}{}

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
}

if query.Path == "" {
utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.New("missing `path` parameter"))
return
}

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"))
return
} else if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return
}

mountPoint, err := ctr.Mount()
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to mount the container"))
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)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return
}

stats, err := copier.Stat(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)})
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)
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 {
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)
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))
return
}
}

func statsToHeader(stats *copier.StatForItem) (string, error) {
statsDTO := 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,
}

jsonBytes, err := json.Marshal(&statsDTO)
if err != nil {
return "", errors.Wrap(err, "failed to serialize file stats")
}

buff := bytes.NewBuffer(make([]byte, 0, 128))
base64encoder := base64.NewEncoder(base64.StdEncoding, buff)

_, err = base64encoder.Write(jsonBytes)
if err != nil {
return "", err
}

err = base64encoder.Close()
if err != nil {
return "", err
}

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 pathWithVolumeMount(runtime *libpod.Runtime, volDestName, volName, path string) (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)
}

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))
}

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, string) {
if !filepath.IsAbs(path) {
path = filepath.Join(string(os.PathSeparator), path)
}

return m.Source, strings.TrimPrefix(path, m.Destination)
}
Loading

0 comments on commit ce45b71

Please sign in to comment.