mirror of
https://github.com/containers/podman.git
synced 2025-06-03 20:33:20 +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 {
|
||||
if !filepath.IsAbs(hostDir) {
|
||||
return errors.Errorf("invalid host path, must be an absolute path %q", hostDir)
|
||||
}
|
||||
if _, err := os.Stat(hostDir); err != nil {
|
||||
return errors.Wrapf(err, "error checking path %q", hostDir)
|
||||
if filepath.IsAbs(hostDir) {
|
||||
if _, err := os.Stat(hostDir); err != nil {
|
||||
return errors.Wrapf(err, "error checking path %q", hostDir)
|
||||
}
|
||||
}
|
||||
// If hostDir is not an absolute path, that means the user wants to create a
|
||||
// named volume. This will be done later on in the code.
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,11 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) {
|
||||
storageOpts := new(storage.StoreOptions)
|
||||
options := []libpod.RuntimeOption{}
|
||||
|
||||
_, volumePath, err := util.GetDefaultStoreOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.IsSet("uidmap") || c.IsSet("gidmap") || c.IsSet("subuidmap") || c.IsSet("subgidmap") {
|
||||
mappings, err := util.ParseIDMapping(c.StringSlice("uidmap"), c.StringSlice("gidmap"), c.String("subuidmap"), c.String("subgidmap"))
|
||||
if err != nil {
|
||||
@ -90,6 +95,7 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) {
|
||||
if c.IsSet("infra-command") {
|
||||
options = append(options, libpod.WithDefaultInfraCommand(c.String("infra-command")))
|
||||
}
|
||||
options = append(options, libpod.WithVolumePath(volumePath))
|
||||
if c.IsSet("config") {
|
||||
return libpod.NewRuntimeFromConfig(c.String("config"), options...)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/syslog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime/pprof"
|
||||
@ -16,7 +17,6 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
lsyslog "github.com/sirupsen/logrus/hooks/syslog"
|
||||
"github.com/urfave/cli"
|
||||
"log/syslog"
|
||||
)
|
||||
|
||||
// This is populated by the Makefile from the VERSION file
|
||||
@ -102,6 +102,7 @@ func main() {
|
||||
umountCommand,
|
||||
unpauseCommand,
|
||||
versionCommand,
|
||||
volumeCommand,
|
||||
waitCommand,
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
gosignal "os/signal"
|
||||
|
||||
"github.com/containers/libpod/libpod"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
@ -11,8 +14,6 @@ import (
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"os"
|
||||
gosignal "os/signal"
|
||||
)
|
||||
|
||||
type RawTtyFormatter struct {
|
||||
@ -208,6 +209,35 @@ func getPodsFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Pod, error
|
||||
return pods, lastError
|
||||
}
|
||||
|
||||
func getVolumesFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Volume, error) {
|
||||
args := c.Args()
|
||||
var (
|
||||
vols []*libpod.Volume
|
||||
lastError error
|
||||
err error
|
||||
)
|
||||
|
||||
if c.Bool("all") {
|
||||
vols, err = r.Volumes()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to get all volumes")
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range args {
|
||||
vol, err := r.GetVolume(i)
|
||||
if err != nil {
|
||||
if lastError != nil {
|
||||
logrus.Errorf("%q", lastError)
|
||||
}
|
||||
lastError = errors.Wrapf(err, "unable to find volume %s", i)
|
||||
continue
|
||||
}
|
||||
vols = append(vols, vol)
|
||||
}
|
||||
return vols, lastError
|
||||
}
|
||||
|
||||
//printParallelOutput takes the map of parallel worker results and outputs them
|
||||
// to stdout
|
||||
func printParallelOutput(m map[string]error, errCount int) error {
|
||||
|
26
cmd/podman/volume.go
Normal file
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-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-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)|
|
||||
|
@ -689,6 +689,23 @@ __podman_images() {
|
||||
__podman_q images $images_args | awk "$awk_script" | grep -v '<none>$'
|
||||
}
|
||||
|
||||
# __podman_complete_volumes applies completion of volumes based on the current
|
||||
# value of `$cur` or the value of the optional first option `--cur`, if given.
|
||||
__podman_complete_volumes() {
|
||||
local current="$cur"
|
||||
if [ "$1" = "--cur" ] ; then
|
||||
current="$2"
|
||||
shift 2
|
||||
fi
|
||||
COMPREPLY=( $(compgen -W "$(__podman_volume "$@")" -- "$current") )
|
||||
}
|
||||
|
||||
__podman_complete_volume_names() {
|
||||
local names=( $(__podman_q volume ls --quiet) )
|
||||
COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") )
|
||||
}
|
||||
|
||||
|
||||
_podman_attach() {
|
||||
local options_with_args="
|
||||
--detach-keys
|
||||
@ -2536,6 +2553,128 @@ _podman_pod() {
|
||||
esac
|
||||
}
|
||||
|
||||
_podman_volume_create() {
|
||||
local options_with_args="
|
||||
--driver
|
||||
--label
|
||||
-l
|
||||
--opt
|
||||
-o
|
||||
"
|
||||
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
_complete_ "$options_with_args" "$boolean_options"
|
||||
}
|
||||
|
||||
_podman_volume_ls() {
|
||||
local options_with_args="
|
||||
--filter
|
||||
--format
|
||||
-f
|
||||
"
|
||||
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--quiet
|
||||
-q
|
||||
"
|
||||
|
||||
_complete_ "$options_with_args" "$boolean_options"
|
||||
}
|
||||
|
||||
_podman_volume_inspect() {
|
||||
local options_with_args="
|
||||
--format
|
||||
-f
|
||||
"
|
||||
|
||||
local boolean_options="
|
||||
--all
|
||||
-a
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
_complete_ "$options_with_args" "$boolean_options"
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
__podman_complete_volume_names
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_podman_volume_rm() {
|
||||
local options_with_args=""
|
||||
|
||||
local boolean_options="
|
||||
--all
|
||||
-a
|
||||
--force
|
||||
-f
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
_complete_ "$options_with_args" "$boolean_options"
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
__podman_complete_volume_names
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_podman_volume_prune() {
|
||||
local options_with_args=""
|
||||
|
||||
local boolean_options="
|
||||
--force
|
||||
-f
|
||||
--help
|
||||
-h
|
||||
"
|
||||
|
||||
_complete_ "$options_with_args" "$boolean_options"
|
||||
}
|
||||
|
||||
_podman_volume() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
"
|
||||
subcommands="
|
||||
create
|
||||
inspect
|
||||
ls
|
||||
rm
|
||||
prune
|
||||
"
|
||||
local aliases="
|
||||
list
|
||||
remove
|
||||
"
|
||||
__podman_subcommands "$subcommands $aliases" && return
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) )
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_podman_podman() {
|
||||
local options_with_args="
|
||||
--config -c
|
||||
@ -2596,6 +2735,7 @@ _podman_podman() {
|
||||
unpause
|
||||
varlink
|
||||
version
|
||||
volume
|
||||
wait
|
||||
"
|
||||
|
||||
|
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 {
|
||||
return errors.Wrapf(err, "error creating all pods bucket")
|
||||
}
|
||||
if _, err := tx.CreateBucketIfNotExists(volBkt); err != nil {
|
||||
return errors.Wrapf(err, "error creating volume bucket")
|
||||
}
|
||||
if _, err := tx.CreateBucketIfNotExists(allVolsBkt); err != nil {
|
||||
return errors.Wrapf(err, "error creating all volumes bucket")
|
||||
}
|
||||
if _, err := tx.CreateBucketIfNotExists(runtimeConfigBkt); err != nil {
|
||||
return errors.Wrapf(err, "error creating runtime-config bucket")
|
||||
}
|
||||
@ -1150,6 +1156,378 @@ func (s *BoltState) PodContainers(pod *Pod) ([]*Container, error) {
|
||||
return ctrs, nil
|
||||
}
|
||||
|
||||
// AddVolume adds the given volume to the state. It also adds ctrDepID to
|
||||
// the sub bucket holding the container dependencies that this volume has
|
||||
func (s *BoltState) AddVolume(volume *Volume) error {
|
||||
if !s.valid {
|
||||
return ErrDBClosed
|
||||
}
|
||||
|
||||
if !volume.valid {
|
||||
return ErrVolumeRemoved
|
||||
}
|
||||
|
||||
volName := []byte(volume.Name())
|
||||
|
||||
volConfigJSON, err := json.Marshal(volume.config)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name())
|
||||
}
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allVolsBkt, err := getAllVolsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if we already have a volume with the given name
|
||||
volExists := allVolsBkt.Get(volName)
|
||||
if volExists != nil {
|
||||
return errors.Wrapf(ErrVolumeExists, "name %s is in use", volume.Name())
|
||||
}
|
||||
|
||||
// We are good to add the volume
|
||||
// Make a bucket for it
|
||||
newVol, err := volBkt.CreateBucket(volName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating bucket for volume %s", volume.Name())
|
||||
}
|
||||
|
||||
// Make a subbucket for the containers using the volume. Dependent container IDs will be addedremoved to
|
||||
// this bucket in addcontainer/removeContainer
|
||||
if _, err := newVol.CreateBucket(volDependenciesBkt); err != nil {
|
||||
return errors.Wrapf(err, "error creating bucket for containers using volume %s", volume.Name())
|
||||
}
|
||||
|
||||
if err := newVol.Put(configKey, volConfigJSON); err != nil {
|
||||
return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name())
|
||||
}
|
||||
|
||||
if err := allVolsBkt.Put(volName, volName); err != nil {
|
||||
return errors.Wrapf(err, "error storing volume %s in all volumes bucket in DB", volume.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveVolCtrDep updates the container dependencies sub bucket of the given volume.
|
||||
// It deletes it from the bucket when found.
|
||||
// This is important when force removing a volume and we want to get rid of the dependencies.
|
||||
func (s *BoltState) RemoveVolCtrDep(volume *Volume, ctrID string) error {
|
||||
if ctrID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !s.valid {
|
||||
return ErrDBBadConfig
|
||||
}
|
||||
|
||||
if !volume.valid {
|
||||
return ErrVolumeRemoved
|
||||
}
|
||||
|
||||
volName := []byte(volume.Name())
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volDB := volBkt.Bucket(volName)
|
||||
if volDB == nil {
|
||||
volume.valid = false
|
||||
return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
|
||||
}
|
||||
|
||||
// Make a subbucket for the containers using the volume
|
||||
ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
|
||||
depCtrID := []byte(ctrID)
|
||||
if depExists := ctrDepsBkt.Get(depCtrID); depExists != nil {
|
||||
if err := ctrDepsBkt.Delete(depCtrID); err != nil {
|
||||
return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctrID, volume.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveVolume removes the given volume from the state
|
||||
func (s *BoltState) RemoveVolume(volume *Volume) error {
|
||||
if !s.valid {
|
||||
return ErrDBClosed
|
||||
}
|
||||
|
||||
if !volume.valid {
|
||||
return ErrVolumeRemoved
|
||||
}
|
||||
|
||||
volName := []byte(volume.Name())
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allVolsBkt, err := getAllVolsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the volume exists
|
||||
volDB := volBkt.Bucket(volName)
|
||||
if volDB == nil {
|
||||
volume.valid = false
|
||||
return errors.Wrapf(ErrNoSuchVolume, "volume %s does not exist in DB", volume.Name())
|
||||
}
|
||||
|
||||
// Check if volume is not being used by any container
|
||||
// This should never be nil
|
||||
// But if it is, we can assume that no containers are using
|
||||
// the volume.
|
||||
volCtrsBkt := volDB.Bucket(volDependenciesBkt)
|
||||
if volCtrsBkt != nil {
|
||||
var deps []string
|
||||
err = volCtrsBkt.ForEach(func(id, value []byte) error {
|
||||
deps = append(deps, string(id))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error getting list of dependencies from dependencies bucket for volumes %q", volume.Name())
|
||||
}
|
||||
if len(deps) > 0 {
|
||||
return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by container(s) %s", volume.Name(), strings.Join(deps, ","))
|
||||
}
|
||||
}
|
||||
|
||||
// volume is ready for removal
|
||||
// Let's kick it out
|
||||
if err := allVolsBkt.Delete(volName); err != nil {
|
||||
return errors.Wrapf(err, "error removing volume %s from all volumes bucket in DB", volume.Name())
|
||||
}
|
||||
if err := volBkt.DeleteBucket(volName); err != nil {
|
||||
return errors.Wrapf(err, "error removing volume %s from DB", volume.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// AllVolumes returns all volumes present in the state
|
||||
func (s *BoltState) AllVolumes() ([]*Volume, error) {
|
||||
if !s.valid {
|
||||
return nil, ErrDBClosed
|
||||
}
|
||||
|
||||
volumes := []*Volume{}
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
allVolsBucket, err := getAllVolsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volBucket, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = allVolsBucket.ForEach(func(id, name []byte) error {
|
||||
volExists := volBucket.Bucket(id)
|
||||
// This check can be removed if performance becomes an
|
||||
// issue, but much less helpful errors will be produced
|
||||
if volExists == nil {
|
||||
return errors.Wrapf(ErrInternal, "inconsistency in state - volume %s is in all volumes bucket but volume not found", string(id))
|
||||
}
|
||||
|
||||
volume := new(Volume)
|
||||
volume.config = new(VolumeConfig)
|
||||
|
||||
if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
|
||||
if errors.Cause(err) != ErrNSMismatch {
|
||||
logrus.Errorf("Error retrieving volume %s from the database: %v", string(id), err)
|
||||
}
|
||||
} else {
|
||||
volumes = append(volumes, volume)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// Volume retrieves a volume from full name
|
||||
func (s *BoltState) Volume(name string) (*Volume, error) {
|
||||
if name == "" {
|
||||
return nil, ErrEmptyID
|
||||
}
|
||||
|
||||
if !s.valid {
|
||||
return nil, ErrDBClosed
|
||||
}
|
||||
|
||||
volName := []byte(name)
|
||||
|
||||
volume := new(Volume)
|
||||
volume.config = new(VolumeConfig)
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.getVolumeFromDB(volName, volume, volBkt)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return volume, nil
|
||||
}
|
||||
|
||||
// HasVolume returns true if the given volume exists in the state, otherwise it returns false
|
||||
func (s *BoltState) HasVolume(name string) (bool, error) {
|
||||
if name == "" {
|
||||
return false, ErrEmptyID
|
||||
}
|
||||
|
||||
if !s.valid {
|
||||
return false, ErrDBClosed
|
||||
}
|
||||
|
||||
volName := []byte(name)
|
||||
|
||||
exists := false
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volDB := volBkt.Bucket(volName)
|
||||
if volDB != nil {
|
||||
exists = true
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// VolumeInUse checks if any container is using the volume
|
||||
// It returns a slice of the IDs of the containers using the given
|
||||
// volume. If the slice is empty, no containers use the given volume
|
||||
func (s *BoltState) VolumeInUse(volume *Volume) ([]string, error) {
|
||||
if !s.valid {
|
||||
return nil, ErrDBClosed
|
||||
}
|
||||
|
||||
if !volume.valid {
|
||||
return nil, ErrVolumeRemoved
|
||||
}
|
||||
|
||||
depCtrs := []string{}
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.closeDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
volBucket, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volDB := volBucket.Bucket([]byte(volume.Name()))
|
||||
if volDB == nil {
|
||||
volume.valid = false
|
||||
return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in DB", volume.Name())
|
||||
}
|
||||
|
||||
dependsBkt := volDB.Bucket(volDependenciesBkt)
|
||||
if dependsBkt == nil {
|
||||
return errors.Wrapf(ErrInternal, "volume %s has no dependencies bucket", volume.Name())
|
||||
}
|
||||
|
||||
// Iterate through and add dependencies
|
||||
err = dependsBkt.ForEach(func(id, value []byte) error {
|
||||
depCtrs = append(depCtrs, string(id))
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return depCtrs, nil
|
||||
}
|
||||
|
||||
// AddPod adds the given pod to the state.
|
||||
func (s *BoltState) AddPod(pod *Pod) error {
|
||||
if !s.valid {
|
||||
|
@ -21,15 +21,18 @@ const (
|
||||
allCtrsName = "all-ctrs"
|
||||
podName = "pod"
|
||||
allPodsName = "allPods"
|
||||
volName = "vol"
|
||||
allVolsName = "allVolumes"
|
||||
runtimeConfigName = "runtime-config"
|
||||
|
||||
configName = "config"
|
||||
stateName = "state"
|
||||
dependenciesName = "dependencies"
|
||||
netNSName = "netns"
|
||||
containersName = "containers"
|
||||
podIDName = "pod-id"
|
||||
namespaceName = "namespace"
|
||||
configName = "config"
|
||||
stateName = "state"
|
||||
dependenciesName = "dependencies"
|
||||
volCtrDependencies = "vol-dependencies"
|
||||
netNSName = "netns"
|
||||
containersName = "containers"
|
||||
podIDName = "pod-id"
|
||||
namespaceName = "namespace"
|
||||
|
||||
staticDirName = "static-dir"
|
||||
tmpDirName = "tmp-dir"
|
||||
@ -47,15 +50,18 @@ var (
|
||||
allCtrsBkt = []byte(allCtrsName)
|
||||
podBkt = []byte(podName)
|
||||
allPodsBkt = []byte(allPodsName)
|
||||
volBkt = []byte(volName)
|
||||
allVolsBkt = []byte(allVolsName)
|
||||
runtimeConfigBkt = []byte(runtimeConfigName)
|
||||
|
||||
configKey = []byte(configName)
|
||||
stateKey = []byte(stateName)
|
||||
dependenciesBkt = []byte(dependenciesName)
|
||||
netNSKey = []byte(netNSName)
|
||||
containersBkt = []byte(containersName)
|
||||
podIDKey = []byte(podIDName)
|
||||
namespaceKey = []byte(namespaceName)
|
||||
configKey = []byte(configName)
|
||||
stateKey = []byte(stateName)
|
||||
dependenciesBkt = []byte(dependenciesName)
|
||||
volDependenciesBkt = []byte(volCtrDependencies)
|
||||
netNSKey = []byte(netNSName)
|
||||
containersBkt = []byte(containersName)
|
||||
podIDKey = []byte(podIDName)
|
||||
namespaceKey = []byte(namespaceName)
|
||||
|
||||
staticDirKey = []byte(staticDirName)
|
||||
tmpDirKey = []byte(tmpDirName)
|
||||
@ -234,6 +240,22 @@ func getAllPodsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
return bkt, nil
|
||||
}
|
||||
|
||||
func getVolBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
bkt := tx.Bucket(volBkt)
|
||||
if bkt == nil {
|
||||
return nil, errors.Wrapf(ErrDBBadConfig, "volumes bucket not found in DB")
|
||||
}
|
||||
return bkt, nil
|
||||
}
|
||||
|
||||
func getAllVolsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
bkt := tx.Bucket(allVolsBkt)
|
||||
if bkt == nil {
|
||||
return nil, errors.Wrapf(ErrDBBadConfig, "all volumes bucket not found in DB")
|
||||
}
|
||||
return bkt, nil
|
||||
}
|
||||
|
||||
func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
bkt := tx.Bucket(runtimeConfigBkt)
|
||||
if bkt == nil {
|
||||
@ -315,6 +337,35 @@ func (s *BoltState) getPodFromDB(id []byte, pod *Pod, podBkt *bolt.Bucket) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bucket) error {
|
||||
volDB := volBkt.Bucket(name)
|
||||
if volDB == nil {
|
||||
return errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found", string(name))
|
||||
}
|
||||
|
||||
volConfigBytes := volDB.Get(configKey)
|
||||
if volConfigBytes == nil {
|
||||
return errors.Wrapf(ErrInternal, "volume %s is missing configuration key in DB", string(name))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(volConfigBytes, volume.config); err != nil {
|
||||
return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
|
||||
}
|
||||
|
||||
// Get the lock
|
||||
lockPath := filepath.Join(s.runtime.lockDir, string(name))
|
||||
lock, err := storage.GetLockfile(lockPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving lockfile for volume %s", string(name))
|
||||
}
|
||||
volume.lock = lock
|
||||
|
||||
volume.runtime = s.runtime
|
||||
volume.valid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a container to the DB
|
||||
// If pod is not nil, the container is added to the pod as well
|
||||
func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
|
||||
@ -376,6 +427,11 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
|
||||
return err
|
||||
}
|
||||
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a pod was given, check if it exists
|
||||
var podDB *bolt.Bucket
|
||||
var podCtrs *bolt.Bucket
|
||||
@ -508,6 +564,25 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Add container to volume dependencies bucket if container is using a named volume
|
||||
for _, vol := range ctr.config.Spec.Mounts {
|
||||
if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
|
||||
volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
|
||||
|
||||
volDB := volBkt.Bucket([]byte(volName))
|
||||
if volDB == nil {
|
||||
return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volName)
|
||||
}
|
||||
|
||||
ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
|
||||
if depExists := ctrDepsBkt.Get(ctrID); depExists == nil {
|
||||
if err := ctrDepsBkt.Put(ctrID, ctrID); err != nil {
|
||||
return errors.Wrapf(err, "error storing container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
@ -545,6 +620,11 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
|
||||
return err
|
||||
}
|
||||
|
||||
volBkt, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Does the pod exist?
|
||||
var podDB *bolt.Bucket
|
||||
if pod != nil {
|
||||
@ -663,5 +743,25 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
|
||||
}
|
||||
}
|
||||
|
||||
// Remove container from volume dependencies bucket if container is using a named volume
|
||||
for _, vol := range ctr.config.Spec.Mounts {
|
||||
if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
|
||||
volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
|
||||
|
||||
volDB := volBkt.Bucket([]byte(volName))
|
||||
if volDB == nil {
|
||||
// Let's assume the volume was already deleted and continue to remove the container
|
||||
continue
|
||||
}
|
||||
|
||||
ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
|
||||
if depExists := ctrDepsBkt.Get(ctrID); depExists != nil {
|
||||
if err := ctrDepsBkt.Delete(ctrID); err != nil {
|
||||
return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -74,6 +74,11 @@ func getTestContainer(id, name, locksDir string) (*Container, error) {
|
||||
"/test/file.test": "/test2/file2.test",
|
||||
},
|
||||
},
|
||||
runtime: &Runtime{
|
||||
config: &RuntimeConfig{
|
||||
VolumePath: "/does/not/exist/tmp/volumes",
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
package libpod
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/containers/libpod/pkg/inspect"
|
||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -48,6 +51,17 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
|
||||
hostnamePath = getPath
|
||||
}
|
||||
|
||||
var mounts []specs.Mount
|
||||
for i, mnt := range spec.Mounts {
|
||||
mounts = append(mounts, mnt)
|
||||
// We only want to show the name of the named volume in the inspect
|
||||
// output, so split the path and get the name out of it.
|
||||
if strings.Contains(mnt.Source, c.runtime.config.VolumePath) {
|
||||
split := strings.Split(mnt.Source[len(c.runtime.config.VolumePath)+1:], "/")
|
||||
mounts[i].Source = split[0]
|
||||
}
|
||||
}
|
||||
|
||||
data := &inspect.ContainerInspectData{
|
||||
ID: config.ID,
|
||||
Created: config.CreatedTime,
|
||||
@ -85,7 +99,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
|
||||
AppArmorProfile: spec.Process.ApparmorProfile,
|
||||
ExecIDs: execIDs,
|
||||
GraphDriver: driverData,
|
||||
Mounts: spec.Mounts,
|
||||
Mounts: mounts,
|
||||
Dependencies: c.Dependencies(),
|
||||
NetworkSettings: &inspect.NetworkSettings{
|
||||
Bridge: "", // TODO
|
||||
|
@ -11,18 +11,24 @@ var (
|
||||
ErrNoSuchPod = errors.New("no such pod")
|
||||
// ErrNoSuchImage indicates the requested image does not exist
|
||||
ErrNoSuchImage = errors.New("no such image")
|
||||
// ErrNoSuchVolume indicates the requested volume does not exist
|
||||
ErrNoSuchVolume = errors.New("no such volume")
|
||||
|
||||
// ErrCtrExists indicates a container with the same name or ID already
|
||||
// exists
|
||||
ErrCtrExists = errors.New("container already exists")
|
||||
// ErrPodExists indicates a pod with the same name or ID already exists
|
||||
ErrPodExists = errors.New("pod already exists")
|
||||
// ErrImageExists indicated an image with the same ID already exists
|
||||
// ErrImageExists indicates an image with the same ID already exists
|
||||
ErrImageExists = errors.New("image already exists")
|
||||
// ErrVolumeExists indicates a volume with the same name already exists
|
||||
ErrVolumeExists = errors.New("volume already exists")
|
||||
|
||||
// ErrCtrStateInvalid indicates a container is in an improper state for
|
||||
// the requested operation
|
||||
ErrCtrStateInvalid = errors.New("container state improper")
|
||||
// ErrVolumeBeingUsed indicates that a volume is being used by at least one container
|
||||
ErrVolumeBeingUsed = errors.New("volume is being used")
|
||||
|
||||
// ErrRuntimeFinalized indicates that the runtime has already been
|
||||
// created and cannot be modified
|
||||
@ -33,6 +39,9 @@ var (
|
||||
// ErrPodFinalized indicates that the pod has already been created and
|
||||
// cannot be modified
|
||||
ErrPodFinalized = errors.New("pod has been finalized")
|
||||
// ErrVolumeFinalized indicates that the volume has already been created and
|
||||
// cannot be modified
|
||||
ErrVolumeFinalized = errors.New("volume has been finalized")
|
||||
|
||||
// ErrInvalidArg indicates that an invalid argument was passed
|
||||
ErrInvalidArg = errors.New("invalid argument")
|
||||
@ -55,6 +64,9 @@ var (
|
||||
// ErrPodRemoved indicates that the pod has already been removed and no
|
||||
// further operations can be performed on it
|
||||
ErrPodRemoved = errors.New("pod has already been removed")
|
||||
// ErrVolumeRemoved indicates that the volume has already been removed and
|
||||
// no further operations can be performed on it
|
||||
ErrVolumeRemoved = errors.New("volume has already been removed")
|
||||
|
||||
// ErrDBClosed indicates that the connection to the state database has
|
||||
// already been closed
|
||||
|
@ -18,8 +18,10 @@ type InMemoryState struct {
|
||||
pods map[string]*Pod
|
||||
// Maps container ID to container struct.
|
||||
containers map[string]*Container
|
||||
volumes map[string]*Volume
|
||||
// Maps container ID to a list of IDs of dependencies.
|
||||
ctrDepends map[string][]string
|
||||
ctrDepends map[string][]string
|
||||
volumeDepends map[string][]string
|
||||
// Maps pod ID to a map of container ID to container struct.
|
||||
podContainers map[string]map[string]*Container
|
||||
// Global name registry - ensures name uniqueness and performs lookups.
|
||||
@ -46,8 +48,10 @@ func NewInMemoryState() (State, error) {
|
||||
|
||||
state.pods = make(map[string]*Pod)
|
||||
state.containers = make(map[string]*Container)
|
||||
state.volumes = make(map[string]*Volume)
|
||||
|
||||
state.ctrDepends = make(map[string][]string)
|
||||
state.volumeDepends = make(map[string][]string)
|
||||
|
||||
state.podContainers = make(map[string]map[string]*Container)
|
||||
|
||||
@ -244,6 +248,14 @@ func (s *InMemoryState) AddContainer(ctr *Container) error {
|
||||
s.addCtrToDependsMap(ctr.ID(), depCtr)
|
||||
}
|
||||
|
||||
// Add container to volume dependencies
|
||||
for _, vol := range ctr.config.Spec.Mounts {
|
||||
if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
|
||||
volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
|
||||
s.addCtrToVolDependsMap(ctr.ID(), volName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -294,6 +306,14 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error {
|
||||
s.removeCtrFromDependsMap(ctr.ID(), depCtr)
|
||||
}
|
||||
|
||||
// Remove container from volume dependencies
|
||||
for _, vol := range ctr.config.Spec.Mounts {
|
||||
if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
|
||||
volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
|
||||
s.removeCtrFromVolDependsMap(ctr.ID(), volName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -358,6 +378,114 @@ func (s *InMemoryState) ContainerInUse(ctr *Container) ([]string, error) {
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
// Volume retrieves a volume from its full name
|
||||
func (s *InMemoryState) Volume(name string) (*Volume, error) {
|
||||
if name == "" {
|
||||
return nil, ErrEmptyID
|
||||
}
|
||||
|
||||
vol, ok := s.volumes[name]
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(ErrNoSuchCtr, "no volume with name %s found", name)
|
||||
}
|
||||
|
||||
return vol, nil
|
||||
}
|
||||
|
||||
// HasVolume checks if a volume with the given name is present in the state
|
||||
func (s *InMemoryState) HasVolume(name string) (bool, error) {
|
||||
if name == "" {
|
||||
return false, ErrEmptyID
|
||||
}
|
||||
|
||||
_, ok := s.volumes[name]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AddVolume adds a volume to the state
|
||||
func (s *InMemoryState) AddVolume(volume *Volume) error {
|
||||
if !volume.valid {
|
||||
return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name())
|
||||
}
|
||||
|
||||
if _, ok := s.volumes[volume.Name()]; ok {
|
||||
return errors.Wrapf(ErrVolumeExists, "volume with name %s already exists in state", volume.Name())
|
||||
}
|
||||
|
||||
s.volumes[volume.Name()] = volume
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveVolume removes a volume from the state
|
||||
func (s *InMemoryState) RemoveVolume(volume *Volume) error {
|
||||
// Ensure we don't remove a volume which containers depend on
|
||||
deps, ok := s.volumeDepends[volume.Name()]
|
||||
if ok && len(deps) != 0 {
|
||||
depsStr := strings.Join(deps, ", ")
|
||||
return errors.Wrapf(ErrVolumeExists, "the following containers depend on volume %s: %s", volume.Name(), depsStr)
|
||||
}
|
||||
|
||||
if _, ok := s.volumes[volume.Name()]; !ok {
|
||||
volume.valid = false
|
||||
return errors.Wrapf(ErrVolumeRemoved, "no volume exists in state with name %s", volume.Name())
|
||||
}
|
||||
|
||||
delete(s.volumes, volume.Name())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveVolCtrDep updates the container dependencies of the volume
|
||||
func (s *InMemoryState) RemoveVolCtrDep(volume *Volume, ctrID string) error {
|
||||
if !volume.valid {
|
||||
return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name())
|
||||
}
|
||||
|
||||
if _, ok := s.volumes[volume.Name()]; !ok {
|
||||
return errors.Wrapf(ErrNoSuchVolume, "volume with name %s doesn't exists in state", volume.Name())
|
||||
}
|
||||
|
||||
// Remove container that is using this volume
|
||||
s.removeCtrFromVolDependsMap(ctrID, volume.Name())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VolumeInUse checks if the given volume is being used by at least one container
|
||||
func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) {
|
||||
if !volume.valid {
|
||||
return nil, ErrVolumeRemoved
|
||||
}
|
||||
|
||||
// If the volume does not exist, return error
|
||||
if _, ok := s.volumes[volume.Name()]; !ok {
|
||||
volume.valid = false
|
||||
return nil, errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found in state", volume.Name())
|
||||
}
|
||||
|
||||
arr, ok := s.volumeDepends[volume.Name()]
|
||||
if !ok {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
// AllVolumes returns all volumes that exist in the state
|
||||
func (s *InMemoryState) AllVolumes() ([]*Volume, error) {
|
||||
allVols := make([]*Volume, 0, len(s.volumes))
|
||||
for _, v := range s.volumes {
|
||||
allVols = append(allVols, v)
|
||||
}
|
||||
|
||||
return allVols, nil
|
||||
}
|
||||
|
||||
// AllContainers retrieves all containers from the state
|
||||
func (s *InMemoryState) AllContainers() ([]*Container, error) {
|
||||
ctrs := make([]*Container, 0, len(s.containers))
|
||||
@ -945,6 +1073,44 @@ func (s *InMemoryState) removeCtrFromDependsMap(ctrID, dependsID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add a container to the dependency mappings for the volume
|
||||
func (s *InMemoryState) addCtrToVolDependsMap(depCtrID, volName string) {
|
||||
if volName != "" {
|
||||
arr, ok := s.volumeDepends[volName]
|
||||
if !ok {
|
||||
// Do not have a mapping for that volume yet
|
||||
s.volumeDepends[volName] = []string{depCtrID}
|
||||
} else {
|
||||
// Have a mapping for the volume
|
||||
arr = append(arr, depCtrID)
|
||||
s.volumeDepends[volName] = arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a container from the dependency mappings for the volume
|
||||
func (s *InMemoryState) removeCtrFromVolDependsMap(depCtrID, volName string) {
|
||||
if volName != "" {
|
||||
arr, ok := s.volumeDepends[volName]
|
||||
if !ok {
|
||||
// Internal state seems inconsistent
|
||||
// But the dependency is definitely gone
|
||||
// So just return
|
||||
return
|
||||
}
|
||||
|
||||
newArr := make([]string, 0, len(arr))
|
||||
|
||||
for _, id := range arr {
|
||||
if id != depCtrID {
|
||||
newArr = append(newArr, id)
|
||||
}
|
||||
}
|
||||
|
||||
s.volumeDepends[volName] = newArr
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can access a pod or container, or if that is blocked by
|
||||
// namespaces.
|
||||
func (s *InMemoryState) checkNSMatch(id, ns string) error {
|
||||
|
@ -327,6 +327,22 @@ func WithNamespace(ns string) RuntimeOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithVolumePath sets the path under which all named volumes
|
||||
// should be created.
|
||||
// The path changes based on whethe rthe user is running as root
|
||||
// or not.
|
||||
func WithVolumePath(volPath string) RuntimeOption {
|
||||
return func(rt *Runtime) error {
|
||||
if rt.valid {
|
||||
return ErrRuntimeFinalized
|
||||
}
|
||||
|
||||
rt.config.VolumePath = volPath
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultInfraImage sets the infra image for libpod.
|
||||
// An infra image is used for inter-container kernel
|
||||
// namespace sharing within a pod. Typically, an infra
|
||||
@ -1125,6 +1141,70 @@ func withIsInfra() CtrCreateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Volume Creation Options
|
||||
|
||||
// WithVolumeName sets the name of the volume.
|
||||
func WithVolumeName(name string) VolumeCreateOption {
|
||||
return func(volume *Volume) error {
|
||||
if volume.valid {
|
||||
return ErrVolumeFinalized
|
||||
}
|
||||
|
||||
// Check the name against a regex
|
||||
if !nameRegex.MatchString(name) {
|
||||
return errors.Wrapf(ErrInvalidArg, "name must match regex [a-zA-Z0-9_-]+")
|
||||
}
|
||||
volume.config.Name = name
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithVolumeLabels sets the labels of the volume.
|
||||
func WithVolumeLabels(labels map[string]string) VolumeCreateOption {
|
||||
return func(volume *Volume) error {
|
||||
if volume.valid {
|
||||
return ErrVolumeFinalized
|
||||
}
|
||||
|
||||
volume.config.Labels = make(map[string]string)
|
||||
for key, value := range labels {
|
||||
volume.config.Labels[key] = value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithVolumeDriver sets the driver of the volume.
|
||||
func WithVolumeDriver(driver string) VolumeCreateOption {
|
||||
return func(volume *Volume) error {
|
||||
if volume.valid {
|
||||
return ErrVolumeFinalized
|
||||
}
|
||||
|
||||
volume.config.Driver = driver
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithVolumeOptions sets the options of the volume.
|
||||
func WithVolumeOptions(options map[string]string) VolumeCreateOption {
|
||||
return func(volume *Volume) error {
|
||||
if volume.valid {
|
||||
return ErrVolumeFinalized
|
||||
}
|
||||
|
||||
volume.config.Options = make(map[string]string)
|
||||
for key, value := range options {
|
||||
volume.config.Options[key] = value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Pod Creation Options
|
||||
|
||||
// WithPodName sets the name of the pod.
|
||||
|
@ -92,6 +92,7 @@ type RuntimeConfig struct {
|
||||
// Not included in on-disk config, use the dedicated containers/storage
|
||||
// configuration file instead
|
||||
StorageConfig storage.StoreOptions `toml:"-"`
|
||||
VolumePath string `toml:"volume_path"`
|
||||
// ImageDefaultTransport is the default transport method used to fetch
|
||||
// images
|
||||
ImageDefaultTransport string `toml:"image_default_transport"`
|
||||
@ -278,12 +279,13 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
|
||||
|
||||
if rootless.IsRootless() {
|
||||
// If we're rootless, override the default storage config
|
||||
storageConf, err := util.GetDefaultStoreOptions()
|
||||
storageConf, volumePath, err := util.GetDefaultStoreOptions()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error retrieving rootless storage config")
|
||||
}
|
||||
runtime.config.StorageConfig = storageConf
|
||||
runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod")
|
||||
runtime.config.VolumePath = volumePath
|
||||
}
|
||||
|
||||
configPath := ConfigPath
|
||||
|
@ -154,6 +154,24 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
|
||||
}
|
||||
}()
|
||||
|
||||
// Go through the volume mounts and check for named volumes
|
||||
// If the named volme already exists continue, otherwise create
|
||||
// the storage for the named volume.
|
||||
for i, vol := range ctr.config.Spec.Mounts {
|
||||
if vol.Source[0] != '/' && isNamedVolume(vol.Source) {
|
||||
volInfo, err := r.state.Volume(vol.Source)
|
||||
if err != nil {
|
||||
newVol, err := r.newVolume(ctx, WithVolumeName(vol.Source))
|
||||
if err != nil {
|
||||
logrus.Errorf("error creating named volume %q: %v", vol.Source, err)
|
||||
}
|
||||
ctr.config.Spec.Mounts[i].Source = newVol.MountPoint()
|
||||
continue
|
||||
}
|
||||
ctr.config.Spec.Mounts[i].Source = volInfo.MountPoint()
|
||||
}
|
||||
}
|
||||
|
||||
if ctr.config.LogPath == "" {
|
||||
ctr.config.LogPath = filepath.Join(ctr.config.StaticDir, "ctr.log")
|
||||
}
|
||||
@ -170,6 +188,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
|
||||
}
|
||||
ctr.config.Mounts = append(ctr.config.Mounts, ctr.config.ShmDir)
|
||||
}
|
||||
|
||||
// Add the container to the state
|
||||
// TODO: May be worth looking into recovering from name/ID collisions here
|
||||
if ctr.config.Pod != "" {
|
||||
@ -474,3 +493,11 @@ func (r *Runtime) GetLatestContainer() (*Container, error) {
|
||||
}
|
||||
return ctrs[lastCreatedIndex], nil
|
||||
}
|
||||
|
||||
// Check if volName is a named volume and not one of the default mounts we add to containers
|
||||
func isNamedVolume(volName string) bool {
|
||||
if volName != "proc" && volName != "tmpfs" && volName != "devpts" && volName != "shm" && volName != "mqueue" && volName != "sysfs" && volName != "cgroup" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
107
libpod/runtime_volume.go
Normal file
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
|
||||
// returned.
|
||||
AllPods() ([]*Pod, error)
|
||||
|
||||
// Volume accepts full name of volume
|
||||
// If the volume doesn't exist, an error will be returned
|
||||
Volume(volName string) (*Volume, error)
|
||||
// HasVolume returns true if volName exists in the state,
|
||||
// otherwise it returns false
|
||||
HasVolume(volName string) (bool, error)
|
||||
// VolumeInUse goes through the container dependencies of a volume
|
||||
// and checks if the volume is being used by any container. If it is
|
||||
// a slice of container IDs using the volume is returned
|
||||
VolumeInUse(volume *Volume) ([]string, error)
|
||||
// AddVolume adds the specified volume to state. The volume's name
|
||||
// must be unique within the list of existing volumes
|
||||
AddVolume(volume *Volume) error
|
||||
// RemoveVolCtrDep updates the list of container dependencies that the
|
||||
// volume has. It either deletes the dependent container ID from
|
||||
// the sub-bucket
|
||||
RemoveVolCtrDep(volume *Volume, ctrID string) error
|
||||
// RemoveVolume removes the specified volume.
|
||||
// Only volumes that have no container dependencies can be removed
|
||||
RemoveVolume(volume *Volume) error
|
||||
// AllVolumes returns all the volumes available in the state
|
||||
AllVolumes() ([]*Volume, error)
|
||||
}
|
||||
|
63
libpod/volume.go
Normal file
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
|
||||
}
|
||||
|
||||
// GetRootlessStorageOpts returns the storage ops for containers running as non root
|
||||
func GetRootlessStorageOpts() (storage.StoreOptions, error) {
|
||||
var opts storage.StoreOptions
|
||||
|
||||
// GetRootlessDirInfo returns the parent path of where the storage for containers and
|
||||
// volumes will be in rootless mode
|
||||
func GetRootlessDirInfo() (string, string, error) {
|
||||
rootlessRuntime, err := GetRootlessRuntimeDir()
|
||||
if err != nil {
|
||||
return opts, err
|
||||
return "", "", err
|
||||
}
|
||||
opts.RunRoot = rootlessRuntime
|
||||
|
||||
dataDir := os.Getenv("XDG_DATA_HOME")
|
||||
if dataDir == "" {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
return opts, fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty")
|
||||
return "", "", fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty")
|
||||
}
|
||||
// runc doesn't like symlinks in the rootfs path, and at least
|
||||
// on CoreOS /home is a symlink to /var/home, so resolve any symlink.
|
||||
resolvedHome, err := filepath.EvalSymlinks(home)
|
||||
if err != nil {
|
||||
return opts, errors.Wrapf(err, "cannot resolve %s", home)
|
||||
return "", "", errors.Wrapf(err, "cannot resolve %s", home)
|
||||
}
|
||||
dataDir = filepath.Join(resolvedHome, ".local", "share")
|
||||
}
|
||||
return dataDir, rootlessRuntime, nil
|
||||
}
|
||||
|
||||
// GetRootlessStorageOpts returns the storage opts for containers running as non root
|
||||
func GetRootlessStorageOpts() (storage.StoreOptions, error) {
|
||||
var opts storage.StoreOptions
|
||||
|
||||
dataDir, rootlessRuntime, err := GetRootlessDirInfo()
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
opts.RunRoot = rootlessRuntime
|
||||
opts.GraphRoot = filepath.Join(dataDir, "containers", "storage")
|
||||
if path, err := exec.LookPath("fuse-overlayfs"); err == nil {
|
||||
opts.GraphDriverName = "overlay"
|
||||
@ -284,6 +294,15 @@ func GetRootlessStorageOpts() (storage.StoreOptions, error) {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// GetRootlessVolumeInfo returns where all the name volumes will be created in rootless mode
|
||||
func GetRootlessVolumeInfo() (string, error) {
|
||||
dataDir, _, err := GetRootlessDirInfo()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dataDir, "containers", "storage", "volumes"), nil
|
||||
}
|
||||
|
||||
type tomlOptionsConfig struct {
|
||||
MountProgram string `toml:"mount_program"`
|
||||
}
|
||||
@ -313,14 +332,21 @@ func getTomlStorage(storeOptions *storage.StoreOptions) *tomlConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
// GetDefaultStoreOptions returns the default storage options for containers.
|
||||
func GetDefaultStoreOptions() (storage.StoreOptions, error) {
|
||||
// GetDefaultStoreOptions returns the storage ops for containers and the volume path
|
||||
// for the volume API
|
||||
// It also returns the path where all named volumes will be created using the volume API
|
||||
func GetDefaultStoreOptions() (storage.StoreOptions, string, error) {
|
||||
storageOpts := storage.DefaultStoreOptions
|
||||
volumePath := "/var/lib/containers/storage"
|
||||
if rootless.IsRootless() {
|
||||
var err error
|
||||
storageOpts, err = GetRootlessStorageOpts()
|
||||
if err != nil {
|
||||
return storageOpts, err
|
||||
return storageOpts, volumePath, err
|
||||
}
|
||||
volumePath, err = GetRootlessVolumeInfo()
|
||||
if err != nil {
|
||||
return storageOpts, volumePath, err
|
||||
}
|
||||
|
||||
storageConf := filepath.Join(os.Getenv("HOME"), ".config/containers/storage.conf")
|
||||
@ -330,7 +356,7 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) {
|
||||
os.MkdirAll(filepath.Dir(storageConf), 0755)
|
||||
file, err := os.OpenFile(storageConf, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
if err != nil {
|
||||
return storageOpts, errors.Wrapf(err, "cannot open %s", storageConf)
|
||||
return storageOpts, volumePath, errors.Wrapf(err, "cannot open %s", storageConf)
|
||||
}
|
||||
|
||||
tomlConfiguration := getTomlStorage(&storageOpts)
|
||||
@ -341,5 +367,5 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return storageOpts, nil
|
||||
return storageOpts, volumePath, nil
|
||||
}
|
||||
|
@ -227,6 +227,17 @@ func (p *PodmanTestIntegration) CleanupPod() {
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupVolume cleans up the temporary store
|
||||
func (p *PodmanTestIntegration) CleanupVolume() {
|
||||
// Remove all containers
|
||||
session := p.Podman([]string{"volume", "rm", "-fa"})
|
||||
session.Wait(90)
|
||||
// Nuke tempdir
|
||||
if err := os.RemoveAll(p.TempDir); err != nil {
|
||||
fmt.Printf("%q\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// PullImages pulls multiple images
|
||||
func (p *PodmanTestIntegration) PullImages(images []string) error {
|
||||
for _, i := range images {
|
||||
|
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