diff options
-rw-r--r-- | cmd/podmanV2/volumes/inspect.go | 74 | ||||
-rw-r--r-- | cmd/podmanV2/volumes/list.go | 98 | ||||
-rw-r--r-- | cmd/podmanV2/volumes/prune.go | 74 | ||||
-rw-r--r-- | cmd/podmanV2/volumes/rm.go | 64 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | libpod/runtime_volume.go | 16 | ||||
-rw-r--r-- | pkg/adapter/runtime.go | 18 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/volumes.go | 140 | ||||
-rw-r--r-- | pkg/api/server/register_volumes.go | 10 | ||||
-rw-r--r-- | pkg/api/server/swagger.go | 7 | ||||
-rw-r--r-- | pkg/bindings/volumes/volumes.go | 15 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 9 | ||||
-rw-r--r-- | pkg/domain/entities/types.go | 8 | ||||
-rw-r--r-- | pkg/domain/entities/volumes.go | 89 | ||||
-rw-r--r-- | pkg/domain/filters/volumes.go | 70 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 20 | ||||
-rw-r--r-- | pkg/domain/infra/abi/volumes.go | 108 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/containers.go | 7 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/runtime.go | 18 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/volumes.go | 54 | ||||
-rw-r--r-- | pkg/varlinkapi/volumes.go | 24 |
21 files changed, 716 insertions, 208 deletions
diff --git a/cmd/podmanV2/volumes/inspect.go b/cmd/podmanV2/volumes/inspect.go new file mode 100644 index 000000000..4d9720432 --- /dev/null +++ b/cmd/podmanV2/volumes/inspect.go @@ -0,0 +1,74 @@ +package volumes + +import ( + "encoding/json" + "fmt" + "html/template" + "os" + + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +var ( + volumeInspectDescription = `Display detailed information on one or more volumes. + + Use a Go template to change the format from JSON.` + inspectCommand = &cobra.Command{ + Use: "inspect [flags] VOLUME [VOLUME...]", + Short: "Display detailed information on one or more volumes", + Long: volumeInspectDescription, + RunE: inspect, + Example: `podman volume inspect myvol + podman volume inspect --all + podman volume inspect --format "{{.Driver}} {{.Scope}}" myvol`, + } +) + +var ( + inspectOpts = entities.VolumeInspectOptions{} + inspectFormat string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCommand, + Parent: volumeCmd, + }) + flags := inspectCommand.Flags() + flags.BoolVarP(&inspectOpts.All, "all", "a", false, "Inspect all volumes") + flags.StringVarP(&inspectFormat, "format", "f", "json", "Format volume output using Go template") +} + +func inspect(cmd *cobra.Command, args []string) error { + if (inspectOpts.All && len(args) > 0) || (!inspectOpts.All && len(args) < 1) { + return errors.New("provide one or more volume names or use --all") + } + responses, err := registry.ContainerEngine().VolumeInspect(context.Background(), args, inspectOpts) + if err != nil { + return err + } + switch inspectFormat { + case "", formats.JSONString: + jsonOut, err := json.MarshalIndent(responses, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshalling inspect JSON") + } + fmt.Println(string(jsonOut)) + default: + tmpl, err := template.New("volumeInspect").Parse(inspectFormat) + if err != nil { + return err + } + if err := tmpl.Execute(os.Stdout, responses); err != nil { + return err + } + } + return nil + +} diff --git a/cmd/podmanV2/volumes/list.go b/cmd/podmanV2/volumes/list.go new file mode 100644 index 000000000..c38f78c73 --- /dev/null +++ b/cmd/podmanV2/volumes/list.go @@ -0,0 +1,98 @@ +package volumes + +import ( + "context" + "html/template" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + volumeLsDescription = ` +podman volume ls + +List all available volumes. The output of the volumes can be filtered +and the output format can be changed to JSON or a user specified Go template.` + lsCommand = &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Args: cobra.NoArgs, + Short: "List volumes", + Long: volumeLsDescription, + RunE: list, + } +) + +var ( + // Temporary struct to hold cli values. + cliOpts = struct { + Filter []string + Format string + Quiet bool + }{} + lsOpts = entities.VolumeListOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: lsCommand, + Parent: volumeCmd, + }) + flags := lsCommand.Flags() + flags.StringSliceVarP(&cliOpts.Filter, "filter", "f", []string{}, "Filter volume output") + flags.StringVar(&cliOpts.Format, "format", "{{.Driver}}\t{{.Name}}\n", "Format volume output using Go template") + flags.BoolVarP(&cliOpts.Quiet, "quiet", "q", false, "Print volume output in quiet mode") +} + +func list(cmd *cobra.Command, args []string) error { + var w io.Writer = os.Stdout + if cliOpts.Quiet && cmd.Flag("format").Changed { + return errors.New("quiet and format flags cannot be used together") + } + for _, f := range cliOpts.Filter { + filterSplit := strings.Split(f, "=") + if len(filterSplit) < 2 { + return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) + } + lsOpts.Filter[filterSplit[0]] = append(lsOpts.Filter[filterSplit[0]], filterSplit[1:]...) + } + responses, err := registry.ContainerEngine().VolumeList(context.Background(), lsOpts) + if err != nil { + return err + } + // "\t" from the command line is not being recognized as a tab + // replacing the string "\t" to a tab character if the user passes in "\t" + cliOpts.Format = strings.Replace(cliOpts.Format, `\t`, "\t", -1) + if cliOpts.Quiet { + cliOpts.Format = "{{.Name}}\n" + } + headers := "DRIVER\tVOLUME NAME\n" + row := cliOpts.Format + if !strings.HasSuffix(cliOpts.Format, "\n") { + row += "\n" + } + format := "{{range . }}" + row + "{{end}}" + if !cliOpts.Quiet && !cmd.Flag("format").Changed { + w = tabwriter.NewWriter(os.Stdout, 12, 2, 2, ' ', 0) + format = headers + format + } + tmpl, err := template.New("listVolume").Parse(format) + if err != nil { + return err + } + if err := tmpl.Execute(w, responses); err != nil { + return err + } + if flusher, ok := w.(interface{ Flush() error }); ok { + return flusher.Flush() + } + return nil +} diff --git a/cmd/podmanV2/volumes/prune.go b/cmd/podmanV2/volumes/prune.go new file mode 100644 index 000000000..148065f56 --- /dev/null +++ b/cmd/podmanV2/volumes/prune.go @@ -0,0 +1,74 @@ +package volumes + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + volumePruneDescription = `Volumes that are not currently owned by a container will be removed. + + The command prompts for confirmation which can be overridden with the --force flag. + Note all data will be destroyed.` + pruneCommand = &cobra.Command{ + Use: "prune", + Args: cobra.NoArgs, + Short: "Remove all unused volumes", + Long: volumePruneDescription, + RunE: prune, + } +) + +var ( + pruneOptions entities.VolumePruneOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pruneCommand, + Parent: volumeCmd, + }) + flags := pruneCommand.Flags() + flags.BoolVarP(&pruneOptions.Force, "force", "f", false, "Do not prompt for confirmation") +} + +func prune(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + // Prompt for confirmation if --force is not set + if !pruneOptions.Force { + reader := bufio.NewReader(os.Stdin) + fmt.Println("WARNING! This will remove all volumes not used by at least one container.") + fmt.Print("Are you sure you want to continue? [y/N] ") + answer, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "error reading input") + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + responses, err := registry.ContainerEngine().VolumePrune(context.Background(), pruneOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/volumes/rm.go b/cmd/podmanV2/volumes/rm.go new file mode 100644 index 000000000..b019285d8 --- /dev/null +++ b/cmd/podmanV2/volumes/rm.go @@ -0,0 +1,64 @@ +package volumes + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + volumeRmDescription = `Remove one or more existing volumes. + + By default only volumes that are not being used by any containers will be removed. To remove the volumes anyways, use the --force flag.` + rmCommand = &cobra.Command{ + Use: "rm [flags] VOLUME [VOLUME...]", + Aliases: []string{"remove"}, + Short: "Remove one or more volumes", + Long: volumeRmDescription, + RunE: rm, + Example: `podman volume rm myvol1 myvol2 + podman volume rm --all + podman volume rm --force myvol`, + } +) + +var ( + rmOptions = entities.VolumeRmOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCommand, + Parent: volumeCmd, + }) + flags := rmCommand.Flags() + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all volumes") + flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Remove a volume by force, even if it is being used by a container") +} + +func rm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if (len(args) > 0 && rmOptions.All) || (len(args) < 1 && !rmOptions.All) { + return errors.New("choose either one or more volumes or all") + } + responses, err := registry.ContainerEngine().VolumeRm(context.Background(), args, rmOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} @@ -59,6 +59,7 @@ require ( github.com/varlink/go v0.0.0-20190502142041-0f1d566d194b github.com/vishvananda/netlink v1.1.0 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 + golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2 gopkg.in/yaml.v2 v2.2.8 diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index efc3c5bd9..d522ffb6c 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -5,6 +5,7 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" + "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" ) @@ -35,7 +36,6 @@ func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force bool) error return nil } } - return r.removeVolume(ctx, v, force) } @@ -130,26 +130,24 @@ func (r *Runtime) GetAllVolumes() ([]*Volume, error) { } // PruneVolumes removes unused volumes from the system -func (r *Runtime) PruneVolumes(ctx context.Context) ([]string, []error) { +func (r *Runtime) PruneVolumes(ctx context.Context) ([]*entities.VolumePruneReport, error) { var ( - prunedIDs []string - pruneErrors []error + reports []*entities.VolumePruneReport ) vols, err := r.GetAllVolumes() if err != nil { - pruneErrors = append(pruneErrors, err) - return nil, pruneErrors + return nil, err } for _, vol := range vols { if err := r.RemoveVolume(ctx, vol, false); err != nil { if errors.Cause(err) != define.ErrVolumeBeingUsed && errors.Cause(err) != define.ErrVolumeRemoved { - pruneErrors = append(pruneErrors, err) + reports = append(reports, &entities.VolumePruneReport{Id: vol.Name(), Err: err}) } continue } vol.newVolumeEvent(events.Prune) - prunedIDs = append(prunedIDs, vol.Name()) + reports = append(reports, &entities.VolumePruneReport{Id: vol.Name()}) } - return prunedIDs, pruneErrors + return reports, nil } diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index dfe6b7f07..7817a1f98 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -347,7 +347,23 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti // PruneVolumes is a wrapper function for libpod PruneVolumes func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) { - return r.Runtime.PruneVolumes(ctx) + var ( + vids []string + errs []error + ) + reports, err := r.Runtime.PruneVolumes(ctx) + if err != nil { + errs = append(errs, err) + return vids, errs + } + for _, r := range reports { + if r.Err == nil { + vids = append(vids, r.Id) + } else { + errs = append(errs, r.Err) + } + } + return vids, errs } // SaveImage is a wrapper function for saving an image to the local filesystem diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 06ca1d225..e61d272f4 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -3,16 +3,15 @@ package libpod import ( "encoding/json" "net/http" - "strings" "github.com/containers/libpod/cmd/podman/shared" "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/entities" + "github.com/containers/libpod/pkg/domain/filters" "github.com/gorilla/schema" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) func CreateVolume(w http.ResponseWriter, r *http.Request) { @@ -65,14 +64,14 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { return } volResponse := entities.VolumeConfigResponse{ - Name: config.Name, - Labels: config.Labels, - Driver: config.Driver, - MountPoint: config.MountPoint, - CreatedTime: config.CreatedTime, - Options: config.Options, - UID: config.UID, - GID: config.GID, + Name: config.Name, + Driver: config.Driver, + Mountpoint: config.MountPoint, + CreatedAt: config.CreatedTime, + Labels: config.Labels, + Options: config.Options, + UID: config.UID, + GID: config.GID, } utils.WriteResponse(w, http.StatusOK, volResponse) } @@ -85,21 +84,27 @@ func InspectVolume(w http.ResponseWriter, r *http.Request) { vol, err := runtime.GetVolume(name) if err != nil { utils.VolumeNotFound(w, name, err) + return } - inspect, err := vol.Inspect() - if err != nil { - utils.InternalServerError(w, err) + volResponse := entities.VolumeConfigResponse{ + Name: vol.Name(), + Driver: vol.Driver(), + Mountpoint: vol.MountPoint(), + CreatedAt: vol.CreatedTime(), + Labels: vol.Labels(), + Scope: vol.Scope(), + Options: vol.Options(), + UID: vol.UID(), + GID: vol.GID(), } - utils.WriteResponse(w, http.StatusOK, inspect) + utils.WriteResponse(w, http.StatusOK, volResponse) } func ListVolumes(w http.ResponseWriter, r *http.Request) { var ( decoder = r.Context().Value("decoder").(*schema.Decoder) - err error runtime = r.Context().Value("runtime").(*libpod.Runtime) - volumeConfigs []*libpod.VolumeConfig - volumeFilters []libpod.VolumeFilter + volumeConfigs []*entities.VolumeListReport ) query := struct { Filters map[string][]string `schema:"filters"` @@ -113,25 +118,30 @@ func ListVolumes(w http.ResponseWriter, r *http.Request) { return } - if len(query.Filters) > 0 { - volumeFilters, err = generateVolumeFilters(query.Filters) - if err != nil { - utils.InternalServerError(w, err) - 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 } for _, v := range vols { - config, err := v.Config() - if err != nil { - utils.InternalServerError(w, err) - return + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), } - volumeConfigs = append(volumeConfigs, config) + volumeConfigs = append(volumeConfigs, &entities.VolumeListReport{VolumeConfigResponse: config}) } utils.WriteResponse(w, http.StatusOK, volumeConfigs) } @@ -140,14 +150,10 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) ) - pruned, errs := runtime.PruneVolumes(r.Context()) - if errs != nil { - if len(errs) > 1 { - for _, err := range errs { - log.Infof("Request Failed(%s): %s", http.StatusText(http.StatusInternalServerError), err.Error()) - } - } - utils.InternalServerError(w, errs[len(errs)-1]) + pruned, err := runtime.PruneVolumes(r.Context()) + if err != nil { + utils.InternalServerError(w, err) + return } utils.WriteResponse(w, http.StatusOK, pruned) } @@ -184,65 +190,3 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusNoContent, "") } - -func generateVolumeFilters(filters map[string][]string) ([]libpod.VolumeFilter, error) { - var vf []libpod.VolumeFilter - for filter, v := range filters { - for _, val := range v { - switch filter { - case "name": - nameVal := val - vf = append(vf, func(v *libpod.Volume) bool { - return nameVal == v.Name() - }) - case "driver": - driverVal := val - vf = append(vf, func(v *libpod.Volume) bool { - return v.Driver() == driverVal - }) - case "scope": - scopeVal := val - vf = append(vf, func(v *libpod.Volume) bool { - return v.Scope() == scopeVal - }) - case "label": - filterArray := strings.SplitN(val, "=", 2) - filterKey := filterArray[0] - var filterVal string - if len(filterArray) > 1 { - filterVal = filterArray[1] - } else { - filterVal = "" - } - vf = append(vf, func(v *libpod.Volume) bool { - for labelKey, labelValue := range v.Labels() { - if labelKey == filterKey && ("" == filterVal || labelValue == filterVal) { - return true - } - } - return false - }) - case "opt": - filterArray := strings.SplitN(val, "=", 2) - filterKey := filterArray[0] - var filterVal string - if len(filterArray) > 1 { - filterVal = filterArray[1] - } else { - filterVal = "" - } - vf = append(vf, func(v *libpod.Volume) bool { - for labelKey, labelValue := range v.Options() { - if labelKey == filterKey && ("" == filterVal || labelValue == filterVal) { - return true - } - } - return false - }) - default: - return nil, errors.Errorf("%q is in an invalid volume filter", filter) - } - } - } - return vf, nil -} diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go index 2cf249cc3..93b972b6b 100644 --- a/pkg/api/server/register_volumes.go +++ b/pkg/api/server/register_volumes.go @@ -53,8 +53,8 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // produces: // - application/json // responses: - // '204': - // description: no error + // '200': + // "$ref": "#/responses/VolumePruneResponse" // '500': // "$ref": "#/responses/InternalError" r.Handle(VersionedPath("/libpod/volumes/prune"), s.APIHandler(libpod.PruneVolumes)).Methods(http.MethodPost) @@ -71,11 +71,11 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // - application/json // responses: // '200': - // "$ref": "#/responses/InspectVolumeResponse" + // "$ref": "#/responses/VolumeCreateResponse" // '404': - // "$ref": "#/responses/NoSuchVolume" + // "$ref": "#/responses/NoSuchVolume" // '500': - // "$ref": "#/responses/InternalError" + // "$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 // --- diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go index 2e1a269f2..50ce42b3b 100644 --- a/pkg/api/server/swagger.go +++ b/pkg/api/server/swagger.go @@ -151,6 +151,13 @@ type ok struct { } } +// Volume prune response +// swagger:response VolumePruneResponse +type swagVolumePruneResponse struct { + // in:body + Body []entities.VolumePruneReport +} + // Volume create response // swagger:response VolumeCreateResponse type swagVolumeCreateResponse struct { diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index a2164e0af..cef9246cb 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" - "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/domain/entities" jsoniter "github.com/json-iterator/go" @@ -35,9 +34,9 @@ func Create(ctx context.Context, config entities.VolumeCreateOptions) (*entities } // Inspect returns low-level information about a volume. -func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, error) { +func Inspect(ctx context.Context, nameOrID string) (*entities.VolumeConfigResponse, error) { var ( - inspect libpod.InspectVolumeData + inspect entities.VolumeConfigResponse ) conn, err := bindings.GetClient(ctx) if err != nil { @@ -52,9 +51,9 @@ func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, e // List returns the configurations for existing volumes in the form of a slice. Optionally, filters // can be used to refine the list of volumes. -func List(ctx context.Context, filters map[string][]string) ([]*libpod.VolumeConfig, error) { +func List(ctx context.Context, filters map[string][]string) ([]*entities.VolumeListReport, error) { var ( - vols []*libpod.VolumeConfig + vols []*entities.VolumeListReport ) conn, err := bindings.GetClient(ctx) if err != nil { @@ -76,9 +75,9 @@ func List(ctx context.Context, filters map[string][]string) ([]*libpod.VolumeCon } // Prune removes unused volumes from the local filesystem. -func Prune(ctx context.Context) ([]string, error) { +func Prune(ctx context.Context) ([]*entities.VolumePruneReport, error) { var ( - pruned []string + pruned []*entities.VolumePruneReport ) conn, err := bindings.GetClient(ctx) if err != nil { @@ -86,7 +85,7 @@ func Prune(ctx context.Context) ([]string, error) { } response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/prune", nil) if err != nil { - return pruned, err + return nil, err } return pruned, response.Process(&pruned) } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 2887f0b51..2efdbd602 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -5,7 +5,6 @@ import ( ) type ContainerEngine interface { - ContainerPrune(ctx context.Context) (*ContainerPruneReport, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerKill(ctx context.Context, namesOrIds []string, options KillOptions) ([]*KillReport, error) ContainerPause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) @@ -14,10 +13,10 @@ type ContainerEngine interface { ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) - PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error) PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) - PodPrune(ctx context.Context) (*PodPruneReport, error) VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error) - VolumeDelete(ctx context.Context, opts VolumeDeleteOptions) (*VolumeDeleteReport, error) - VolumePrune(ctx context.Context) (*VolumePruneReport, error) + VolumeInspect(ctx context.Context, namesOrIds []string, opts VolumeInspectOptions) ([]*VolumeInspectReport, error) + VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) + VolumePrune(ctx context.Context, opts VolumePruneOptions) ([]*VolumePruneReport, error) + VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) } diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index 6f947dc4d..e7757a74b 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -13,13 +13,5 @@ type Report struct { Err map[string]error } -type ContainerDeleteOptions struct{} -type ContainerDeleteReport struct{ Report } -type ContainerPruneReport struct{ Report } - type PodDeleteReport struct{ Report } type PodPruneOptions struct{} -type PodPruneReport struct{ Report } -type VolumeDeleteOptions struct{} -type VolumeDeleteReport struct{ Report } -type VolumePruneReport struct{ Report } diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index ad12d0d01..23c066083 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -1,6 +1,8 @@ package entities -import "time" +import ( + "time" +) // swagger:model VolumeCreate type VolumeCreateOptions struct { @@ -20,22 +22,71 @@ type IdOrNameResponse struct { } type VolumeConfigResponse struct { - // Name of the volume. - Name string `json:"name"` - Labels map[string]string `json:"labels"` - // The volume driver. Empty string or local does not activate a volume - // driver, all other volumes will. - Driver string `json:"volumeDriver"` - // The location the volume is mounted at. - MountPoint string `json:"mountPoint"` - // Time the volume was created. - CreatedTime time.Time `json:"createdAt,omitempty"` - // Options to pass to the volume driver. For the local driver, this is - // a list of mount options. For other drivers, they are passed to the - // volume driver handling the volume. - Options map[string]string `json:"volumeOptions,omitempty"` - // UID the volume will be created as. - UID int `json:"uid"` - // GID the volume will be created as. - GID int `json:"gid"` + // Name is the name of the volume. + Name string `json:"Name"` + // Driver is the driver used to create the volume. + // This will be properly implemented in a future version. + Driver string `json:"Driver"` + // Mountpoint is the path on the host where the volume is mounted. + Mountpoint string `json:"Mountpoint"` + // CreatedAt is the date and time the volume was created at. This is not + // stored for older Libpod volumes; if so, it will be omitted. + CreatedAt time.Time `json:"CreatedAt,omitempty"` + // Status is presently unused and provided only for Docker compatibility. + // In the future it will be used to return information on the volume's + // current state. + Status map[string]string `json:"Status,omitempty"` + // Labels includes the volume's configured labels, key:value pairs that + // can be passed during volume creation to provide information for third + // party tools. + Labels map[string]string `json:"Labels"` + // Scope is unused and provided solely for Docker compatibility. It is + // unconditionally set to "local". + Scope string `json:"Scope"` + // Options is a set of options that were used when creating the volume. + // It is presently not used. + Options map[string]string `json:"Options"` + // UID is the UID that the volume was created with. + UID int `json:"UID,omitempty"` + // GID is the GID that the volume was created with. + GID int `json:"GID,omitempty"` + // Anonymous indicates that the volume was created as an anonymous + // volume for a specific container, and will be be removed when any + // container using it is removed. + Anonymous bool `json:"Anonymous,omitempty"` +} + +type VolumeRmOptions struct { + All bool + Force bool +} + +type VolumeRmReport struct { + Err error + Id string +} + +type VolumeInspectOptions struct { + All bool +} + +type VolumeInspectReport struct { + *VolumeConfigResponse +} + +type VolumePruneOptions struct { + Force bool +} + +type VolumePruneReport struct { + Err error + Id string +} + +type VolumeListOptions struct { + Filter map[string][]string +} + +type VolumeListReport struct { + VolumeConfigResponse } diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go new file mode 100644 index 000000000..f97c3f570 --- /dev/null +++ b/pkg/domain/filters/volumes.go @@ -0,0 +1,70 @@ +package filters + +import ( + "strings" + + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" +) + +func GenerateVolumeFilters(filters map[string][]string) ([]libpod.VolumeFilter, error) { + var vf []libpod.VolumeFilter + for filter, v := range filters { + for _, val := range v { + switch filter { + case "name": + nameVal := val + vf = append(vf, func(v *libpod.Volume) bool { + return nameVal == v.Name() + }) + case "driver": + driverVal := val + vf = append(vf, func(v *libpod.Volume) bool { + return v.Driver() == driverVal + }) + case "scope": + scopeVal := val + vf = append(vf, func(v *libpod.Volume) bool { + return v.Scope() == scopeVal + }) + case "label": + filterArray := strings.SplitN(val, "=", 2) + filterKey := filterArray[0] + var filterVal string + if len(filterArray) > 1 { + filterVal = filterArray[1] + } else { + filterVal = "" + } + vf = append(vf, func(v *libpod.Volume) bool { + for labelKey, labelValue := range v.Labels() { + if labelKey == filterKey && ("" == filterVal || labelValue == filterVal) { + return true + } + } + return false + }) + case "opt": + filterArray := strings.SplitN(val, "=", 2) + filterKey := filterArray[0] + var filterVal string + if len(filterArray) > 1 { + filterVal = filterArray[1] + } else { + filterVal = "" + } + vf = append(vf, func(v *libpod.Volume) bool { + for labelKey, labelValue := range v.Options() { + if labelKey == filterKey && ("" == filterVal || labelValue == filterVal) { + return true + } + } + return false + }) + default: + return nil, errors.Errorf("%q is in an invalid volume filter", filter) + } + } + } + return vf, nil +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 47deb010a..a3da310c2 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -239,23 +239,3 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, } return reports, nil } - -func (ic *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { - panic("implement me") -} - -func (ic *ContainerEngine) PodDelete(ctx context.Context, opts entities.PodPruneOptions) (*entities.PodDeleteReport, error) { - panic("implement me") -} - -func (ic *ContainerEngine) PodPrune(ctx context.Context) (*entities.PodPruneReport, error) { - panic("implement me") -} - -func (ic *ContainerEngine) VolumeDelete(ctx context.Context, opts entities.VolumeDeleteOptions) (*entities.VolumeDeleteReport, error) { - panic("implement me") -} - -func (ic *ContainerEngine) VolumePrune(ctx context.Context) (*entities.VolumePruneReport, error) { - panic("implement me") -} diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index 0783af441..0cc20474e 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -7,7 +7,9 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/filters" "github.com/containers/libpod/pkg/domain/infra/abi/parse" + "github.com/pkg/errors" ) func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) { @@ -36,3 +38,109 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum } return &entities.IdOrNameResponse{IdOrName: vol.Name()}, nil } + +func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, opts entities.VolumeRmOptions) ([]*entities.VolumeRmReport, error) { + var ( + err error + reports []*entities.VolumeRmReport + vols []*libpod.Volume + ) + if opts.All { + vols, err = ic.Libpod.Volumes() + if err != nil { + return nil, err + } + } else { + for _, id := range namesOrIds { + vol, err := ic.Libpod.LookupVolume(id) + if err != nil { + reports = append(reports, &entities.VolumeRmReport{ + Err: err, + Id: id, + }) + continue + } + vols = append(vols, vol) + } + } + for _, vol := range vols { + reports = append(reports, &entities.VolumeRmReport{ + Err: ic.Libpod.RemoveVolume(ctx, vol, opts.Force), + Id: vol.Name(), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []string, opts entities.VolumeInspectOptions) ([]*entities.VolumeInspectReport, error) { + var ( + err error + reports []*entities.VolumeInspectReport + vols []*libpod.Volume + ) + + // Note: as with previous implementation, a single failure here + // results a return. + if opts.All { + vols, err = ic.Libpod.GetAllVolumes() + if err != nil { + return nil, err + } + } else { + for _, v := range namesOrIds { + vol, err := ic.Libpod.LookupVolume(v) + if err != nil { + return nil, errors.Wrapf(err, "error inspecting volume %s", v) + } + vols = append(vols, vol) + } + } + for _, v := range vols { + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), + } + reports = append(reports, &entities.VolumeInspectReport{&config}) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { + return ic.Libpod.PruneVolumes(ctx) +} + +func (ic *ContainerEngine) VolumeList(ctx context.Context, opts entities.VolumeListOptions) ([]*entities.VolumeListReport, error) { + var ( + reports []*entities.VolumeListReport + ) + volumeFilters, err := filters.GenerateVolumeFilters(opts.Filter) + if err != nil { + return nil, err + } + vols, err := ic.Libpod.Volumes(volumeFilters...) + if err != nil { + return nil, err + } + for _, v := range vols { + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), + } + reports = append(reports, &entities.VolumeListReport{config}) + } + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 21f62df6b..a8ecff41b 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -33,13 +33,6 @@ func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []strin return responses, nil } -func (r *ContainerEngine) ContainerDelete(ctx context.Context, opts entities.ContainerDeleteOptions) (*entities.ContainerDeleteReport, error) { - panic("implement me") -} - -func (r *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { - panic("implement me") -} func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { var ( reports []*entities.PauseUnpauseReport diff --git a/pkg/domain/infra/tunnel/runtime.go b/pkg/domain/infra/tunnel/runtime.go index eb9b34e4a..c111f99e9 100644 --- a/pkg/domain/infra/tunnel/runtime.go +++ b/pkg/domain/infra/tunnel/runtime.go @@ -2,8 +2,6 @@ package tunnel import ( "context" - - "github.com/containers/libpod/pkg/domain/entities" ) // Image-related runtime using an ssh-tunnel to utilize Podman service @@ -15,19 +13,3 @@ type ImageEngine struct { type ContainerEngine struct { ClientCxt context.Context } - -func (r *ContainerEngine) PodDelete(ctx context.Context, opts entities.PodPruneOptions) (*entities.PodDeleteReport, error) { - panic("implement me") -} - -func (r *ContainerEngine) PodPrune(ctx context.Context) (*entities.PodPruneReport, error) { - panic("implement me") -} - -func (r *ContainerEngine) VolumeDelete(ctx context.Context, opts entities.VolumeDeleteOptions) (*entities.VolumeDeleteReport, error) { - panic("implement me") -} - -func (r *ContainerEngine) VolumePrune(ctx context.Context) (*entities.VolumePruneReport, error) { - panic("implement me") -} diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index 49cf6a2f6..e48a7fa7c 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -14,3 +14,57 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum } return &entities.IdOrNameResponse{IdOrName: response.Name}, nil } + +func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, opts entities.VolumeRmOptions) ([]*entities.VolumeRmReport, error) { + var ( + reports []*entities.VolumeRmReport + ) + + if opts.All { + vols, err := volumes.List(ic.ClientCxt, nil) + if err != nil { + return nil, err + } + for _, v := range vols { + namesOrIds = append(namesOrIds, v.Name) + } + } + for _, id := range namesOrIds { + reports = append(reports, &entities.VolumeRmReport{ + Err: volumes.Remove(ic.ClientCxt, id, &opts.Force), + Id: id, + }) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []string, opts entities.VolumeInspectOptions) ([]*entities.VolumeInspectReport, error) { + var ( + reports []*entities.VolumeInspectReport + ) + if opts.All { + vols, err := volumes.List(ic.ClientCxt, nil) + if err != nil { + return nil, err + } + for _, v := range vols { + namesOrIds = append(namesOrIds, v.Name) + } + } + for _, id := range namesOrIds { + data, err := volumes.Inspect(ic.ClientCxt, id) + if err != nil { + return nil, err + } + reports = append(reports, &entities.VolumeInspectReport{VolumeConfigResponse: data}) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { + return volumes.Prune(ic.ClientCxt) +} + +func (ic *ContainerEngine) VolumeList(ctx context.Context, opts entities.VolumeListOptions) ([]*entities.VolumeListReport, error) { + return volumes.List(ic.ClientCxt, opts.Filter) +} diff --git a/pkg/varlinkapi/volumes.go b/pkg/varlinkapi/volumes.go index 2dddd3008..cbb4a70cc 100644 --- a/pkg/varlinkapi/volumes.go +++ b/pkg/varlinkapi/volumes.go @@ -105,16 +105,20 @@ func (i *LibpodAPI) InspectVolume(call iopodman.VarlinkCall, name string) error // VolumesPrune removes unused images via a varlink call func (i *LibpodAPI) VolumesPrune(call iopodman.VarlinkCall) error { - var errs []string - prunedNames, prunedErrors := i.Runtime.PruneVolumes(getContext()) - if len(prunedErrors) == 0 { - return call.ReplyVolumesPrune(prunedNames, []string{}) + var ( + prunedErrors []string + prunedNames []string + ) + responses, err := i.Runtime.PruneVolumes(getContext()) + if err != nil { + return call.ReplyVolumesPrune([]string{}, []string{err.Error()}) } - - // We need to take the errors and capture their strings to go back over - // varlink - for _, e := range prunedErrors { - errs = append(errs, e.Error()) + for _, i := range responses { + if i.Err == nil { + prunedNames = append(prunedNames, i.Id) + } else { + prunedErrors = append(prunedErrors, i.Err.Error()) + } } - return call.ReplyVolumesPrune(prunedNames, errs) + return call.ReplyVolumesPrune(prunedNames, prunedErrors) } |