-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8126 from matejvasek/impl-apiv2-archive
Implement containers/{id or name}/archive api
- Loading branch information
Showing
2 changed files
with
451 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.