aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com>2018-12-06 08:59:13 -0800
committerGitHub <noreply@github.com>2018-12-06 08:59:13 -0800
commit5c6e02b55be974f08e3b1e895046a3cf167fd3f7 (patch)
tree7d0e2a5a0ec706e5f4dc6b12ddbb8a1002232b24
parent3e60de629d5feaff1ac15173b4ff1e5325dfa5dc (diff)
parent375831e97693af4d894cf647af4dad57ef21948d (diff)
downloadpodman-5c6e02b55be974f08e3b1e895046a3cf167fd3f7.tar.gz
podman-5c6e02b55be974f08e3b1e895046a3cf167fd3f7.tar.bz2
podman-5c6e02b55be974f08e3b1e895046a3cf167fd3f7.zip
Merge pull request #1904 from umohnani8/volume
Add "podman volume" command
-rw-r--r--cmd/podman/create_cli.go11
-rw-r--r--cmd/podman/libpodruntime/runtime.go6
-rw-r--r--cmd/podman/main.go3
-rw-r--r--cmd/podman/utils.go34
-rw-r--r--cmd/podman/volume.go26
-rw-r--r--cmd/podman/volume_create.go97
-rw-r--r--cmd/podman/volume_inspect.go63
-rw-r--r--cmd/podman/volume_ls.go308
-rw-r--r--cmd/podman/volume_prune.go86
-rw-r--r--cmd/podman/volume_rm.go71
-rw-r--r--commands.md5
-rw-r--r--completions/bash/podman140
-rw-r--r--docs/podman-volume-create.1.md48
-rw-r--r--docs/podman-volume-inspect.1.md45
-rw-r--r--docs/podman-volume-ls.1.md49
-rw-r--r--docs/podman-volume-prune.1.md38
-rw-r--r--docs/podman-volume-rm.1.md45
-rw-r--r--docs/podman-volume.1.md23
-rw-r--r--libpod/boltdb_state.go378
-rw-r--r--libpod/boltdb_state_internal.go128
-rw-r--r--libpod/common_test.go5
-rw-r--r--libpod/container_inspect.go16
-rw-r--r--libpod/errors.go14
-rw-r--r--libpod/in_memory_state.go168
-rw-r--r--libpod/options.go80
-rw-r--r--libpod/runtime.go4
-rw-r--r--libpod/runtime_ctr.go27
-rw-r--r--libpod/runtime_volume.go107
-rw-r--r--libpod/runtime_volume_linux.go132
-rw-r--r--libpod/state.go23
-rw-r--r--libpod/volume.go63
-rw-r--r--libpod/volume_internal.go29
-rw-r--r--pkg/util/utils.go52
-rw-r--r--test/e2e/libpod_suite_test.go11
-rw-r--r--test/e2e/volume_create_test.go60
-rw-r--r--test/e2e/volume_inspect_test.go77
-rw-r--r--test/e2e/volume_ls_test.go84
-rw-r--r--test/e2e/volume_prune_test.go64
-rw-r--r--test/e2e/volume_rm_test.go91
39 files changed, 2672 insertions, 39 deletions
diff --git a/cmd/podman/create_cli.go b/cmd/podman/create_cli.go
index 218e9b806..b3a30d185 100644
--- a/cmd/podman/create_cli.go
+++ b/cmd/podman/create_cli.go
@@ -201,12 +201,13 @@ func parseVolumesFrom(volumesFrom []string) error {
}
func validateVolumeHostDir(hostDir string) error {
- if !filepath.IsAbs(hostDir) {
- return errors.Errorf("invalid host path, must be an absolute path %q", hostDir)
- }
- if _, err := os.Stat(hostDir); err != nil {
- return errors.Wrapf(err, "error checking path %q", hostDir)
+ if filepath.IsAbs(hostDir) {
+ if _, err := os.Stat(hostDir); err != nil {
+ return errors.Wrapf(err, "error checking path %q", hostDir)
+ }
}
+ // If hostDir is not an absolute path, that means the user wants to create a
+ // named volume. This will be done later on in the code.
return nil
}
diff --git a/cmd/podman/libpodruntime/runtime.go b/cmd/podman/libpodruntime/runtime.go
index 0dc6bcf18..d7a0dd931 100644
--- a/cmd/podman/libpodruntime/runtime.go
+++ b/cmd/podman/libpodruntime/runtime.go
@@ -14,6 +14,11 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) {
storageOpts := new(storage.StoreOptions)
options := []libpod.RuntimeOption{}
+ _, volumePath, err := util.GetDefaultStoreOptions()
+ if err != nil {
+ return nil, err
+ }
+
if c.IsSet("uidmap") || c.IsSet("gidmap") || c.IsSet("subuidmap") || c.IsSet("subgidmap") {
mappings, err := util.ParseIDMapping(c.StringSlice("uidmap"), c.StringSlice("gidmap"), c.String("subuidmap"), c.String("subgidmap"))
if err != nil {
@@ -90,6 +95,7 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) {
if c.IsSet("infra-command") {
options = append(options, libpod.WithDefaultInfraCommand(c.String("infra-command")))
}
+ options = append(options, libpod.WithVolumePath(volumePath))
if c.IsSet("config") {
return libpod.NewRuntimeFromConfig(c.String("config"), options...)
}
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index bcae04575..280448dc8 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -2,6 +2,7 @@ package main
import (
"fmt"
+ "log/syslog"
"os"
"os/exec"
"runtime/pprof"
@@ -16,7 +17,6 @@ import (
"github.com/sirupsen/logrus"
lsyslog "github.com/sirupsen/logrus/hooks/syslog"
"github.com/urfave/cli"
- "log/syslog"
)
// This is populated by the Makefile from the VERSION file
@@ -102,6 +102,7 @@ func main() {
umountCommand,
unpauseCommand,
versionCommand,
+ volumeCommand,
waitCommand,
}
diff --git a/cmd/podman/utils.go b/cmd/podman/utils.go
index 5735156c2..a59535b43 100644
--- a/cmd/podman/utils.go
+++ b/cmd/podman/utils.go
@@ -3,6 +3,9 @@ package main
import (
"context"
"fmt"
+ "os"
+ gosignal "os/signal"
+
"github.com/containers/libpod/libpod"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
@@ -11,8 +14,6 @@ import (
"github.com/urfave/cli"
"golang.org/x/crypto/ssh/terminal"
"k8s.io/client-go/tools/remotecommand"
- "os"
- gosignal "os/signal"
)
type RawTtyFormatter struct {
@@ -208,6 +209,35 @@ func getPodsFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Pod, error
return pods, lastError
}
+func getVolumesFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Volume, error) {
+ args := c.Args()
+ var (
+ vols []*libpod.Volume
+ lastError error
+ err error
+ )
+
+ if c.Bool("all") {
+ vols, err = r.Volumes()
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to get all volumes")
+ }
+ }
+
+ for _, i := range args {
+ vol, err := r.GetVolume(i)
+ if err != nil {
+ if lastError != nil {
+ logrus.Errorf("%q", lastError)
+ }
+ lastError = errors.Wrapf(err, "unable to find volume %s", i)
+ continue
+ }
+ vols = append(vols, vol)
+ }
+ return vols, lastError
+}
+
//printParallelOutput takes the map of parallel worker results and outputs them
// to stdout
func printParallelOutput(m map[string]error, errCount int) error {
diff --git a/cmd/podman/volume.go b/cmd/podman/volume.go
new file mode 100644
index 000000000..913592e74
--- /dev/null
+++ b/cmd/podman/volume.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "github.com/urfave/cli"
+)
+
+var (
+ volumeDescription = `Manage volumes.
+
+Volumes are created in and can be shared between containers.`
+
+ volumeSubCommands = []cli.Command{
+ volumeCreateCommand,
+ volumeLsCommand,
+ volumeRmCommand,
+ volumeInspectCommand,
+ volumePruneCommand,
+ }
+ volumeCommand = cli.Command{
+ Name: "volume",
+ Usage: "Manage volumes",
+ Description: volumeDescription,
+ UseShortOptionHandling: true,
+ Subcommands: volumeSubCommands,
+ }
+)
diff --git a/cmd/podman/volume_create.go b/cmd/podman/volume_create.go
new file mode 100644
index 000000000..0b5f8d1e3
--- /dev/null
+++ b/cmd/podman/volume_create.go
@@ -0,0 +1,97 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/containers/libpod/cmd/podman/libpodruntime"
+ "github.com/containers/libpod/libpod"
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+var volumeCreateDescription = `
+podman volume create
+
+Creates a new volume. If using the default driver, "local", the volume will
+be created at.`
+
+var volumeCreateFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "driver",
+ Usage: "Specify volume driver name (default local)",
+ },
+ cli.StringSliceFlag{
+ Name: "label, l",
+ Usage: "Set metadata for a volume (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "opt, o",
+ Usage: "Set driver specific options (default [])",
+ },
+}
+
+var volumeCreateCommand = cli.Command{
+ Name: "create",
+ Usage: "Create a new volume",
+ Description: volumeCreateDescription,
+ Flags: volumeCreateFlags,
+ Action: volumeCreateCmd,
+ SkipArgReorder: true,
+ ArgsUsage: "[VOLUME-NAME]",
+ UseShortOptionHandling: true,
+}
+
+func volumeCreateCmd(c *cli.Context) error {
+ var (
+ options []libpod.VolumeCreateOption
+ err error
+ volName string
+ )
+
+ if err = validateFlags(c, volumeCreateFlags); err != nil {
+ return err
+ }
+
+ runtime, err := libpodruntime.GetRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ if len(c.Args()) > 1 {
+ return errors.Errorf("too many arguments, create takes at most 1 argument")
+ }
+
+ if len(c.Args()) > 0 {
+ volName = c.Args()[0]
+ options = append(options, libpod.WithVolumeName(volName))
+ }
+
+ if c.IsSet("driver") {
+ options = append(options, libpod.WithVolumeDriver(c.String("driver")))
+ }
+
+ labels, err := getAllLabels([]string{}, c.StringSlice("label"))
+ if err != nil {
+ return errors.Wrapf(err, "unable to process labels")
+ }
+ if len(labels) != 0 {
+ options = append(options, libpod.WithVolumeLabels(labels))
+ }
+
+ opts, err := getAllLabels([]string{}, c.StringSlice("opt"))
+ if err != nil {
+ return errors.Wrapf(err, "unable to process options")
+ }
+ if len(options) != 0 {
+ options = append(options, libpod.WithVolumeOptions(opts))
+ }
+
+ vol, err := runtime.NewVolume(getContext(), options...)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%s\n", vol.Name())
+
+ return nil
+}
diff --git a/cmd/podman/volume_inspect.go b/cmd/podman/volume_inspect.go
new file mode 100644
index 000000000..152f1d098
--- /dev/null
+++ b/cmd/podman/volume_inspect.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "github.com/containers/libpod/cmd/podman/libpodruntime"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var volumeInspectDescription = `
+podman volume inspect
+
+Display detailed information on one or more volumes. Can change the format
+from JSON to a Go template.
+`
+
+var volumeInspectFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "Inspect all volumes",
+ },
+ cli.StringFlag{
+ Name: "format, f",
+ Usage: "Format volume output using Go template",
+ Value: "json",
+ },
+}
+
+var volumeInspectCommand = cli.Command{
+ Name: "inspect",
+ Usage: "Display detailed information on one or more volumes",
+ Description: volumeInspectDescription,
+ Flags: volumeInspectFlags,
+ Action: volumeInspectCmd,
+ SkipArgReorder: true,
+ ArgsUsage: "[VOLUME-NAME ...]",
+ UseShortOptionHandling: true,
+}
+
+func volumeInspectCmd(c *cli.Context) error {
+ var err error
+
+ if err = validateFlags(c, volumeInspectFlags); err != nil {
+ return err
+ }
+
+ runtime, err := libpodruntime.GetRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ opts := volumeLsOptions{
+ Format: c.String("format"),
+ }
+
+ vols, lastError := getVolumesFromContext(c, runtime)
+ if lastError != nil {
+ logrus.Errorf("%q", lastError)
+ }
+
+ return generateVolLsOutput(vols, opts, runtime)
+}
diff --git a/cmd/podman/volume_ls.go b/cmd/podman/volume_ls.go
new file mode 100644
index 000000000..0f94549ee
--- /dev/null
+++ b/cmd/podman/volume_ls.go
@@ -0,0 +1,308 @@
+package main
+
+import (
+ "reflect"
+ "strings"
+
+ "github.com/containers/libpod/cmd/podman/formats"
+ "github.com/containers/libpod/cmd/podman/libpodruntime"
+ "github.com/containers/libpod/libpod"
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+// volumeOptions is the "ls" command options
+type volumeLsOptions struct {
+ Format string
+ Quiet bool
+}
+
+// volumeLsTemplateParams is the template parameters to list the volumes
+type volumeLsTemplateParams struct {
+ Name string
+ Labels string
+ MountPoint string
+ Driver string
+ Options string
+ Scope string
+}
+
+// volumeLsJSONParams is the JSON parameters to list the volumes
+type volumeLsJSONParams struct {
+ Name string `json:"name"`
+ Labels map[string]string `json:"labels"`
+ MountPoint string `json:"mountPoint"`
+ Driver string `json:"driver"`
+ Options map[string]string `json:"options"`
+ Scope string `json:"scope"`
+}
+
+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.
+`
+
+var volumeLsFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "filter, f",
+ Usage: "Filter volume output",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Format volume output using Go template",
+ Value: "table {{.Driver}}\t{{.Name}}",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "Print volume output in quiet mode",
+ },
+}
+
+var volumeLsCommand = cli.Command{
+ Name: "ls",
+ Aliases: []string{"list"},
+ Usage: "List volumes",
+ Description: volumeLsDescription,
+ Flags: volumeLsFlags,
+ Action: volumeLsCmd,
+ SkipArgReorder: true,
+ UseShortOptionHandling: true,
+}
+
+func volumeLsCmd(c *cli.Context) error {
+ if err := validateFlags(c, volumeLsFlags); err != nil {
+ return err
+ }
+
+ runtime, err := libpodruntime.GetRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ if len(c.Args()) > 0 {
+ return errors.Errorf("too many arguments, ls takes no arguments")
+ }
+
+ opts := volumeLsOptions{
+ Quiet: c.Bool("quiet"),
+ }
+ opts.Format = genVolLsFormat(c)
+
+ // Get the filter functions based on any filters set
+ var filterFuncs []libpod.VolumeFilter
+ if c.String("filter") != "" {
+ filters := strings.Split(c.String("filter"), ",")
+ for _, f := range filters {
+ 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)
+ }
+ generatedFunc, err := generateVolumeFilterFuncs(filterSplit[0], filterSplit[1], runtime)
+ if err != nil {
+ return errors.Wrapf(err, "invalid filter")
+ }
+ filterFuncs = append(filterFuncs, generatedFunc)
+ }
+ }
+
+ volumes, err := runtime.GetAllVolumes()
+ if err != nil {
+ return err
+ }
+
+ // Get the volumes that match the filter
+ volsFiltered := make([]*libpod.Volume, 0, len(volumes))
+ for _, vol := range volumes {
+ include := true
+ for _, filter := range filterFuncs {
+ include = include && filter(vol)
+ }
+
+ if include {
+ volsFiltered = append(volsFiltered, vol)
+ }
+ }
+ return generateVolLsOutput(volsFiltered, opts, runtime)
+}
+
+// generate the template based on conditions given
+func genVolLsFormat(c *cli.Context) string {
+ var format string
+ if c.String("format") != "" {
+ // "\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"
+ format = strings.Replace(c.String("format"), `\t`, "\t", -1)
+ }
+ if c.Bool("quiet") {
+ format = "{{.Name}}"
+ }
+ return format
+}
+
+// Convert output to genericParams for printing
+func volLsToGeneric(templParams []volumeLsTemplateParams, JSONParams []volumeLsJSONParams) (genericParams []interface{}) {
+ if len(templParams) > 0 {
+ for _, v := range templParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+ }
+ for _, v := range JSONParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+}
+
+// generate the accurate header based on template given
+func (vol *volumeLsTemplateParams) volHeaderMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(vol))
+ values := make(map[string]string)
+
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ if value == "Name" {
+ value = "Volume" + value
+ }
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+// getVolTemplateOutput returns all the volumes in the volumeLsTemplateParams format
+func getVolTemplateOutput(lsParams []volumeLsJSONParams, opts volumeLsOptions) ([]volumeLsTemplateParams, error) {
+ var lsOutput []volumeLsTemplateParams
+
+ for _, lsParam := range lsParams {
+ var (
+ labels string
+ options string
+ )
+
+ for k, v := range lsParam.Labels {
+ label := k
+ if v != "" {
+ label += "=" + v
+ }
+ labels += label
+ }
+ for k, v := range lsParam.Options {
+ option := k
+ if v != "" {
+ option += "=" + v
+ }
+ options += option
+ }
+ params := volumeLsTemplateParams{
+ Name: lsParam.Name,
+ Driver: lsParam.Driver,
+ MountPoint: lsParam.MountPoint,
+ Scope: lsParam.Scope,
+ Labels: labels,
+ Options: options,
+ }
+
+ lsOutput = append(lsOutput, params)
+ }
+ return lsOutput, nil
+}
+
+// getVolJSONParams returns the volumes in JSON format
+func getVolJSONParams(volumes []*libpod.Volume, opts volumeLsOptions, runtime *libpod.Runtime) ([]volumeLsJSONParams, error) {
+ var lsOutput []volumeLsJSONParams
+
+ for _, volume := range volumes {
+ params := volumeLsJSONParams{
+ Name: volume.Name(),
+ Labels: volume.Labels(),
+ MountPoint: volume.MountPoint(),
+ Driver: volume.Driver(),
+ Options: volume.Options(),
+ Scope: volume.Scope(),
+ }
+
+ lsOutput = append(lsOutput, params)
+ }
+ return lsOutput, nil
+}
+
+// generateVolLsOutput generates the output based on the format, JSON or Go Template, and prints it out
+func generateVolLsOutput(volumes []*libpod.Volume, opts volumeLsOptions, runtime *libpod.Runtime) error {
+ if len(volumes) == 0 && opts.Format != formats.JSONString {
+ return nil
+ }
+ lsOutput, err := getVolJSONParams(volumes, opts, runtime)
+ if err != nil {
+ return err
+ }
+ var out formats.Writer
+
+ switch opts.Format {
+ case formats.JSONString:
+ if err != nil {
+ return errors.Wrapf(err, "unable to create JSON for volume output")
+ }
+ out = formats.JSONStructArray{Output: volLsToGeneric([]volumeLsTemplateParams{}, lsOutput)}
+ default:
+ lsOutput, err := getVolTemplateOutput(lsOutput, opts)
+ if err != nil {
+ return errors.Wrapf(err, "unable to create volume output")
+ }
+ out = formats.StdoutTemplateArray{Output: volLsToGeneric(lsOutput, []volumeLsJSONParams{}), Template: opts.Format, Fields: lsOutput[0].volHeaderMap()}
+ }
+ return formats.Writer(out).Out()
+}
+
+// generateVolumeFilterFuncs returns the true if the volume matches the filter set, otherwise it returns false.
+func generateVolumeFilterFuncs(filter, filterValue string, runtime *libpod.Runtime) (func(volume *libpod.Volume) bool, error) {
+ switch filter {
+ case "name":
+ return func(v *libpod.Volume) bool {
+ return strings.Contains(v.Name(), filterValue)
+ }, nil
+ case "driver":
+ return func(v *libpod.Volume) bool {
+ return v.Driver() == filterValue
+ }, nil
+ case "scope":
+ return func(v *libpod.Volume) bool {
+ return v.Scope() == filterValue
+ }, nil
+ case "label":
+ filterArray := strings.SplitN(filterValue, "=", 2)
+ filterKey := filterArray[0]
+ if len(filterArray) > 1 {
+ filterValue = filterArray[1]
+ } else {
+ filterValue = ""
+ }
+ return func(v *libpod.Volume) bool {
+ for labelKey, labelValue := range v.Labels() {
+ if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) {
+ return true
+ }
+ }
+ return false
+ }, nil
+ case "opt":
+ filterArray := strings.SplitN(filterValue, "=", 2)
+ filterKey := filterArray[0]
+ if len(filterArray) > 1 {
+ filterValue = filterArray[1]
+ } else {
+ filterValue = ""
+ }
+ return func(v *libpod.Volume) bool {
+ for labelKey, labelValue := range v.Options() {
+ if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) {
+ return true
+ }
+ }
+ return false
+ }, nil
+ }
+ return nil, errors.Errorf("%s is an invalid filter", filter)
+}
diff --git a/cmd/podman/volume_prune.go b/cmd/podman/volume_prune.go
new file mode 100644
index 000000000..652c50f42
--- /dev/null
+++ b/cmd/podman/volume_prune.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/containers/libpod/cmd/podman/libpodruntime"
+ "github.com/containers/libpod/libpod"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var volumePruneDescription = `
+podman volume prune
+
+Remove all unused volumes. Will prompt for confirmation if not
+using force.
+`
+
+var volumePruneFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "force, f",
+ Usage: "Do not prompt for confirmation",
+ },
+}
+
+var volumePruneCommand = cli.Command{
+ Name: "prune",
+ Usage: "Remove all unused volumes",
+ Description: volumePruneDescription,
+ Flags: volumePruneFlags,
+ Action: volumePruneCmd,
+ SkipArgReorder: true,
+ UseShortOptionHandling: true,
+}
+
+func volumePruneCmd(c *cli.Context) error {
+ var lastError error
+
+ if err := validateFlags(c, volumePruneFlags); err != nil {
+ return err
+ }
+
+ runtime, err := libpodruntime.GetRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ ctx := getContext()
+
+ // Prompt for confirmation if --force is not set
+ if !c.Bool("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] ")
+ ans, err := reader.ReadString('\n')
+ if err != nil {
+ return errors.Wrapf(err, "error reading input")
+ }
+ if strings.ToLower(ans)[0] != 'y' {
+ return nil
+ }
+ }
+
+ volumes, err := runtime.GetAllVolumes()
+ if err != nil {
+ return err
+ }
+
+ for _, vol := range volumes {
+ err = runtime.RemoveVolume(ctx, vol, false, true)
+ if err == nil {
+ fmt.Println(vol.Name())
+ } else if err != libpod.ErrVolumeBeingUsed {
+ if lastError != nil {
+ logrus.Errorf("%q", lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to remove volume %q", vol.Name())
+ }
+ }
+ return lastError
+}
diff --git a/cmd/podman/volume_rm.go b/cmd/podman/volume_rm.go
new file mode 100644
index 000000000..3fb623624
--- /dev/null
+++ b/cmd/podman/volume_rm.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/containers/libpod/cmd/podman/libpodruntime"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var volumeRmDescription = `
+podman volume rm
+
+Remove one or more existing volumes. Will only remove volumes that are
+not being used by any containers. To remove the volumes anyways, use the
+--force flag.
+`
+
+var volumeRmFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "Remove all volumes",
+ },
+ cli.BoolFlag{
+ Name: "force, f",
+ Usage: "Remove a volume by force, even if it is being used by a container",
+ },
+}
+
+var volumeRmCommand = cli.Command{
+ Name: "rm",
+ Aliases: []string{"remove"},
+ Usage: "Remove one or more volumes",
+ Description: volumeRmDescription,
+ Flags: volumeRmFlags,
+ Action: volumeRmCmd,
+ ArgsUsage: "[VOLUME-NAME ...]",
+ SkipArgReorder: true,
+ UseShortOptionHandling: true,
+}
+
+func volumeRmCmd(c *cli.Context) error {
+ var err error
+
+ if err = validateFlags(c, volumeRmFlags); err != nil {
+ return err
+ }
+
+ runtime, err := libpodruntime.GetRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ ctx := getContext()
+
+ vols, lastError := getVolumesFromContext(c, runtime)
+ for _, vol := range vols {
+ err = runtime.RemoveVolume(ctx, vol, c.Bool("force"), false)
+ if err != nil {
+ if lastError != nil {
+ logrus.Errorf("%q", lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to remove volume %q", vol.Name())
+ } else {
+ fmt.Println(vol.Name())
+ }
+ }
+ return lastError
+}
diff --git a/commands.md b/commands.md
index c84938e64..8e1a2dd7a 100644
--- a/commands.md
+++ b/commands.md
@@ -62,4 +62,9 @@
| [podman-unpause(1)](/docs/podman-unpause.1.md) | Unpause one or more running containers |[![...](/docs/play.png)](https://asciinema.org/a/141292)|
| [podman-varlink(1)](/docs/podman-varlink.1.md) | Run the varlink backend ||
| [podman-version(1)](/docs/podman-version.1.md) | Display the version information |[![...](/docs/play.png)](https://asciinema.org/a/mfrn61pjZT9Fc8L4NbfdSqfgu)|
+| [podman-volume-create(1)](/docs/podman-volume-create.1.md) | Create a volume ||
+| [podman-volume-inspect(1)](/docs/podman-volume-inspect.1.md) | Get detailed information on one or more volumes ||
+| [podman-volume-ls(1)](/docs/podman-volume-ls.1.md) | List all the available volumes ||
+| [podman-volume-rm(1)](/docs/podman-volume-rm.1.md) | Remove one or more volumes ||
+| [podman-volume-prune(1)](/docs/podman-volume-prune.1.md) | Remove all unused volumes ||
| [podman-wait(1)](/docs/podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes |[![...](/docs/play.png)](https://asciinema.org/a/QNPGKdjWuPgI96GcfkycQtah0)|
diff --git a/completions/bash/podman b/completions/bash/podman
index b8b6b353e..3382b6c5a 100644
--- a/completions/bash/podman
+++ b/completions/bash/podman
@@ -689,6 +689,23 @@ __podman_images() {
__podman_q images $images_args | awk "$awk_script" | grep -v '<none>$'
}
+# __podman_complete_volumes applies completion of volumes based on the current
+# value of `$cur` or the value of the optional first option `--cur`, if given.
+__podman_complete_volumes() {
+ local current="$cur"
+ if [ "$1" = "--cur" ] ; then
+ current="$2"
+ shift 2
+ fi
+ COMPREPLY=( $(compgen -W "$(__podman_volume "$@")" -- "$current") )
+}
+
+__podman_complete_volume_names() {
+ local names=( $(__podman_q volume ls --quiet) )
+ COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") )
+}
+
+
_podman_attach() {
local options_with_args="
--detach-keys
@@ -2536,6 +2553,128 @@ _podman_pod() {
esac
}
+_podman_volume_create() {
+ local options_with_args="
+ --driver
+ --label
+ -l
+ --opt
+ -o
+ "
+
+ local boolean_options="
+ --help
+ -h
+ "
+
+ _complete_ "$options_with_args" "$boolean_options"
+}
+
+_podman_volume_ls() {
+ local options_with_args="
+ --filter
+ --format
+ -f
+ "
+
+ local boolean_options="
+ --help
+ -h
+ --quiet
+ -q
+ "
+
+ _complete_ "$options_with_args" "$boolean_options"
+}
+
+_podman_volume_inspect() {
+ local options_with_args="
+ --format
+ -f
+ "
+
+ local boolean_options="
+ --all
+ -a
+ --help
+ -h
+ "
+
+ _complete_ "$options_with_args" "$boolean_options"
+ case "$cur" in
+ -*)
+ COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
+ ;;
+ *)
+ __podman_complete_volume_names
+ ;;
+ esac
+}
+
+_podman_volume_rm() {
+ local options_with_args=""
+
+ local boolean_options="
+ --all
+ -a
+ --force
+ -f
+ --help
+ -h
+ "
+
+ _complete_ "$options_with_args" "$boolean_options"
+ case "$cur" in
+ -*)
+ COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
+ ;;
+ *)
+ __podman_complete_volume_names
+ ;;
+ esac
+}
+
+_podman_volume_prune() {
+ local options_with_args=""
+
+ local boolean_options="
+ --force
+ -f
+ --help
+ -h
+ "
+
+ _complete_ "$options_with_args" "$boolean_options"
+}
+
+_podman_volume() {
+ local boolean_options="
+ --help
+ -h
+ "
+ subcommands="
+ create
+ inspect
+ ls
+ rm
+ prune
+ "
+ local aliases="
+ list
+ remove
+ "
+ __podman_subcommands "$subcommands $aliases" && return
+
+ case "$cur" in
+ -*)
+ COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
+ ;;
+ *)
+ COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) )
+ ;;
+ esac
+}
+
_podman_podman() {
local options_with_args="
--config -c
@@ -2596,6 +2735,7 @@ _podman_podman() {
unpause
varlink
version
+ volume
wait
"
diff --git a/docs/podman-volume-create.1.md b/docs/podman-volume-create.1.md
new file mode 100644
index 000000000..795d7b449
--- /dev/null
+++ b/docs/podman-volume-create.1.md
@@ -0,0 +1,48 @@
+% podman-volume-create(1)
+
+## NAME
+podman\-volume\-create - Create a new volume
+
+## SYNOPSIS
+**podman volume create** [*options*]
+
+## DESCRIPTION
+
+Creates an empty volume and prepares it to be used by containers. The volume
+can be created with a specific name, if a name is not given a random name is
+generated. You can add metadata to the volume by using the **--label** flag and
+driver options can be set using the **--opt** flag.
+
+## OPTIONS
+
+**--driver**=""
+
+Specify the volume driver name (default local).
+
+**--help**
+
+Print usage statement
+
+**-l**, **--label**=[]
+
+Set metadata for a volume (e.g., --label mykey=value).
+
+**-o**, **--opt**=[]
+
+Set driver specific options.
+
+## EXAMPLES
+
+```
+$ podman volume create myvol
+
+$ podman volume create
+
+$ podman volume create --label foo=bar myvol
+```
+
+## SEE ALSO
+podman-volume(1)
+
+## HISTORY
+November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/docs/podman-volume-inspect.1.md b/docs/podman-volume-inspect.1.md
new file mode 100644
index 000000000..6d5b184ee
--- /dev/null
+++ b/docs/podman-volume-inspect.1.md
@@ -0,0 +1,45 @@
+% podman-volume-inspect(1)
+
+## NAME
+podman\-volume\-inspect - Inspect one or more volumes
+
+## SYNOPSIS
+**podman volume inspect** [*options*]
+
+## DESCRIPTION
+
+Display detailed information on one or more volumes. The output can be formated using
+the **--format** flag and a Go template. To get detailed information about all the
+existing volumes, use the **--all** flag.
+
+
+## OPTIONS
+
+**-a**, **--all**=""
+
+Inspect all volumes.
+
+**--format**=""
+
+Format volume output using Go template
+
+**--help**
+
+Print usage statement
+
+
+## EXAMPLES
+
+```
+$ podman volume inspect myvol
+
+$ podman volume inspect --all
+
+$ podman volume inspect --format "{{.Driver}} {{.Scope}}" myvol
+```
+
+## SEE ALSO
+podman-volume(1)
+
+## HISTORY
+November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/docs/podman-volume-ls.1.md b/docs/podman-volume-ls.1.md
new file mode 100644
index 000000000..c061e27fe
--- /dev/null
+++ b/docs/podman-volume-ls.1.md
@@ -0,0 +1,49 @@
+% podman-volume-ls(1)
+
+## NAME
+podman\-volume\-ls - List volumes
+
+## SYNOPSIS
+**podman volume ls** [*options*]
+
+## DESCRIPTION
+
+Lists all the volumes that exist. The output can be filtered using the **--filter**
+flag and can be formatted to either JSON or a Go template using the **--format**
+flag. Use the **--quiet** flag to print only the volume names.
+
+## OPTIONS
+
+**--filter**=""
+
+Filter volume output.
+
+**--format**=""
+
+Format volume output using Go template.
+
+**--help**
+
+Print usage statement.
+
+**-q**, **--quiet**=[]
+
+Print volume output in quiet mode. Only print the volume names.
+
+## EXAMPLES
+
+```
+$ podman volume ls
+
+$ podman volume ls --format json
+
+$ podman volume ls --format "{{.Driver}} {{.Scope}}"
+
+$ podman volume ls --filter name=foo,label=blue
+```
+
+## SEE ALSO
+podman-volume(1)
+
+## HISTORY
+November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/docs/podman-volume-prune.1.md b/docs/podman-volume-prune.1.md
new file mode 100644
index 000000000..a06bb2fa4
--- /dev/null
+++ b/docs/podman-volume-prune.1.md
@@ -0,0 +1,38 @@
+% podman-volume-prune(1)
+
+## NAME
+podman\-volume\-prune - Remove all unused volumes
+
+## SYNOPSIS
+**podman volume rm** [*options*]
+
+## DESCRIPTION
+
+Removes all unused volumes. You will be prompted to confirm the removal of all the
+unused volumes. To bypass the confirmation, use the **--force** flag.
+
+
+## OPTIONS
+
+**-f**, **--force**=""
+
+Do not prompt for confirmation.
+
+**--help**
+
+Print usage statement
+
+
+## EXAMPLES
+
+```
+$ podman volume prune
+
+$ podman volume prune --force
+```
+
+## SEE ALSO
+podman-volume(1)
+
+## HISTORY
+November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/docs/podman-volume-rm.1.md b/docs/podman-volume-rm.1.md
new file mode 100644
index 000000000..c23d7675c
--- /dev/null
+++ b/docs/podman-volume-rm.1.md
@@ -0,0 +1,45 @@
+% podman-volume-rm(1)
+
+## NAME
+podman\-volume\-rm - Remove one or more volumes
+
+## SYNOPSIS
+**podman volume rm** [*options*]
+
+## DESCRIPTION
+
+Removes one ore more volumes. Only volumes that are not being used will be removed.
+If a volume is being used by a container, an error will be returned unless the **--force**
+flag is being used. To remove all the volumes, use the **--all** flag.
+
+
+## OPTIONS
+
+**-a**, **--all**=""
+
+Remove all volumes.
+
+**-f**, **--force**=""
+
+Remove a volume by force, even if it is being used by a container
+
+**--help**
+
+Print usage statement
+
+
+## EXAMPLES
+
+```
+$ podman volume rm myvol1 myvol2
+
+$ podman volume rm --all
+
+$ podman volume rm --force myvol
+```
+
+## SEE ALSO
+podman-volume(1)
+
+## HISTORY
+November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/docs/podman-volume.1.md b/docs/podman-volume.1.md
new file mode 100644
index 000000000..ac32abbd6
--- /dev/null
+++ b/docs/podman-volume.1.md
@@ -0,0 +1,23 @@
+% podman-volume(1)
+
+## NAME
+podman\-volume - Simple management tool for volumes.
+
+## SYNOPSIS
+**podman volume** *subcommand*
+
+## DESCRIPTION
+podman volume is a set of subcommands that manage volumes.
+
+## SUBCOMMANDS
+
+| Subcommand | Description |
+| ------------------------------------------------- | ------------------------------------------------------------------------------ |
+| [podman-volume-create(1)](podman-volume-create.1.md) | Create a new volume. |
+| [podman-volume-inspect(1)](podman-volume-inspect.1.md) | Get detailed information on one or more volumes. |
+| [podman-volume-ls(1)](podman-volume-ls.1.md) | List all the available volumes. |
+| [podman-volume-rm(1)](podman-volume-rm.1.md) | Remove one or more volumes. |
+| [podman-volume-prune(1)](podman-volume-prune.1.md) | Remove all unused volumes. |
+
+## HISTORY
+November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go
index cb661d4e9..b154d8bda 100644
--- a/libpod/boltdb_state.go
+++ b/libpod/boltdb_state.go
@@ -94,6 +94,12 @@ func NewBoltState(path string, runtime *Runtime) (State, error) {
if _, err := tx.CreateBucketIfNotExists(allPodsBkt); err != nil {
return errors.Wrapf(err, "error creating all pods bucket")
}
+ if _, err := tx.CreateBucketIfNotExists(volBkt); err != nil {
+ return errors.Wrapf(err, "error creating volume bucket")
+ }
+ if _, err := tx.CreateBucketIfNotExists(allVolsBkt); err != nil {
+ return errors.Wrapf(err, "error creating all volumes bucket")
+ }
if _, err := tx.CreateBucketIfNotExists(runtimeConfigBkt); err != nil {
return errors.Wrapf(err, "error creating runtime-config bucket")
}
@@ -1150,6 +1156,378 @@ func (s *BoltState) PodContainers(pod *Pod) ([]*Container, error) {
return ctrs, nil
}
+// AddVolume adds the given volume to the state. It also adds ctrDepID to
+// the sub bucket holding the container dependencies that this volume has
+func (s *BoltState) AddVolume(volume *Volume) error {
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !volume.valid {
+ return ErrVolumeRemoved
+ }
+
+ volName := []byte(volume.Name())
+
+ volConfigJSON, err := json.Marshal(volume.config)
+ if err != nil {
+ return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name())
+ }
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.Update(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ allVolsBkt, err := getAllVolsBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ // Check if we already have a volume with the given name
+ volExists := allVolsBkt.Get(volName)
+ if volExists != nil {
+ return errors.Wrapf(ErrVolumeExists, "name %s is in use", volume.Name())
+ }
+
+ // We are good to add the volume
+ // Make a bucket for it
+ newVol, err := volBkt.CreateBucket(volName)
+ if err != nil {
+ return errors.Wrapf(err, "error creating bucket for volume %s", volume.Name())
+ }
+
+ // Make a subbucket for the containers using the volume. Dependent container IDs will be addedremoved to
+ // this bucket in addcontainer/removeContainer
+ if _, err := newVol.CreateBucket(volDependenciesBkt); err != nil {
+ return errors.Wrapf(err, "error creating bucket for containers using volume %s", volume.Name())
+ }
+
+ if err := newVol.Put(configKey, volConfigJSON); err != nil {
+ return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name())
+ }
+
+ if err := allVolsBkt.Put(volName, volName); err != nil {
+ return errors.Wrapf(err, "error storing volume %s in all volumes bucket in DB", volume.Name())
+ }
+
+ return nil
+ })
+ return err
+}
+
+// RemoveVolCtrDep updates the container dependencies sub bucket of the given volume.
+// It deletes it from the bucket when found.
+// This is important when force removing a volume and we want to get rid of the dependencies.
+func (s *BoltState) RemoveVolCtrDep(volume *Volume, ctrID string) error {
+ if ctrID == "" {
+ return nil
+ }
+
+ if !s.valid {
+ return ErrDBBadConfig
+ }
+
+ if !volume.valid {
+ return ErrVolumeRemoved
+ }
+
+ volName := []byte(volume.Name())
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.Update(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volDB := volBkt.Bucket(volName)
+ if volDB == nil {
+ volume.valid = false
+ return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
+ }
+
+ // Make a subbucket for the containers using the volume
+ ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
+ depCtrID := []byte(ctrID)
+ if depExists := ctrDepsBkt.Get(depCtrID); depExists != nil {
+ if err := ctrDepsBkt.Delete(depCtrID); err != nil {
+ return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctrID, volume.Name())
+ }
+ }
+
+ return nil
+ })
+ return err
+}
+
+// RemoveVolume removes the given volume from the state
+func (s *BoltState) RemoveVolume(volume *Volume) error {
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !volume.valid {
+ return ErrVolumeRemoved
+ }
+
+ volName := []byte(volume.Name())
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.Update(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ allVolsBkt, err := getAllVolsBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ // Check if the volume exists
+ volDB := volBkt.Bucket(volName)
+ if volDB == nil {
+ volume.valid = false
+ return errors.Wrapf(ErrNoSuchVolume, "volume %s does not exist in DB", volume.Name())
+ }
+
+ // Check if volume is not being used by any container
+ // This should never be nil
+ // But if it is, we can assume that no containers are using
+ // the volume.
+ volCtrsBkt := volDB.Bucket(volDependenciesBkt)
+ if volCtrsBkt != nil {
+ var deps []string
+ err = volCtrsBkt.ForEach(func(id, value []byte) error {
+ deps = append(deps, string(id))
+ return nil
+ })
+ if err != nil {
+ return errors.Wrapf(err, "error getting list of dependencies from dependencies bucket for volumes %q", volume.Name())
+ }
+ if len(deps) > 0 {
+ return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by container(s) %s", volume.Name(), strings.Join(deps, ","))
+ }
+ }
+
+ // volume is ready for removal
+ // Let's kick it out
+ if err := allVolsBkt.Delete(volName); err != nil {
+ return errors.Wrapf(err, "error removing volume %s from all volumes bucket in DB", volume.Name())
+ }
+ if err := volBkt.DeleteBucket(volName); err != nil {
+ return errors.Wrapf(err, "error removing volume %s from DB", volume.Name())
+ }
+
+ return nil
+ })
+ return err
+}
+
+// AllVolumes returns all volumes present in the state
+func (s *BoltState) AllVolumes() ([]*Volume, error) {
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ volumes := []*Volume{}
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return nil, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ allVolsBucket, err := getAllVolsBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volBucket, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+ err = allVolsBucket.ForEach(func(id, name []byte) error {
+ volExists := volBucket.Bucket(id)
+ // This check can be removed if performance becomes an
+ // issue, but much less helpful errors will be produced
+ if volExists == nil {
+ return errors.Wrapf(ErrInternal, "inconsistency in state - volume %s is in all volumes bucket but volume not found", string(id))
+ }
+
+ volume := new(Volume)
+ volume.config = new(VolumeConfig)
+
+ if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
+ if errors.Cause(err) != ErrNSMismatch {
+ logrus.Errorf("Error retrieving volume %s from the database: %v", string(id), err)
+ }
+ } else {
+ volumes = append(volumes, volume)
+ }
+
+ return nil
+ })
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return volumes, nil
+}
+
+// Volume retrieves a volume from full name
+func (s *BoltState) Volume(name string) (*Volume, error) {
+ if name == "" {
+ return nil, ErrEmptyID
+ }
+
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ volName := []byte(name)
+
+ volume := new(Volume)
+ volume.config = new(VolumeConfig)
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return nil, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ return s.getVolumeFromDB(volName, volume, volBkt)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return volume, nil
+}
+
+// HasVolume returns true if the given volume exists in the state, otherwise it returns false
+func (s *BoltState) HasVolume(name string) (bool, error) {
+ if name == "" {
+ return false, ErrEmptyID
+ }
+
+ if !s.valid {
+ return false, ErrDBClosed
+ }
+
+ volName := []byte(name)
+
+ exists := false
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return false, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volDB := volBkt.Bucket(volName)
+ if volDB != nil {
+ exists = true
+ }
+
+ return nil
+ })
+ if err != nil {
+ return false, err
+ }
+
+ return exists, nil
+}
+
+// VolumeInUse checks if any container is using the volume
+// It returns a slice of the IDs of the containers using the given
+// volume. If the slice is empty, no containers use the given volume
+func (s *BoltState) VolumeInUse(volume *Volume) ([]string, error) {
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ if !volume.valid {
+ return nil, ErrVolumeRemoved
+ }
+
+ depCtrs := []string{}
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return nil, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ volBucket, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volDB := volBucket.Bucket([]byte(volume.Name()))
+ if volDB == nil {
+ volume.valid = false
+ return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in DB", volume.Name())
+ }
+
+ dependsBkt := volDB.Bucket(volDependenciesBkt)
+ if dependsBkt == nil {
+ return errors.Wrapf(ErrInternal, "volume %s has no dependencies bucket", volume.Name())
+ }
+
+ // Iterate through and add dependencies
+ err = dependsBkt.ForEach(func(id, value []byte) error {
+ depCtrs = append(depCtrs, string(id))
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return depCtrs, nil
+}
+
// AddPod adds the given pod to the state.
func (s *BoltState) AddPod(pod *Pod) error {
if !s.valid {
diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go
index 3f00657ea..0970f4d41 100644
--- a/libpod/boltdb_state_internal.go
+++ b/libpod/boltdb_state_internal.go
@@ -21,15 +21,18 @@ const (
allCtrsName = "all-ctrs"
podName = "pod"
allPodsName = "allPods"
+ volName = "vol"
+ allVolsName = "allVolumes"
runtimeConfigName = "runtime-config"
- configName = "config"
- stateName = "state"
- dependenciesName = "dependencies"
- netNSName = "netns"
- containersName = "containers"
- podIDName = "pod-id"
- namespaceName = "namespace"
+ configName = "config"
+ stateName = "state"
+ dependenciesName = "dependencies"
+ volCtrDependencies = "vol-dependencies"
+ netNSName = "netns"
+ containersName = "containers"
+ podIDName = "pod-id"
+ namespaceName = "namespace"
staticDirName = "static-dir"
tmpDirName = "tmp-dir"
@@ -47,15 +50,18 @@ var (
allCtrsBkt = []byte(allCtrsName)
podBkt = []byte(podName)
allPodsBkt = []byte(allPodsName)
+ volBkt = []byte(volName)
+ allVolsBkt = []byte(allVolsName)
runtimeConfigBkt = []byte(runtimeConfigName)
- configKey = []byte(configName)
- stateKey = []byte(stateName)
- dependenciesBkt = []byte(dependenciesName)
- netNSKey = []byte(netNSName)
- containersBkt = []byte(containersName)
- podIDKey = []byte(podIDName)
- namespaceKey = []byte(namespaceName)
+ configKey = []byte(configName)
+ stateKey = []byte(stateName)
+ dependenciesBkt = []byte(dependenciesName)
+ volDependenciesBkt = []byte(volCtrDependencies)
+ netNSKey = []byte(netNSName)
+ containersBkt = []byte(containersName)
+ podIDKey = []byte(podIDName)
+ namespaceKey = []byte(namespaceName)
staticDirKey = []byte(staticDirName)
tmpDirKey = []byte(tmpDirName)
@@ -234,6 +240,22 @@ func getAllPodsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
return bkt, nil
}
+func getVolBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
+ bkt := tx.Bucket(volBkt)
+ if bkt == nil {
+ return nil, errors.Wrapf(ErrDBBadConfig, "volumes bucket not found in DB")
+ }
+ return bkt, nil
+}
+
+func getAllVolsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
+ bkt := tx.Bucket(allVolsBkt)
+ if bkt == nil {
+ return nil, errors.Wrapf(ErrDBBadConfig, "all volumes bucket not found in DB")
+ }
+ return bkt, nil
+}
+
func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(runtimeConfigBkt)
if bkt == nil {
@@ -315,6 +337,35 @@ func (s *BoltState) getPodFromDB(id []byte, pod *Pod, podBkt *bolt.Bucket) error
return nil
}
+func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bucket) error {
+ volDB := volBkt.Bucket(name)
+ if volDB == nil {
+ return errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found", string(name))
+ }
+
+ volConfigBytes := volDB.Get(configKey)
+ if volConfigBytes == nil {
+ return errors.Wrapf(ErrInternal, "volume %s is missing configuration key in DB", string(name))
+ }
+
+ if err := json.Unmarshal(volConfigBytes, volume.config); err != nil {
+ return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
+ }
+
+ // Get the lock
+ lockPath := filepath.Join(s.runtime.lockDir, string(name))
+ lock, err := storage.GetLockfile(lockPath)
+ if err != nil {
+ return errors.Wrapf(err, "error retrieving lockfile for volume %s", string(name))
+ }
+ volume.lock = lock
+
+ volume.runtime = s.runtime
+ volume.valid = true
+
+ return nil
+}
+
// Add a container to the DB
// If pod is not nil, the container is added to the pod as well
func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
@@ -376,6 +427,11 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
return err
}
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
// If a pod was given, check if it exists
var podDB *bolt.Bucket
var podCtrs *bolt.Bucket
@@ -508,6 +564,25 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
}
}
+ // Add container to volume dependencies bucket if container is using a named volume
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+
+ volDB := volBkt.Bucket([]byte(volName))
+ if volDB == nil {
+ return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volName)
+ }
+
+ ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
+ if depExists := ctrDepsBkt.Get(ctrID); depExists == nil {
+ if err := ctrDepsBkt.Put(ctrID, ctrID); err != nil {
+ return errors.Wrapf(err, "error storing container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName)
+ }
+ }
+ }
+ }
+
return nil
})
return err
@@ -545,6 +620,11 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
return err
}
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
// Does the pod exist?
var podDB *bolt.Bucket
if pod != nil {
@@ -663,5 +743,25 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
}
}
+ // Remove container from volume dependencies bucket if container is using a named volume
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+
+ volDB := volBkt.Bucket([]byte(volName))
+ if volDB == nil {
+ // Let's assume the volume was already deleted and continue to remove the container
+ continue
+ }
+
+ ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
+ if depExists := ctrDepsBkt.Get(ctrID); depExists != nil {
+ if err := ctrDepsBkt.Delete(ctrID); err != nil {
+ return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName)
+ }
+ }
+ }
+ }
+
return nil
}
diff --git a/libpod/common_test.go b/libpod/common_test.go
index b7fee2764..81c8f1920 100644
--- a/libpod/common_test.go
+++ b/libpod/common_test.go
@@ -74,6 +74,11 @@ func getTestContainer(id, name, locksDir string) (*Container, error) {
"/test/file.test": "/test2/file2.test",
},
},
+ runtime: &Runtime{
+ config: &RuntimeConfig{
+ VolumePath: "/does/not/exist/tmp/volumes",
+ },
+ },
valid: true,
}
diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go
index 9b07198bc..06a0c9f32 100644
--- a/libpod/container_inspect.go
+++ b/libpod/container_inspect.go
@@ -1,8 +1,11 @@
package libpod
import (
+ "strings"
+
"github.com/containers/libpod/pkg/inspect"
"github.com/cri-o/ocicni/pkg/ocicni"
+ specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
@@ -48,6 +51,17 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
hostnamePath = getPath
}
+ var mounts []specs.Mount
+ for i, mnt := range spec.Mounts {
+ mounts = append(mounts, mnt)
+ // We only want to show the name of the named volume in the inspect
+ // output, so split the path and get the name out of it.
+ if strings.Contains(mnt.Source, c.runtime.config.VolumePath) {
+ split := strings.Split(mnt.Source[len(c.runtime.config.VolumePath)+1:], "/")
+ mounts[i].Source = split[0]
+ }
+ }
+
data := &inspect.ContainerInspectData{
ID: config.ID,
Created: config.CreatedTime,
@@ -85,7 +99,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
AppArmorProfile: spec.Process.ApparmorProfile,
ExecIDs: execIDs,
GraphDriver: driverData,
- Mounts: spec.Mounts,
+ Mounts: mounts,
Dependencies: c.Dependencies(),
NetworkSettings: &inspect.NetworkSettings{
Bridge: "", // TODO
diff --git a/libpod/errors.go b/libpod/errors.go
index 75b4928da..d6614141c 100644
--- a/libpod/errors.go
+++ b/libpod/errors.go
@@ -11,18 +11,24 @@ var (
ErrNoSuchPod = errors.New("no such pod")
// ErrNoSuchImage indicates the requested image does not exist
ErrNoSuchImage = errors.New("no such image")
+ // ErrNoSuchVolume indicates the requested volume does not exist
+ ErrNoSuchVolume = errors.New("no such volume")
// ErrCtrExists indicates a container with the same name or ID already
// exists
ErrCtrExists = errors.New("container already exists")
// ErrPodExists indicates a pod with the same name or ID already exists
ErrPodExists = errors.New("pod already exists")
- // ErrImageExists indicated an image with the same ID already exists
+ // ErrImageExists indicates an image with the same ID already exists
ErrImageExists = errors.New("image already exists")
+ // ErrVolumeExists indicates a volume with the same name already exists
+ ErrVolumeExists = errors.New("volume already exists")
// ErrCtrStateInvalid indicates a container is in an improper state for
// the requested operation
ErrCtrStateInvalid = errors.New("container state improper")
+ // ErrVolumeBeingUsed indicates that a volume is being used by at least one container
+ ErrVolumeBeingUsed = errors.New("volume is being used")
// ErrRuntimeFinalized indicates that the runtime has already been
// created and cannot be modified
@@ -33,6 +39,9 @@ var (
// ErrPodFinalized indicates that the pod has already been created and
// cannot be modified
ErrPodFinalized = errors.New("pod has been finalized")
+ // ErrVolumeFinalized indicates that the volume has already been created and
+ // cannot be modified
+ ErrVolumeFinalized = errors.New("volume has been finalized")
// ErrInvalidArg indicates that an invalid argument was passed
ErrInvalidArg = errors.New("invalid argument")
@@ -55,6 +64,9 @@ var (
// ErrPodRemoved indicates that the pod has already been removed and no
// further operations can be performed on it
ErrPodRemoved = errors.New("pod has already been removed")
+ // ErrVolumeRemoved indicates that the volume has already been removed and
+ // no further operations can be performed on it
+ ErrVolumeRemoved = errors.New("volume has already been removed")
// ErrDBClosed indicates that the connection to the state database has
// already been closed
diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go
index 77eba0cc6..314799309 100644
--- a/libpod/in_memory_state.go
+++ b/libpod/in_memory_state.go
@@ -18,8 +18,10 @@ type InMemoryState struct {
pods map[string]*Pod
// Maps container ID to container struct.
containers map[string]*Container
+ volumes map[string]*Volume
// Maps container ID to a list of IDs of dependencies.
- ctrDepends map[string][]string
+ ctrDepends map[string][]string
+ volumeDepends map[string][]string
// Maps pod ID to a map of container ID to container struct.
podContainers map[string]map[string]*Container
// Global name registry - ensures name uniqueness and performs lookups.
@@ -46,8 +48,10 @@ func NewInMemoryState() (State, error) {
state.pods = make(map[string]*Pod)
state.containers = make(map[string]*Container)
+ state.volumes = make(map[string]*Volume)
state.ctrDepends = make(map[string][]string)
+ state.volumeDepends = make(map[string][]string)
state.podContainers = make(map[string]map[string]*Container)
@@ -244,6 +248,14 @@ func (s *InMemoryState) AddContainer(ctr *Container) error {
s.addCtrToDependsMap(ctr.ID(), depCtr)
}
+ // Add container to volume dependencies
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+ s.addCtrToVolDependsMap(ctr.ID(), volName)
+ }
+ }
+
return nil
}
@@ -294,6 +306,14 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error {
s.removeCtrFromDependsMap(ctr.ID(), depCtr)
}
+ // Remove container from volume dependencies
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+ s.removeCtrFromVolDependsMap(ctr.ID(), volName)
+ }
+ }
+
return nil
}
@@ -358,6 +378,114 @@ func (s *InMemoryState) ContainerInUse(ctr *Container) ([]string, error) {
return arr, nil
}
+// Volume retrieves a volume from its full name
+func (s *InMemoryState) Volume(name string) (*Volume, error) {
+ if name == "" {
+ return nil, ErrEmptyID
+ }
+
+ vol, ok := s.volumes[name]
+ if !ok {
+ return nil, errors.Wrapf(ErrNoSuchCtr, "no volume with name %s found", name)
+ }
+
+ return vol, nil
+}
+
+// HasVolume checks if a volume with the given name is present in the state
+func (s *InMemoryState) HasVolume(name string) (bool, error) {
+ if name == "" {
+ return false, ErrEmptyID
+ }
+
+ _, ok := s.volumes[name]
+ if !ok {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// AddVolume adds a volume to the state
+func (s *InMemoryState) AddVolume(volume *Volume) error {
+ if !volume.valid {
+ return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name())
+ }
+
+ if _, ok := s.volumes[volume.Name()]; ok {
+ return errors.Wrapf(ErrVolumeExists, "volume with name %s already exists in state", volume.Name())
+ }
+
+ s.volumes[volume.Name()] = volume
+
+ return nil
+}
+
+// RemoveVolume removes a volume from the state
+func (s *InMemoryState) RemoveVolume(volume *Volume) error {
+ // Ensure we don't remove a volume which containers depend on
+ deps, ok := s.volumeDepends[volume.Name()]
+ if ok && len(deps) != 0 {
+ depsStr := strings.Join(deps, ", ")
+ return errors.Wrapf(ErrVolumeExists, "the following containers depend on volume %s: %s", volume.Name(), depsStr)
+ }
+
+ if _, ok := s.volumes[volume.Name()]; !ok {
+ volume.valid = false
+ return errors.Wrapf(ErrVolumeRemoved, "no volume exists in state with name %s", volume.Name())
+ }
+
+ delete(s.volumes, volume.Name())
+
+ return nil
+}
+
+// RemoveVolCtrDep updates the container dependencies of the volume
+func (s *InMemoryState) RemoveVolCtrDep(volume *Volume, ctrID string) error {
+ if !volume.valid {
+ return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name())
+ }
+
+ if _, ok := s.volumes[volume.Name()]; !ok {
+ return errors.Wrapf(ErrNoSuchVolume, "volume with name %s doesn't exists in state", volume.Name())
+ }
+
+ // Remove container that is using this volume
+ s.removeCtrFromVolDependsMap(ctrID, volume.Name())
+
+ return nil
+}
+
+// VolumeInUse checks if the given volume is being used by at least one container
+func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) {
+ if !volume.valid {
+ return nil, ErrVolumeRemoved
+ }
+
+ // If the volume does not exist, return error
+ if _, ok := s.volumes[volume.Name()]; !ok {
+ volume.valid = false
+ return nil, errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found in state", volume.Name())
+ }
+
+ arr, ok := s.volumeDepends[volume.Name()]
+ if !ok {
+ return []string{}, nil
+ }
+
+ return arr, nil
+}
+
+// AllVolumes returns all volumes that exist in the state
+func (s *InMemoryState) AllVolumes() ([]*Volume, error) {
+ allVols := make([]*Volume, 0, len(s.volumes))
+ for _, v := range s.volumes {
+ allVols = append(allVols, v)
+ }
+
+ return allVols, nil
+}
+
// AllContainers retrieves all containers from the state
func (s *InMemoryState) AllContainers() ([]*Container, error) {
ctrs := make([]*Container, 0, len(s.containers))
@@ -945,6 +1073,44 @@ func (s *InMemoryState) removeCtrFromDependsMap(ctrID, dependsID string) {
}
}
+// Add a container to the dependency mappings for the volume
+func (s *InMemoryState) addCtrToVolDependsMap(depCtrID, volName string) {
+ if volName != "" {
+ arr, ok := s.volumeDepends[volName]
+ if !ok {
+ // Do not have a mapping for that volume yet
+ s.volumeDepends[volName] = []string{depCtrID}
+ } else {
+ // Have a mapping for the volume
+ arr = append(arr, depCtrID)
+ s.volumeDepends[volName] = arr
+ }
+ }
+}
+
+// Remove a container from the dependency mappings for the volume
+func (s *InMemoryState) removeCtrFromVolDependsMap(depCtrID, volName string) {
+ if volName != "" {
+ arr, ok := s.volumeDepends[volName]
+ if !ok {
+ // Internal state seems inconsistent
+ // But the dependency is definitely gone
+ // So just return
+ return
+ }
+
+ newArr := make([]string, 0, len(arr))
+
+ for _, id := range arr {
+ if id != depCtrID {
+ newArr = append(newArr, id)
+ }
+ }
+
+ s.volumeDepends[volName] = newArr
+ }
+}
+
// Check if we can access a pod or container, or if that is blocked by
// namespaces.
func (s *InMemoryState) checkNSMatch(id, ns string) error {
diff --git a/libpod/options.go b/libpod/options.go
index 3e43d73f0..352e6a506 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -327,6 +327,22 @@ func WithNamespace(ns string) RuntimeOption {
}
}
+// WithVolumePath sets the path under which all named volumes
+// should be created.
+// The path changes based on whethe rthe user is running as root
+// or not.
+func WithVolumePath(volPath string) RuntimeOption {
+ return func(rt *Runtime) error {
+ if rt.valid {
+ return ErrRuntimeFinalized
+ }
+
+ rt.config.VolumePath = volPath
+
+ return nil
+ }
+}
+
// WithDefaultInfraImage sets the infra image for libpod.
// An infra image is used for inter-container kernel
// namespace sharing within a pod. Typically, an infra
@@ -1125,6 +1141,70 @@ func withIsInfra() CtrCreateOption {
}
}
+// Volume Creation Options
+
+// WithVolumeName sets the name of the volume.
+func WithVolumeName(name string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ // Check the name against a regex
+ if !nameRegex.MatchString(name) {
+ return errors.Wrapf(ErrInvalidArg, "name must match regex [a-zA-Z0-9_-]+")
+ }
+ volume.config.Name = name
+
+ return nil
+ }
+}
+
+// WithVolumeLabels sets the labels of the volume.
+func WithVolumeLabels(labels map[string]string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ volume.config.Labels = make(map[string]string)
+ for key, value := range labels {
+ volume.config.Labels[key] = value
+ }
+
+ return nil
+ }
+}
+
+// WithVolumeDriver sets the driver of the volume.
+func WithVolumeDriver(driver string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ volume.config.Driver = driver
+
+ return nil
+ }
+}
+
+// WithVolumeOptions sets the options of the volume.
+func WithVolumeOptions(options map[string]string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ volume.config.Options = make(map[string]string)
+ for key, value := range options {
+ volume.config.Options[key] = value
+ }
+
+ return nil
+ }
+}
+
// Pod Creation Options
// WithPodName sets the name of the pod.
diff --git a/libpod/runtime.go b/libpod/runtime.go
index 6b6a8cc2d..82473aae9 100644
--- a/libpod/runtime.go
+++ b/libpod/runtime.go
@@ -92,6 +92,7 @@ type RuntimeConfig struct {
// Not included in on-disk config, use the dedicated containers/storage
// configuration file instead
StorageConfig storage.StoreOptions `toml:"-"`
+ VolumePath string `toml:"volume_path"`
// ImageDefaultTransport is the default transport method used to fetch
// images
ImageDefaultTransport string `toml:"image_default_transport"`
@@ -278,12 +279,13 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
if rootless.IsRootless() {
// If we're rootless, override the default storage config
- storageConf, err := util.GetDefaultStoreOptions()
+ storageConf, volumePath, err := util.GetDefaultStoreOptions()
if err != nil {
return nil, errors.Wrapf(err, "error retrieving rootless storage config")
}
runtime.config.StorageConfig = storageConf
runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod")
+ runtime.config.VolumePath = volumePath
}
configPath := ConfigPath
diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go
index 09d0ec042..ba8eaacbe 100644
--- a/libpod/runtime_ctr.go
+++ b/libpod/runtime_ctr.go
@@ -154,6 +154,24 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
}
}()
+ // Go through the volume mounts and check for named volumes
+ // If the named volme already exists continue, otherwise create
+ // the storage for the named volume.
+ for i, vol := range ctr.config.Spec.Mounts {
+ if vol.Source[0] != '/' && isNamedVolume(vol.Source) {
+ volInfo, err := r.state.Volume(vol.Source)
+ if err != nil {
+ newVol, err := r.newVolume(ctx, WithVolumeName(vol.Source))
+ if err != nil {
+ logrus.Errorf("error creating named volume %q: %v", vol.Source, err)
+ }
+ ctr.config.Spec.Mounts[i].Source = newVol.MountPoint()
+ continue
+ }
+ ctr.config.Spec.Mounts[i].Source = volInfo.MountPoint()
+ }
+ }
+
if ctr.config.LogPath == "" {
ctr.config.LogPath = filepath.Join(ctr.config.StaticDir, "ctr.log")
}
@@ -170,6 +188,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
}
ctr.config.Mounts = append(ctr.config.Mounts, ctr.config.ShmDir)
}
+
// Add the container to the state
// TODO: May be worth looking into recovering from name/ID collisions here
if ctr.config.Pod != "" {
@@ -474,3 +493,11 @@ func (r *Runtime) GetLatestContainer() (*Container, error) {
}
return ctrs[lastCreatedIndex], nil
}
+
+// Check if volName is a named volume and not one of the default mounts we add to containers
+func isNamedVolume(volName string) bool {
+ if volName != "proc" && volName != "tmpfs" && volName != "devpts" && volName != "shm" && volName != "mqueue" && volName != "sysfs" && volName != "cgroup" {
+ return true
+ }
+ return false
+}
diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go
new file mode 100644
index 000000000..3921758ee
--- /dev/null
+++ b/libpod/runtime_volume.go
@@ -0,0 +1,107 @@
+package libpod
+
+import (
+ "context"
+)
+
+// Contains the public Runtime API for volumes
+
+// A VolumeCreateOption is a functional option which alters the Volume created by
+// NewVolume
+type VolumeCreateOption func(*Volume) error
+
+// VolumeFilter is a function to determine whether a volume is included in command
+// output. Volumes to be outputted are tested using the function. a true return will
+// include the volume, a false return will exclude it.
+type VolumeFilter func(*Volume) bool
+
+// RemoveVolume removes a volumes
+func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force, prune bool) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ if !r.valid {
+ return ErrRuntimeStopped
+ }
+
+ if !v.valid {
+ if ok, _ := r.state.HasVolume(v.Name()); !ok {
+ // Volume probably already removed
+ // Or was never in the runtime to begin with
+ return nil
+ }
+ }
+
+ v.lock.Lock()
+ defer v.lock.Unlock()
+
+ return r.removeVolume(ctx, v, force, prune)
+}
+
+// GetVolume retrieves a volume by its name
+func (r *Runtime) GetVolume(name string) (*Volume, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+
+ return r.state.Volume(name)
+}
+
+// HasVolume checks to see if a volume with the given name exists
+func (r *Runtime) HasVolume(name string) (bool, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return false, ErrRuntimeStopped
+ }
+
+ return r.state.HasVolume(name)
+}
+
+// Volumes retrieves all volumes
+// Filters can be provided which will determine which volumes are included in the
+// output. Multiple filters are handled by ANDing their output, so only volumes
+// matching all filters are returned
+func (r *Runtime) Volumes(filters ...VolumeFilter) ([]*Volume, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+
+ vols, err := r.state.AllVolumes()
+ if err != nil {
+ return nil, err
+ }
+
+ volsFiltered := make([]*Volume, 0, len(vols))
+ for _, vol := range vols {
+ include := true
+ for _, filter := range filters {
+ include = include && filter(vol)
+ }
+
+ if include {
+ volsFiltered = append(volsFiltered, vol)
+ }
+ }
+
+ return volsFiltered, nil
+}
+
+// GetAllVolumes retrieves all the volumes
+func (r *Runtime) GetAllVolumes() ([]*Volume, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+
+ return r.state.AllVolumes()
+}
diff --git a/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go
new file mode 100644
index 000000000..5cc0938f0
--- /dev/null
+++ b/libpod/runtime_volume_linux.go
@@ -0,0 +1,132 @@
+// +build linux
+
+package libpod
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/containers/storage"
+ "github.com/containers/storage/pkg/stringid"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// NewVolume creates a new empty volume
+func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+ return r.newVolume(ctx, options...)
+}
+
+// newVolume creates a new empty volume
+func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) {
+ volume, err := newVolume(r)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating volume")
+ }
+
+ for _, option := range options {
+ if err := option(volume); err != nil {
+ return nil, errors.Wrapf(err, "error running volume create option")
+ }
+ }
+
+ if volume.config.Name == "" {
+ volume.config.Name = stringid.GenerateNonCryptoID()
+ }
+ // TODO: support for other volume drivers
+ if volume.config.Driver == "" {
+ volume.config.Driver = "local"
+ }
+ // TODO: determine when the scope is global and set it to that
+ if volume.config.Scope == "" {
+ volume.config.Scope = "local"
+ }
+
+ // Create the mountpoint of this volume
+ fullVolPath := filepath.Join(r.config.VolumePath, volume.config.Name, "_data")
+ if err := os.MkdirAll(fullVolPath, 0755); err != nil {
+ return nil, errors.Wrapf(err, "error creating volume directory %q", fullVolPath)
+ }
+ _, mountLabel, err := label.InitLabels([]string{})
+ if err != nil {
+ return nil, errors.Wrapf(err, "error getting default mountlabels")
+ }
+ if err := label.ReleaseLabel(mountLabel); err != nil {
+ return nil, errors.Wrapf(err, "error releasing label %q", mountLabel)
+ }
+ if err := label.Relabel(fullVolPath, mountLabel, true); err != nil {
+ return nil, errors.Wrapf(err, "error setting selinux label to %q", fullVolPath)
+ }
+ volume.config.MountPoint = fullVolPath
+
+ // Path our lock file will reside at
+ lockPath := filepath.Join(r.lockDir, volume.config.Name)
+ // Grab a lockfile at the given path
+ lock, err := storage.GetLockfile(lockPath)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating lockfile for new volume")
+ }
+ volume.lock = lock
+
+ volume.valid = true
+
+ // Add the volume to state
+ if err := r.state.AddVolume(volume); err != nil {
+ return nil, errors.Wrapf(err, "error adding volume to state")
+ }
+
+ return volume, nil
+}
+
+// removeVolume removes the specified volume from state as well tears down its mountpoint and storage
+func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force, prune bool) error {
+ if !v.valid {
+ return ErrNoSuchVolume
+ }
+
+ deps, err := r.state.VolumeInUse(v)
+ if err != nil {
+ return err
+ }
+ if len(deps) != 0 {
+ if prune {
+ return ErrVolumeBeingUsed
+ }
+ depsStr := strings.Join(deps, ", ")
+ if !force {
+ return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by the following container(s): %s", v.Name(), depsStr)
+ }
+ // If using force, log the warning that the volume is being used by at least one container
+ logrus.Warnf("volume %s is being used by the following container(s): %s", v.Name(), depsStr)
+ // Remove the container dependencies so we can go ahead and delete the volume
+ for _, dep := range deps {
+ if err := r.state.RemoveVolCtrDep(v, dep); err != nil {
+ return errors.Wrapf(err, "unable to remove container dependency %q from volume %q while trying to delete volume by force", dep, v.Name())
+ }
+ }
+ }
+
+ // Delete the mountpoint path of the volume, that is delete the volume from /var/lib/containers/storage/volumes
+ if err := v.teardownStorage(); err != nil {
+ return errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name())
+ }
+
+ // Remove the volume from the state
+ if err := r.state.RemoveVolume(v); err != nil {
+ return errors.Wrapf(err, "error removing volume %s", v.Name())
+ }
+
+ // Set volume as invalid so it can no longer be used
+ v.valid = false
+
+ return nil
+}
diff --git a/libpod/state.go b/libpod/state.go
index 06c2003d8..88d89f673 100644
--- a/libpod/state.go
+++ b/libpod/state.go
@@ -153,4 +153,27 @@ type State interface {
// If a namespace has been set, only pods in that namespace will be
// returned.
AllPods() ([]*Pod, error)
+
+ // Volume accepts full name of volume
+ // If the volume doesn't exist, an error will be returned
+ Volume(volName string) (*Volume, error)
+ // HasVolume returns true if volName exists in the state,
+ // otherwise it returns false
+ HasVolume(volName string) (bool, error)
+ // VolumeInUse goes through the container dependencies of a volume
+ // and checks if the volume is being used by any container. If it is
+ // a slice of container IDs using the volume is returned
+ VolumeInUse(volume *Volume) ([]string, error)
+ // AddVolume adds the specified volume to state. The volume's name
+ // must be unique within the list of existing volumes
+ AddVolume(volume *Volume) error
+ // RemoveVolCtrDep updates the list of container dependencies that the
+ // volume has. It either deletes the dependent container ID from
+ // the sub-bucket
+ RemoveVolCtrDep(volume *Volume, ctrID string) error
+ // RemoveVolume removes the specified volume.
+ // Only volumes that have no container dependencies can be removed
+ RemoveVolume(volume *Volume) error
+ // AllVolumes returns all the volumes available in the state
+ AllVolumes() ([]*Volume, error)
}
diff --git a/libpod/volume.go b/libpod/volume.go
new file mode 100644
index 000000000..b732e8aa7
--- /dev/null
+++ b/libpod/volume.go
@@ -0,0 +1,63 @@
+package libpod
+
+import "github.com/containers/storage"
+
+// Volume is the type used to create named volumes
+// TODO: all volumes should be created using this and the Volume API
+type Volume struct {
+ config *VolumeConfig
+
+ valid bool
+ runtime *Runtime
+ lock storage.Locker
+}
+
+// VolumeConfig holds the volume's config information
+//easyjson:json
+type VolumeConfig struct {
+ Name string `json:"name"`
+ Labels map[string]string `json:"labels"`
+ MountPoint string `json:"mountPoint"`
+ Driver string `json:"driver"`
+ Options map[string]string `json:"options"`
+ Scope string `json:"scope"`
+}
+
+// Name retrieves the volume's name
+func (v *Volume) Name() string {
+ return v.config.Name
+}
+
+// Labels returns the volume's labels
+func (v *Volume) Labels() map[string]string {
+ labels := make(map[string]string)
+ for key, value := range v.config.Labels {
+ labels[key] = value
+ }
+ return labels
+}
+
+// MountPoint returns the volume's mountpoint on the host
+func (v *Volume) MountPoint() string {
+ return v.config.MountPoint
+}
+
+// Driver returns the volume's driver
+func (v *Volume) Driver() string {
+ return v.config.Driver
+}
+
+// Options return the volume's options
+func (v *Volume) Options() map[string]string {
+ options := make(map[string]string)
+ for key, value := range v.config.Options {
+ options[key] = value
+ }
+
+ return options
+}
+
+// Scope returns the scope of the volume
+func (v *Volume) Scope() string {
+ return v.config.Scope
+}
diff --git a/libpod/volume_internal.go b/libpod/volume_internal.go
new file mode 100644
index 000000000..800e6d106
--- /dev/null
+++ b/libpod/volume_internal.go
@@ -0,0 +1,29 @@
+package libpod
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// VolumePath is the path under which all volumes that are created using the
+// local driver will be created
+// const VolumePath = "/var/lib/containers/storage/volumes"
+
+// Creates a new volume
+func newVolume(runtime *Runtime) (*Volume, error) {
+ volume := new(Volume)
+ volume.config = new(VolumeConfig)
+ volume.runtime = runtime
+ volume.config.Labels = make(map[string]string)
+ volume.config.Options = make(map[string]string)
+
+ return volume, nil
+}
+
+// teardownStorage deletes the volume from volumePath
+func (v *Volume) teardownStorage() error {
+ if !v.valid {
+ return ErrNoSuchVolume
+ }
+ return os.RemoveAll(filepath.Join(v.runtime.config.VolumePath, v.Name()))
+}
diff --git a/pkg/util/utils.go b/pkg/util/utils.go
index e483253a4..f567f2675 100644
--- a/pkg/util/utils.go
+++ b/pkg/util/utils.go
@@ -250,30 +250,40 @@ func GetRootlessRuntimeDir() (string, error) {
return runtimeDir, nil
}
-// GetRootlessStorageOpts returns the storage ops for containers running as non root
-func GetRootlessStorageOpts() (storage.StoreOptions, error) {
- var opts storage.StoreOptions
-
+// GetRootlessDirInfo returns the parent path of where the storage for containers and
+// volumes will be in rootless mode
+func GetRootlessDirInfo() (string, string, error) {
rootlessRuntime, err := GetRootlessRuntimeDir()
if err != nil {
- return opts, err
+ return "", "", err
}
- opts.RunRoot = rootlessRuntime
dataDir := os.Getenv("XDG_DATA_HOME")
if dataDir == "" {
home := os.Getenv("HOME")
if home == "" {
- return opts, fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty")
+ return "", "", fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty")
}
// runc doesn't like symlinks in the rootfs path, and at least
// on CoreOS /home is a symlink to /var/home, so resolve any symlink.
resolvedHome, err := filepath.EvalSymlinks(home)
if err != nil {
- return opts, errors.Wrapf(err, "cannot resolve %s", home)
+ return "", "", errors.Wrapf(err, "cannot resolve %s", home)
}
dataDir = filepath.Join(resolvedHome, ".local", "share")
}
+ return dataDir, rootlessRuntime, nil
+}
+
+// GetRootlessStorageOpts returns the storage opts for containers running as non root
+func GetRootlessStorageOpts() (storage.StoreOptions, error) {
+ var opts storage.StoreOptions
+
+ dataDir, rootlessRuntime, err := GetRootlessDirInfo()
+ if err != nil {
+ return opts, err
+ }
+ opts.RunRoot = rootlessRuntime
opts.GraphRoot = filepath.Join(dataDir, "containers", "storage")
if path, err := exec.LookPath("fuse-overlayfs"); err == nil {
opts.GraphDriverName = "overlay"
@@ -284,6 +294,15 @@ func GetRootlessStorageOpts() (storage.StoreOptions, error) {
return opts, nil
}
+// GetRootlessVolumeInfo returns where all the name volumes will be created in rootless mode
+func GetRootlessVolumeInfo() (string, error) {
+ dataDir, _, err := GetRootlessDirInfo()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(dataDir, "containers", "storage", "volumes"), nil
+}
+
type tomlOptionsConfig struct {
MountProgram string `toml:"mount_program"`
}
@@ -313,14 +332,21 @@ func getTomlStorage(storeOptions *storage.StoreOptions) *tomlConfig {
return config
}
-// GetDefaultStoreOptions returns the default storage options for containers.
-func GetDefaultStoreOptions() (storage.StoreOptions, error) {
+// GetDefaultStoreOptions returns the storage ops for containers and the volume path
+// for the volume API
+// It also returns the path where all named volumes will be created using the volume API
+func GetDefaultStoreOptions() (storage.StoreOptions, string, error) {
storageOpts := storage.DefaultStoreOptions
+ volumePath := "/var/lib/containers/storage"
if rootless.IsRootless() {
var err error
storageOpts, err = GetRootlessStorageOpts()
if err != nil {
- return storageOpts, err
+ return storageOpts, volumePath, err
+ }
+ volumePath, err = GetRootlessVolumeInfo()
+ if err != nil {
+ return storageOpts, volumePath, err
}
storageConf := filepath.Join(os.Getenv("HOME"), ".config/containers/storage.conf")
@@ -330,7 +356,7 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) {
os.MkdirAll(filepath.Dir(storageConf), 0755)
file, err := os.OpenFile(storageConf, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
- return storageOpts, errors.Wrapf(err, "cannot open %s", storageConf)
+ return storageOpts, volumePath, errors.Wrapf(err, "cannot open %s", storageConf)
}
tomlConfiguration := getTomlStorage(&storageOpts)
@@ -341,5 +367,5 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) {
}
}
}
- return storageOpts, nil
+ return storageOpts, volumePath, nil
}
diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go
index f4f154ef2..268db5aa1 100644
--- a/test/e2e/libpod_suite_test.go
+++ b/test/e2e/libpod_suite_test.go
@@ -227,6 +227,17 @@ func (p *PodmanTestIntegration) CleanupPod() {
}
}
+// CleanupVolume cleans up the temporary store
+func (p *PodmanTestIntegration) CleanupVolume() {
+ // Remove all containers
+ session := p.Podman([]string{"volume", "rm", "-fa"})
+ session.Wait(90)
+ // Nuke tempdir
+ if err := os.RemoveAll(p.TempDir); err != nil {
+ fmt.Printf("%q\n", err)
+ }
+}
+
// PullImages pulls multiple images
func (p *PodmanTestIntegration) PullImages(images []string) error {
for _, i := range images {
diff --git a/test/e2e/volume_create_test.go b/test/e2e/volume_create_test.go
new file mode 100644
index 000000000..50ee63f2a
--- /dev/null
+++ b/test/e2e/volume_create_test.go
@@ -0,0 +1,60 @@
+package integration
+
+import (
+ "fmt"
+ "os"
+
+ . "github.com/containers/libpod/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Podman volume create", func() {
+ var (
+ tempdir string
+ err error
+ podmanTest *PodmanTestIntegration
+ )
+
+ BeforeEach(func() {
+ tempdir, err = CreateTempDirInTempDir()
+ if err != nil {
+ os.Exit(1)
+ }
+ podmanTest = PodmanTestCreate(tempdir)
+ podmanTest.RestoreAllArtifacts()
+ })
+
+ AfterEach(func() {
+ podmanTest.CleanupVolume()
+ f := CurrentGinkgoTestDescription()
+ timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+ GinkgoWriter.Write([]byte(timedResult))
+ })
+
+ It("podman create volume", func() {
+ session := podmanTest.Podman([]string{"volume", "create"})
+ session.WaitWithDefaultTimeout()
+ volName := session.OutputToString()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ check := podmanTest.Podman([]string{"volume", "ls", "-q"})
+ check.WaitWithDefaultTimeout()
+ match, _ := check.GrepString(volName)
+ Expect(match).To(BeTrue())
+ Expect(len(check.OutputToStringArray())).To(Equal(1))
+ })
+
+ It("podman create volume with name", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ volName := session.OutputToString()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ check := podmanTest.Podman([]string{"volume", "ls", "-q"})
+ check.WaitWithDefaultTimeout()
+ match, _ := check.GrepString(volName)
+ Expect(match).To(BeTrue())
+ Expect(len(check.OutputToStringArray())).To(Equal(1))
+ })
+})
diff --git a/test/e2e/volume_inspect_test.go b/test/e2e/volume_inspect_test.go
new file mode 100644
index 000000000..d0d5a601e
--- /dev/null
+++ b/test/e2e/volume_inspect_test.go
@@ -0,0 +1,77 @@
+package integration
+
+import (
+ "fmt"
+ "os"
+
+ . "github.com/containers/libpod/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Podman volume inspect", func() {
+ var (
+ tempdir string
+ err error
+ podmanTest *PodmanTestIntegration
+ )
+
+ BeforeEach(func() {
+ tempdir, err = CreateTempDirInTempDir()
+ if err != nil {
+ os.Exit(1)
+ }
+ podmanTest = PodmanTestCreate(tempdir)
+ podmanTest.RestoreAllArtifacts()
+ })
+
+ AfterEach(func() {
+ podmanTest.CleanupVolume()
+ f := CurrentGinkgoTestDescription()
+ timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+ GinkgoWriter.Write([]byte(timedResult))
+ })
+
+ It("podman inspect volume", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ volName := session.OutputToString()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "inspect", volName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.IsJSONOutputValid()).To(BeTrue())
+ })
+
+ It("podman inspect volume with Go format", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ volName := session.OutputToString()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "inspect", "--format", "{{.Name}}", volName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.OutputToString()).To(Equal(volName))
+ })
+
+ It("podman inspect volume with --all flag", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol1"})
+ session.WaitWithDefaultTimeout()
+ volName1 := session.OutputToString()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "create", "myvol2"})
+ session.WaitWithDefaultTimeout()
+ volName2 := session.OutputToString()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "inspect", "--format", "{{.Name}}", "--all"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(2))
+ Expect(session.OutputToStringArray()[0]).To(Equal(volName1))
+ Expect(session.OutputToStringArray()[1]).To(Equal(volName2))
+ })
+})
diff --git a/test/e2e/volume_ls_test.go b/test/e2e/volume_ls_test.go
new file mode 100644
index 000000000..119d29d9b
--- /dev/null
+++ b/test/e2e/volume_ls_test.go
@@ -0,0 +1,84 @@
+package integration
+
+import (
+ "fmt"
+ "os"
+
+ . "github.com/containers/libpod/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Podman volume ls", func() {
+ var (
+ tempdir string
+ err error
+ podmanTest *PodmanTestIntegration
+ )
+
+ BeforeEach(func() {
+ tempdir, err = CreateTempDirInTempDir()
+ if err != nil {
+ os.Exit(1)
+ }
+ podmanTest = PodmanTestCreate(tempdir)
+ podmanTest.RestoreAllArtifacts()
+ })
+
+ AfterEach(func() {
+ podmanTest.CleanupVolume()
+ f := CurrentGinkgoTestDescription()
+ timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+ GinkgoWriter.Write([]byte(timedResult))
+ })
+
+ It("podman ls volume", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(2))
+ })
+
+ It("podman ls volume with JSON format", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls", "--format", "json"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(session.IsJSONOutputValid()).To(BeTrue())
+ })
+
+ It("podman ls volume with Go template", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls", "--format", "table {{.Name}} {{.Driver}} {{.Scope}}"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(2))
+ })
+
+ It("podman ls volume with --filter flag", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "--label", "foo=bar", "myvol"})
+ volName := session.OutputToString()
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "create"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls", "--filter", "label=foo"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(2))
+ Expect(session.OutputToStringArray()[1]).To(ContainSubstring(volName))
+ })
+})
diff --git a/test/e2e/volume_prune_test.go b/test/e2e/volume_prune_test.go
new file mode 100644
index 000000000..8c0a10e77
--- /dev/null
+++ b/test/e2e/volume_prune_test.go
@@ -0,0 +1,64 @@
+package integration
+
+import (
+ "fmt"
+ "os"
+
+ . "github.com/containers/libpod/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Podman volume prune", func() {
+ var (
+ tempdir string
+ err error
+ podmanTest *PodmanTestIntegration
+ )
+
+ BeforeEach(func() {
+ tempdir, err = CreateTempDirInTempDir()
+ if err != nil {
+ os.Exit(1)
+ }
+ podmanTest = PodmanTestCreate(tempdir)
+ podmanTest.RestoreAllArtifacts()
+ })
+
+ AfterEach(func() {
+ podmanTest.CleanupVolume()
+ f := CurrentGinkgoTestDescription()
+ timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+ GinkgoWriter.Write([]byte(timedResult))
+ })
+
+ It("podman prune volume", func() {
+ session := podmanTest.Podman([]string{"volume", "create"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "create"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"create", "-v", "myvol:/myvol", ALPINE, "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(4))
+
+ session = podmanTest.Podman([]string{"volume", "prune", "--force"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(2))
+
+ podmanTest.Cleanup()
+ })
+})
diff --git a/test/e2e/volume_rm_test.go b/test/e2e/volume_rm_test.go
new file mode 100644
index 000000000..cebb09467
--- /dev/null
+++ b/test/e2e/volume_rm_test.go
@@ -0,0 +1,91 @@
+package integration
+
+import (
+ "fmt"
+ "os"
+
+ . "github.com/containers/libpod/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Podman volume rm", func() {
+ var (
+ tempdir string
+ err error
+ podmanTest *PodmanTestIntegration
+ )
+
+ BeforeEach(func() {
+ tempdir, err = CreateTempDirInTempDir()
+ if err != nil {
+ os.Exit(1)
+ }
+ podmanTest = PodmanTestCreate(tempdir)
+ podmanTest.RestoreAllArtifacts()
+ })
+
+ AfterEach(func() {
+ podmanTest.CleanupVolume()
+ f := CurrentGinkgoTestDescription()
+ timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+ GinkgoWriter.Write([]byte(timedResult))
+ })
+
+ It("podman rm volume", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "rm", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(0))
+ })
+
+ It("podman rm with --force flag", func() {
+ session := podmanTest.Podman([]string{"create", "-v", "myvol:/myvol", ALPINE, "ls"})
+ cid := session.OutputToString()
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "rm", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Not(Equal(0)))
+ Expect(session.ErrorToString()).To(ContainSubstring(cid))
+
+ session = podmanTest.Podman([]string{"volume", "rm", "-f", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(0))
+
+ podmanTest.Cleanup()
+ })
+
+ It("podman rm with --all flag", func() {
+ session := podmanTest.Podman([]string{"volume", "create", "myvol"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "create"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "rm", "-a"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"volume", "ls"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(0))
+ })
+})