diff --git a/pkg/api/handlers/compat/volumes.go b/pkg/api/handlers/compat/volumes.go new file mode 100644 index 000000000000..fe4c5cf31ef1 --- /dev/null +++ b/pkg/api/handlers/compat/volumes.go @@ -0,0 +1,239 @@ +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 { + 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) +} diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go index 1d5abd830ef1..03e179e9bfc1 100644 --- a/pkg/api/server/register_volumes.go +++ b/pkg/api/server/register_volumes.go @@ -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" ) @@ -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= Matches volumes based on their driver. + // - label= or label=: Matches volumes based on the presence of a label alone or a label and a value. + // - 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 } diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index c99b39f2d75e..a266832f5068 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -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,34 @@ 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 + } +} + +// swagger:response DockerVolumePruneResponse +type SwagDockerVolumePruneResponse struct { + // in:body + Body struct { + docker_api_types.VolumesPruneReport + } +}