From 4e2d18db903e19c6cb56d75da0f69e42abe64c47 Mon Sep 17 00:00:00 2001
From: Matej Vasek <mvasek@redhat.com>
Date: Wed, 4 Nov 2020 20:18:02 +0100
Subject: Implement containers/{id or name}/archive

Signed-off-by: Matej Vasek <mvasek@redhat.com>
---
 pkg/api/handlers/compat/containers_archive.go | 341 +++++++++++++++++++++++++-
 1 file changed, 337 insertions(+), 4 deletions(-)

(limited to 'pkg/api')

diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go
index 293a17e0f..f42a821cd 100644
--- a/pkg/api/handlers/compat/containers_archive.go
+++ b/pkg/api/handlers/compat/containers_archive.go
@@ -1,12 +1,345 @@
 package compat
 
 import (
-	"errors"
-	"net/http"
-
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"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"
+	"path/filepath"
+	"strings"
+
+	"github.com/gorilla/schema"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+	"net/http"
+	"os"
+	"time"
 )
 
 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(true); 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(true); 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
+}
+
+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, err := pathWithBindMountSource(mount, ctrPath)
+		if err != nil {
+			return "", "", errors.Wrapf(err, "error getting source path from bind mount %s", mount.Destination)
+		}
+		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, error) {
+	if !filepath.IsAbs(path) {
+		path = filepath.Join(string(os.PathSeparator), path)
+	}
+	return m.Source, strings.TrimPrefix(path, m.Destination), nil
 }
-- 
cgit v1.2.3-54-g00ecf