diff options
64 files changed, 3456 insertions, 62 deletions
diff --git a/cmd/podman/container.go b/cmd/podman/container.go index b6262f890..b0232c874 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -22,7 +22,7 @@ var ( mountCommand, pauseCommand, portCommand, - // pruneCommand, + pruneContainersCommand, refreshCommand, restartCommand, restoreCommand, diff --git a/cmd/podman/containers_prune.go b/cmd/podman/containers_prune.go new file mode 100644 index 000000000..92604e82f --- /dev/null +++ b/cmd/podman/containers_prune.go @@ -0,0 +1,74 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + pruneContainersDescription = ` + podman container prune + + Removes all exited containers +` + + pruneContainersCommand = cli.Command{ + Name: "prune", + Usage: "Remove all stopped containers", + Description: pruneContainersDescription, + Action: pruneContainersCmd, + OnUsageError: usageErrorHandler, + } +) + +func pruneContainersCmd(c *cli.Context) error { + var ( + deleteFuncs []shared.ParallelWorkerInput + ) + + ctx := getContext() + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + filter := func(c *libpod.Container) bool { + state, _ := c.State() + if state == libpod.ContainerStateStopped || (state == libpod.ContainerStateExited && err == nil && c.PodID() == "") { + return true + } + return false + } + delContainers, err := runtime.GetContainers(filter) + if err != nil { + return err + } + if len(delContainers) < 1 { + return nil + } + for _, container := range delContainers { + con := container + f := func() error { + return runtime.RemoveContainer(ctx, con, c.Bool("force")) + } + + deleteFuncs = append(deleteFuncs, shared.ParallelWorkerInput{ + ContainerID: con.ID(), + ParallelFunc: f, + }) + } + maxWorkers := shared.Parallelize("rm") + if c.GlobalIsSet("max-workers") { + maxWorkers = c.GlobalInt("max-workers") + } + logrus.Debugf("Setting maximum workers to %d", maxWorkers) + + // Run the parallel funcs + deleteErrors, errCount := shared.ParallelExecuteWorkerPool(maxWorkers, deleteFuncs) + return printParallelOutput(deleteErrors, errCount) +} diff --git a/cmd/podman/create_cli.go b/cmd/podman/create_cli.go index 218e9b806..b3a30d185 100644 --- a/cmd/podman/create_cli.go +++ b/cmd/podman/create_cli.go @@ -201,12 +201,13 @@ func parseVolumesFrom(volumesFrom []string) error { } func validateVolumeHostDir(hostDir string) error { - if !filepath.IsAbs(hostDir) { - return errors.Errorf("invalid host path, must be an absolute path %q", hostDir) - } - if _, err := os.Stat(hostDir); err != nil { - return errors.Wrapf(err, "error checking path %q", hostDir) + if filepath.IsAbs(hostDir) { + if _, err := os.Stat(hostDir); err != nil { + return errors.Wrapf(err, "error checking path %q", hostDir) + } } + // If hostDir is not an absolute path, that means the user wants to create a + // named volume. This will be done later on in the code. return nil } diff --git a/cmd/podman/image.go b/cmd/podman/image.go index 418b442e3..95af36df5 100644 --- a/cmd/podman/image.go +++ b/cmd/podman/image.go @@ -13,7 +13,7 @@ var ( inspectCommand, loadCommand, lsImagesCommand, - // pruneCommand, + pruneImagesCommand, pullCommand, pushCommand, rmImageCommand, diff --git a/cmd/podman/images_prune.go b/cmd/podman/images_prune.go new file mode 100644 index 000000000..cb72a498f --- /dev/null +++ b/cmd/podman/images_prune.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + pruneImagesDescription = ` + podman image prune + + Removes all unnamed images from local storage +` + + pruneImagesCommand = cli.Command{ + Name: "prune", + Usage: "Remove unused images", + Description: pruneImagesDescription, + Action: pruneImagesCmd, + OnUsageError: usageErrorHandler, + } +) + +func pruneImagesCmd(c *cli.Context) error { + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + return shared.Prune(runtime.ImageRuntime()) +} diff --git a/cmd/podman/libpodruntime/runtime.go b/cmd/podman/libpodruntime/runtime.go index 0dc6bcf18..d7a0dd931 100644 --- a/cmd/podman/libpodruntime/runtime.go +++ b/cmd/podman/libpodruntime/runtime.go @@ -14,6 +14,11 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) { storageOpts := new(storage.StoreOptions) options := []libpod.RuntimeOption{} + _, volumePath, err := util.GetDefaultStoreOptions() + if err != nil { + return nil, err + } + if c.IsSet("uidmap") || c.IsSet("gidmap") || c.IsSet("subuidmap") || c.IsSet("subgidmap") { mappings, err := util.ParseIDMapping(c.StringSlice("uidmap"), c.StringSlice("gidmap"), c.String("subuidmap"), c.String("subgidmap")) if err != nil { @@ -90,6 +95,7 @@ func GetRuntime(c *cli.Context) (*libpod.Runtime, error) { if c.IsSet("infra-command") { options = append(options, libpod.WithDefaultInfraCommand(c.String("infra-command"))) } + options = append(options, libpod.WithVolumePath(volumePath)) if c.IsSet("config") { return libpod.NewRuntimeFromConfig(c.String("config"), options...) } diff --git a/cmd/podman/main.go b/cmd/podman/main.go index bcae04575..280448dc8 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log/syslog" "os" "os/exec" "runtime/pprof" @@ -16,7 +17,6 @@ import ( "github.com/sirupsen/logrus" lsyslog "github.com/sirupsen/logrus/hooks/syslog" "github.com/urfave/cli" - "log/syslog" ) // This is populated by the Makefile from the VERSION file @@ -102,6 +102,7 @@ func main() { umountCommand, unpauseCommand, versionCommand, + volumeCommand, waitCommand, } diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index 0b03388a2..7a4a80769 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -200,6 +200,10 @@ var ( Usage: "Sort output by command, created, id, image, names, runningfor, size, or status", Value: "created", }, + cli.BoolFlag{ + Name: "sync", + Usage: "Sync container state with OCI runtime", + }, } psDescription = "Prints out information about the containers" psCommand = cli.Command{ @@ -260,6 +264,7 @@ func psCmd(c *cli.Context) error { Size: c.Bool("size"), Namespace: c.Bool("namespace"), Sort: c.String("sort"), + Sync: c.Bool("sync"), } filters := c.StringSlice("filter") diff --git a/cmd/podman/shared/container.go b/cmd/podman/shared/container.go index d0e892961..90ce193f7 100644 --- a/cmd/podman/shared/container.go +++ b/cmd/podman/shared/container.go @@ -45,6 +45,7 @@ type PsOptions struct { Sort string Label string Namespace bool + Sync bool } // BatchContainerStruct is the return obkect from BatchContainer and contains @@ -126,6 +127,12 @@ func NewBatchContainer(ctr *libpod.Container, opts PsOptions) (PsContainerOutput pso PsContainerOutput ) batchErr := ctr.Batch(func(c *libpod.Container) error { + if opts.Sync { + if err := c.Sync(); err != nil { + return err + } + } + conState, err = c.State() if err != nil { return errors.Wrapf(err, "unable to obtain container state") diff --git a/cmd/podman/shared/prune.go b/cmd/podman/shared/prune.go new file mode 100644 index 000000000..90cfe4475 --- /dev/null +++ b/cmd/podman/shared/prune.go @@ -0,0 +1,24 @@ +package shared + +import ( + "fmt" + "github.com/pkg/errors" + + "github.com/containers/libpod/libpod/image" +) + +// Prune removes all unnamed and unused images from the local store +func Prune(ir *image.Runtime) error { + pruneImages, err := ir.GetPruneImages() + if err != nil { + return err + } + + for _, i := range pruneImages { + if err := i.Remove(true); err != nil { + return errors.Wrapf(err, "failed to remove %s", i.ID()) + } + fmt.Println(i.ID()) + } + return nil +} diff --git a/cmd/podman/utils.go b/cmd/podman/utils.go index 5735156c2..a59535b43 100644 --- a/cmd/podman/utils.go +++ b/cmd/podman/utils.go @@ -3,6 +3,9 @@ package main import ( "context" "fmt" + "os" + gosignal "os/signal" + "github.com/containers/libpod/libpod" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/term" @@ -11,8 +14,6 @@ import ( "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" "k8s.io/client-go/tools/remotecommand" - "os" - gosignal "os/signal" ) type RawTtyFormatter struct { @@ -208,6 +209,35 @@ func getPodsFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Pod, error return pods, lastError } +func getVolumesFromContext(c *cli.Context, r *libpod.Runtime) ([]*libpod.Volume, error) { + args := c.Args() + var ( + vols []*libpod.Volume + lastError error + err error + ) + + if c.Bool("all") { + vols, err = r.Volumes() + if err != nil { + return nil, errors.Wrapf(err, "unable to get all volumes") + } + } + + for _, i := range args { + vol, err := r.GetVolume(i) + if err != nil { + if lastError != nil { + logrus.Errorf("%q", lastError) + } + lastError = errors.Wrapf(err, "unable to find volume %s", i) + continue + } + vols = append(vols, vol) + } + return vols, lastError +} + //printParallelOutput takes the map of parallel worker results and outputs them // to stdout func printParallelOutput(m map[string]error, errCount int) error { diff --git a/cmd/podman/volume.go b/cmd/podman/volume.go new file mode 100644 index 000000000..913592e74 --- /dev/null +++ b/cmd/podman/volume.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/urfave/cli" +) + +var ( + volumeDescription = `Manage volumes. + +Volumes are created in and can be shared between containers.` + + volumeSubCommands = []cli.Command{ + volumeCreateCommand, + volumeLsCommand, + volumeRmCommand, + volumeInspectCommand, + volumePruneCommand, + } + volumeCommand = cli.Command{ + Name: "volume", + Usage: "Manage volumes", + Description: volumeDescription, + UseShortOptionHandling: true, + Subcommands: volumeSubCommands, + } +) diff --git a/cmd/podman/volume_create.go b/cmd/podman/volume_create.go new file mode 100644 index 000000000..0b5f8d1e3 --- /dev/null +++ b/cmd/podman/volume_create.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var volumeCreateDescription = ` +podman volume create + +Creates a new volume. If using the default driver, "local", the volume will +be created at.` + +var volumeCreateFlags = []cli.Flag{ + cli.StringFlag{ + Name: "driver", + Usage: "Specify volume driver name (default local)", + }, + cli.StringSliceFlag{ + Name: "label, l", + Usage: "Set metadata for a volume (default [])", + }, + cli.StringSliceFlag{ + Name: "opt, o", + Usage: "Set driver specific options (default [])", + }, +} + +var volumeCreateCommand = cli.Command{ + Name: "create", + Usage: "Create a new volume", + Description: volumeCreateDescription, + Flags: volumeCreateFlags, + Action: volumeCreateCmd, + SkipArgReorder: true, + ArgsUsage: "[VOLUME-NAME]", + UseShortOptionHandling: true, +} + +func volumeCreateCmd(c *cli.Context) error { + var ( + options []libpod.VolumeCreateOption + err error + volName string + ) + + if err = validateFlags(c, volumeCreateFlags); err != nil { + return err + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + if len(c.Args()) > 1 { + return errors.Errorf("too many arguments, create takes at most 1 argument") + } + + if len(c.Args()) > 0 { + volName = c.Args()[0] + options = append(options, libpod.WithVolumeName(volName)) + } + + if c.IsSet("driver") { + options = append(options, libpod.WithVolumeDriver(c.String("driver"))) + } + + labels, err := getAllLabels([]string{}, c.StringSlice("label")) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + if len(labels) != 0 { + options = append(options, libpod.WithVolumeLabels(labels)) + } + + opts, err := getAllLabels([]string{}, c.StringSlice("opt")) + if err != nil { + return errors.Wrapf(err, "unable to process options") + } + if len(options) != 0 { + options = append(options, libpod.WithVolumeOptions(opts)) + } + + vol, err := runtime.NewVolume(getContext(), options...) + if err != nil { + return err + } + fmt.Printf("%s\n", vol.Name()) + + return nil +} diff --git a/cmd/podman/volume_inspect.go b/cmd/podman/volume_inspect.go new file mode 100644 index 000000000..152f1d098 --- /dev/null +++ b/cmd/podman/volume_inspect.go @@ -0,0 +1,63 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var volumeInspectDescription = ` +podman volume inspect + +Display detailed information on one or more volumes. Can change the format +from JSON to a Go template. +` + +var volumeInspectFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "Inspect all volumes", + }, + cli.StringFlag{ + Name: "format, f", + Usage: "Format volume output using Go template", + Value: "json", + }, +} + +var volumeInspectCommand = cli.Command{ + Name: "inspect", + Usage: "Display detailed information on one or more volumes", + Description: volumeInspectDescription, + Flags: volumeInspectFlags, + Action: volumeInspectCmd, + SkipArgReorder: true, + ArgsUsage: "[VOLUME-NAME ...]", + UseShortOptionHandling: true, +} + +func volumeInspectCmd(c *cli.Context) error { + var err error + + if err = validateFlags(c, volumeInspectFlags); err != nil { + return err + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + opts := volumeLsOptions{ + Format: c.String("format"), + } + + vols, lastError := getVolumesFromContext(c, runtime) + if lastError != nil { + logrus.Errorf("%q", lastError) + } + + return generateVolLsOutput(vols, opts, runtime) +} diff --git a/cmd/podman/volume_ls.go b/cmd/podman/volume_ls.go new file mode 100644 index 000000000..0f94549ee --- /dev/null +++ b/cmd/podman/volume_ls.go @@ -0,0 +1,308 @@ +package main + +import ( + "reflect" + "strings" + + "github.com/containers/libpod/cmd/podman/formats" + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +// volumeOptions is the "ls" command options +type volumeLsOptions struct { + Format string + Quiet bool +} + +// volumeLsTemplateParams is the template parameters to list the volumes +type volumeLsTemplateParams struct { + Name string + Labels string + MountPoint string + Driver string + Options string + Scope string +} + +// volumeLsJSONParams is the JSON parameters to list the volumes +type volumeLsJSONParams struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` + MountPoint string `json:"mountPoint"` + Driver string `json:"driver"` + Options map[string]string `json:"options"` + Scope string `json:"scope"` +} + +var volumeLsDescription = ` +podman volume ls + +List all available volumes. The output of the volumes can be filtered +and the output format can be changed to JSON or a user specified Go template. +` + +var volumeLsFlags = []cli.Flag{ + cli.StringFlag{ + Name: "filter, f", + Usage: "Filter volume output", + }, + cli.StringFlag{ + Name: "format", + Usage: "Format volume output using Go template", + Value: "table {{.Driver}}\t{{.Name}}", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Print volume output in quiet mode", + }, +} + +var volumeLsCommand = cli.Command{ + Name: "ls", + Aliases: []string{"list"}, + Usage: "List volumes", + Description: volumeLsDescription, + Flags: volumeLsFlags, + Action: volumeLsCmd, + SkipArgReorder: true, + UseShortOptionHandling: true, +} + +func volumeLsCmd(c *cli.Context) error { + if err := validateFlags(c, volumeLsFlags); err != nil { + return err + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + if len(c.Args()) > 0 { + return errors.Errorf("too many arguments, ls takes no arguments") + } + + opts := volumeLsOptions{ + Quiet: c.Bool("quiet"), + } + opts.Format = genVolLsFormat(c) + + // Get the filter functions based on any filters set + var filterFuncs []libpod.VolumeFilter + if c.String("filter") != "" { + filters := strings.Split(c.String("filter"), ",") + for _, f := range filters { + filterSplit := strings.Split(f, "=") + if len(filterSplit) < 2 { + return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) + } + generatedFunc, err := generateVolumeFilterFuncs(filterSplit[0], filterSplit[1], runtime) + if err != nil { + return errors.Wrapf(err, "invalid filter") + } + filterFuncs = append(filterFuncs, generatedFunc) + } + } + + volumes, err := runtime.GetAllVolumes() + if err != nil { + return err + } + + // Get the volumes that match the filter + volsFiltered := make([]*libpod.Volume, 0, len(volumes)) + for _, vol := range volumes { + include := true + for _, filter := range filterFuncs { + include = include && filter(vol) + } + + if include { + volsFiltered = append(volsFiltered, vol) + } + } + return generateVolLsOutput(volsFiltered, opts, runtime) +} + +// generate the template based on conditions given +func genVolLsFormat(c *cli.Context) string { + var format string + if c.String("format") != "" { + // "\t" from the command line is not being recognized as a tab + // replacing the string "\t" to a tab character if the user passes in "\t" + format = strings.Replace(c.String("format"), `\t`, "\t", -1) + } + if c.Bool("quiet") { + format = "{{.Name}}" + } + return format +} + +// Convert output to genericParams for printing +func volLsToGeneric(templParams []volumeLsTemplateParams, JSONParams []volumeLsJSONParams) (genericParams []interface{}) { + if len(templParams) > 0 { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) + } + return + } + for _, v := range JSONParams { + genericParams = append(genericParams, interface{}(v)) + } + return +} + +// generate the accurate header based on template given +func (vol *volumeLsTemplateParams) volHeaderMap() map[string]string { + v := reflect.Indirect(reflect.ValueOf(vol)) + values := make(map[string]string) + + for i := 0; i < v.NumField(); i++ { + key := v.Type().Field(i).Name + value := key + if value == "Name" { + value = "Volume" + value + } + values[key] = strings.ToUpper(splitCamelCase(value)) + } + return values +} + +// getVolTemplateOutput returns all the volumes in the volumeLsTemplateParams format +func getVolTemplateOutput(lsParams []volumeLsJSONParams, opts volumeLsOptions) ([]volumeLsTemplateParams, error) { + var lsOutput []volumeLsTemplateParams + + for _, lsParam := range lsParams { + var ( + labels string + options string + ) + + for k, v := range lsParam.Labels { + label := k + if v != "" { + label += "=" + v + } + labels += label + } + for k, v := range lsParam.Options { + option := k + if v != "" { + option += "=" + v + } + options += option + } + params := volumeLsTemplateParams{ + Name: lsParam.Name, + Driver: lsParam.Driver, + MountPoint: lsParam.MountPoint, + Scope: lsParam.Scope, + Labels: labels, + Options: options, + } + + lsOutput = append(lsOutput, params) + } + return lsOutput, nil +} + +// getVolJSONParams returns the volumes in JSON format +func getVolJSONParams(volumes []*libpod.Volume, opts volumeLsOptions, runtime *libpod.Runtime) ([]volumeLsJSONParams, error) { + var lsOutput []volumeLsJSONParams + + for _, volume := range volumes { + params := volumeLsJSONParams{ + Name: volume.Name(), + Labels: volume.Labels(), + MountPoint: volume.MountPoint(), + Driver: volume.Driver(), + Options: volume.Options(), + Scope: volume.Scope(), + } + + lsOutput = append(lsOutput, params) + } + return lsOutput, nil +} + +// generateVolLsOutput generates the output based on the format, JSON or Go Template, and prints it out +func generateVolLsOutput(volumes []*libpod.Volume, opts volumeLsOptions, runtime *libpod.Runtime) error { + if len(volumes) == 0 && opts.Format != formats.JSONString { + return nil + } + lsOutput, err := getVolJSONParams(volumes, opts, runtime) + if err != nil { + return err + } + var out formats.Writer + + switch opts.Format { + case formats.JSONString: + if err != nil { + return errors.Wrapf(err, "unable to create JSON for volume output") + } + out = formats.JSONStructArray{Output: volLsToGeneric([]volumeLsTemplateParams{}, lsOutput)} + default: + lsOutput, err := getVolTemplateOutput(lsOutput, opts) + if err != nil { + return errors.Wrapf(err, "unable to create volume output") + } + out = formats.StdoutTemplateArray{Output: volLsToGeneric(lsOutput, []volumeLsJSONParams{}), Template: opts.Format, Fields: lsOutput[0].volHeaderMap()} + } + return formats.Writer(out).Out() +} + +// generateVolumeFilterFuncs returns the true if the volume matches the filter set, otherwise it returns false. +func generateVolumeFilterFuncs(filter, filterValue string, runtime *libpod.Runtime) (func(volume *libpod.Volume) bool, error) { + switch filter { + case "name": + return func(v *libpod.Volume) bool { + return strings.Contains(v.Name(), filterValue) + }, nil + case "driver": + return func(v *libpod.Volume) bool { + return v.Driver() == filterValue + }, nil + case "scope": + return func(v *libpod.Volume) bool { + return v.Scope() == filterValue + }, nil + case "label": + filterArray := strings.SplitN(filterValue, "=", 2) + filterKey := filterArray[0] + if len(filterArray) > 1 { + filterValue = filterArray[1] + } else { + filterValue = "" + } + return func(v *libpod.Volume) bool { + for labelKey, labelValue := range v.Labels() { + if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) { + return true + } + } + return false + }, nil + case "opt": + filterArray := strings.SplitN(filterValue, "=", 2) + filterKey := filterArray[0] + if len(filterArray) > 1 { + filterValue = filterArray[1] + } else { + filterValue = "" + } + return func(v *libpod.Volume) bool { + for labelKey, labelValue := range v.Options() { + if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) { + return true + } + } + return false + }, nil + } + return nil, errors.Errorf("%s is an invalid filter", filter) +} diff --git a/cmd/podman/volume_prune.go b/cmd/podman/volume_prune.go new file mode 100644 index 000000000..652c50f42 --- /dev/null +++ b/cmd/podman/volume_prune.go @@ -0,0 +1,86 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var volumePruneDescription = ` +podman volume prune + +Remove all unused volumes. Will prompt for confirmation if not +using force. +` + +var volumePruneFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "Do not prompt for confirmation", + }, +} + +var volumePruneCommand = cli.Command{ + Name: "prune", + Usage: "Remove all unused volumes", + Description: volumePruneDescription, + Flags: volumePruneFlags, + Action: volumePruneCmd, + SkipArgReorder: true, + UseShortOptionHandling: true, +} + +func volumePruneCmd(c *cli.Context) error { + var lastError error + + if err := validateFlags(c, volumePruneFlags); err != nil { + return err + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + ctx := getContext() + + // Prompt for confirmation if --force is not set + if !c.Bool("force") { + reader := bufio.NewReader(os.Stdin) + fmt.Println("WARNING! This will remove all volumes not used by at least one container.") + fmt.Print("Are you sure you want to continue? [y/N] ") + ans, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "error reading input") + } + if strings.ToLower(ans)[0] != 'y' { + return nil + } + } + + volumes, err := runtime.GetAllVolumes() + if err != nil { + return err + } + + for _, vol := range volumes { + err = runtime.RemoveVolume(ctx, vol, false, true) + if err == nil { + fmt.Println(vol.Name()) + } else if err != libpod.ErrVolumeBeingUsed { + if lastError != nil { + logrus.Errorf("%q", lastError) + } + lastError = errors.Wrapf(err, "failed to remove volume %q", vol.Name()) + } + } + return lastError +} diff --git a/cmd/podman/volume_rm.go b/cmd/podman/volume_rm.go new file mode 100644 index 000000000..3fb623624 --- /dev/null +++ b/cmd/podman/volume_rm.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var volumeRmDescription = ` +podman volume rm + +Remove one or more existing volumes. Will only remove volumes that are +not being used by any containers. To remove the volumes anyways, use the +--force flag. +` + +var volumeRmFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "Remove all volumes", + }, + cli.BoolFlag{ + Name: "force, f", + Usage: "Remove a volume by force, even if it is being used by a container", + }, +} + +var volumeRmCommand = cli.Command{ + Name: "rm", + Aliases: []string{"remove"}, + Usage: "Remove one or more volumes", + Description: volumeRmDescription, + Flags: volumeRmFlags, + Action: volumeRmCmd, + ArgsUsage: "[VOLUME-NAME ...]", + SkipArgReorder: true, + UseShortOptionHandling: true, +} + +func volumeRmCmd(c *cli.Context) error { + var err error + + if err = validateFlags(c, volumeRmFlags); err != nil { + return err + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + ctx := getContext() + + vols, lastError := getVolumesFromContext(c, runtime) + for _, vol := range vols { + err = runtime.RemoveVolume(ctx, vol, c.Bool("force"), false) + if err != nil { + if lastError != nil { + logrus.Errorf("%q", lastError) + } + lastError = errors.Wrapf(err, "failed to remove volume %q", vol.Name()) + } else { + fmt.Println(vol.Name()) + } + } + return lastError +} diff --git a/commands.md b/commands.md index c84938e64..8e1a2dd7a 100644 --- a/commands.md +++ b/commands.md @@ -62,4 +62,9 @@ | [podman-unpause(1)](/docs/podman-unpause.1.md) | Unpause one or more running containers |[![...](/docs/play.png)](https://asciinema.org/a/141292)| | [podman-varlink(1)](/docs/podman-varlink.1.md) | Run the varlink backend || | [podman-version(1)](/docs/podman-version.1.md) | Display the version information |[![...](/docs/play.png)](https://asciinema.org/a/mfrn61pjZT9Fc8L4NbfdSqfgu)| +| [podman-volume-create(1)](/docs/podman-volume-create.1.md) | Create a volume || +| [podman-volume-inspect(1)](/docs/podman-volume-inspect.1.md) | Get detailed information on one or more volumes || +| [podman-volume-ls(1)](/docs/podman-volume-ls.1.md) | List all the available volumes || +| [podman-volume-rm(1)](/docs/podman-volume-rm.1.md) | Remove one or more volumes || +| [podman-volume-prune(1)](/docs/podman-volume-prune.1.md) | Remove all unused volumes || | [podman-wait(1)](/docs/podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes |[![...](/docs/play.png)](https://asciinema.org/a/QNPGKdjWuPgI96GcfkycQtah0)| diff --git a/completions/bash/podman b/completions/bash/podman index 9518cfa22..3382b6c5a 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -689,6 +689,23 @@ __podman_images() { __podman_q images $images_args | awk "$awk_script" | grep -v '<none>$' } +# __podman_complete_volumes applies completion of volumes based on the current +# value of `$cur` or the value of the optional first option `--cur`, if given. +__podman_complete_volumes() { + local current="$cur" + if [ "$1" = "--cur" ] ; then + current="$2" + shift 2 + fi + COMPREPLY=( $(compgen -W "$(__podman_volume "$@")" -- "$current") ) +} + +__podman_complete_volume_names() { + local names=( $(__podman_q volume ls --quiet) ) + COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") ) +} + + _podman_attach() { local options_with_args=" --detach-keys @@ -871,6 +888,7 @@ _podman_container() { create diff exec + exists export inspect kill @@ -879,6 +897,7 @@ _podman_container() { mount pause port + prune refresh restart restore @@ -1210,11 +1229,13 @@ _podman_image() { " subcommands=" build + exists history import inspect load ls + prune pull push rm @@ -2037,6 +2058,7 @@ _podman_ps() { --quiet -q --size -s --namespace --ns + --sync " _complete_ "$options_with_args" "$boolean_options" } @@ -2227,6 +2249,26 @@ _podman_container_runlabel() { esac } +_podman_images_prune() { + local options_with_args=" + " + + local boolean_options=" + -h + --help + " +} + +_podman_container_prune() { + local options_with_args=" + " + + local boolean_options=" + -h + --help + " +} + _podman_container_exists() { local options_with_args=" " @@ -2511,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 @@ -2571,6 +2735,7 @@ _podman_podman() { unpause varlink version + volume wait " diff --git a/docs/podman-container-prune.1.md b/docs/podman-container-prune.1.md new file mode 100644 index 000000000..1f3ef1d41 --- /dev/null +++ b/docs/podman-container-prune.1.md @@ -0,0 +1,31 @@ +% PODMAN(1) Podman Man Pages +% Brent Baude +% December 2018 +# NAME +podman-container-prune - Remove all stopped containers + +# SYNOPSIS +**podman container prune** +[**-h**|**--help**] + +# DESCRIPTION +**podman container prune** removes all stopped containers from local storage. + +## Examples ## + +Remove all stopped containers from local storage +``` +$ sudo podman container prune +878392adf2e6c5c9bb1fc19b69d37d2e98c8abf9d539c0bce4b15b46bbcce471 +37664467fbe3618bf9479c34393ac29c02696675addf1750f9e346581636cde7 +ed0c6468b8e1cb641b4621d1fe30cb477e1fefc5c0bceb66feaf2f7cb50e5962 +6ac6c8f0067b7a4682e6b8e18902665b57d1a0e07e885d9abcd382232a543ccd +fff1c5b6c3631746055ec40598ce8ecaa4b82aef122f9e3a85b03b55c0d06c23 +602d343cd47e7cb3dfc808282a9900a3e4555747787ec6723bb68cedab8384d5 +``` + +## SEE ALSO +podman(1), podman-ps + +# HISTORY +December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/podman-container.1.md b/docs/podman-container.1.md index aa5dfa82c..3675d9719 100644 --- a/docs/podman-container.1.md +++ b/docs/podman-container.1.md @@ -29,6 +29,7 @@ The container command allows you to manage containers | mount | [podman-mount(1)](podman-mount.1.md) | Mount a working container's root filesystem. | | pause | [podman-pause(1)](podman-pause.1.md) | Pause one or more containers. | | port | [podman-port(1)](podman-port.1.md) | List port mappings for the container. | +| prune | [podman-container-prune(1)](podman-container-prune.1.md) | Remove all stopped containers from local storage | | refresh | [podman-refresh(1)](podman-container-refresh.1.md) | Refresh the state of all containers | | restart | [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. | | restore | [podman-container-restore(1)](podman-container-restore.1.md) | Restores one or more containers from a checkpoint. | diff --git a/docs/podman-image-prune.1.md b/docs/podman-image-prune.1.md new file mode 100644 index 000000000..db76b26e0 --- /dev/null +++ b/docs/podman-image-prune.1.md @@ -0,0 +1,32 @@ +% PODMAN(1) Podman Man Pages +% Brent Baude +% December 2018 +# NAME +podman-image-prune - Remove all unused images + +# SYNOPSIS +**podman image prune** +[**-h**|**--help**] + +# DESCRIPTION +**podman image prune** removes all unused images from local storage. An unused image +is defined as an image that does not have any containers based on it. + +## Examples ## + +Remove all unused images from local storage +``` +$ sudo podman image prune +f3e20dc537fb04cb51672a5cb6fdf2292e61d411315549391a0d1f64e4e3097e +324a7a3b2e0135f4226ffdd473e4099fd9e477a74230cdc35de69e84c0f9d907 +6125002719feb1ddf3030acab1df6156da7ce0e78e571e9b6e9c250424d6220c +91e732da5657264c6f4641b8d0c4001c218ae6c1adb9dcef33ad00cafd37d8b6 +e4e5109420323221f170627c138817770fb64832da7d8fe2babd863148287fca +77a57fa8285e9656dbb7b23d9efa837a106957409ddd702f995605af27a45ebe +``` + +## SEE ALSO +podman(1), podman-images + +# HISTORY +December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/podman-image.1.md b/docs/podman-image.1.md index 446f8667d..8b812af11 100644 --- a/docs/podman-image.1.md +++ b/docs/podman-image.1.md @@ -21,6 +21,7 @@ The image command allows you to manage images | load | [podman-load(1)](podman-load.1.md) | Load an image from the docker archive. | | ls | [podman-images(1)](podman-images.1.md) | Prints out information about images. | | pull | [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. | +| prune| [podman-container-prune(1)](podman-container-prune.1.md) | Removed all unused images from the local store | | push | [podman-push(1)](podman-push.1.md) | Push an image from local storage to elsewhere. | | rm | [podman-rm(1)](podman-rmi.1.md) | Removes one or more locally stored images. | | save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. | diff --git a/docs/podman-ps.1.md b/docs/podman-ps.1.md index 7333a1095..8b86703d8 100644 --- a/docs/podman-ps.1.md +++ b/docs/podman-ps.1.md @@ -103,6 +103,13 @@ Valid filters are listed below: Print usage statement +**--sync** + +Force a sync of container state with the OCI runtime. +In some cases, a container's state in the runtime can become out of sync with Podman's state. +This will update Podman's state based on what the OCI runtime reports. +Forcibly syncing is much slower, but can resolve inconsistent state issues. + ## EXAMPLES ``` diff --git a/docs/podman-rmi.1.md b/docs/podman-rmi.1.md index f035897ee..9c080c9f1 100644 --- a/docs/podman-rmi.1.md +++ b/docs/podman-rmi.1.md @@ -19,15 +19,25 @@ Remove all images in the local storage. This option will cause podman to remove all containers that are using the image before removing the image from the system. -## EXAMPLE - -podman rmi imageID +Remove an image by its short ID +``` +podman rmi c0ed59d05ff7 +``` +Remove an image and its associated containers. +``` podman rmi --force imageID +```` -podman rmi imageID1 imageID2 imageID3 +Remove multiple images by their shortened IDs. +``` +podman rmi c4dfb1609ee2 93fd78260bd1 c0ed59d05ff7 +``` +Remove all images and containers. +``` podman rmi -a -f +``` ## SEE ALSO podman(1) diff --git a/docs/podman-volume-create.1.md b/docs/podman-volume-create.1.md new file mode 100644 index 000000000..795d7b449 --- /dev/null +++ b/docs/podman-volume-create.1.md @@ -0,0 +1,48 @@ +% podman-volume-create(1) + +## NAME +podman\-volume\-create - Create a new volume + +## SYNOPSIS +**podman volume create** [*options*] + +## DESCRIPTION + +Creates an empty volume and prepares it to be used by containers. The volume +can be created with a specific name, if a name is not given a random name is +generated. You can add metadata to the volume by using the **--label** flag and +driver options can be set using the **--opt** flag. + +## OPTIONS + +**--driver**="" + +Specify the volume driver name (default local). + +**--help** + +Print usage statement + +**-l**, **--label**=[] + +Set metadata for a volume (e.g., --label mykey=value). + +**-o**, **--opt**=[] + +Set driver specific options. + +## EXAMPLES + +``` +$ podman volume create myvol + +$ podman volume create + +$ podman volume create --label foo=bar myvol +``` + +## SEE ALSO +podman-volume(1) + +## HISTORY +November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/podman-volume-inspect.1.md b/docs/podman-volume-inspect.1.md new file mode 100644 index 000000000..6d5b184ee --- /dev/null +++ b/docs/podman-volume-inspect.1.md @@ -0,0 +1,45 @@ +% podman-volume-inspect(1) + +## NAME +podman\-volume\-inspect - Inspect one or more volumes + +## SYNOPSIS +**podman volume inspect** [*options*] + +## DESCRIPTION + +Display detailed information on one or more volumes. The output can be formated using +the **--format** flag and a Go template. To get detailed information about all the +existing volumes, use the **--all** flag. + + +## OPTIONS + +**-a**, **--all**="" + +Inspect all volumes. + +**--format**="" + +Format volume output using Go template + +**--help** + +Print usage statement + + +## EXAMPLES + +``` +$ podman volume inspect myvol + +$ podman volume inspect --all + +$ podman volume inspect --format "{{.Driver}} {{.Scope}}" myvol +``` + +## SEE ALSO +podman-volume(1) + +## HISTORY +November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/podman-volume-ls.1.md b/docs/podman-volume-ls.1.md new file mode 100644 index 000000000..c061e27fe --- /dev/null +++ b/docs/podman-volume-ls.1.md @@ -0,0 +1,49 @@ +% podman-volume-ls(1) + +## NAME +podman\-volume\-ls - List volumes + +## SYNOPSIS +**podman volume ls** [*options*] + +## DESCRIPTION + +Lists all the volumes that exist. The output can be filtered using the **--filter** +flag and can be formatted to either JSON or a Go template using the **--format** +flag. Use the **--quiet** flag to print only the volume names. + +## OPTIONS + +**--filter**="" + +Filter volume output. + +**--format**="" + +Format volume output using Go template. + +**--help** + +Print usage statement. + +**-q**, **--quiet**=[] + +Print volume output in quiet mode. Only print the volume names. + +## EXAMPLES + +``` +$ podman volume ls + +$ podman volume ls --format json + +$ podman volume ls --format "{{.Driver}} {{.Scope}}" + +$ podman volume ls --filter name=foo,label=blue +``` + +## SEE ALSO +podman-volume(1) + +## HISTORY +November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/podman-volume-prune.1.md b/docs/podman-volume-prune.1.md new file mode 100644 index 000000000..a06bb2fa4 --- /dev/null +++ b/docs/podman-volume-prune.1.md @@ -0,0 +1,38 @@ +% podman-volume-prune(1) + +## NAME +podman\-volume\-prune - Remove all unused volumes + +## SYNOPSIS +**podman volume rm** [*options*] + +## DESCRIPTION + +Removes all unused volumes. You will be prompted to confirm the removal of all the +unused volumes. To bypass the confirmation, use the **--force** flag. + + +## OPTIONS + +**-f**, **--force**="" + +Do not prompt for confirmation. + +**--help** + +Print usage statement + + +## EXAMPLES + +``` +$ podman volume prune + +$ podman volume prune --force +``` + +## SEE ALSO +podman-volume(1) + +## HISTORY +November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/podman-volume-rm.1.md b/docs/podman-volume-rm.1.md new file mode 100644 index 000000000..c23d7675c --- /dev/null +++ b/docs/podman-volume-rm.1.md @@ -0,0 +1,45 @@ +% podman-volume-rm(1) + +## NAME +podman\-volume\-rm - Remove one or more volumes + +## SYNOPSIS +**podman volume rm** [*options*] + +## DESCRIPTION + +Removes one ore more volumes. Only volumes that are not being used will be removed. +If a volume is being used by a container, an error will be returned unless the **--force** +flag is being used. To remove all the volumes, use the **--all** flag. + + +## OPTIONS + +**-a**, **--all**="" + +Remove all volumes. + +**-f**, **--force**="" + +Remove a volume by force, even if it is being used by a container + +**--help** + +Print usage statement + + +## EXAMPLES + +``` +$ podman volume rm myvol1 myvol2 + +$ podman volume rm --all + +$ podman volume rm --force myvol +``` + +## SEE ALSO +podman-volume(1) + +## HISTORY +November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/podman-volume.1.md b/docs/podman-volume.1.md new file mode 100644 index 000000000..ac32abbd6 --- /dev/null +++ b/docs/podman-volume.1.md @@ -0,0 +1,23 @@ +% podman-volume(1) + +## NAME +podman\-volume - Simple management tool for volumes. + +## SYNOPSIS +**podman volume** *subcommand* + +## DESCRIPTION +podman volume is a set of subcommands that manage volumes. + +## SUBCOMMANDS + +| Subcommand | Description | +| ------------------------------------------------- | ------------------------------------------------------------------------------ | +| [podman-volume-create(1)](podman-volume-create.1.md) | Create a new volume. | +| [podman-volume-inspect(1)](podman-volume-inspect.1.md) | Get detailed information on one or more volumes. | +| [podman-volume-ls(1)](podman-volume-ls.1.md) | List all the available volumes. | +| [podman-volume-rm(1)](podman-volume-rm.1.md) | Remove one or more volumes. | +| [podman-volume-prune(1)](podman-volume-prune.1.md) | Remove all unused volumes. | + +## HISTORY +November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/docs/tutorials/podman_tutorial.md b/docs/tutorials/podman_tutorial.md index ce94d7d15..659973b28 100644 --- a/docs/tutorials/podman_tutorial.md +++ b/docs/tutorials/podman_tutorial.md @@ -24,7 +24,7 @@ acquire the source, and build it. sudo dnf install -y git runc libassuan-devel golang golang-github-cpuguy83-go-md2man glibc-static \ gpgme-devel glib2-devel device-mapper-devel libseccomp-devel \ atomic-registries iptables skopeo-containers containernetworking-cni \ - conmon + conmon ostree-devel ``` ### Building and installing podman @@ -54,7 +54,7 @@ tutorial. For this tutorial, the Ubuntu **artful-server-cloudimg** image was use #### Installing base packages ```console sudo apt-get update -sudo apt-get install libdevmapper-dev libglib2.0-dev libgpgme11-dev golang libseccomp-dev \ +sudo apt-get install libdevmapper-dev libglib2.0-dev libgpgme11-dev golang libseccomp-dev libostree-dev \ go-md2man libprotobuf-dev libprotobuf-c0-dev libseccomp-dev python3-setuptools ``` #### Building and installing conmon diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go index cb661d4e9..b154d8bda 100644 --- a/libpod/boltdb_state.go +++ b/libpod/boltdb_state.go @@ -94,6 +94,12 @@ func NewBoltState(path string, runtime *Runtime) (State, error) { if _, err := tx.CreateBucketIfNotExists(allPodsBkt); err != nil { return errors.Wrapf(err, "error creating all pods bucket") } + if _, err := tx.CreateBucketIfNotExists(volBkt); err != nil { + return errors.Wrapf(err, "error creating volume bucket") + } + if _, err := tx.CreateBucketIfNotExists(allVolsBkt); err != nil { + return errors.Wrapf(err, "error creating all volumes bucket") + } if _, err := tx.CreateBucketIfNotExists(runtimeConfigBkt); err != nil { return errors.Wrapf(err, "error creating runtime-config bucket") } @@ -1150,6 +1156,378 @@ func (s *BoltState) PodContainers(pod *Pod) ([]*Container, error) { return ctrs, nil } +// AddVolume adds the given volume to the state. It also adds ctrDepID to +// the sub bucket holding the container dependencies that this volume has +func (s *BoltState) AddVolume(volume *Volume) error { + if !s.valid { + return ErrDBClosed + } + + if !volume.valid { + return ErrVolumeRemoved + } + + volName := []byte(volume.Name()) + + volConfigJSON, err := json.Marshal(volume.config) + if err != nil { + return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name()) + } + + db, err := s.getDBCon() + if err != nil { + return err + } + defer s.closeDBCon(db) + + err = db.Update(func(tx *bolt.Tx) error { + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + + allVolsBkt, err := getAllVolsBucket(tx) + if err != nil { + return err + } + + // Check if we already have a volume with the given name + volExists := allVolsBkt.Get(volName) + if volExists != nil { + return errors.Wrapf(ErrVolumeExists, "name %s is in use", volume.Name()) + } + + // We are good to add the volume + // Make a bucket for it + newVol, err := volBkt.CreateBucket(volName) + if err != nil { + return errors.Wrapf(err, "error creating bucket for volume %s", volume.Name()) + } + + // Make a subbucket for the containers using the volume. Dependent container IDs will be addedremoved to + // this bucket in addcontainer/removeContainer + if _, err := newVol.CreateBucket(volDependenciesBkt); err != nil { + return errors.Wrapf(err, "error creating bucket for containers using volume %s", volume.Name()) + } + + if err := newVol.Put(configKey, volConfigJSON); err != nil { + return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name()) + } + + if err := allVolsBkt.Put(volName, volName); err != nil { + return errors.Wrapf(err, "error storing volume %s in all volumes bucket in DB", volume.Name()) + } + + return nil + }) + return err +} + +// RemoveVolCtrDep updates the container dependencies sub bucket of the given volume. +// It deletes it from the bucket when found. +// This is important when force removing a volume and we want to get rid of the dependencies. +func (s *BoltState) RemoveVolCtrDep(volume *Volume, ctrID string) error { + if ctrID == "" { + return nil + } + + if !s.valid { + return ErrDBBadConfig + } + + if !volume.valid { + return ErrVolumeRemoved + } + + volName := []byte(volume.Name()) + + db, err := s.getDBCon() + if err != nil { + return err + } + defer s.closeDBCon(db) + + err = db.Update(func(tx *bolt.Tx) error { + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + + volDB := volBkt.Bucket(volName) + if volDB == nil { + volume.valid = false + return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volume.Name()) + } + + // Make a subbucket for the containers using the volume + ctrDepsBkt := volDB.Bucket(volDependenciesBkt) + depCtrID := []byte(ctrID) + if depExists := ctrDepsBkt.Get(depCtrID); depExists != nil { + if err := ctrDepsBkt.Delete(depCtrID); err != nil { + return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctrID, volume.Name()) + } + } + + return nil + }) + return err +} + +// RemoveVolume removes the given volume from the state +func (s *BoltState) RemoveVolume(volume *Volume) error { + if !s.valid { + return ErrDBClosed + } + + if !volume.valid { + return ErrVolumeRemoved + } + + volName := []byte(volume.Name()) + + db, err := s.getDBCon() + if err != nil { + return err + } + defer s.closeDBCon(db) + + err = db.Update(func(tx *bolt.Tx) error { + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + + allVolsBkt, err := getAllVolsBucket(tx) + if err != nil { + return err + } + + // Check if the volume exists + volDB := volBkt.Bucket(volName) + if volDB == nil { + volume.valid = false + return errors.Wrapf(ErrNoSuchVolume, "volume %s does not exist in DB", volume.Name()) + } + + // Check if volume is not being used by any container + // This should never be nil + // But if it is, we can assume that no containers are using + // the volume. + volCtrsBkt := volDB.Bucket(volDependenciesBkt) + if volCtrsBkt != nil { + var deps []string + err = volCtrsBkt.ForEach(func(id, value []byte) error { + deps = append(deps, string(id)) + return nil + }) + if err != nil { + return errors.Wrapf(err, "error getting list of dependencies from dependencies bucket for volumes %q", volume.Name()) + } + if len(deps) > 0 { + return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by container(s) %s", volume.Name(), strings.Join(deps, ",")) + } + } + + // volume is ready for removal + // Let's kick it out + if err := allVolsBkt.Delete(volName); err != nil { + return errors.Wrapf(err, "error removing volume %s from all volumes bucket in DB", volume.Name()) + } + if err := volBkt.DeleteBucket(volName); err != nil { + return errors.Wrapf(err, "error removing volume %s from DB", volume.Name()) + } + + return nil + }) + return err +} + +// AllVolumes returns all volumes present in the state +func (s *BoltState) AllVolumes() ([]*Volume, error) { + if !s.valid { + return nil, ErrDBClosed + } + + volumes := []*Volume{} + + db, err := s.getDBCon() + if err != nil { + return nil, err + } + defer s.closeDBCon(db) + + err = db.View(func(tx *bolt.Tx) error { + allVolsBucket, err := getAllVolsBucket(tx) + if err != nil { + return err + } + + volBucket, err := getVolBucket(tx) + if err != nil { + return err + } + err = allVolsBucket.ForEach(func(id, name []byte) error { + volExists := volBucket.Bucket(id) + // This check can be removed if performance becomes an + // issue, but much less helpful errors will be produced + if volExists == nil { + return errors.Wrapf(ErrInternal, "inconsistency in state - volume %s is in all volumes bucket but volume not found", string(id)) + } + + volume := new(Volume) + volume.config = new(VolumeConfig) + + if err := s.getVolumeFromDB(id, volume, volBucket); err != nil { + if errors.Cause(err) != ErrNSMismatch { + logrus.Errorf("Error retrieving volume %s from the database: %v", string(id), err) + } + } else { + volumes = append(volumes, volume) + } + + return nil + }) + return err + }) + if err != nil { + return nil, err + } + + return volumes, nil +} + +// Volume retrieves a volume from full name +func (s *BoltState) Volume(name string) (*Volume, error) { + if name == "" { + return nil, ErrEmptyID + } + + if !s.valid { + return nil, ErrDBClosed + } + + volName := []byte(name) + + volume := new(Volume) + volume.config = new(VolumeConfig) + + db, err := s.getDBCon() + if err != nil { + return nil, err + } + defer s.closeDBCon(db) + + err = db.View(func(tx *bolt.Tx) error { + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + + return s.getVolumeFromDB(volName, volume, volBkt) + }) + if err != nil { + return nil, err + } + + return volume, nil +} + +// HasVolume returns true if the given volume exists in the state, otherwise it returns false +func (s *BoltState) HasVolume(name string) (bool, error) { + if name == "" { + return false, ErrEmptyID + } + + if !s.valid { + return false, ErrDBClosed + } + + volName := []byte(name) + + exists := false + + db, err := s.getDBCon() + if err != nil { + return false, err + } + defer s.closeDBCon(db) + + err = db.View(func(tx *bolt.Tx) error { + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + + volDB := volBkt.Bucket(volName) + if volDB != nil { + exists = true + } + + return nil + }) + if err != nil { + return false, err + } + + return exists, nil +} + +// VolumeInUse checks if any container is using the volume +// It returns a slice of the IDs of the containers using the given +// volume. If the slice is empty, no containers use the given volume +func (s *BoltState) VolumeInUse(volume *Volume) ([]string, error) { + if !s.valid { + return nil, ErrDBClosed + } + + if !volume.valid { + return nil, ErrVolumeRemoved + } + + depCtrs := []string{} + + db, err := s.getDBCon() + if err != nil { + return nil, err + } + defer s.closeDBCon(db) + + err = db.View(func(tx *bolt.Tx) error { + volBucket, err := getVolBucket(tx) + if err != nil { + return err + } + + volDB := volBucket.Bucket([]byte(volume.Name())) + if volDB == nil { + volume.valid = false + return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in DB", volume.Name()) + } + + dependsBkt := volDB.Bucket(volDependenciesBkt) + if dependsBkt == nil { + return errors.Wrapf(ErrInternal, "volume %s has no dependencies bucket", volume.Name()) + } + + // Iterate through and add dependencies + err = dependsBkt.ForEach(func(id, value []byte) error { + depCtrs = append(depCtrs, string(id)) + + return nil + }) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return depCtrs, nil +} + // AddPod adds the given pod to the state. func (s *BoltState) AddPod(pod *Pod) error { if !s.valid { diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go index 3f00657ea..0970f4d41 100644 --- a/libpod/boltdb_state_internal.go +++ b/libpod/boltdb_state_internal.go @@ -21,15 +21,18 @@ const ( allCtrsName = "all-ctrs" podName = "pod" allPodsName = "allPods" + volName = "vol" + allVolsName = "allVolumes" runtimeConfigName = "runtime-config" - configName = "config" - stateName = "state" - dependenciesName = "dependencies" - netNSName = "netns" - containersName = "containers" - podIDName = "pod-id" - namespaceName = "namespace" + configName = "config" + stateName = "state" + dependenciesName = "dependencies" + volCtrDependencies = "vol-dependencies" + netNSName = "netns" + containersName = "containers" + podIDName = "pod-id" + namespaceName = "namespace" staticDirName = "static-dir" tmpDirName = "tmp-dir" @@ -47,15 +50,18 @@ var ( allCtrsBkt = []byte(allCtrsName) podBkt = []byte(podName) allPodsBkt = []byte(allPodsName) + volBkt = []byte(volName) + allVolsBkt = []byte(allVolsName) runtimeConfigBkt = []byte(runtimeConfigName) - configKey = []byte(configName) - stateKey = []byte(stateName) - dependenciesBkt = []byte(dependenciesName) - netNSKey = []byte(netNSName) - containersBkt = []byte(containersName) - podIDKey = []byte(podIDName) - namespaceKey = []byte(namespaceName) + configKey = []byte(configName) + stateKey = []byte(stateName) + dependenciesBkt = []byte(dependenciesName) + volDependenciesBkt = []byte(volCtrDependencies) + netNSKey = []byte(netNSName) + containersBkt = []byte(containersName) + podIDKey = []byte(podIDName) + namespaceKey = []byte(namespaceName) staticDirKey = []byte(staticDirName) tmpDirKey = []byte(tmpDirName) @@ -234,6 +240,22 @@ func getAllPodsBucket(tx *bolt.Tx) (*bolt.Bucket, error) { return bkt, nil } +func getVolBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + bkt := tx.Bucket(volBkt) + if bkt == nil { + return nil, errors.Wrapf(ErrDBBadConfig, "volumes bucket not found in DB") + } + return bkt, nil +} + +func getAllVolsBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + bkt := tx.Bucket(allVolsBkt) + if bkt == nil { + return nil, errors.Wrapf(ErrDBBadConfig, "all volumes bucket not found in DB") + } + return bkt, nil +} + func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) { bkt := tx.Bucket(runtimeConfigBkt) if bkt == nil { @@ -315,6 +337,35 @@ func (s *BoltState) getPodFromDB(id []byte, pod *Pod, podBkt *bolt.Bucket) error return nil } +func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bucket) error { + volDB := volBkt.Bucket(name) + if volDB == nil { + return errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found", string(name)) + } + + volConfigBytes := volDB.Get(configKey) + if volConfigBytes == nil { + return errors.Wrapf(ErrInternal, "volume %s is missing configuration key in DB", string(name)) + } + + if err := json.Unmarshal(volConfigBytes, volume.config); err != nil { + return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name)) + } + + // Get the lock + lockPath := filepath.Join(s.runtime.lockDir, string(name)) + lock, err := storage.GetLockfile(lockPath) + if err != nil { + return errors.Wrapf(err, "error retrieving lockfile for volume %s", string(name)) + } + volume.lock = lock + + volume.runtime = s.runtime + volume.valid = true + + return nil +} + // Add a container to the DB // If pod is not nil, the container is added to the pod as well func (s *BoltState) addContainer(ctr *Container, pod *Pod) error { @@ -376,6 +427,11 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error { return err } + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + // If a pod was given, check if it exists var podDB *bolt.Bucket var podCtrs *bolt.Bucket @@ -508,6 +564,25 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error { } } + // Add container to volume dependencies bucket if container is using a named volume + for _, vol := range ctr.config.Spec.Mounts { + if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) { + volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0] + + volDB := volBkt.Bucket([]byte(volName)) + if volDB == nil { + return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volName) + } + + ctrDepsBkt := volDB.Bucket(volDependenciesBkt) + if depExists := ctrDepsBkt.Get(ctrID); depExists == nil { + if err := ctrDepsBkt.Put(ctrID, ctrID); err != nil { + return errors.Wrapf(err, "error storing container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName) + } + } + } + } + return nil }) return err @@ -545,6 +620,11 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error return err } + volBkt, err := getVolBucket(tx) + if err != nil { + return err + } + // Does the pod exist? var podDB *bolt.Bucket if pod != nil { @@ -663,5 +743,25 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error } } + // Remove container from volume dependencies bucket if container is using a named volume + for _, vol := range ctr.config.Spec.Mounts { + if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) { + volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0] + + volDB := volBkt.Bucket([]byte(volName)) + if volDB == nil { + // Let's assume the volume was already deleted and continue to remove the container + continue + } + + ctrDepsBkt := volDB.Bucket(volDependenciesBkt) + if depExists := ctrDepsBkt.Get(ctrID); depExists != nil { + if err := ctrDepsBkt.Delete(ctrID); err != nil { + return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName) + } + } + } + } + return nil } diff --git a/libpod/common_test.go b/libpod/common_test.go index b7fee2764..81c8f1920 100644 --- a/libpod/common_test.go +++ b/libpod/common_test.go @@ -74,6 +74,11 @@ func getTestContainer(id, name, locksDir string) (*Container, error) { "/test/file.test": "/test2/file2.test", }, }, + runtime: &Runtime{ + config: &RuntimeConfig{ + VolumePath: "/does/not/exist/tmp/volumes", + }, + }, valid: true, } diff --git a/libpod/container_api.go b/libpod/container_api.go index bc92cae69..09bc46905 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -675,22 +675,27 @@ func (c *Container) Batch(batchFunc func(*Container) error) error { return err } -// Sync updates the current state of the container, checking whether its state -// has changed -// Sync can only be used inside Batch() - otherwise, it will be done -// automatically. -// When called outside Batch(), Sync() is a no-op +// Sync updates the status of a container by querying the OCI runtime. +// If the container has not been created inside the OCI runtime, nothing will be +// done. +// Most of the time, Podman does not explicitly query the OCI runtime for +// container status, and instead relies upon exit files created by conmon. +// This can cause a disconnect between running state and what Podman sees in +// cases where Conmon was killed unexpected, or runc was upgraded. +// Running a manual Sync() ensures that container state will be correct in +// such situations. func (c *Container) Sync() error { if !c.batched { - return nil + c.lock.Lock() + defer c.lock.Unlock() } // If runtime knows about the container, update its status in runtime // And then save back to disk if (c.state.State != ContainerStateUnknown) && - (c.state.State != ContainerStateConfigured) { + (c.state.State != ContainerStateConfigured) && + (c.state.State != ContainerStateExited) { oldState := c.state.State - // TODO: optionally replace this with a stat for the exit file if err := c.runtime.ociRuntime.updateContainerStatus(c, true); err != nil { return err } diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index 9b07198bc..06a0c9f32 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -1,8 +1,11 @@ package libpod import ( + "strings" + "github.com/containers/libpod/pkg/inspect" "github.com/cri-o/ocicni/pkg/ocicni" + specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) @@ -48,6 +51,17 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data) hostnamePath = getPath } + var mounts []specs.Mount + for i, mnt := range spec.Mounts { + mounts = append(mounts, mnt) + // We only want to show the name of the named volume in the inspect + // output, so split the path and get the name out of it. + if strings.Contains(mnt.Source, c.runtime.config.VolumePath) { + split := strings.Split(mnt.Source[len(c.runtime.config.VolumePath)+1:], "/") + mounts[i].Source = split[0] + } + } + data := &inspect.ContainerInspectData{ ID: config.ID, Created: config.CreatedTime, @@ -85,7 +99,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data) AppArmorProfile: spec.Process.ApparmorProfile, ExecIDs: execIDs, GraphDriver: driverData, - Mounts: spec.Mounts, + Mounts: mounts, Dependencies: c.Dependencies(), NetworkSettings: &inspect.NetworkSettings{ Bridge: "", // TODO diff --git a/libpod/errors.go b/libpod/errors.go index 75b4928da..d6614141c 100644 --- a/libpod/errors.go +++ b/libpod/errors.go @@ -11,18 +11,24 @@ var ( ErrNoSuchPod = errors.New("no such pod") // ErrNoSuchImage indicates the requested image does not exist ErrNoSuchImage = errors.New("no such image") + // ErrNoSuchVolume indicates the requested volume does not exist + ErrNoSuchVolume = errors.New("no such volume") // ErrCtrExists indicates a container with the same name or ID already // exists ErrCtrExists = errors.New("container already exists") // ErrPodExists indicates a pod with the same name or ID already exists ErrPodExists = errors.New("pod already exists") - // ErrImageExists indicated an image with the same ID already exists + // ErrImageExists indicates an image with the same ID already exists ErrImageExists = errors.New("image already exists") + // ErrVolumeExists indicates a volume with the same name already exists + ErrVolumeExists = errors.New("volume already exists") // ErrCtrStateInvalid indicates a container is in an improper state for // the requested operation ErrCtrStateInvalid = errors.New("container state improper") + // ErrVolumeBeingUsed indicates that a volume is being used by at least one container + ErrVolumeBeingUsed = errors.New("volume is being used") // ErrRuntimeFinalized indicates that the runtime has already been // created and cannot be modified @@ -33,6 +39,9 @@ var ( // ErrPodFinalized indicates that the pod has already been created and // cannot be modified ErrPodFinalized = errors.New("pod has been finalized") + // ErrVolumeFinalized indicates that the volume has already been created and + // cannot be modified + ErrVolumeFinalized = errors.New("volume has been finalized") // ErrInvalidArg indicates that an invalid argument was passed ErrInvalidArg = errors.New("invalid argument") @@ -55,6 +64,9 @@ var ( // ErrPodRemoved indicates that the pod has already been removed and no // further operations can be performed on it ErrPodRemoved = errors.New("pod has already been removed") + // ErrVolumeRemoved indicates that the volume has already been removed and + // no further operations can be performed on it + ErrVolumeRemoved = errors.New("volume has already been removed") // ErrDBClosed indicates that the connection to the state database has // already been closed diff --git a/libpod/image/prune.go b/libpod/image/prune.go new file mode 100644 index 000000000..6a1f160d5 --- /dev/null +++ b/libpod/image/prune.go @@ -0,0 +1,26 @@ +package image + +// GetPruneImages returns a slice of images that have no names/unused +func (ir *Runtime) GetPruneImages() ([]*Image, error) { + var ( + unamedImages []*Image + ) + allImages, err := ir.GetImages() + if err != nil { + return nil, err + } + for _, i := range allImages { + if len(i.Names()) == 0 { + unamedImages = append(unamedImages, i) + continue + } + containers, err := i.Containers() + if err != nil { + return nil, err + } + if len(containers) < 1 { + unamedImages = append(unamedImages, i) + } + } + return unamedImages, nil +} diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go index 77eba0cc6..314799309 100644 --- a/libpod/in_memory_state.go +++ b/libpod/in_memory_state.go @@ -18,8 +18,10 @@ type InMemoryState struct { pods map[string]*Pod // Maps container ID to container struct. containers map[string]*Container + volumes map[string]*Volume // Maps container ID to a list of IDs of dependencies. - ctrDepends map[string][]string + ctrDepends map[string][]string + volumeDepends map[string][]string // Maps pod ID to a map of container ID to container struct. podContainers map[string]map[string]*Container // Global name registry - ensures name uniqueness and performs lookups. @@ -46,8 +48,10 @@ func NewInMemoryState() (State, error) { state.pods = make(map[string]*Pod) state.containers = make(map[string]*Container) + state.volumes = make(map[string]*Volume) state.ctrDepends = make(map[string][]string) + state.volumeDepends = make(map[string][]string) state.podContainers = make(map[string]map[string]*Container) @@ -244,6 +248,14 @@ func (s *InMemoryState) AddContainer(ctr *Container) error { s.addCtrToDependsMap(ctr.ID(), depCtr) } + // Add container to volume dependencies + for _, vol := range ctr.config.Spec.Mounts { + if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) { + volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0] + s.addCtrToVolDependsMap(ctr.ID(), volName) + } + } + return nil } @@ -294,6 +306,14 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error { s.removeCtrFromDependsMap(ctr.ID(), depCtr) } + // Remove container from volume dependencies + for _, vol := range ctr.config.Spec.Mounts { + if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) { + volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0] + s.removeCtrFromVolDependsMap(ctr.ID(), volName) + } + } + return nil } @@ -358,6 +378,114 @@ func (s *InMemoryState) ContainerInUse(ctr *Container) ([]string, error) { return arr, nil } +// Volume retrieves a volume from its full name +func (s *InMemoryState) Volume(name string) (*Volume, error) { + if name == "" { + return nil, ErrEmptyID + } + + vol, ok := s.volumes[name] + if !ok { + return nil, errors.Wrapf(ErrNoSuchCtr, "no volume with name %s found", name) + } + + return vol, nil +} + +// HasVolume checks if a volume with the given name is present in the state +func (s *InMemoryState) HasVolume(name string) (bool, error) { + if name == "" { + return false, ErrEmptyID + } + + _, ok := s.volumes[name] + if !ok { + return false, nil + } + + return true, nil +} + +// AddVolume adds a volume to the state +func (s *InMemoryState) AddVolume(volume *Volume) error { + if !volume.valid { + return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name()) + } + + if _, ok := s.volumes[volume.Name()]; ok { + return errors.Wrapf(ErrVolumeExists, "volume with name %s already exists in state", volume.Name()) + } + + s.volumes[volume.Name()] = volume + + return nil +} + +// RemoveVolume removes a volume from the state +func (s *InMemoryState) RemoveVolume(volume *Volume) error { + // Ensure we don't remove a volume which containers depend on + deps, ok := s.volumeDepends[volume.Name()] + if ok && len(deps) != 0 { + depsStr := strings.Join(deps, ", ") + return errors.Wrapf(ErrVolumeExists, "the following containers depend on volume %s: %s", volume.Name(), depsStr) + } + + if _, ok := s.volumes[volume.Name()]; !ok { + volume.valid = false + return errors.Wrapf(ErrVolumeRemoved, "no volume exists in state with name %s", volume.Name()) + } + + delete(s.volumes, volume.Name()) + + return nil +} + +// RemoveVolCtrDep updates the container dependencies of the volume +func (s *InMemoryState) RemoveVolCtrDep(volume *Volume, ctrID string) error { + if !volume.valid { + return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name()) + } + + if _, ok := s.volumes[volume.Name()]; !ok { + return errors.Wrapf(ErrNoSuchVolume, "volume with name %s doesn't exists in state", volume.Name()) + } + + // Remove container that is using this volume + s.removeCtrFromVolDependsMap(ctrID, volume.Name()) + + return nil +} + +// VolumeInUse checks if the given volume is being used by at least one container +func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) { + if !volume.valid { + return nil, ErrVolumeRemoved + } + + // If the volume does not exist, return error + if _, ok := s.volumes[volume.Name()]; !ok { + volume.valid = false + return nil, errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found in state", volume.Name()) + } + + arr, ok := s.volumeDepends[volume.Name()] + if !ok { + return []string{}, nil + } + + return arr, nil +} + +// AllVolumes returns all volumes that exist in the state +func (s *InMemoryState) AllVolumes() ([]*Volume, error) { + allVols := make([]*Volume, 0, len(s.volumes)) + for _, v := range s.volumes { + allVols = append(allVols, v) + } + + return allVols, nil +} + // AllContainers retrieves all containers from the state func (s *InMemoryState) AllContainers() ([]*Container, error) { ctrs := make([]*Container, 0, len(s.containers)) @@ -945,6 +1073,44 @@ func (s *InMemoryState) removeCtrFromDependsMap(ctrID, dependsID string) { } } +// Add a container to the dependency mappings for the volume +func (s *InMemoryState) addCtrToVolDependsMap(depCtrID, volName string) { + if volName != "" { + arr, ok := s.volumeDepends[volName] + if !ok { + // Do not have a mapping for that volume yet + s.volumeDepends[volName] = []string{depCtrID} + } else { + // Have a mapping for the volume + arr = append(arr, depCtrID) + s.volumeDepends[volName] = arr + } + } +} + +// Remove a container from the dependency mappings for the volume +func (s *InMemoryState) removeCtrFromVolDependsMap(depCtrID, volName string) { + if volName != "" { + arr, ok := s.volumeDepends[volName] + if !ok { + // Internal state seems inconsistent + // But the dependency is definitely gone + // So just return + return + } + + newArr := make([]string, 0, len(arr)) + + for _, id := range arr { + if id != depCtrID { + newArr = append(newArr, id) + } + } + + s.volumeDepends[volName] = newArr + } +} + // Check if we can access a pod or container, or if that is blocked by // namespaces. func (s *InMemoryState) checkNSMatch(id, ns string) error { diff --git a/libpod/options.go b/libpod/options.go index 3e43d73f0..352e6a506 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -327,6 +327,22 @@ func WithNamespace(ns string) RuntimeOption { } } +// WithVolumePath sets the path under which all named volumes +// should be created. +// The path changes based on whethe rthe user is running as root +// or not. +func WithVolumePath(volPath string) RuntimeOption { + return func(rt *Runtime) error { + if rt.valid { + return ErrRuntimeFinalized + } + + rt.config.VolumePath = volPath + + return nil + } +} + // WithDefaultInfraImage sets the infra image for libpod. // An infra image is used for inter-container kernel // namespace sharing within a pod. Typically, an infra @@ -1125,6 +1141,70 @@ func withIsInfra() CtrCreateOption { } } +// Volume Creation Options + +// WithVolumeName sets the name of the volume. +func WithVolumeName(name string) VolumeCreateOption { + return func(volume *Volume) error { + if volume.valid { + return ErrVolumeFinalized + } + + // Check the name against a regex + if !nameRegex.MatchString(name) { + return errors.Wrapf(ErrInvalidArg, "name must match regex [a-zA-Z0-9_-]+") + } + volume.config.Name = name + + return nil + } +} + +// WithVolumeLabels sets the labels of the volume. +func WithVolumeLabels(labels map[string]string) VolumeCreateOption { + return func(volume *Volume) error { + if volume.valid { + return ErrVolumeFinalized + } + + volume.config.Labels = make(map[string]string) + for key, value := range labels { + volume.config.Labels[key] = value + } + + return nil + } +} + +// WithVolumeDriver sets the driver of the volume. +func WithVolumeDriver(driver string) VolumeCreateOption { + return func(volume *Volume) error { + if volume.valid { + return ErrVolumeFinalized + } + + volume.config.Driver = driver + + return nil + } +} + +// WithVolumeOptions sets the options of the volume. +func WithVolumeOptions(options map[string]string) VolumeCreateOption { + return func(volume *Volume) error { + if volume.valid { + return ErrVolumeFinalized + } + + volume.config.Options = make(map[string]string) + for key, value := range options { + volume.config.Options[key] = value + } + + return nil + } +} + // Pod Creation Options // WithPodName sets the name of the pod. diff --git a/libpod/runtime.go b/libpod/runtime.go index 6b6a8cc2d..82473aae9 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -92,6 +92,7 @@ type RuntimeConfig struct { // Not included in on-disk config, use the dedicated containers/storage // configuration file instead StorageConfig storage.StoreOptions `toml:"-"` + VolumePath string `toml:"volume_path"` // ImageDefaultTransport is the default transport method used to fetch // images ImageDefaultTransport string `toml:"image_default_transport"` @@ -278,12 +279,13 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) { if rootless.IsRootless() { // If we're rootless, override the default storage config - storageConf, err := util.GetDefaultStoreOptions() + storageConf, volumePath, err := util.GetDefaultStoreOptions() if err != nil { return nil, errors.Wrapf(err, "error retrieving rootless storage config") } runtime.config.StorageConfig = storageConf runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod") + runtime.config.VolumePath = volumePath } configPath := ConfigPath diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 09d0ec042..ba8eaacbe 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -154,6 +154,24 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options .. } }() + // Go through the volume mounts and check for named volumes + // If the named volme already exists continue, otherwise create + // the storage for the named volume. + for i, vol := range ctr.config.Spec.Mounts { + if vol.Source[0] != '/' && isNamedVolume(vol.Source) { + volInfo, err := r.state.Volume(vol.Source) + if err != nil { + newVol, err := r.newVolume(ctx, WithVolumeName(vol.Source)) + if err != nil { + logrus.Errorf("error creating named volume %q: %v", vol.Source, err) + } + ctr.config.Spec.Mounts[i].Source = newVol.MountPoint() + continue + } + ctr.config.Spec.Mounts[i].Source = volInfo.MountPoint() + } + } + if ctr.config.LogPath == "" { ctr.config.LogPath = filepath.Join(ctr.config.StaticDir, "ctr.log") } @@ -170,6 +188,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options .. } ctr.config.Mounts = append(ctr.config.Mounts, ctr.config.ShmDir) } + // Add the container to the state // TODO: May be worth looking into recovering from name/ID collisions here if ctr.config.Pod != "" { @@ -474,3 +493,11 @@ func (r *Runtime) GetLatestContainer() (*Container, error) { } return ctrs[lastCreatedIndex], nil } + +// Check if volName is a named volume and not one of the default mounts we add to containers +func isNamedVolume(volName string) bool { + if volName != "proc" && volName != "tmpfs" && volName != "devpts" && volName != "shm" && volName != "mqueue" && volName != "sysfs" && volName != "cgroup" { + return true + } + return false +} diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go new file mode 100644 index 000000000..3921758ee --- /dev/null +++ b/libpod/runtime_volume.go @@ -0,0 +1,107 @@ +package libpod + +import ( + "context" +) + +// Contains the public Runtime API for volumes + +// A VolumeCreateOption is a functional option which alters the Volume created by +// NewVolume +type VolumeCreateOption func(*Volume) error + +// VolumeFilter is a function to determine whether a volume is included in command +// output. Volumes to be outputted are tested using the function. a true return will +// include the volume, a false return will exclude it. +type VolumeFilter func(*Volume) bool + +// RemoveVolume removes a volumes +func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force, prune bool) error { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return ErrRuntimeStopped + } + + if !v.valid { + if ok, _ := r.state.HasVolume(v.Name()); !ok { + // Volume probably already removed + // Or was never in the runtime to begin with + return nil + } + } + + v.lock.Lock() + defer v.lock.Unlock() + + return r.removeVolume(ctx, v, force, prune) +} + +// GetVolume retrieves a volume by its name +func (r *Runtime) GetVolume(name string) (*Volume, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + return r.state.Volume(name) +} + +// HasVolume checks to see if a volume with the given name exists +func (r *Runtime) HasVolume(name string) (bool, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return false, ErrRuntimeStopped + } + + return r.state.HasVolume(name) +} + +// Volumes retrieves all volumes +// Filters can be provided which will determine which volumes are included in the +// output. Multiple filters are handled by ANDing their output, so only volumes +// matching all filters are returned +func (r *Runtime) Volumes(filters ...VolumeFilter) ([]*Volume, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + vols, err := r.state.AllVolumes() + if err != nil { + return nil, err + } + + volsFiltered := make([]*Volume, 0, len(vols)) + for _, vol := range vols { + include := true + for _, filter := range filters { + include = include && filter(vol) + } + + if include { + volsFiltered = append(volsFiltered, vol) + } + } + + return volsFiltered, nil +} + +// GetAllVolumes retrieves all the volumes +func (r *Runtime) GetAllVolumes() ([]*Volume, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + return r.state.AllVolumes() +} diff --git a/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go new file mode 100644 index 000000000..5cc0938f0 --- /dev/null +++ b/libpod/runtime_volume_linux.go @@ -0,0 +1,132 @@ +// +build linux + +package libpod + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/containers/storage" + "github.com/containers/storage/pkg/stringid" + "github.com/opencontainers/selinux/go-selinux/label" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// NewVolume creates a new empty volume +func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + return r.newVolume(ctx, options...) +} + +// newVolume creates a new empty volume +func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) { + volume, err := newVolume(r) + if err != nil { + return nil, errors.Wrapf(err, "error creating volume") + } + + for _, option := range options { + if err := option(volume); err != nil { + return nil, errors.Wrapf(err, "error running volume create option") + } + } + + if volume.config.Name == "" { + volume.config.Name = stringid.GenerateNonCryptoID() + } + // TODO: support for other volume drivers + if volume.config.Driver == "" { + volume.config.Driver = "local" + } + // TODO: determine when the scope is global and set it to that + if volume.config.Scope == "" { + volume.config.Scope = "local" + } + + // Create the mountpoint of this volume + fullVolPath := filepath.Join(r.config.VolumePath, volume.config.Name, "_data") + if err := os.MkdirAll(fullVolPath, 0755); err != nil { + return nil, errors.Wrapf(err, "error creating volume directory %q", fullVolPath) + } + _, mountLabel, err := label.InitLabels([]string{}) + if err != nil { + return nil, errors.Wrapf(err, "error getting default mountlabels") + } + if err := label.ReleaseLabel(mountLabel); err != nil { + return nil, errors.Wrapf(err, "error releasing label %q", mountLabel) + } + if err := label.Relabel(fullVolPath, mountLabel, true); err != nil { + return nil, errors.Wrapf(err, "error setting selinux label to %q", fullVolPath) + } + volume.config.MountPoint = fullVolPath + + // Path our lock file will reside at + lockPath := filepath.Join(r.lockDir, volume.config.Name) + // Grab a lockfile at the given path + lock, err := storage.GetLockfile(lockPath) + if err != nil { + return nil, errors.Wrapf(err, "error creating lockfile for new volume") + } + volume.lock = lock + + volume.valid = true + + // Add the volume to state + if err := r.state.AddVolume(volume); err != nil { + return nil, errors.Wrapf(err, "error adding volume to state") + } + + return volume, nil +} + +// removeVolume removes the specified volume from state as well tears down its mountpoint and storage +func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force, prune bool) error { + if !v.valid { + return ErrNoSuchVolume + } + + deps, err := r.state.VolumeInUse(v) + if err != nil { + return err + } + if len(deps) != 0 { + if prune { + return ErrVolumeBeingUsed + } + depsStr := strings.Join(deps, ", ") + if !force { + return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by the following container(s): %s", v.Name(), depsStr) + } + // If using force, log the warning that the volume is being used by at least one container + logrus.Warnf("volume %s is being used by the following container(s): %s", v.Name(), depsStr) + // Remove the container dependencies so we can go ahead and delete the volume + for _, dep := range deps { + if err := r.state.RemoveVolCtrDep(v, dep); err != nil { + return errors.Wrapf(err, "unable to remove container dependency %q from volume %q while trying to delete volume by force", dep, v.Name()) + } + } + } + + // Delete the mountpoint path of the volume, that is delete the volume from /var/lib/containers/storage/volumes + if err := v.teardownStorage(); err != nil { + return errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name()) + } + + // Remove the volume from the state + if err := r.state.RemoveVolume(v); err != nil { + return errors.Wrapf(err, "error removing volume %s", v.Name()) + } + + // Set volume as invalid so it can no longer be used + v.valid = false + + return nil +} diff --git a/libpod/state.go b/libpod/state.go index 06c2003d8..88d89f673 100644 --- a/libpod/state.go +++ b/libpod/state.go @@ -153,4 +153,27 @@ type State interface { // If a namespace has been set, only pods in that namespace will be // returned. AllPods() ([]*Pod, error) + + // Volume accepts full name of volume + // If the volume doesn't exist, an error will be returned + Volume(volName string) (*Volume, error) + // HasVolume returns true if volName exists in the state, + // otherwise it returns false + HasVolume(volName string) (bool, error) + // VolumeInUse goes through the container dependencies of a volume + // and checks if the volume is being used by any container. If it is + // a slice of container IDs using the volume is returned + VolumeInUse(volume *Volume) ([]string, error) + // AddVolume adds the specified volume to state. The volume's name + // must be unique within the list of existing volumes + AddVolume(volume *Volume) error + // RemoveVolCtrDep updates the list of container dependencies that the + // volume has. It either deletes the dependent container ID from + // the sub-bucket + RemoveVolCtrDep(volume *Volume, ctrID string) error + // RemoveVolume removes the specified volume. + // Only volumes that have no container dependencies can be removed + RemoveVolume(volume *Volume) error + // AllVolumes returns all the volumes available in the state + AllVolumes() ([]*Volume, error) } diff --git a/libpod/volume.go b/libpod/volume.go new file mode 100644 index 000000000..b732e8aa7 --- /dev/null +++ b/libpod/volume.go @@ -0,0 +1,63 @@ +package libpod + +import "github.com/containers/storage" + +// Volume is the type used to create named volumes +// TODO: all volumes should be created using this and the Volume API +type Volume struct { + config *VolumeConfig + + valid bool + runtime *Runtime + lock storage.Locker +} + +// VolumeConfig holds the volume's config information +//easyjson:json +type VolumeConfig struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` + MountPoint string `json:"mountPoint"` + Driver string `json:"driver"` + Options map[string]string `json:"options"` + Scope string `json:"scope"` +} + +// Name retrieves the volume's name +func (v *Volume) Name() string { + return v.config.Name +} + +// Labels returns the volume's labels +func (v *Volume) Labels() map[string]string { + labels := make(map[string]string) + for key, value := range v.config.Labels { + labels[key] = value + } + return labels +} + +// MountPoint returns the volume's mountpoint on the host +func (v *Volume) MountPoint() string { + return v.config.MountPoint +} + +// Driver returns the volume's driver +func (v *Volume) Driver() string { + return v.config.Driver +} + +// Options return the volume's options +func (v *Volume) Options() map[string]string { + options := make(map[string]string) + for key, value := range v.config.Options { + options[key] = value + } + + return options +} + +// Scope returns the scope of the volume +func (v *Volume) Scope() string { + return v.config.Scope +} diff --git a/libpod/volume_internal.go b/libpod/volume_internal.go new file mode 100644 index 000000000..800e6d106 --- /dev/null +++ b/libpod/volume_internal.go @@ -0,0 +1,29 @@ +package libpod + +import ( + "os" + "path/filepath" +) + +// VolumePath is the path under which all volumes that are created using the +// local driver will be created +// const VolumePath = "/var/lib/containers/storage/volumes" + +// Creates a new volume +func newVolume(runtime *Runtime) (*Volume, error) { + volume := new(Volume) + volume.config = new(VolumeConfig) + volume.runtime = runtime + volume.config.Labels = make(map[string]string) + volume.config.Options = make(map[string]string) + + return volume, nil +} + +// teardownStorage deletes the volume from volumePath +func (v *Volume) teardownStorage() error { + if !v.valid { + return ErrNoSuchVolume + } + return os.RemoveAll(filepath.Join(v.runtime.config.VolumePath, v.Name())) +} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index e483253a4..f567f2675 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -250,30 +250,40 @@ func GetRootlessRuntimeDir() (string, error) { return runtimeDir, nil } -// GetRootlessStorageOpts returns the storage ops for containers running as non root -func GetRootlessStorageOpts() (storage.StoreOptions, error) { - var opts storage.StoreOptions - +// GetRootlessDirInfo returns the parent path of where the storage for containers and +// volumes will be in rootless mode +func GetRootlessDirInfo() (string, string, error) { rootlessRuntime, err := GetRootlessRuntimeDir() if err != nil { - return opts, err + return "", "", err } - opts.RunRoot = rootlessRuntime dataDir := os.Getenv("XDG_DATA_HOME") if dataDir == "" { home := os.Getenv("HOME") if home == "" { - return opts, fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty") + return "", "", fmt.Errorf("neither XDG_DATA_HOME nor HOME was set non-empty") } // runc doesn't like symlinks in the rootfs path, and at least // on CoreOS /home is a symlink to /var/home, so resolve any symlink. resolvedHome, err := filepath.EvalSymlinks(home) if err != nil { - return opts, errors.Wrapf(err, "cannot resolve %s", home) + return "", "", errors.Wrapf(err, "cannot resolve %s", home) } dataDir = filepath.Join(resolvedHome, ".local", "share") } + return dataDir, rootlessRuntime, nil +} + +// GetRootlessStorageOpts returns the storage opts for containers running as non root +func GetRootlessStorageOpts() (storage.StoreOptions, error) { + var opts storage.StoreOptions + + dataDir, rootlessRuntime, err := GetRootlessDirInfo() + if err != nil { + return opts, err + } + opts.RunRoot = rootlessRuntime opts.GraphRoot = filepath.Join(dataDir, "containers", "storage") if path, err := exec.LookPath("fuse-overlayfs"); err == nil { opts.GraphDriverName = "overlay" @@ -284,6 +294,15 @@ func GetRootlessStorageOpts() (storage.StoreOptions, error) { return opts, nil } +// GetRootlessVolumeInfo returns where all the name volumes will be created in rootless mode +func GetRootlessVolumeInfo() (string, error) { + dataDir, _, err := GetRootlessDirInfo() + if err != nil { + return "", err + } + return filepath.Join(dataDir, "containers", "storage", "volumes"), nil +} + type tomlOptionsConfig struct { MountProgram string `toml:"mount_program"` } @@ -313,14 +332,21 @@ func getTomlStorage(storeOptions *storage.StoreOptions) *tomlConfig { return config } -// GetDefaultStoreOptions returns the default storage options for containers. -func GetDefaultStoreOptions() (storage.StoreOptions, error) { +// GetDefaultStoreOptions returns the storage ops for containers and the volume path +// for the volume API +// It also returns the path where all named volumes will be created using the volume API +func GetDefaultStoreOptions() (storage.StoreOptions, string, error) { storageOpts := storage.DefaultStoreOptions + volumePath := "/var/lib/containers/storage" if rootless.IsRootless() { var err error storageOpts, err = GetRootlessStorageOpts() if err != nil { - return storageOpts, err + return storageOpts, volumePath, err + } + volumePath, err = GetRootlessVolumeInfo() + if err != nil { + return storageOpts, volumePath, err } storageConf := filepath.Join(os.Getenv("HOME"), ".config/containers/storage.conf") @@ -330,7 +356,7 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) { os.MkdirAll(filepath.Dir(storageConf), 0755) file, err := os.OpenFile(storageConf, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) if err != nil { - return storageOpts, errors.Wrapf(err, "cannot open %s", storageConf) + return storageOpts, volumePath, errors.Wrapf(err, "cannot open %s", storageConf) } tomlConfiguration := getTomlStorage(&storageOpts) @@ -341,5 +367,5 @@ func GetDefaultStoreOptions() (storage.StoreOptions, error) { } } } - return storageOpts, nil + return storageOpts, volumePath, nil } diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go index f4f154ef2..268db5aa1 100644 --- a/test/e2e/libpod_suite_test.go +++ b/test/e2e/libpod_suite_test.go @@ -227,6 +227,17 @@ func (p *PodmanTestIntegration) CleanupPod() { } } +// CleanupVolume cleans up the temporary store +func (p *PodmanTestIntegration) CleanupVolume() { + // Remove all containers + session := p.Podman([]string{"volume", "rm", "-fa"}) + session.Wait(90) + // Nuke tempdir + if err := os.RemoveAll(p.TempDir); err != nil { + fmt.Printf("%q\n", err) + } +} + // PullImages pulls multiple images func (p *PodmanTestIntegration) PullImages(images []string) error { for _, i := range images { diff --git a/test/e2e/prune_test.go b/test/e2e/prune_test.go new file mode 100644 index 000000000..6679a676c --- /dev/null +++ b/test/e2e/prune_test.go @@ -0,0 +1,88 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var pruneImage = ` +FROM alpine:latest +LABEL RUN podman --version +RUN apk update +RUN apk add bash` + +var _ = Describe("Podman 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.Cleanup() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman container prune containers", func() { + top := podmanTest.RunTopContainer("") + top.WaitWithDefaultTimeout() + Expect(top.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"run", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + prune := podmanTest.Podman([]string{"container", "prune"}) + prune.WaitWithDefaultTimeout() + Expect(prune.ExitCode()).To(Equal(0)) + + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + }) + + It("podman image prune none images", func() { + podmanTest.BuildImage(pruneImage, "alpine_bash:latest", "true") + + none := podmanTest.Podman([]string{"images", "-a"}) + none.WaitWithDefaultTimeout() + Expect(none.ExitCode()).To(Equal(0)) + hasNone, _ := none.GrepString("<none>") + Expect(hasNone).To(BeTrue()) + + prune := podmanTest.Podman([]string{"image", "prune"}) + prune.WaitWithDefaultTimeout() + Expect(prune.ExitCode()).To(Equal(0)) + + after := podmanTest.Podman([]string{"images", "-a"}) + after.WaitWithDefaultTimeout() + Expect(none.ExitCode()).To(Equal(0)) + hasNoneAfter, _ := after.GrepString("<none>") + Expect(hasNoneAfter).To(BeFalse()) + }) + + It("podman image prune unused images", func() { + prune := podmanTest.Podman([]string{"image", "prune"}) + prune.WaitWithDefaultTimeout() + Expect(prune.ExitCode()).To(Equal(0)) + + images := podmanTest.Podman([]string{"images", "-a"}) + images.WaitWithDefaultTimeout() + // all images are unused, so they all should be deleted! + Expect(len(images.OutputToStringArray())).To(Equal(0)) + }) + +}) diff --git a/test/e2e/volume_create_test.go b/test/e2e/volume_create_test.go new file mode 100644 index 000000000..50ee63f2a --- /dev/null +++ b/test/e2e/volume_create_test.go @@ -0,0 +1,60 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman volume create", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.CleanupVolume() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman create volume", func() { + session := podmanTest.Podman([]string{"volume", "create"}) + session.WaitWithDefaultTimeout() + volName := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + check := podmanTest.Podman([]string{"volume", "ls", "-q"}) + check.WaitWithDefaultTimeout() + match, _ := check.GrepString(volName) + Expect(match).To(BeTrue()) + Expect(len(check.OutputToStringArray())).To(Equal(1)) + }) + + It("podman create volume with name", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + volName := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + check := podmanTest.Podman([]string{"volume", "ls", "-q"}) + check.WaitWithDefaultTimeout() + match, _ := check.GrepString(volName) + Expect(match).To(BeTrue()) + Expect(len(check.OutputToStringArray())).To(Equal(1)) + }) +}) diff --git a/test/e2e/volume_inspect_test.go b/test/e2e/volume_inspect_test.go new file mode 100644 index 000000000..d0d5a601e --- /dev/null +++ b/test/e2e/volume_inspect_test.go @@ -0,0 +1,77 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman volume inspect", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.CleanupVolume() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman inspect volume", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + volName := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "inspect", volName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.IsJSONOutputValid()).To(BeTrue()) + }) + + It("podman inspect volume with Go format", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + volName := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "inspect", "--format", "{{.Name}}", volName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(volName)) + }) + + It("podman inspect volume with --all flag", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol1"}) + session.WaitWithDefaultTimeout() + volName1 := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create", "myvol2"}) + session.WaitWithDefaultTimeout() + volName2 := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "inspect", "--format", "{{.Name}}", "--all"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(2)) + Expect(session.OutputToStringArray()[0]).To(Equal(volName1)) + Expect(session.OutputToStringArray()[1]).To(Equal(volName2)) + }) +}) diff --git a/test/e2e/volume_ls_test.go b/test/e2e/volume_ls_test.go new file mode 100644 index 000000000..119d29d9b --- /dev/null +++ b/test/e2e/volume_ls_test.go @@ -0,0 +1,84 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman volume ls", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.CleanupVolume() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman ls volume", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(2)) + }) + + It("podman ls volume with JSON format", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls", "--format", "json"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.IsJSONOutputValid()).To(BeTrue()) + }) + + It("podman ls volume with Go template", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls", "--format", "table {{.Name}} {{.Driver}} {{.Scope}}"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(2)) + }) + + It("podman ls volume with --filter flag", func() { + session := podmanTest.Podman([]string{"volume", "create", "--label", "foo=bar", "myvol"}) + volName := session.OutputToString() + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls", "--filter", "label=foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(2)) + Expect(session.OutputToStringArray()[1]).To(ContainSubstring(volName)) + }) +}) diff --git a/test/e2e/volume_prune_test.go b/test/e2e/volume_prune_test.go new file mode 100644 index 000000000..8c0a10e77 --- /dev/null +++ b/test/e2e/volume_prune_test.go @@ -0,0 +1,64 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman volume prune", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.CleanupVolume() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman prune volume", func() { + session := podmanTest.Podman([]string{"volume", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"create", "-v", "myvol:/myvol", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(4)) + + session = podmanTest.Podman([]string{"volume", "prune", "--force"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(2)) + + podmanTest.Cleanup() + }) +}) diff --git a/test/e2e/volume_rm_test.go b/test/e2e/volume_rm_test.go new file mode 100644 index 000000000..cebb09467 --- /dev/null +++ b/test/e2e/volume_rm_test.go @@ -0,0 +1,91 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman volume rm", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.CleanupVolume() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman rm volume", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "rm", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(0)) + }) + + It("podman rm with --force flag", func() { + session := podmanTest.Podman([]string{"create", "-v", "myvol:/myvol", ALPINE, "ls"}) + cid := session.OutputToString() + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "rm", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + Expect(session.ErrorToString()).To(ContainSubstring(cid)) + + session = podmanTest.Podman([]string{"volume", "rm", "-f", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(0)) + + podmanTest.Cleanup() + }) + + It("podman rm with --all flag", func() { + session := podmanTest.Podman([]string{"volume", "create", "myvol"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "rm", "-a"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(0)) + }) +}) diff --git a/vendor.conf b/vendor.conf index 51907f763..94eb6fccc 100644 --- a/vendor.conf +++ b/vendor.conf @@ -12,7 +12,7 @@ github.com/containerd/continuity master github.com/containernetworking/cni v0.7.0-alpha1 github.com/containernetworking/plugins 1562a1e60ed101aacc5e08ed9dbeba8e9f3d4ec1 github.com/containers/image bd10b1b53b2976f215b3f2f848fb8e7cad779aeb -github.com/containers/storage ad0f9c4dfa38fcb160f430ff1d653dc3dae03810 +github.com/containers/storage db40f96d853dfced60c563e61fb66ba231ce7c8d github.com/containers/psgo 5dde6da0bc8831b35243a847625bcf18183bd1ee github.com/coreos/go-systemd v14 github.com/cri-o/ocicni 2d2983e40c242322a56c22a903785e7f83eb378c diff --git a/vendor/github.com/containers/storage/drivers/copy/copy.go b/vendor/github.com/containers/storage/drivers/copy/copy.go new file mode 100644 index 000000000..2617824c5 --- /dev/null +++ b/vendor/github.com/containers/storage/drivers/copy/copy.go @@ -0,0 +1,277 @@ +// +build linux + +package copy + +/* +#include <linux/fs.h> + +#ifndef FICLONE +#define FICLONE _IOW(0x94, 9, int) +#endif +*/ +import "C" +import ( + "container/list" + "fmt" + "io" + "os" + "path/filepath" + "syscall" + "time" + + "github.com/containers/storage/pkg/pools" + "github.com/containers/storage/pkg/system" + rsystem "github.com/opencontainers/runc/libcontainer/system" + "golang.org/x/sys/unix" +) + +// Mode indicates whether to use hardlink or copy content +type Mode int + +const ( + // Content creates a new file, and copies the content of the file + Content Mode = iota + // Hardlink creates a new hardlink to the existing file + Hardlink +) + +func copyRegular(srcPath, dstPath string, fileinfo os.FileInfo, copyWithFileRange, copyWithFileClone *bool) error { + srcFile, err := os.Open(srcPath) + if err != nil { + return err + } + defer srcFile.Close() + + // If the destination file already exists, we shouldn't blow it away + dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileinfo.Mode()) + if err != nil { + return err + } + defer dstFile.Close() + + if *copyWithFileClone { + _, _, err = unix.Syscall(unix.SYS_IOCTL, dstFile.Fd(), C.FICLONE, srcFile.Fd()) + if err == nil { + return nil + } + + *copyWithFileClone = false + if err == unix.EXDEV { + *copyWithFileRange = false + } + } + if *copyWithFileRange { + err = doCopyWithFileRange(srcFile, dstFile, fileinfo) + // Trying the file_clone may not have caught the exdev case + // as the ioctl may not have been available (therefore EINVAL) + if err == unix.EXDEV || err == unix.ENOSYS { + *copyWithFileRange = false + } else { + return err + } + } + return legacyCopy(srcFile, dstFile) +} + +func doCopyWithFileRange(srcFile, dstFile *os.File, fileinfo os.FileInfo) error { + amountLeftToCopy := fileinfo.Size() + + for amountLeftToCopy > 0 { + n, err := unix.CopyFileRange(int(srcFile.Fd()), nil, int(dstFile.Fd()), nil, int(amountLeftToCopy), 0) + if err != nil { + return err + } + + amountLeftToCopy = amountLeftToCopy - int64(n) + } + + return nil +} + +func legacyCopy(srcFile io.Reader, dstFile io.Writer) error { + _, err := pools.Copy(dstFile, srcFile) + + return err +} + +func copyXattr(srcPath, dstPath, attr string) error { + data, err := system.Lgetxattr(srcPath, attr) + if err != nil { + return err + } + if data != nil { + if err := system.Lsetxattr(dstPath, attr, data, 0); err != nil { + return err + } + } + return nil +} + +type fileID struct { + dev uint64 + ino uint64 +} + +type dirMtimeInfo struct { + dstPath *string + stat *syscall.Stat_t +} + +// DirCopy copies or hardlinks the contents of one directory to another, +// properly handling xattrs, and soft links +// +// Copying xattrs can be opted out of by passing false for copyXattrs. +func DirCopy(srcDir, dstDir string, copyMode Mode, copyXattrs bool) error { + copyWithFileRange := true + copyWithFileClone := true + + // This is a map of source file inodes to dst file paths + copiedFiles := make(map[fileID]string) + + dirsToSetMtimes := list.New() + err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + // Rebase path + relPath, err := filepath.Rel(srcDir, srcPath) + if err != nil { + return err + } + + dstPath := filepath.Join(dstDir, relPath) + if err != nil { + return err + } + + stat, ok := f.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("Unable to get raw syscall.Stat_t data for %s", srcPath) + } + + isHardlink := false + + switch f.Mode() & os.ModeType { + case 0: // Regular file + id := fileID{dev: stat.Dev, ino: stat.Ino} + if copyMode == Hardlink { + isHardlink = true + if err2 := os.Link(srcPath, dstPath); err2 != nil { + return err2 + } + } else if hardLinkDstPath, ok := copiedFiles[id]; ok { + if err2 := os.Link(hardLinkDstPath, dstPath); err2 != nil { + return err2 + } + } else { + if err2 := copyRegular(srcPath, dstPath, f, ©WithFileRange, ©WithFileClone); err2 != nil { + return err2 + } + copiedFiles[id] = dstPath + } + + case os.ModeDir: + if err := os.Mkdir(dstPath, f.Mode()); err != nil && !os.IsExist(err) { + return err + } + + case os.ModeSymlink: + link, err := os.Readlink(srcPath) + if err != nil { + return err + } + + if err := os.Symlink(link, dstPath); err != nil { + return err + } + + case os.ModeNamedPipe: + fallthrough + case os.ModeSocket: + if err := unix.Mkfifo(dstPath, stat.Mode); err != nil { + return err + } + + case os.ModeDevice: + if rsystem.RunningInUserNS() { + // cannot create a device if running in user namespace + return nil + } + if err := unix.Mknod(dstPath, stat.Mode, int(stat.Rdev)); err != nil { + return err + } + + default: + return fmt.Errorf("unknown file type for %s", srcPath) + } + + // Everything below is copying metadata from src to dst. All this metadata + // already shares an inode for hardlinks. + if isHardlink { + return nil + } + + if err := os.Lchown(dstPath, int(stat.Uid), int(stat.Gid)); err != nil { + return err + } + + if copyXattrs { + if err := doCopyXattrs(srcPath, dstPath); err != nil { + return err + } + } + + isSymlink := f.Mode()&os.ModeSymlink != 0 + + // There is no LChmod, so ignore mode for symlink. Also, this + // must happen after chown, as that can modify the file mode + if !isSymlink { + if err := os.Chmod(dstPath, f.Mode()); err != nil { + return err + } + } + + // system.Chtimes doesn't support a NOFOLLOW flag atm + // nolint: unconvert + if f.IsDir() { + dirsToSetMtimes.PushFront(&dirMtimeInfo{dstPath: &dstPath, stat: stat}) + } else if !isSymlink { + aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + mTime := time.Unix(int64(stat.Mtim.Sec), int64(stat.Mtim.Nsec)) + if err := system.Chtimes(dstPath, aTime, mTime); err != nil { + return err + } + } else { + ts := []syscall.Timespec{stat.Atim, stat.Mtim} + if err := system.LUtimesNano(dstPath, ts); err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + for e := dirsToSetMtimes.Front(); e != nil; e = e.Next() { + mtimeInfo := e.Value.(*dirMtimeInfo) + ts := []syscall.Timespec{mtimeInfo.stat.Atim, mtimeInfo.stat.Mtim} + if err := system.LUtimesNano(*mtimeInfo.dstPath, ts); err != nil { + return err + } + } + + return nil +} + +func doCopyXattrs(srcPath, dstPath string) error { + if err := copyXattr(srcPath, dstPath, "security.capability"); err != nil { + return err + } + + // We need to copy this attribute if it appears in an overlay upper layer, as + // this function is used to copy those. It is set by overlay if a directory + // is removed and then re-created and should not inherit anything from the + // same dir in the lower dir. + return copyXattr(srcPath, dstPath, "trusted.overlay.opaque") +} diff --git a/vendor/github.com/containers/storage/drivers/devmapper/deviceset.go b/vendor/github.com/containers/storage/drivers/devmapper/deviceset.go index 2801dfdc5..b6f22e90a 100644 --- a/vendor/github.com/containers/storage/drivers/devmapper/deviceset.go +++ b/vendor/github.com/containers/storage/drivers/devmapper/deviceset.go @@ -2401,7 +2401,7 @@ func (devices *DeviceSet) MountDevice(hash, path string, moptions graphdriver.Mo addNouuid := strings.Contains("nouuid", mountOptions) mountOptions = strings.Join(moptions.Options, ",") if addNouuid { - mountOptions = fmt.Sprintf("nouuid,", mountOptions) + mountOptions = fmt.Sprintf("nouuid,%s", mountOptions) } } diff --git a/vendor/github.com/containers/storage/drivers/vfs/copy_linux.go b/vendor/github.com/containers/storage/drivers/vfs/copy_linux.go new file mode 100644 index 000000000..8137fcf67 --- /dev/null +++ b/vendor/github.com/containers/storage/drivers/vfs/copy_linux.go @@ -0,0 +1,7 @@ +package vfs + +import "github.com/containers/storage/drivers/copy" + +func dirCopy(srcDir, dstDir string) error { + return copy.DirCopy(srcDir, dstDir, copy.Content, false) +} diff --git a/vendor/github.com/containers/storage/drivers/vfs/copy_unsupported.go b/vendor/github.com/containers/storage/drivers/vfs/copy_unsupported.go new file mode 100644 index 000000000..8ac80ee1d --- /dev/null +++ b/vendor/github.com/containers/storage/drivers/vfs/copy_unsupported.go @@ -0,0 +1,9 @@ +// +build !linux + +package vfs // import "github.com/containers/storage/drivers/vfs" + +import "github.com/containers/storage/pkg/chrootarchive" + +func dirCopy(srcDir, dstDir string) error { + return chrootarchive.NewArchiver(nil).CopyWithTar(srcDir, dstDir) +} diff --git a/vendor/github.com/containers/storage/drivers/vfs/driver.go b/vendor/github.com/containers/storage/drivers/vfs/driver.go index e3a67a69b..f7f3c75ba 100644 --- a/vendor/github.com/containers/storage/drivers/vfs/driver.go +++ b/vendor/github.com/containers/storage/drivers/vfs/driver.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/containers/storage/drivers" - "github.com/containers/storage/pkg/chrootarchive" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/ostree" "github.com/containers/storage/pkg/system" @@ -15,8 +14,8 @@ import ( ) var ( - // CopyWithTar defines the copy method to use. - CopyWithTar = chrootarchive.NewArchiver(nil).CopyWithTar + // CopyDir defines the copy method to use. + CopyDir = dirCopy ) func init() { @@ -141,7 +140,7 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, ro bool if err != nil { return fmt.Errorf("%s: %s", parent, err) } - if err := CopyWithTar(parentDir, dir); err != nil { + if err := dirCopy(parentDir, dir); err != nil { return err } } diff --git a/vendor/github.com/containers/storage/pkg/archive/example_changes.go b/vendor/github.com/containers/storage/pkg/archive/example_changes.go new file mode 100644 index 000000000..70f9c5564 --- /dev/null +++ b/vendor/github.com/containers/storage/pkg/archive/example_changes.go @@ -0,0 +1,97 @@ +// +build ignore + +// Simple tool to create an archive stream from an old and new directory +// +// By default it will stream the comparison of two temporary directories with junk files +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "path" + + "github.com/containers/storage/pkg/archive" + "github.com/sirupsen/logrus" +) + +var ( + flDebug = flag.Bool("D", false, "debugging output") + flNewDir = flag.String("newdir", "", "") + flOldDir = flag.String("olddir", "", "") + log = logrus.New() +) + +func main() { + flag.Usage = func() { + fmt.Println("Produce a tar from comparing two directory paths. By default a demo tar is created of around 200 files (including hardlinks)") + fmt.Printf("%s [OPTIONS]\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + log.Out = os.Stderr + if (len(os.Getenv("DEBUG")) > 0) || *flDebug { + logrus.SetLevel(logrus.DebugLevel) + } + var newDir, oldDir string + + if len(*flNewDir) == 0 { + var err error + newDir, err = ioutil.TempDir("", "storage-test-newDir") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(newDir) + if _, err := prepareUntarSourceDirectory(100, newDir, true); err != nil { + log.Fatal(err) + } + } else { + newDir = *flNewDir + } + + if len(*flOldDir) == 0 { + oldDir, err := ioutil.TempDir("", "storage-test-oldDir") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(oldDir) + } else { + oldDir = *flOldDir + } + + changes, err := archive.ChangesDirs(newDir, oldDir) + if err != nil { + log.Fatal(err) + } + + a, err := archive.ExportChanges(newDir, changes) + if err != nil { + log.Fatal(err) + } + defer a.Close() + + i, err := io.Copy(os.Stdout, a) + if err != nil && err != io.EOF { + log.Fatal(err) + } + fmt.Fprintf(os.Stderr, "wrote archive of %d bytes", i) +} + +func prepareUntarSourceDirectory(numberOfFiles int, targetPath string, makeLinks bool) (int, error) { + fileData := []byte("fooo") + for n := 0; n < numberOfFiles; n++ { + fileName := fmt.Sprintf("file-%d", n) + if err := ioutil.WriteFile(path.Join(targetPath, fileName), fileData, 0700); err != nil { + return 0, err + } + if makeLinks { + if err := os.Link(path.Join(targetPath, fileName), path.Join(targetPath, fileName+"-link")); err != nil { + return 0, err + } + } + } + totalSize := numberOfFiles * len(fileData) + return totalSize, nil +} diff --git a/vendor/github.com/containers/storage/vendor.conf b/vendor/github.com/containers/storage/vendor.conf index 059ae94f0..fa52584d7 100644 --- a/vendor/github.com/containers/storage/vendor.conf +++ b/vendor/github.com/containers/storage/vendor.conf @@ -9,7 +9,7 @@ github.com/mistifyio/go-zfs c0224de804d438efd11ea6e52ada8014537d6062 github.com/opencontainers/go-digest master github.com/opencontainers/runc 6c22e77604689db8725fa866f0f2ec0b3e8c3a07 github.com/opencontainers/selinux 36a9bc45a08c85f2c52bd9eb32e20267876773bd -github.com/ostreedev/ostree-go aeb02c6b6aa2889db3ef62f7855650755befd460 +github.com/ostreedev/ostree-go master github.com/pborman/uuid 1b00554d822231195d1babd97ff4a781231955c9 github.com/pkg/errors master github.com/pmezard/go-difflib v1.0.0 @@ -21,3 +21,5 @@ github.com/tchap/go-patricia v2.2.6 github.com/vbatts/tar-split v0.10.2 golang.org/x/net 7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6 golang.org/x/sys 07c182904dbd53199946ba614a412c61d3c548f5 +gotest.tools master +github.com/google/go-cmp master |