-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 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
1 parent
5fe122b
commit b7dcce1
Showing
2 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters