Merge pull request #1904 from umohnani8/volume

Add "podman volume" command
This commit is contained in:
OpenShift Merge Robot
2018-12-06 08:59:13 -08:00
committed by GitHub
39 changed files with 2672 additions and 39 deletions

View File

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

View File

@ -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...)
}

View File

@ -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,
}

View File

@ -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 {

26
cmd/podman/volume.go Normal file
View File

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

View File

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

View File

@ -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)
}

308
cmd/podman/volume_ls.go Normal file
View File

@ -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)
}

View File

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

71
cmd/podman/volume_rm.go Normal file
View File

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

View File

@ -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)|

View File

@ -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
"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

23
docs/podman-volume.1.md Normal file
View File

@ -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>

View File

@ -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 {

View File

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

View File

@ -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,
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

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

107
libpod/runtime_volume.go Normal file
View File

@ -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()
}

View File

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

View File

@ -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)
}

63
libpod/volume.go Normal file
View File

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

29
libpod/volume_internal.go Normal file
View File

@ -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()))
}

View File

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

View File

@ -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 {

View File

@ -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))
})
})

View File

@ -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))
})
})

View File

@ -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))
})
})

View File

@ -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()
})
})

View File

@ -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))
})
})