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 yet support the `dangling` filter when listing
  * we don't exclude the `libpod` specific `opts` filter when listing
  * 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
  • Loading branch information
maybe-sybr committed Jun 24, 2020
1 parent 5fe122b commit b7dcce1
Show file tree
Hide file tree
Showing 2 changed files with 383 additions and 0 deletions.
263 changes: 263 additions & 0 deletions pkg/api/handlers/compat/volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
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"
)

// swagger:response DockerVolumeList
type swagDockerVolumeListResponse struct {
// in:body
Body struct {
docker_api_types_volume.VolumeListOKBody
}
}

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
}

// XXX: This will reject `dangling` in the `filters` since we don't yet
// understand it there. This manifests as a 500 response complaining about
// the "invalid" volume filter.
// XXX: This will also accept the `opts` filter which isn't included in the
// Docker engine v1.40 API spec.
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)
}

// 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
}
}

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, "")
}

// swagger:response DockerVolumePruneResponse
type swagDockerVolumePruneResponse struct {
// in:body
Body struct {
docker_api_types.VolumesPruneReport
}
}

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
}
pruned_ids := make([]string, 0, len(pruned))
for k := range pruned {
// XXX: This drops any pruning per-volume error messages on the floor
pruned_ids = append(pruned_ids, k)
}
pruneResponse := docker_api_types.VolumesPruneReport{
VolumesDeleted: pruned_ids,
// 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
}

0 comments on commit b7dcce1

Please sign in to comment.