mirror of
https://github.com/containers/podman.git
synced 2025-06-28 22:53:21 +08:00
Merge pull request #1904 from umohnani8/volume
Add "podman volume" command
This commit is contained in:
@ -201,12 +201,13 @@ func parseVolumesFrom(volumesFrom []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateVolumeHostDir(hostDir string) error {
|
func validateVolumeHostDir(hostDir string) error {
|
||||||
if !filepath.IsAbs(hostDir) {
|
if filepath.IsAbs(hostDir) {
|
||||||
return errors.Errorf("invalid host path, must be an absolute path %q", hostDir)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(hostDir); err != nil {
|
if _, err := os.Stat(hostDir); err != nil {
|
||||||
return errors.Wrapf(err, "error checking path %q", hostDir)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,11 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) {
|
|||||||
storageOpts := new(storage.StoreOptions)
|
storageOpts := new(storage.StoreOptions)
|
||||||
options := []libpod.RuntimeOption{}
|
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") {
|
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"))
|
mappings, err := util.ParseIDMapping(c.StringSlice("uidmap"), c.StringSlice("gidmap"), c.String("subuidmap"), c.String("subgidmap"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -90,6 +95,7 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) {
|
|||||||
if c.IsSet("infra-command") {
|
if c.IsSet("infra-command") {
|
||||||
options = append(options, libpod.WithDefaultInfraCommand(c.String("infra-command")))
|
options = append(options, libpod.WithDefaultInfraCommand(c.String("infra-command")))
|
||||||
}
|
}
|
||||||
|
options = append(options, libpod.WithVolumePath(volumePath))
|
||||||
if c.IsSet("config") {
|
if c.IsSet("config") {
|
||||||
return libpod.NewRuntimeFromConfig(c.String("config"), options...)
|
return libpod.NewRuntimeFromConfig(c.String("config"), options...)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/syslog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
@ -16,7 +17,6 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
lsyslog "github.com/sirupsen/logrus/hooks/syslog"
|
lsyslog "github.com/sirupsen/logrus/hooks/syslog"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"log/syslog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is populated by the Makefile from the VERSION file
|
// This is populated by the Makefile from the VERSION file
|
||||||
@ -102,6 +102,7 @@ func main() {
|
|||||||
umountCommand,
|
umountCommand,
|
||||||
unpauseCommand,
|
unpauseCommand,
|
||||||
versionCommand,
|
versionCommand,
|
||||||
|
volumeCommand,
|
||||||
waitCommand,
|
waitCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
gosignal "os/signal"
|
||||||
|
|
||||||
"github.com/containers/libpod/libpod"
|
"github.com/containers/libpod/libpod"
|
||||||
"github.com/docker/docker/pkg/signal"
|
"github.com/docker/docker/pkg/signal"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
@ -11,8 +14,6 @@ import (
|
|||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
"k8s.io/client-go/tools/remotecommand"
|
"k8s.io/client-go/tools/remotecommand"
|
||||||
"os"
|
|
||||||
gosignal "os/signal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RawTtyFormatter struct {
|
type RawTtyFormatter struct {
|
||||||
@ -208,6 +209,35 @@ func getPodsFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Pod, error
|
|||||||
return pods, lastError
|
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
|
//printParallelOutput takes the map of parallel worker results and outputs them
|
||||||
// to stdout
|
// to stdout
|
||||||
func printParallelOutput(m map[string]error, errCount int) error {
|
func printParallelOutput(m map[string]error, errCount int) error {
|
||||||
|
26
cmd/podman/volume.go
Normal file
26
cmd/podman/volume.go
Normal 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,
|
||||||
|
}
|
||||||
|
)
|
97
cmd/podman/volume_create.go
Normal file
97
cmd/podman/volume_create.go
Normal 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
|
||||||
|
}
|
63
cmd/podman/volume_inspect.go
Normal file
63
cmd/podman/volume_inspect.go
Normal 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
308
cmd/podman/volume_ls.go
Normal 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)
|
||||||
|
}
|
86
cmd/podman/volume_prune.go
Normal file
86
cmd/podman/volume_prune.go
Normal 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
71
cmd/podman/volume_rm.go
Normal 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
|
||||||
|
}
|
@ -62,4 +62,9 @@
|
|||||||
| [podman-unpause(1)](/docs/podman-unpause.1.md) | Unpause one or more running containers |[](https://asciinema.org/a/141292)|
|
| [podman-unpause(1)](/docs/podman-unpause.1.md) | Unpause one or more running containers |[](https://asciinema.org/a/141292)|
|
||||||
| [podman-varlink(1)](/docs/podman-varlink.1.md) | Run the varlink backend ||
|
| [podman-varlink(1)](/docs/podman-varlink.1.md) | Run the varlink backend ||
|
||||||
| [podman-version(1)](/docs/podman-version.1.md) | Display the version information |[](https://asciinema.org/a/mfrn61pjZT9Fc8L4NbfdSqfgu)|
|
| [podman-version(1)](/docs/podman-version.1.md) | Display the version information |[](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 |[](https://asciinema.org/a/QNPGKdjWuPgI96GcfkycQtah0)|
|
| [podman-wait(1)](/docs/podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes |[](https://asciinema.org/a/QNPGKdjWuPgI96GcfkycQtah0)|
|
||||||
|
@ -689,6 +689,23 @@ __podman_images() {
|
|||||||
__podman_q images $images_args | awk "$awk_script" | grep -v '<none>$'
|
__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() {
|
_podman_attach() {
|
||||||
local options_with_args="
|
local options_with_args="
|
||||||
--detach-keys
|
--detach-keys
|
||||||
@ -2536,6 +2553,128 @@ _podman_pod() {
|
|||||||
esac
|
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() {
|
_podman_podman() {
|
||||||
local options_with_args="
|
local options_with_args="
|
||||||
--config -c
|
--config -c
|
||||||
@ -2596,6 +2735,7 @@ _podman_podman() {
|
|||||||
unpause
|
unpause
|
||||||
varlink
|
varlink
|
||||||
version
|
version
|
||||||
|
volume
|
||||||
wait
|
wait
|
||||||
"
|
"
|
||||||
|
|
||||||
|
48
docs/podman-volume-create.1.md
Normal file
48
docs/podman-volume-create.1.md
Normal 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>
|
45
docs/podman-volume-inspect.1.md
Normal file
45
docs/podman-volume-inspect.1.md
Normal 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>
|
49
docs/podman-volume-ls.1.md
Normal file
49
docs/podman-volume-ls.1.md
Normal 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>
|
38
docs/podman-volume-prune.1.md
Normal file
38
docs/podman-volume-prune.1.md
Normal 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>
|
45
docs/podman-volume-rm.1.md
Normal file
45
docs/podman-volume-rm.1.md
Normal 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
23
docs/podman-volume.1.md
Normal 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>
|
@ -94,6 +94,12 @@ func NewBoltState(path string, runtime *Runtime) (State, error) {
|
|||||||
if _, err := tx.CreateBucketIfNotExists(allPodsBkt); err != nil {
|
if _, err := tx.CreateBucketIfNotExists(allPodsBkt); err != nil {
|
||||||
return errors.Wrapf(err, "error creating all pods bucket")
|
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 {
|
if _, err := tx.CreateBucketIfNotExists(runtimeConfigBkt); err != nil {
|
||||||
return errors.Wrapf(err, "error creating runtime-config bucket")
|
return errors.Wrapf(err, "error creating runtime-config bucket")
|
||||||
}
|
}
|
||||||
@ -1150,6 +1156,378 @@ func (s *BoltState) PodContainers(pod *Pod) ([]*Container, error) {
|
|||||||
return ctrs, nil
|
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.
|
// AddPod adds the given pod to the state.
|
||||||
func (s *BoltState) AddPod(pod *Pod) error {
|
func (s *BoltState) AddPod(pod *Pod) error {
|
||||||
if !s.valid {
|
if !s.valid {
|
||||||
|
@ -21,11 +21,14 @@ const (
|
|||||||
allCtrsName = "all-ctrs"
|
allCtrsName = "all-ctrs"
|
||||||
podName = "pod"
|
podName = "pod"
|
||||||
allPodsName = "allPods"
|
allPodsName = "allPods"
|
||||||
|
volName = "vol"
|
||||||
|
allVolsName = "allVolumes"
|
||||||
runtimeConfigName = "runtime-config"
|
runtimeConfigName = "runtime-config"
|
||||||
|
|
||||||
configName = "config"
|
configName = "config"
|
||||||
stateName = "state"
|
stateName = "state"
|
||||||
dependenciesName = "dependencies"
|
dependenciesName = "dependencies"
|
||||||
|
volCtrDependencies = "vol-dependencies"
|
||||||
netNSName = "netns"
|
netNSName = "netns"
|
||||||
containersName = "containers"
|
containersName = "containers"
|
||||||
podIDName = "pod-id"
|
podIDName = "pod-id"
|
||||||
@ -47,11 +50,14 @@ var (
|
|||||||
allCtrsBkt = []byte(allCtrsName)
|
allCtrsBkt = []byte(allCtrsName)
|
||||||
podBkt = []byte(podName)
|
podBkt = []byte(podName)
|
||||||
allPodsBkt = []byte(allPodsName)
|
allPodsBkt = []byte(allPodsName)
|
||||||
|
volBkt = []byte(volName)
|
||||||
|
allVolsBkt = []byte(allVolsName)
|
||||||
runtimeConfigBkt = []byte(runtimeConfigName)
|
runtimeConfigBkt = []byte(runtimeConfigName)
|
||||||
|
|
||||||
configKey = []byte(configName)
|
configKey = []byte(configName)
|
||||||
stateKey = []byte(stateName)
|
stateKey = []byte(stateName)
|
||||||
dependenciesBkt = []byte(dependenciesName)
|
dependenciesBkt = []byte(dependenciesName)
|
||||||
|
volDependenciesBkt = []byte(volCtrDependencies)
|
||||||
netNSKey = []byte(netNSName)
|
netNSKey = []byte(netNSName)
|
||||||
containersBkt = []byte(containersName)
|
containersBkt = []byte(containersName)
|
||||||
podIDKey = []byte(podIDName)
|
podIDKey = []byte(podIDName)
|
||||||
@ -234,6 +240,22 @@ func getAllPodsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
|||||||
return bkt, nil
|
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) {
|
func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
bkt := tx.Bucket(runtimeConfigBkt)
|
bkt := tx.Bucket(runtimeConfigBkt)
|
||||||
if bkt == nil {
|
if bkt == nil {
|
||||||
@ -315,6 +337,35 @@ func (s *BoltState) getPodFromDB(id []byte, pod *Pod, podBkt *bolt.Bucket) error
|
|||||||
return nil
|
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
|
// Add a container to the DB
|
||||||
// If pod is not nil, the container is added to the pod as well
|
// If pod is not nil, the container is added to the pod as well
|
||||||
func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
|
func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
|
||||||
@ -376,6 +427,11 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volBkt, err := getVolBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// If a pod was given, check if it exists
|
// If a pod was given, check if it exists
|
||||||
var podDB *bolt.Bucket
|
var podDB *bolt.Bucket
|
||||||
var podCtrs *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 nil
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
@ -545,6 +620,11 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volBkt, err := getVolBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Does the pod exist?
|
// Does the pod exist?
|
||||||
var podDB *bolt.Bucket
|
var podDB *bolt.Bucket
|
||||||
if pod != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,11 @@ func getTestContainer(id, name, locksDir string) (*Container, error) {
|
|||||||
"/test/file.test": "/test2/file2.test",
|
"/test/file.test": "/test2/file2.test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
runtime: &Runtime{
|
||||||
|
config: &RuntimeConfig{
|
||||||
|
VolumePath: "/does/not/exist/tmp/volumes",
|
||||||
|
},
|
||||||
|
},
|
||||||
valid: true,
|
valid: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package libpod
|
package libpod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/libpod/pkg/inspect"
|
"github.com/containers/libpod/pkg/inspect"
|
||||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||||
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,6 +51,17 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
|
|||||||
hostnamePath = getPath
|
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{
|
data := &inspect.ContainerInspectData{
|
||||||
ID: config.ID,
|
ID: config.ID,
|
||||||
Created: config.CreatedTime,
|
Created: config.CreatedTime,
|
||||||
@ -85,7 +99,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
|
|||||||
AppArmorProfile: spec.Process.ApparmorProfile,
|
AppArmorProfile: spec.Process.ApparmorProfile,
|
||||||
ExecIDs: execIDs,
|
ExecIDs: execIDs,
|
||||||
GraphDriver: driverData,
|
GraphDriver: driverData,
|
||||||
Mounts: spec.Mounts,
|
Mounts: mounts,
|
||||||
Dependencies: c.Dependencies(),
|
Dependencies: c.Dependencies(),
|
||||||
NetworkSettings: &inspect.NetworkSettings{
|
NetworkSettings: &inspect.NetworkSettings{
|
||||||
Bridge: "", // TODO
|
Bridge: "", // TODO
|
||||||
|
@ -11,18 +11,24 @@ var (
|
|||||||
ErrNoSuchPod = errors.New("no such pod")
|
ErrNoSuchPod = errors.New("no such pod")
|
||||||
// ErrNoSuchImage indicates the requested image does not exist
|
// ErrNoSuchImage indicates the requested image does not exist
|
||||||
ErrNoSuchImage = errors.New("no such image")
|
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
|
// ErrCtrExists indicates a container with the same name or ID already
|
||||||
// exists
|
// exists
|
||||||
ErrCtrExists = errors.New("container already exists")
|
ErrCtrExists = errors.New("container already exists")
|
||||||
// ErrPodExists indicates a pod with the same name or ID already exists
|
// ErrPodExists indicates a pod with the same name or ID already exists
|
||||||
ErrPodExists = errors.New("pod 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")
|
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
|
// ErrCtrStateInvalid indicates a container is in an improper state for
|
||||||
// the requested operation
|
// the requested operation
|
||||||
ErrCtrStateInvalid = errors.New("container state improper")
|
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
|
// ErrRuntimeFinalized indicates that the runtime has already been
|
||||||
// created and cannot be modified
|
// created and cannot be modified
|
||||||
@ -33,6 +39,9 @@ var (
|
|||||||
// ErrPodFinalized indicates that the pod has already been created and
|
// ErrPodFinalized indicates that the pod has already been created and
|
||||||
// cannot be modified
|
// cannot be modified
|
||||||
ErrPodFinalized = errors.New("pod has been finalized")
|
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 indicates that an invalid argument was passed
|
||||||
ErrInvalidArg = errors.New("invalid argument")
|
ErrInvalidArg = errors.New("invalid argument")
|
||||||
@ -55,6 +64,9 @@ var (
|
|||||||
// ErrPodRemoved indicates that the pod has already been removed and no
|
// ErrPodRemoved indicates that the pod has already been removed and no
|
||||||
// further operations can be performed on it
|
// further operations can be performed on it
|
||||||
ErrPodRemoved = errors.New("pod has already been removed")
|
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
|
// ErrDBClosed indicates that the connection to the state database has
|
||||||
// already been closed
|
// already been closed
|
||||||
|
@ -18,8 +18,10 @@ type InMemoryState struct {
|
|||||||
pods map[string]*Pod
|
pods map[string]*Pod
|
||||||
// Maps container ID to container struct.
|
// Maps container ID to container struct.
|
||||||
containers map[string]*Container
|
containers map[string]*Container
|
||||||
|
volumes map[string]*Volume
|
||||||
// Maps container ID to a list of IDs of dependencies.
|
// 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.
|
// Maps pod ID to a map of container ID to container struct.
|
||||||
podContainers map[string]map[string]*Container
|
podContainers map[string]map[string]*Container
|
||||||
// Global name registry - ensures name uniqueness and performs lookups.
|
// Global name registry - ensures name uniqueness and performs lookups.
|
||||||
@ -46,8 +48,10 @@ func NewInMemoryState() (State, error) {
|
|||||||
|
|
||||||
state.pods = make(map[string]*Pod)
|
state.pods = make(map[string]*Pod)
|
||||||
state.containers = make(map[string]*Container)
|
state.containers = make(map[string]*Container)
|
||||||
|
state.volumes = make(map[string]*Volume)
|
||||||
|
|
||||||
state.ctrDepends = make(map[string][]string)
|
state.ctrDepends = make(map[string][]string)
|
||||||
|
state.volumeDepends = make(map[string][]string)
|
||||||
|
|
||||||
state.podContainers = make(map[string]map[string]*Container)
|
state.podContainers = make(map[string]map[string]*Container)
|
||||||
|
|
||||||
@ -244,6 +248,14 @@ func (s *InMemoryState) AddContainer(ctr *Container) error {
|
|||||||
s.addCtrToDependsMap(ctr.ID(), depCtr)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,6 +306,14 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error {
|
|||||||
s.removeCtrFromDependsMap(ctr.ID(), depCtr)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,6 +378,114 @@ func (s *InMemoryState) ContainerInUse(ctr *Container) ([]string, error) {
|
|||||||
return arr, nil
|
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
|
// AllContainers retrieves all containers from the state
|
||||||
func (s *InMemoryState) AllContainers() ([]*Container, error) {
|
func (s *InMemoryState) AllContainers() ([]*Container, error) {
|
||||||
ctrs := make([]*Container, 0, len(s.containers))
|
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
|
// Check if we can access a pod or container, or if that is blocked by
|
||||||
// namespaces.
|
// namespaces.
|
||||||
func (s *InMemoryState) checkNSMatch(id, ns string) error {
|
func (s *InMemoryState) checkNSMatch(id, ns string) error {
|
||||||
|
@ -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.
|
// WithDefaultInfraImage sets the infra image for libpod.
|
||||||
// An infra image is used for inter-container kernel
|
// An infra image is used for inter-container kernel
|
||||||
// namespace sharing within a pod. Typically, an infra
|
// 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
|
// Pod Creation Options
|
||||||
|
|
||||||
// WithPodName sets the name of the pod.
|
// WithPodName sets the name of the pod.
|
||||||
|
@ -92,6 +92,7 @@ type RuntimeConfig struct {
|
|||||||
// Not included in on-disk config, use the dedicated containers/storage
|
// Not included in on-disk config, use the dedicated containers/storage
|
||||||
// configuration file instead
|
// configuration file instead
|
||||||
StorageConfig storage.StoreOptions `toml:"-"`
|
StorageConfig storage.StoreOptions `toml:"-"`
|
||||||
|
VolumePath string `toml:"volume_path"`
|
||||||
// ImageDefaultTransport is the default transport method used to fetch
|
// ImageDefaultTransport is the default transport method used to fetch
|
||||||
// images
|
// images
|
||||||
ImageDefaultTransport string `toml:"image_default_transport"`
|
ImageDefaultTransport string `toml:"image_default_transport"`
|
||||||
@ -278,12 +279,13 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
|
|||||||
|
|
||||||
if rootless.IsRootless() {
|
if rootless.IsRootless() {
|
||||||
// If we're rootless, override the default storage config
|
// If we're rootless, override the default storage config
|
||||||
storageConf, err := util.GetDefaultStoreOptions()
|
storageConf, volumePath, err := util.GetDefaultStoreOptions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "error retrieving rootless storage config")
|
return nil, errors.Wrapf(err, "error retrieving rootless storage config")
|
||||||
}
|
}
|
||||||
runtime.config.StorageConfig = storageConf
|
runtime.config.StorageConfig = storageConf
|
||||||
runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod")
|
runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod")
|
||||||
|
runtime.config.VolumePath = volumePath
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := ConfigPath
|
configPath := ConfigPath
|
||||||
|
@ -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 == "" {
|
if ctr.config.LogPath == "" {
|
||||||
ctr.config.LogPath = filepath.Join(ctr.config.StaticDir, "ctr.log")
|
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)
|
ctr.config.Mounts = append(ctr.config.Mounts, ctr.config.ShmDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the container to the state
|
// Add the container to the state
|
||||||
// TODO: May be worth looking into recovering from name/ID collisions here
|
// TODO: May be worth looking into recovering from name/ID collisions here
|
||||||
if ctr.config.Pod != "" {
|
if ctr.config.Pod != "" {
|
||||||
@ -474,3 +493,11 @@ func (r *Runtime) GetLatestContainer() (*Container, error) {
|
|||||||
}
|
}
|
||||||
return ctrs[lastCreatedIndex], nil
|
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
107
libpod/runtime_volume.go
Normal 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()
|
||||||
|
}
|
132
libpod/runtime_volume_linux.go
Normal file
132
libpod/runtime_volume_linux.go
Normal 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
|
||||||
|
}
|
@ -153,4 +153,27 @@ type State interface {
|
|||||||
// If a namespace has been set, only pods in that namespace will be
|
// If a namespace has been set, only pods in that namespace will be
|
||||||
// returned.
|
// returned.
|
||||||
AllPods() ([]*Pod, error)
|
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
63
libpod/volume.go
Normal 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
29
libpod/volume_internal.go
Normal 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()))
|
||||||
|
}
|
@ -250,30 +250,40 @@ func GetRootlessRuntimeDir() (string, error) {
|
|||||||
return runtimeDir, nil
|
return runtimeDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRootlessStorageOpts returns the storage ops for containers running as non root
|
// GetRootlessDirInfo returns the parent path of where the storage for containers and
|
||||||
func GetRootlessStorageOpts() (storage.StoreOptions, error) {
|
// volumes will be in rootless mode
|
||||||
var opts storage.StoreOptions
|
func GetRootlessDirInfo() (string, string, error) {
|
||||||
|
|
||||||
rootlessRuntime, err := GetRootlessRuntimeDir()
|
rootlessRuntime, err := GetRootlessRuntimeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return opts, err
|
return "", "", err
|
||||||
}
|
}
|
||||||
opts.RunRoot = rootlessRuntime
|
|
||||||
|
|
||||||
dataDir := os.Getenv("XDG_DATA_HOME")
|
dataDir := os.Getenv("XDG_DATA_HOME")
|
||||||
if dataDir == "" {
|
if dataDir == "" {
|
||||||
home := os.Getenv("HOME")
|
home := os.Getenv("HOME")
|
||||||
if 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
|
// 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.
|
// on CoreOS /home is a symlink to /var/home, so resolve any symlink.
|
||||||
resolvedHome, err := filepath.EvalSymlinks(home)
|
resolvedHome, err := filepath.EvalSymlinks(home)
|
||||||
if err != nil {
|
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")
|
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")
|
opts.GraphRoot = filepath.Join(dataDir, "containers", "storage")
|
||||||
if path, err := exec.LookPath("fuse-overlayfs"); err == nil {
|
if path, err := exec.LookPath("fuse-overlayfs"); err == nil {
|
||||||
opts.GraphDriverName = "overlay"
|
opts.GraphDriverName = "overlay"
|
||||||
@ -284,6 +294,15 @@ func GetRootlessStorageOpts() (storage.StoreOptions, error) {
|
|||||||
return opts, nil
|
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 {
|
type tomlOptionsConfig struct {
|
||||||
MountProgram string `toml:"mount_program"`
|
MountProgram string `toml:"mount_program"`
|
||||||
}
|
}
|
||||||
@ -313,14 +332,21 @@ func getTomlStorage(storeOptions *storage.StoreOptions) *tomlConfig {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultStoreOptions returns the default storage options for containers.
|
// GetDefaultStoreOptions returns the storage ops for containers and the volume path
|
||||||
func GetDefaultStoreOptions() (storage.StoreOptions, error) {
|
// 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
|
storageOpts := storage.DefaultStoreOptions
|
||||||
|
volumePath := "/var/lib/containers/storage"
|
||||||
if rootless.IsRootless() {
|
if rootless.IsRootless() {
|
||||||
var err error
|
var err error
|
||||||
storageOpts, err = GetRootlessStorageOpts()
|
storageOpts, err = GetRootlessStorageOpts()
|
||||||
if err != nil {
|
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")
|
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)
|
os.MkdirAll(filepath.Dir(storageConf), 0755)
|
||||||
file, err := os.OpenFile(storageConf, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
file, err := os.OpenFile(storageConf, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||||
if err != nil {
|
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)
|
tomlConfiguration := getTomlStorage(&storageOpts)
|
||||||
@ -341,5 +367,5 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return storageOpts, nil
|
return storageOpts, volumePath, nil
|
||||||
}
|
}
|
||||||
|
@ -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
|
// PullImages pulls multiple images
|
||||||
func (p *PodmanTestIntegration) PullImages(images []string) error {
|
func (p *PodmanTestIntegration) PullImages(images []string) error {
|
||||||
for _, i := range images {
|
for _, i := range images {
|
||||||
|
60
test/e2e/volume_create_test.go
Normal file
60
test/e2e/volume_create_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
77
test/e2e/volume_inspect_test.go
Normal file
77
test/e2e/volume_inspect_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
84
test/e2e/volume_ls_test.go
Normal file
84
test/e2e/volume_ls_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
64
test/e2e/volume_prune_test.go
Normal file
64
test/e2e/volume_prune_test.go
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
91
test/e2e/volume_rm_test.go
Normal file
91
test/e2e/volume_rm_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
Reference in New Issue
Block a user