Skip to content

Commit

Permalink
APIv2: Add docker compatible volume endpoints
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
maybe-sybr committed Jun 28, 2020
1 parent 673116c commit 9161a4c
Show file tree
Hide file tree
Showing 3 changed files with 394 additions and 0 deletions.
240 changes: 240 additions & 0 deletions pkg/api/handlers/compat/volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
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"
"github.com/gorilla/schema"
"github.com/pkg/errors"

docker_api_types "github.com/docker/docker/api/types"
docker_api_types_volume "github.com/docker/docker/api/types/volume"
)

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 {
switch filter {
case "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)
}
120 changes: 120 additions & 0 deletions pkg/api/server/register_volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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"
)
Expand Down Expand Up @@ -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 volumes 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
}
Loading

0 comments on commit 9161a4c

Please sign in to comment.