mirror of
https://github.com/containers/podman.git
synced 2025-06-06 06:44:53 +08:00
APIv2: Add docker compatible volume endpoints
This change implements docker compatibile endpoint for interacting with volumes. The code is mostly lifted from the `libpod` API handlers but decodes and constructs data using types defined in the docker API package. Some notable support caveats with the current implementation: * we don't return the nullable `Status` or `UsageData` keys when returning volume information for inspect and create endpoints * we don't support filters when pruning * we return a fixed `0` for the `SpaceReclaimed` key when pruning since we have no insight into how much space was freed from runtime Signed-off-by: Matt Brindley <58414429+maybe-sybr@users.noreply.github.com>
This commit is contained in:
238
pkg/api/handlers/compat/volumes.go
Normal file
238
pkg/api/handlers/compat/volumes.go
Normal file
@ -0,0 +1,238 @@
|
||||
package compat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/containers/libpod/libpod"
|
||||
"github.com/containers/libpod/libpod/define"
|
||||
"github.com/containers/libpod/pkg/api/handlers/utils"
|
||||
"github.com/containers/libpod/pkg/domain/filters"
|
||||
"github.com/containers/libpod/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 {
|
||||
config := docker_api_types.Volume{
|
||||
Name: v.Name(),
|
||||
Driver: v.Driver(),
|
||||
Mountpoint: v.MountPoint(),
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
volResponse := docker_api_types.Volume{
|
||||
Name: config.Name,
|
||||
Driver: config.Driver,
|
||||
Mountpoint: config.MountPoint,
|
||||
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
|
||||
}
|
||||
volResponse := docker_api_types.Volume{
|
||||
Name: vol.Name(),
|
||||
Driver: vol.Driver(),
|
||||
Mountpoint: vol.MountPoint(),
|
||||
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
|
||||
}
|
||||
name := utils.GetName(r)
|
||||
vol, err := runtime.LookupVolume(name)
|
||||
if err != nil {
|
||||
utils.VolumeNotFound(w, name, err)
|
||||
return
|
||||
}
|
||||
if err := runtime.RemoveVolume(r.Context(), vol, query.Force); err != nil {
|
||||
if errors.Cause(err) == define.ErrVolumeBeingUsed {
|
||||
utils.Error(w, "volumes being used", http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
utils.WriteResponse(w, http.StatusNoContent, "")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// TODO: We have no ability to pass pruning filters to `PruneVolumes()` so
|
||||
// we'll explicitly reject the request if we see any
|
||||
if len(query.Filters) > 0 {
|
||||
utils.InternalServerError(w, errors.New("filters for pruning volumes is not implemented"))
|
||||
return
|
||||
}
|
||||
|
||||
pruned, err := runtime.PruneVolumes(r.Context())
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
prunedIds := make([]string, 0, len(pruned))
|
||||
for k := range pruned {
|
||||
// XXX: This drops any pruning per-volume error messages on the floor
|
||||
prunedIds = append(prunedIds, k)
|
||||
}
|
||||
pruneResponse := docker_api_types.VolumesPruneReport{
|
||||
VolumesDeleted: prunedIds,
|
||||
// TODO: We don't have any insight into how much space was reclaimed
|
||||
// from `PruneVolumes()` but it's not nullable
|
||||
SpaceReclaimed: 0,
|
||||
}
|
||||
|
||||
utils.WriteResponse(w, http.StatusOK, pruneResponse)
|
||||
}
|
@ -3,12 +3,13 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/containers/libpod/pkg/api/handlers/compat"
|
||||
"github.com/containers/libpod/pkg/api/handlers/libpod"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// swagger:operation POST /libpod/volumes/create volumes createVolume
|
||||
// swagger:operation POST /libpod/volumes/create volumes libpodCreateVolume
|
||||
// ---
|
||||
// summary: Create a volume
|
||||
// parameters:
|
||||
@ -25,7 +26,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/create"), s.APIHandler(libpod.CreateVolume)).Methods(http.MethodPost)
|
||||
// swagger:operation GET /libpod/volumes/json volumes listVolumes
|
||||
// swagger:operation GET /libpod/volumes/json volumes libpodListVolumes
|
||||
// ---
|
||||
// summary: List volumes
|
||||
// description: Returns a list of volumes
|
||||
@ -47,7 +48,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/json"), s.APIHandler(libpod.ListVolumes)).Methods(http.MethodGet)
|
||||
// swagger:operation POST /libpod/volumes/prune volumes pruneVolumes
|
||||
// swagger:operation POST /libpod/volumes/prune volumes libpodPruneVolumes
|
||||
// ---
|
||||
// summary: Prune volumes
|
||||
// produces:
|
||||
@ -58,7 +59,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/prune"), s.APIHandler(libpod.PruneVolumes)).Methods(http.MethodPost)
|
||||
// swagger:operation GET /libpod/volumes/{name}/json volumes inspectVolume
|
||||
// swagger:operation GET /libpod/volumes/{name}/json volumes libpodInspectVolume
|
||||
// ---
|
||||
// summary: Inspect volume
|
||||
// parameters:
|
||||
@ -77,7 +78,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/{name}/json"), s.APIHandler(libpod.InspectVolume)).Methods(http.MethodGet)
|
||||
// swagger:operation DELETE /libpod/volumes/{name} volumes removeVolume
|
||||
// swagger:operation DELETE /libpod/volumes/{name} volumes libpodRemoveVolume
|
||||
// ---
|
||||
// summary: Remove volume
|
||||
// parameters:
|
||||
@ -102,5 +103,124 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
|
||||
// 500:
|
||||
// $ref: "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/libpod/volumes/{name}"), s.APIHandler(libpod.RemoveVolume)).Methods(http.MethodDelete)
|
||||
|
||||
/*
|
||||
* Docker compatibility endpoints
|
||||
*/
|
||||
|
||||
// swagger:operation GET /volumes compat listVolumes
|
||||
// ---
|
||||
// summary: List volumes
|
||||
// description: Returns a list of volume
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - in: query
|
||||
// name: filters
|
||||
// type: string
|
||||
// description: |
|
||||
// JSON encoded value of the filters (a map[string][]string) to process on the volumes list. Available filters:
|
||||
// - driver=<volume-driver-name> Matches volumes based on their driver.
|
||||
// - label=<key> or label=<key>:<value> Matches volumes based on the presence of a label alone or a label and a value.
|
||||
// - name=<volume-name> Matches all of volume name.
|
||||
//
|
||||
// Note:
|
||||
// The boolean `dangling` filter is not yet implemented for this endpoint.
|
||||
// responses:
|
||||
// '200':
|
||||
// "$ref": "#/responses/DockerVolumeList"
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/volumes"), s.APIHandler(compat.ListVolumes)).Methods(http.MethodGet)
|
||||
r.Handle("/volumes", s.APIHandler(compat.ListVolumes)).Methods(http.MethodGet)
|
||||
|
||||
// swagger:operation POST /volumes/create volumes createVolume
|
||||
// ---
|
||||
// summary: Create a volume
|
||||
// parameters:
|
||||
// - in: body
|
||||
// name: create
|
||||
// description: attributes for creating a container
|
||||
// schema:
|
||||
// $ref: "#/definitions/DockerVolumeCreate"
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// '201':
|
||||
// "$ref": "#/responses/DockerVolumeInfoResponse"
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/volumes/create"), s.APIHandler(compat.CreateVolume)).Methods(http.MethodPost)
|
||||
r.Handle("/volumes/create", s.APIHandler(compat.CreateVolume)).Methods(http.MethodPost)
|
||||
|
||||
// swagger:operation GET /volumes/{name} volumes inspectVolume
|
||||
// ---
|
||||
// summary: Inspect volume
|
||||
// parameters:
|
||||
// - in: path
|
||||
// name: name
|
||||
// type: string
|
||||
// required: true
|
||||
// description: the name or ID of the volume
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// '200':
|
||||
// "$ref": "#/responses/DockerVolumeInfoResponse"
|
||||
// '404':
|
||||
// "$ref": "#/responses/NoSuchVolume"
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/volumes/{name}"), s.APIHandler(compat.InspectVolume)).Methods(http.MethodGet)
|
||||
r.Handle("/volumes/{name}", s.APIHandler(compat.InspectVolume)).Methods(http.MethodGet)
|
||||
|
||||
// swagger:operation DELETE /volumes/{name} volumes removeVolume
|
||||
// ---
|
||||
// summary: Remove volume
|
||||
// parameters:
|
||||
// - in: path
|
||||
// name: name
|
||||
// type: string
|
||||
// required: true
|
||||
// description: the name or ID of the volume
|
||||
// - in: query
|
||||
// name: force
|
||||
// type: boolean
|
||||
// description: force removal
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// 204:
|
||||
// description: no error
|
||||
// 404:
|
||||
// "$ref": "#/responses/NoSuchVolume"
|
||||
// 409:
|
||||
// description: Volume is in use and cannot be removed
|
||||
// 500:
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/volumes/{name}"), s.APIHandler(compat.RemoveVolume)).Methods(http.MethodDelete)
|
||||
r.Handle("/volumes/{name}", s.APIHandler(compat.RemoveVolume)).Methods(http.MethodDelete)
|
||||
|
||||
// swagger:operation POST /volumes/prune volumes pruneVolumes
|
||||
// ---
|
||||
// summary: Prune volumes
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - in: query
|
||||
// name: filters
|
||||
// type: string
|
||||
// description: |
|
||||
// JSON encoded value of filters (a map[string][]string) to match volumes against before pruning.
|
||||
//
|
||||
// Note: No filters are currently supported and any filters specified will cause an error response.
|
||||
// responses:
|
||||
// '200':
|
||||
// "$ref": "#/responses/DockerVolumePruneResponse"
|
||||
// '500':
|
||||
// "$ref": "#/responses/InternalError"
|
||||
r.Handle(VersionedPath("/volumes/prune"), s.APIHandler(compat.PruneVolumes)).Methods(http.MethodPost)
|
||||
r.Handle("/volumes/prune", s.APIHandler(compat.PruneVolumes)).Methods(http.MethodPost)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
docker_api_types "github.com/docker/docker/api/types"
|
||||
docker_api_types_volume "github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
// swagger:model VolumeCreate
|
||||
@ -90,3 +93,35 @@ type VolumeListOptions struct {
|
||||
type VolumeListReport struct {
|
||||
VolumeConfigResponse
|
||||
}
|
||||
|
||||
/*
|
||||
* Docker API compatibility types
|
||||
*/
|
||||
// swagger:response DockerVolumeList
|
||||
type SwagDockerVolumeListResponse struct {
|
||||
// in:body
|
||||
Body struct {
|
||||
docker_api_types_volume.VolumeListOKBody
|
||||
}
|
||||
}
|
||||
|
||||
// swagger:model DockerVolumeCreate
|
||||
type DockerVolumeCreate docker_api_types_volume.VolumeCreateBody
|
||||
|
||||
// This response definition is used for both the create and inspect endpoints
|
||||
// swagger:response DockerVolumeInfoResponse
|
||||
type SwagDockerVolumeInfoResponse struct {
|
||||
// in:body
|
||||
Body struct {
|
||||
docker_api_types.Volume
|
||||
}
|
||||
}
|
||||
|
||||
// Volume prune response
|
||||
// swagger:response DockerVolumePruneResponse
|
||||
type SwagDockerVolumePruneResponse struct {
|
||||
// in:body
|
||||
Body struct {
|
||||
docker_api_types.VolumesPruneReport
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user