mirror of
https://github.com/containers/podman.git
synced 2025-10-27 19:34:13 +08:00
This implements support for mounting and unmounting volumes backed by volume plugins. Support for actually retrieving plugins requires a pull request to land in containers.conf and then that to be vendored, and as such is not yet ready. Given this, this code is only compile tested. However, the code for everything past retrieving the plugin has been written - there is support for creating, removing, mounting, and unmounting volumes, which should allow full functionality once the c/common PR is merged. A major change is the signature of the MountPoint function for volumes, which now, by necessity, returns an error. Named volumes managed by a plugin do not have a mountpoint we control; instead, it is managed entirely by the plugin. As such, we need to cache the path in the DB, and calls to retrieve it now need to access the DB (and may fail as such). Notably absent is support for SELinux relabelling and chowning these volumes. Given that we don't manage the mountpoint for these volumes, I am extremely reluctant to try and modify it - we could easily break the plugin trying to chown or relabel it. Also, we had no less than *5* separate implementations of inspecting a volume floating around in pkg/infra/abi and pkg/api/handlers/libpod. And none of them used volume.Inspect(), the only correct way of inspecting volumes. Remove them all and consolidate to using the correct way. Compat API is likely still doing things the wrong way, but that is an issue for another day. Fixes #4304 Signed-off-by: Matthew Heon <matthew.heon@pm.me>
319 lines
9.5 KiB
Go
319 lines
9.5 KiB
Go
package compat
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/containers/podman/v2/libpod"
|
|
"github.com/containers/podman/v2/libpod/define"
|
|
"github.com/containers/podman/v2/pkg/api/handlers"
|
|
"github.com/containers/podman/v2/pkg/api/handlers/utils"
|
|
"github.com/containers/podman/v2/pkg/domain/filters"
|
|
"github.com/containers/podman/v2/pkg/domain/infra/abi/parse"
|
|
docker_api_types "github.com/docker/docker/api/types"
|
|
docker_api_types_volume "github.com/docker/docker/api/types/volume"
|
|
"github.com/gorilla/schema"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func ListVolumes(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
decoder = r.Context().Value("decoder").(*schema.Decoder)
|
|
runtime = r.Context().Value("runtime").(*libpod.Runtime)
|
|
)
|
|
query := struct {
|
|
Filters map[string][]string `schema:"filters"`
|
|
}{
|
|
// override any golang type defaults
|
|
}
|
|
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
|
|
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
|
|
return
|
|
}
|
|
|
|
// Reject any libpod specific filters since `GenerateVolumeFilters()` will
|
|
// happily parse them for us.
|
|
for filter := range query.Filters {
|
|
if filter == "opts" {
|
|
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
|
|
errors.Errorf("unsupported libpod filters passed to docker endpoint"))
|
|
return
|
|
}
|
|
}
|
|
volumeFilters, err := filters.GenerateVolumeFilters(query.Filters)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
vols, err := runtime.Volumes(volumeFilters...)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
volumeConfigs := make([]*docker_api_types.Volume, 0, len(vols))
|
|
for _, v := range vols {
|
|
mp, err := v.MountPoint()
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
config := docker_api_types.Volume{
|
|
Name: v.Name(),
|
|
Driver: v.Driver(),
|
|
Mountpoint: mp,
|
|
CreatedAt: v.CreatedTime().Format(time.RFC3339),
|
|
Labels: v.Labels(),
|
|
Scope: v.Scope(),
|
|
Options: v.Options(),
|
|
}
|
|
volumeConfigs = append(volumeConfigs, &config)
|
|
}
|
|
response := docker_api_types_volume.VolumeListOKBody{
|
|
Volumes: volumeConfigs,
|
|
Warnings: []string{},
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, response)
|
|
}
|
|
|
|
func CreateVolume(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
volumeOptions []libpod.VolumeCreateOption
|
|
runtime = r.Context().Value("runtime").(*libpod.Runtime)
|
|
decoder = r.Context().Value("decoder").(*schema.Decoder)
|
|
)
|
|
/* No query string data*/
|
|
query := struct{}{}
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
|
|
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
|
|
return
|
|
}
|
|
// decode params from body
|
|
input := docker_api_types_volume.VolumeCreateBody{}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()"))
|
|
return
|
|
}
|
|
|
|
// See if the volume exists already
|
|
existingVolume, err := runtime.GetVolume(input.Name)
|
|
if err != nil && errors.Cause(err) != define.ErrNoSuchVolume {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
// if using the compat layer and the volume already exists, we
|
|
// must return a 201 with the same information as create
|
|
if existingVolume != nil && !utils.IsLibpodRequest(r) {
|
|
mp, err := existingVolume.MountPoint()
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
response := docker_api_types.Volume{
|
|
CreatedAt: existingVolume.CreatedTime().Format(time.RFC3339),
|
|
Driver: existingVolume.Driver(),
|
|
Labels: existingVolume.Labels(),
|
|
Mountpoint: mp,
|
|
Name: existingVolume.Name(),
|
|
Options: existingVolume.Options(),
|
|
Scope: existingVolume.Scope(),
|
|
}
|
|
utils.WriteResponse(w, http.StatusCreated, response)
|
|
return
|
|
}
|
|
|
|
if len(input.Name) > 0 {
|
|
volumeOptions = append(volumeOptions, libpod.WithVolumeName(input.Name))
|
|
}
|
|
if len(input.Driver) > 0 {
|
|
volumeOptions = append(volumeOptions, libpod.WithVolumeDriver(input.Driver))
|
|
}
|
|
if len(input.Labels) > 0 {
|
|
volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Labels))
|
|
}
|
|
if len(input.DriverOpts) > 0 {
|
|
parsedOptions, err := parse.VolumeOptions(input.DriverOpts)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
volumeOptions = append(volumeOptions, parsedOptions...)
|
|
}
|
|
vol, err := runtime.NewVolume(r.Context(), volumeOptions...)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
config, err := vol.Config()
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
mp, err := vol.MountPoint()
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
volResponse := docker_api_types.Volume{
|
|
Name: config.Name,
|
|
Driver: config.Driver,
|
|
Mountpoint: mp,
|
|
CreatedAt: config.CreatedTime.Format(time.RFC3339),
|
|
Labels: config.Labels,
|
|
Options: config.Options,
|
|
Scope: "local",
|
|
// ^^ We don't have volume scoping so we'll just claim it's "local"
|
|
// like we do in the `libpod.Volume.Scope()` method
|
|
//
|
|
// TODO: We don't include the volume `Status` or `UsageData`, but both
|
|
// are nullable in the Docker engine API spec so that's fine for now
|
|
}
|
|
utils.WriteResponse(w, http.StatusCreated, volResponse)
|
|
}
|
|
|
|
func InspectVolume(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
runtime = r.Context().Value("runtime").(*libpod.Runtime)
|
|
)
|
|
name := utils.GetName(r)
|
|
vol, err := runtime.GetVolume(name)
|
|
if err != nil {
|
|
utils.VolumeNotFound(w, name, err)
|
|
return
|
|
}
|
|
mp, err := vol.MountPoint()
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
volResponse := docker_api_types.Volume{
|
|
Name: vol.Name(),
|
|
Driver: vol.Driver(),
|
|
Mountpoint: mp,
|
|
CreatedAt: vol.CreatedTime().Format(time.RFC3339),
|
|
Labels: vol.Labels(),
|
|
Options: vol.Options(),
|
|
Scope: vol.Scope(),
|
|
// TODO: As above, we don't return `Status` or `UsageData` yet
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, volResponse)
|
|
}
|
|
|
|
func RemoveVolume(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
runtime = r.Context().Value("runtime").(*libpod.Runtime)
|
|
decoder = r.Context().Value("decoder").(*schema.Decoder)
|
|
)
|
|
query := struct {
|
|
Force bool `schema:"force"`
|
|
}{
|
|
// override any golang type defaults
|
|
}
|
|
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
|
|
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
|
|
return
|
|
}
|
|
|
|
/* The implications for `force` differ between Docker and us, so we can't
|
|
* simply pass the `force` parameter to `runeimt.RemoveVolume()`.
|
|
* Specifically, Docker's behavior seems to be that `force` means "do not
|
|
* error on missing volume"; ours means "remove any not-running containers
|
|
* using the volume at the same time".
|
|
*
|
|
* With this in mind, we only consider the `force` query parameter when we
|
|
* hunt for specified volume by name, using it to selectively return a 204
|
|
* or blow up depending on `force` being truthy or falsey/unset
|
|
* respectively.
|
|
*/
|
|
name := utils.GetName(r)
|
|
vol, err := runtime.LookupVolume(name)
|
|
if err == nil {
|
|
// As above, we do not pass `force` from the query parameters here
|
|
if err := runtime.RemoveVolume(r.Context(), vol, false); err != nil {
|
|
if errors.Cause(err) == define.ErrVolumeBeingUsed {
|
|
utils.Error(w, "volumes being used", http.StatusConflict, err)
|
|
} else {
|
|
utils.InternalServerError(w, err)
|
|
}
|
|
} else {
|
|
// Success
|
|
utils.WriteResponse(w, http.StatusNoContent, nil)
|
|
}
|
|
} else {
|
|
if !query.Force {
|
|
utils.VolumeNotFound(w, name, err)
|
|
} else {
|
|
// Volume does not exist and `force` is truthy - this emulates what
|
|
// Docker would do when told to `force` removal of a nonexistent
|
|
// volume
|
|
utils.WriteResponse(w, http.StatusNoContent, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func PruneVolumes(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
runtime = r.Context().Value("runtime").(*libpod.Runtime)
|
|
decoder = r.Context().Value("decoder").(*schema.Decoder)
|
|
)
|
|
// For some reason the prune filters are query parameters even though this
|
|
// is a POST endpoint
|
|
query := struct {
|
|
Filters map[string][]string `schema:"filters"`
|
|
}{
|
|
// override any golang type defaults
|
|
}
|
|
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
|
|
return
|
|
}
|
|
|
|
f := (url.Values)(query.Filters)
|
|
filterFuncs, err := filters.GenerateVolumeFilters(f)
|
|
if err != nil {
|
|
utils.Error(w, "Something when wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse filters for %s", f.Encode()))
|
|
return
|
|
}
|
|
|
|
pruned, err := runtime.PruneVolumes(r.Context(), filterFuncs)
|
|
if err != nil {
|
|
utils.InternalServerError(w, err)
|
|
return
|
|
}
|
|
|
|
var errorMsg bytes.Buffer
|
|
var reclaimedSpace uint64
|
|
prunedIds := make([]string, 0, len(pruned))
|
|
for _, v := range pruned {
|
|
if v.Err != nil {
|
|
errorMsg.WriteString(v.Err.Error())
|
|
errorMsg.WriteString("; ")
|
|
continue
|
|
}
|
|
prunedIds = append(prunedIds, v.Id)
|
|
reclaimedSpace += v.Size
|
|
}
|
|
if errorMsg.Len() > 0 {
|
|
utils.InternalServerError(w, errors.New(errorMsg.String()))
|
|
return
|
|
}
|
|
|
|
payload := handlers.VolumesPruneReport{
|
|
VolumesPruneReport: docker_api_types.VolumesPruneReport{
|
|
VolumesDeleted: prunedIds,
|
|
SpaceReclaimed: reclaimedSpace,
|
|
},
|
|
}
|
|
utils.WriteResponse(w, http.StatusOK, payload)
|
|
}
|