diff options
Diffstat (limited to 'cmd/podmanV2')
56 files changed, 3994 insertions, 0 deletions
diff --git a/cmd/podmanV2/Makefile b/cmd/podmanV2/Makefile new file mode 100644 index 000000000..147a78d9c --- /dev/null +++ b/cmd/podmanV2/Makefile @@ -0,0 +1,2 @@ +all: + GO111MODULE=off go build -tags 'ABISupport' diff --git a/cmd/podmanV2/README.md b/cmd/podmanV2/README.md new file mode 100644 index 000000000..a17e6f850 --- /dev/null +++ b/cmd/podmanV2/README.md @@ -0,0 +1,113 @@ +# Adding a podman V2 commands + +## Build podman V2 + +```shell script +$ cd $GOPATH/src/github.com/containers/libpod/cmd/podmanV2 +``` +If you wish to include the libpod library in your program, +```shell script +$ go build -tags 'ABISupport' . +``` +The `--remote` flag may be used to connect to the Podman service using the API. +Otherwise, direct calls will be made to the Libpod library. +```shell script +$ go build -tags '!ABISupport' . +``` +The Libpod library is not linked into the executable. +All calls are made via the API and `--remote=False` is an error condition. + +## Adding a new command `podman manifests` +```shell script +$ mkdir -p $GOPATH/src/github.com/containers/libpod/cmd/podmanV2/manifests +``` +Create the file ```$GOPATH/src/github.com/containers/libpod/cmd/podmanV2/manifests/manifest.go``` +```go +package manifests + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // podman _manifests_ + manifestCmd = &cobra.Command{ + Use: "manifest", + Short: "Manage manifests", + Long: "Manage manifests", + Example: "podman manifests IMAGE", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, // Report error if there is no sub command given + } +) +func init() { + // Subscribe command to podman + registry.Commands = append(registry.Commands, registry.CliCommand{ + // _podman manifest_ will support both ABIMode and TunnelMode + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + // The definition for this command + Command: manifestCmd, + }) + // Setup cobra templates, sub commands will inherit + manifestCmd.SetHelpTemplate(registry.HelpTemplate()) + manifestCmd.SetUsageTemplate(registry.UsageTemplate()) +} + +// preRunE populates the image engine for sub commands +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewImageEngine(cmd, args) + return err +} +``` +To "wire" in the `manifest` command, edit the file ```$GOPATH/src/github.com/containers/libpod/cmd/podmanV2/main.go``` to add: +```go +package main + +import _ "github.com/containers/libpod/cmd/podmanV2/manifests" +``` + +## Adding a new sub command `podman manifests list` +Create the file ```$GOPATH/src/github.com/containers/libpod/cmd/podmanV2/manifests/inspect.go``` +```go +package manifests + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // podman manifests _inspect_ + inspectCmd = &cobra.Command{ + Use: "inspect IMAGE", + Short: "Display manifest from image", + Long: "Displays the low-level information on a manifest identified by image name or ID", + RunE: inspect, + Example: "podman manifest DEADBEEF", + } +) + +func init() { + // Subscribe inspect sub command to manifest command + registry.Commands = append(registry.Commands, registry.CliCommand{ + // _podman manifest inspect_ will support both ABIMode and TunnelMode + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + // The definition for this command + Command: inspectCmd, + Parent: manifestCmd, + }) + + // This is where you would configure the cobra flags using inspectCmd.Flags() +} + +// Business logic: cmd is inspectCmd, args is the positional arguments from os.Args +func inspect(cmd *cobra.Command, args []string) error { + // Business logic using registry.ImageEngine + // Do not pull from libpod directly use the domain objects and types + return nil +} +``` diff --git a/cmd/podmanV2/common/netflags.go b/cmd/podmanV2/common/netflags.go new file mode 100644 index 000000000..758f155c8 --- /dev/null +++ b/cmd/podmanV2/common/netflags.go @@ -0,0 +1,108 @@ +package common + +import ( + "net" + + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func getDefaultNetwork() string { + if rootless.IsRootless() { + return "slirp4netns" + } + return "bridge" +} + +func GetNetFlags() *pflag.FlagSet { + netFlags := pflag.FlagSet{} + netFlags.StringSlice( + "add-host", []string{}, + "Add a custom host-to-IP mapping (host:ip) (default [])", + ) + netFlags.StringSlice( + "dns", []string{}, + "Set custom DNS servers", + ) + netFlags.StringSlice( + "dns-opt", []string{}, + "Set custom DNS options", + ) + netFlags.StringSlice( + "dns-search", []string{}, + "Set custom DNS search domains", + ) + netFlags.String( + "ip", "", + "Specify a static IPv4 address for the container", + ) + netFlags.String( + "mac-address", "", + "Container MAC address (e.g. 92:d0:c6:0a:29:33)", + ) + netFlags.String( + "network", getDefaultNetwork(), + "Connect a container to a network", + ) + netFlags.StringSliceP( + "publish", "p", []string{}, + "Publish a container's port, or a range of ports, to the host (default [])", + ) + netFlags.Bool( + "no-hosts", false, + "Do not create /etc/hosts within the container, instead use the version from the image", + ) + return &netFlags +} + +func NetFlagsToNetOptions(cmd *cobra.Command) (*entities.NetOptions, error) { + var ( + err error + ) + opts := entities.NetOptions{} + opts.AddHosts, err = cmd.Flags().GetStringSlice("add-host") + if err != nil { + return nil, err + } + servers, err := cmd.Flags().GetStringSlice("dns") + if err != nil { + return nil, err + } + for _, d := range servers { + if d == "none" { + opts.DNSHost = true + break + } + opts.DNSServers = append(opts.DNSServers, net.ParseIP(d)) + } + opts.DNSSearch, err = cmd.Flags().GetStringSlice("dns-search") + if err != nil { + return nil, err + } + + m, err := cmd.Flags().GetString("mac-address") + if err != nil { + return nil, err + } + if len(m) > 0 { + mac, err := net.ParseMAC(m) + if err != nil { + return nil, err + } + opts.StaticMAC = &mac + } + inputPorts, err := cmd.Flags().GetStringSlice("publish") + if err != nil { + return nil, err + } + if len(inputPorts) > 0 { + opts.PublishPorts, err = createPortBindings(inputPorts) + if err != nil { + return nil, err + } + } + opts.NoHosts, err = cmd.Flags().GetBool("no-hosts") + return &opts, err +} diff --git a/cmd/podmanV2/common/types.go b/cmd/podmanV2/common/types.go new file mode 100644 index 000000000..2427ae975 --- /dev/null +++ b/cmd/podmanV2/common/types.go @@ -0,0 +1,3 @@ +package common + +var DefaultKernelNamespaces = "cgroup,ipc,net,uts" diff --git a/cmd/podmanV2/common/util.go b/cmd/podmanV2/common/util.go new file mode 100644 index 000000000..47bbe12fa --- /dev/null +++ b/cmd/podmanV2/common/util.go @@ -0,0 +1,43 @@ +package common + +import ( + "strconv" + + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" +) + +// createPortBindings iterates ports mappings and exposed ports into a format CNI understands +func createPortBindings(ports []string) ([]ocicni.PortMapping, error) { + // TODO wants someone to rewrite this code in the future + var portBindings []ocicni.PortMapping + // The conversion from []string to natBindings is temporary while mheon reworks the port + // deduplication code. Eventually that step will not be required. + _, natBindings, err := nat.ParsePortSpecs(ports) + if err != nil { + return nil, err + } + for containerPb, hostPb := range natBindings { + var pm ocicni.PortMapping + pm.ContainerPort = int32(containerPb.Int()) + for _, i := range hostPb { + var hostPort int + var err error + pm.HostIP = i.HostIP + if i.HostPort == "" { + hostPort = containerPb.Int() + } else { + hostPort, err = strconv.Atoi(i.HostPort) + if err != nil { + return nil, errors.Wrapf(err, "unable to convert host port to integer") + } + } + + pm.HostPort = int32(hostPort) + pm.Protocol = containerPb.Proto() + portBindings = append(portBindings, pm) + } + } + return portBindings, nil +} diff --git a/cmd/podmanV2/containers/commit.go b/cmd/podmanV2/containers/commit.go new file mode 100644 index 000000000..28eb42f33 --- /dev/null +++ b/cmd/podmanV2/containers/commit.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + commitDescription = `Create an image from a container's changes. Optionally tag the image created, set the author with the --author flag, set the commit message with the --message flag, and make changes to the instructions with the --change flag.` + + commitCommand = &cobra.Command{ + Use: "commit [flags] CONTAINER [IMAGE]", + Short: "Create new image based on the changed container", + Long: commitDescription, + RunE: commit, + PreRunE: preRunE, + Args: cobra.MinimumNArgs(1), + Example: `podman commit -q --message "committing container to image" reverent_golick image-committed + podman commit -q --author "firstName lastName" reverent_golick image-committed + podman commit -q --pause=false containerID image-committed + podman commit containerID`, + } + + // ChangeCmds is the list of valid Changes commands to passed to the Commit call + ChangeCmds = []string{"CMD", "ENTRYPOINT", "ENV", "EXPOSE", "LABEL", "ONBUILD", "STOPSIGNAL", "USER", "VOLUME", "WORKDIR"} +) + +var ( + commitOptions = entities.CommitOptions{ + ImageName: "", + } + iidFile string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: commitCommand, + }) + flags := commitCommand.Flags() + flags.StringArrayVarP(&commitOptions.Changes, "change", "c", []string{}, "Apply the following possible instructions to the created image (default []): "+strings.Join(ChangeCmds, " | ")) + flags.StringVarP(&commitOptions.Format, "format", "f", "oci", "`Format` of the image manifest and metadata") + flags.StringVarP(&iidFile, "iidfile", "", "", "`file` to write the image ID to") + flags.StringVarP(&commitOptions.Message, "message", "m", "", "Set commit message for imported image") + flags.StringVarP(&commitOptions.Author, "author", "a", "", "Set the author for the image committed") + flags.BoolVarP(&commitOptions.Pause, "pause", "p", false, "Pause container during commit") + flags.BoolVarP(&commitOptions.Quiet, "quiet", "q", false, "Suppress output") + flags.BoolVar(&commitOptions.IncludeVolumes, "include-volumes", false, "Include container volumes as image volumes") + +} +func commit(cmd *cobra.Command, args []string) error { + container := args[0] + if len(args) > 1 { + commitOptions.ImageName = args[1] + } + if !commitOptions.Quiet { + commitOptions.Writer = os.Stderr + } + + response, err := registry.ContainerEngine().ContainerCommit(context.Background(), container, commitOptions) + if err != nil { + return err + } + if len(iidFile) > 0 { + if err = ioutil.WriteFile(iidFile, []byte(response.Id), 0644); err != nil { + return errors.Wrapf(err, "failed to write image ID to file %q", iidFile) + } + } + fmt.Println(response.Id) + return nil +} diff --git a/cmd/podmanV2/containers/container.go b/cmd/podmanV2/containers/container.go new file mode 100644 index 000000000..b922eea05 --- /dev/null +++ b/cmd/podmanV2/containers/container.go @@ -0,0 +1,48 @@ +package containers + +import ( + "os" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _container_ + containerCmd = &cobra.Command{ + Use: "container", + Short: "Manage containers", + Long: "Manage containers", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } + + defaultContainerConfig = getDefaultContainerConfig() +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: containerCmd, + }) + containerCmd.SetHelpTemplate(registry.HelpTemplate()) + containerCmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewContainerEngine(cmd, args) + return err +} + +func getDefaultContainerConfig() *config.Config { + defaultContainerConfig, err := config.Default() + if err != nil { + logrus.Error(err) + os.Exit(1) + } + return defaultContainerConfig +} diff --git a/cmd/podmanV2/containers/exists.go b/cmd/podmanV2/containers/exists.go new file mode 100644 index 000000000..22c798fcd --- /dev/null +++ b/cmd/podmanV2/containers/exists.go @@ -0,0 +1,43 @@ +package containers + +import ( + "context" + "os" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + containerExistsDescription = `If the named container exists in local storage, podman container exists exits with 0, otherwise the exit code will be 1.` + + existsCommand = &cobra.Command{ + Use: "exists CONTAINER", + Short: "Check if a container exists in local storage", + Long: containerExistsDescription, + Example: `podman container exists containerID + podman container exists myctr || podman run --name myctr [etc...]`, + RunE: exists, + Args: cobra.ExactArgs(1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: existsCommand, + Parent: containerCmd, + }) +} + +func exists(cmd *cobra.Command, args []string) error { + response, err := registry.ContainerEngine().ContainerExists(context.Background(), args[0]) + if err != nil { + return err + } + if !response.Value { + os.Exit(1) + } + return nil +} diff --git a/cmd/podmanV2/containers/inspect.go b/cmd/podmanV2/containers/inspect.go new file mode 100644 index 000000000..648289f0b --- /dev/null +++ b/cmd/podmanV2/containers/inspect.go @@ -0,0 +1,75 @@ +package containers + +import ( + "context" + "fmt" + "os" + "strings" + "text/template" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" + "github.com/spf13/cobra" +) + +var ( + // podman container _inspect_ + inspectCmd = &cobra.Command{ + Use: "inspect [flags] CONTAINER", + Short: "Display the configuration of a container", + Long: `Displays the low-level information on a container identified by name or ID.`, + PreRunE: preRunE, + RunE: inspect, + Example: `podman container inspect myCtr + podman container inspect -l --format '{{.Id}} {{.Config.Labels}}'`, + } +) + +var ( + inspectOptions entities.ContainerInspectOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCmd, + Parent: containerCmd, + }) + flags := inspectCmd.Flags() + flags.StringVarP(&inspectOptions.Format, "format", "f", "", "Change the output format to a Go template") + flags.BoolVarP(&inspectOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVarP(&inspectOptions.Size, "size", "s", false, "Display total file size") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func inspect(cmd *cobra.Command, args []string) error { + responses, err := registry.ContainerEngine().ContainerInspect(context.Background(), args, inspectOptions) + if err != nil { + return err + } + if inspectOptions.Format == "" { + b, err := jsoniter.MarshalIndent(responses, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil + } + format := inspectOptions.Format + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + tmpl, err := template.New("inspect").Parse(format) + if err != nil { + return err + } + for _, i := range responses { + if err := tmpl.Execute(os.Stdout, i); err != nil { + return err + } + } + return nil +} diff --git a/cmd/podmanV2/containers/kill.go b/cmd/podmanV2/containers/kill.go new file mode 100644 index 000000000..6e6debfec --- /dev/null +++ b/cmd/podmanV2/containers/kill.go @@ -0,0 +1,72 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/signal" + "github.com/spf13/cobra" +) + +var ( + killDescription = "The main process inside each container specified will be sent SIGKILL, or any signal specified with option --signal." + killCommand = &cobra.Command{ + Use: "kill [flags] CONTAINER [CONTAINER...]", + Short: "Kill one or more running containers with a specific signal", + Long: killDescription, + RunE: kill, + PersistentPreRunE: preRunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman kill mywebserver + podman kill 860a4b23 + podman kill --signal TERM ctrID`, + } +) + +var ( + killOptions = entities.KillOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: killCommand, + }) + flags := killCommand.Flags() + flags.BoolVarP(&killOptions.All, "all", "a", false, "Signal all running containers") + flags.StringVarP(&killOptions.Signal, "signal", "s", "KILL", "Signal to send to the container") + flags.BoolVarP(&killOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func kill(cmd *cobra.Command, args []string) error { + var ( + err error + errs utils.OutputErrors + ) + // Check if the signalString provided by the user is valid + // Invalid signals will return err + if _, err = signal.ParseSignalNameOrNumber(killOptions.Signal); err != nil { + return err + } + responses, err := registry.ContainerEngine().ContainerKill(context.Background(), args, killOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/list.go b/cmd/podmanV2/containers/list.go new file mode 100644 index 000000000..630d9bbc7 --- /dev/null +++ b/cmd/podmanV2/containers/list.go @@ -0,0 +1,34 @@ +package containers + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // podman container _list_ + listCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List containers", + Long: "Prints out information about the containers", + RunE: containers, + Example: `podman container list -a + podman container list -a --format "{{.ID}} {{.Image}} {{.Labels}} {{.Mounts}}" + podman container list --size --sort names`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: listCmd, + Parent: containerCmd, + }) +} + +func containers(cmd *cobra.Command, args []string) error { + return nil +} diff --git a/cmd/podmanV2/containers/pause.go b/cmd/podmanV2/containers/pause.go new file mode 100644 index 000000000..a9b91b68f --- /dev/null +++ b/cmd/podmanV2/containers/pause.go @@ -0,0 +1,64 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + pauseDescription = `Pauses one or more running containers. The container name or ID can be used.` + pauseCommand = &cobra.Command{ + Use: "pause [flags] CONTAINER [CONTAINER...]", + Short: "Pause all the processes in one or more containers", + Long: pauseDescription, + RunE: pause, + PersistentPreRunE: preRunE, + Example: `podman pause mywebserver + podman pause 860a4b23 + podman pause -a`, + } + + pauseOpts = entities.PauseUnPauseOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pauseCommand, + }) + flags := pauseCommand.Flags() + flags.BoolVarP(&pauseOpts.All, "all", "a", false, "Pause all running containers") + pauseCommand.SetHelpTemplate(registry.HelpTemplate()) + pauseCommand.SetUsageTemplate(registry.UsageTemplate()) +} + +func pause(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if rootless.IsRootless() && !registry.IsRemote() { + return errors.New("pause is not supported for rootless containers") + } + if len(args) < 1 && !pauseOpts.All { + return errors.Errorf("you must provide at least one container name or id") + } + responses, err := registry.ContainerEngine().ContainerPause(context.Background(), args, pauseOpts) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/ps.go b/cmd/podmanV2/containers/ps.go new file mode 100644 index 000000000..ce3d66c51 --- /dev/null +++ b/cmd/podmanV2/containers/ps.go @@ -0,0 +1,29 @@ +package containers + +import ( + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // podman _ps_ + psCmd = &cobra.Command{ + Use: "ps", + Args: cobra.NoArgs, + Short: listCmd.Short, + Long: listCmd.Long, + PersistentPreRunE: preRunE, + RunE: containers, + Example: strings.Replace(listCmd.Example, "container list", "ps", -1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: psCmd, + }) +} diff --git a/cmd/podmanV2/containers/restart.go b/cmd/podmanV2/containers/restart.go new file mode 100644 index 000000000..1f1bb11fa --- /dev/null +++ b/cmd/podmanV2/containers/restart.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + restartDescription = `Restarts one or more running containers. The container ID or name can be used. + + A timeout before forcibly stopping can be set, but defaults to 10 seconds.` + restartCommand = &cobra.Command{ + Use: "restart [flags] CONTAINER [CONTAINER...]", + Short: "Restart one or more containers", + Long: restartDescription, + RunE: restart, + PersistentPreRunE: preRunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman restart ctrID + podman restart --latest + podman restart ctrID1 ctrID2`, + } +) + +var ( + restartOptions = entities.RestartOptions{} + restartTimeout uint +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: restartCommand, + }) + flags := restartCommand.Flags() + flags.BoolVarP(&restartOptions.All, "all", "a", false, "Restart all non-running containers") + flags.BoolVarP(&restartOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&restartOptions.Running, "running", false, "Restart only running containers when --all is used") + flags.UintVarP(&restartTimeout, "timeout", "t", defaultContainerConfig.Engine.StopTimeout, "Seconds to wait for stop before killing the container") + flags.UintVar(&restartTimeout, "time", defaultContainerConfig.Engine.StopTimeout, "Seconds to wait for stop before killing the container") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func restart(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if len(args) < 1 && !restartOptions.Latest && !restartOptions.All { + return errors.Wrapf(define.ErrInvalidArg, "you must provide at least one container name or ID") + } + + if cmd.Flag("timeout").Changed || cmd.Flag("time").Changed { + restartOptions.Timeout = &restartTimeout + } + responses, err := registry.ContainerEngine().ContainerRestart(context.Background(), args, restartOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/rm.go b/cmd/podmanV2/containers/rm.go new file mode 100644 index 000000000..75655e4cd --- /dev/null +++ b/cmd/podmanV2/containers/rm.go @@ -0,0 +1,94 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + rmDescription = `Removes one or more containers from the host. The container name or ID can be used. + + Command does not remove images. Running or unusable containers will not be removed without the -f option.` + rmCommand = &cobra.Command{ + Use: "rm [flags] CONTAINER [CONTAINER...]", + Short: "Remove one or more containers", + Long: rmDescription, + RunE: rm, + PersistentPreRunE: preRunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, true) + }, + Example: `podman rm imageID + podman rm mywebserver myflaskserver 860a4b23 + podman rm --force --all + podman rm -f c684f0d469f2`, + } +) + +var ( + rmOptions = entities.RmOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCommand, + }) + flags := rmCommand.Flags() + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all containers") + flags.BoolVarP(&rmOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified container is missing") + flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Force removal of a running or unusable container. The default is false") + flags.BoolVarP(&rmOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&rmOptions.Storage, "storage", false, "Remove container from storage library") + flags.BoolVarP(&rmOptions.Volumes, "volumes", "v", false, "Remove anonymous volumes associated with the container") + flags.StringArrayVarP(&rmOptions.CIDFiles, "cidfile", "", nil, "Read the container ID from the file") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("ignore") + _ = flags.MarkHidden("cidfile") + _ = flags.MarkHidden("storage") + } + +} + +func rm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + // Storage conflicts with --all/--latest/--volumes/--cidfile/--ignore + if rmOptions.Storage { + if rmOptions.All || rmOptions.Ignore || rmOptions.Latest || rmOptions.Volumes || rmOptions.CIDFiles != nil { + return errors.Errorf("--storage conflicts with --volumes, --all, --latest, --ignore and --cidfile") + } + } + responses, err := registry.ContainerEngine().ContainerRm(context.Background(), args, rmOptions) + if err != nil { + // TODO exitcode is a global main variable to track exit codes. + // we need this enabled + //if len(c.InputArgs) < 2 { + // exitCode = setExitCode(err) + //} + return err + } + for _, r := range responses { + if r.Err != nil { + // TODO this will not work with the remote client + if errors.Cause(err) == define.ErrWillDeadlock { + logrus.Errorf("Potential deadlock detected - please run 'podman system renumber' to resolve") + } + errs = append(errs, r.Err) + } else { + fmt.Println(r.Id) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/stop.go b/cmd/podmanV2/containers/stop.go new file mode 100644 index 000000000..9a106e8fe --- /dev/null +++ b/cmd/podmanV2/containers/stop.go @@ -0,0 +1,87 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + stopDescription = `Stops one or more running containers. The container name or ID can be used. + + A timeout to forcibly stop the container can also be set but defaults to 10 seconds otherwise.` + stopCommand = &cobra.Command{ + Use: "stop [flags] CONTAINER [CONTAINER...]", + Short: "Stop one or more containers", + Long: stopDescription, + RunE: stop, + PersistentPreRunE: preRunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, true) + }, + Example: `podman stop ctrID + podman stop --latest + podman stop --timeout 2 mywebserver 6e534f14da9d`, + } +) + +var ( + stopOptions = entities.StopOptions{} + stopTimeout uint +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: stopCommand, + }) + flags := stopCommand.Flags() + flags.BoolVarP(&stopOptions.All, "all", "a", false, "Stop all running containers") + flags.BoolVarP(&stopOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified container is missing") + flags.StringArrayVarP(&stopOptions.CIDFiles, "cidfile", "", nil, "Read the container ID from the file") + flags.BoolVarP(&stopOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.UintVar(&stopTimeout, "time", defaultContainerConfig.Engine.StopTimeout, "Seconds to wait for stop before killing the container") + flags.UintVarP(&stopTimeout, "timeout", "t", defaultContainerConfig.Engine.StopTimeout, "Seconds to wait for stop before killing the container") + if registry.EngineOptions.EngineMode == entities.ABIMode { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("cidfile") + _ = flags.MarkHidden("ignore") + } +} + +func stop(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if cmd.Flag("timeout").Changed && cmd.Flag("time").Changed { + return errors.New("the --timeout and --time flags are mutually exclusive") + } + stopOptions.Timeout = defaultContainerConfig.Engine.StopTimeout + if cmd.Flag("timeout").Changed || cmd.Flag("time").Changed { + stopOptions.Timeout = stopTimeout + } + + // TODO How do we access global attributes? + //if c.Bool("trace") { + // span, _ := opentracing.StartSpanFromContext(Ctx, "stopCmd") + // defer span.Finish() + //} + responses, err := registry.ContainerEngine().ContainerStop(context.Background(), args, stopOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/top.go b/cmd/podmanV2/containers/top.go new file mode 100644 index 000000000..a86c12e2a --- /dev/null +++ b/cmd/podmanV2/containers/top.go @@ -0,0 +1,91 @@ +package containers + +import ( + "context" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/psgo" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + topDescription = fmt.Sprintf(`Similar to system "top" command. + + Specify format descriptors to alter the output. + + Running "podman top -l pid pcpu seccomp" will print the process ID, the CPU percentage and the seccomp mode of each process of the latest container. + Format Descriptors: + %s`, strings.Join(psgo.ListDescriptors(), ",")) + + topOptions = entities.TopOptions{} + + topCommand = &cobra.Command{ + Use: "top [flags] CONTAINER [FORMAT-DESCRIPTORS|ARGS]", + Short: "Display the running processes of a container", + Long: topDescription, + PersistentPreRunE: preRunE, + RunE: top, + Args: cobra.ArbitraryArgs, + Example: `podman top ctrID +podman top --latest +podman top ctrID pid seccomp args %C +podman top ctrID -eo user,pid,comm`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: topCommand, + }) + + topCommand.SetHelpTemplate(registry.HelpTemplate()) + topCommand.SetUsageTemplate(registry.UsageTemplate()) + + flags := topCommand.Flags() + flags.SetInterspersed(false) + flags.BoolVar(&topOptions.ListDescriptors, "list-descriptors", false, "") + flags.BoolVarP(&topOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + + _ = flags.MarkHidden("list-descriptors") // meant only for bash completion + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func top(cmd *cobra.Command, args []string) error { + if topOptions.ListDescriptors { + fmt.Println(strings.Join(psgo.ListDescriptors(), "\n")) + return nil + } + + if len(args) < 1 && !topOptions.Latest { + return errors.Errorf("you must provide the name or id of a running container") + } + + if topOptions.Latest { + topOptions.Descriptors = args + } else { + topOptions.NameOrID = args[0] + topOptions.Descriptors = args[1:] + } + + topResponse, err := registry.ContainerEngine().ContainerTop(context.Background(), topOptions) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) + for _, proc := range topResponse.Value { + if _, err := fmt.Fprintln(w, proc); err != nil { + return err + } + } + return w.Flush() +} diff --git a/cmd/podmanV2/containers/unpause.go b/cmd/podmanV2/containers/unpause.go new file mode 100644 index 000000000..6a3179f10 --- /dev/null +++ b/cmd/podmanV2/containers/unpause.go @@ -0,0 +1,61 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + unpauseDescription = `Unpauses one or more previously paused containers. The container name or ID can be used.` + unpauseCommand = &cobra.Command{ + Use: "unpause [flags] CONTAINER [CONTAINER...]", + Short: "Unpause the processes in one or more containers", + Long: unpauseDescription, + RunE: unpause, + PersistentPreRunE: preRunE, + Example: `podman unpause ctrID + podman unpause --all`, + } + unPauseOptions = entities.PauseUnPauseOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: unpauseCommand, + Parent: containerCmd, + }) + flags := unpauseCommand.Flags() + flags.BoolVarP(&unPauseOptions.All, "all", "a", false, "Pause all running containers") +} + +func unpause(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if rootless.IsRootless() && !registry.IsRemote() { + return errors.New("unpause is not supported for rootless containers") + } + if len(args) < 1 && !unPauseOptions.All { + return errors.Errorf("you must provide at least one container name or id") + } + responses, err := registry.ContainerEngine().ContainerUnpause(context.Background(), args, unPauseOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/utils.go b/cmd/podmanV2/containers/utils.go new file mode 100644 index 000000000..0c09d3e40 --- /dev/null +++ b/cmd/podmanV2/containers/utils.go @@ -0,0 +1 @@ +package containers diff --git a/cmd/podmanV2/containers/wait.go b/cmd/podmanV2/containers/wait.go new file mode 100644 index 000000000..bf3c86200 --- /dev/null +++ b/cmd/podmanV2/containers/wait.go @@ -0,0 +1,82 @@ +package containers + +import ( + "context" + "fmt" + "time" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + waitDescription = `Block until one or more containers stop and then print their exit codes. +` + waitCommand = &cobra.Command{ + Use: "wait [flags] CONTAINER [CONTAINER...]", + Short: "Block on one or more containers", + Long: waitDescription, + RunE: wait, + PersistentPreRunE: preRunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman wait --latest + podman wait --interval 5000 ctrID + podman wait ctrID1 ctrID2`, + } +) + +var ( + waitOptions = entities.WaitOptions{} + waitCondition string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: waitCommand, + }) + + flags := waitCommand.Flags() + flags.DurationVarP(&waitOptions.Interval, "interval", "i", time.Duration(250), "Milliseconds to wait before polling for completion") + flags.BoolVarP(&waitOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.StringVar(&waitCondition, "condition", "stopped", "Condition to wait on") + if registry.EngineOptions.EngineMode == entities.ABIMode { + // TODO: This is the same as V1. We could skip creating the flag altogether in V2... + _ = flags.MarkHidden("latest") + } +} + +func wait(cmd *cobra.Command, args []string) error { + var ( + err error + errs utils.OutputErrors + ) + if waitOptions.Interval == 0 { + return errors.New("interval must be greater then 0") + } + + waitOptions.Condition, err = define.StringToContainerStatus(waitCondition) + if err != nil { + return err + } + + responses, err := registry.ContainerEngine().ContainerWait(context.Background(), args, waitOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Error == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Error) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/images/exists.go b/cmd/podmanV2/images/exists.go new file mode 100644 index 000000000..d35d6825e --- /dev/null +++ b/cmd/podmanV2/images/exists.go @@ -0,0 +1,40 @@ +package images + +import ( + "os" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + existsCmd = &cobra.Command{ + Use: "exists IMAGE", + Short: "Check if an image exists in local storage", + Long: `If the named image exists in local storage, podman image exists exits with 0, otherwise the exit code will be 1.`, + Args: cobra.ExactArgs(1), + RunE: exists, + Example: `podman image exists ID + podman image exists IMAGE && podman pull IMAGE`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: existsCmd, + Parent: imageCmd, + }) +} + +func exists(cmd *cobra.Command, args []string) error { + found, err := registry.ImageEngine().Exists(registry.GetContext(), args[0]) + if err != nil { + return err + } + if !found.Value { + os.Exit(1) + } + return nil +} diff --git a/cmd/podmanV2/images/history.go b/cmd/podmanV2/images/history.go new file mode 100644 index 000000000..f6f15e2f2 --- /dev/null +++ b/cmd/podmanV2/images/history.go @@ -0,0 +1,123 @@ +package images + +import ( + "context" + "fmt" + "os" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/report" + "github.com/containers/libpod/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + long = `Displays the history of an image. + + The information can be printed out in an easy to read, or user specified format, and can be truncated.` + + // podman _history_ + historyCmd = &cobra.Command{ + Use: "history [flags] IMAGE", + Short: "Show history of a specified image", + Long: long, + Example: "podman history quay.io/fedora/fedora", + Args: cobra.ExactArgs(1), + PersistentPreRunE: preRunE, + RunE: history, + } + + opts = struct { + human bool + noTrunc bool + quiet bool + format string + }{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: historyCmd, + }) + + historyCmd.SetHelpTemplate(registry.HelpTemplate()) + historyCmd.SetUsageTemplate(registry.UsageTemplate()) + + flags := historyCmd.Flags() + flags.StringVar(&opts.format, "format", "", "Change the output to JSON or a Go template") + flags.BoolVarP(&opts.human, "human", "H", false, "Display sizes and dates in human readable format") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") + flags.BoolVar(&opts.noTrunc, "notruncate", false, "Do not truncate the output") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Display the numeric IDs only") +} + +func history(cmd *cobra.Command, args []string) error { + results, err := registry.ImageEngine().History(context.Background(), args[0], entities.ImageHistoryOptions{}) + if err != nil { + return err + } + + if opts.format == "json" { + var err error + if len(results.Layers) == 0 { + _, err = fmt.Fprintf(os.Stdout, "[]\n") + } else { + // ah-hoc change to "Created": type and format + type layer struct { + entities.ImageHistoryLayer + Created string `json:"Created"` + } + + layers := make([]layer, len(results.Layers)) + for i, l := range results.Layers { + layers[i].ImageHistoryLayer = l + layers[i].Created = time.Unix(l.Created, 0).Format(time.RFC3339) + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + enc := json.NewEncoder(os.Stdout) + err = enc.Encode(layers) + } + return err + } + + // Defaults + hdr := "ID\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n" + row := "{{slice .ID 0 12}}\t{{humanDuration .Created}}\t{{ellipsis .CreatedBy 45}}\t{{.Size}}\t{{.Comment}}\n" + + if len(opts.format) > 0 { + hdr = "" + row = opts.format + if !strings.HasSuffix(opts.format, "\n") { + row += "\n" + } + } else { + switch { + case opts.human: + row = "{{slice .ID 0 12}}\t{{humanDuration .Created}}\t{{ellipsis .CreatedBy 45}}\t{{humanSize .Size}}\t{{.Comment}}\n" + case opts.noTrunc: + row = "{{.ID}}\t{{humanDuration .Created}}\t{{.CreatedBy}}\t{{humanSize .Size}}\t{{.Comment}}\n" + case opts.quiet: + hdr = "" + row = "{{.ID}}\n" + } + } + format := hdr + "{{range . }}" + row + "{{end}}" + + tmpl := template.Must(template.New("report").Funcs(report.PodmanTemplateFuncs()).Parse(format)) + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + + _, _ = w.Write(report.ReportHeader("id", "created", "created by", "size", "comment")) + err = tmpl.Execute(w, results.Layers) + if err != nil { + fmt.Fprintln(os.Stderr, errors.Wrapf(err, "Failed to print report")) + } + w.Flush() + return nil +} diff --git a/cmd/podmanV2/images/image.go b/cmd/podmanV2/images/image.go new file mode 100644 index 000000000..9fc7b21d1 --- /dev/null +++ b/cmd/podmanV2/images/image.go @@ -0,0 +1,35 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _image_ + imageCmd = &cobra.Command{ + Use: "image", + Short: "Manage images", + Long: "Manage images", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageCmd, + }) + imageCmd.SetHelpTemplate(registry.HelpTemplate()) + imageCmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + if _, err := registry.NewImageEngine(cmd, args); err != nil { + return err + } + return nil +} diff --git a/cmd/podmanV2/images/images.go b/cmd/podmanV2/images/images.go new file mode 100644 index 000000000..d00f0996e --- /dev/null +++ b/cmd/podmanV2/images/images.go @@ -0,0 +1,33 @@ +package images + +import ( + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // podman _images_ Alias for podman image _list_ + imagesCmd = &cobra.Command{ + Use: strings.Replace(listCmd.Use, "list", "images", 1), + Args: listCmd.Args, + Short: listCmd.Short, + Long: listCmd.Long, + PreRunE: preRunE, + RunE: listCmd.RunE, + Example: strings.Replace(listCmd.Example, "podman image list", "podman images", -1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imagesCmd, + }) + imagesCmd.SetHelpTemplate(registry.HelpTemplate()) + imagesCmd.SetUsageTemplate(registry.UsageTemplate()) + + imageListFlagSet(imagesCmd.Flags()) +} diff --git a/cmd/podmanV2/images/inspect.go b/cmd/podmanV2/images/inspect.go new file mode 100644 index 000000000..f8fd44571 --- /dev/null +++ b/cmd/podmanV2/images/inspect.go @@ -0,0 +1,124 @@ +package images + +import ( + "strings" + + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + inspectOpts = entities.ImageInspectOptions{} + + // Command: podman image _inspect_ + inspectCmd = &cobra.Command{ + Use: "inspect [flags] IMAGE", + Short: "Display the configuration of an image", + Long: `Displays the low-level information on an image identified by name or ID.`, + PreRunE: populateEngines, + RunE: imageInspect, + Example: `podman image inspect alpine`, + } + + containerEngine entities.ContainerEngine +) + +// Inspect is unique in that it needs both an ImageEngine and a ContainerEngine +func populateEngines(cmd *cobra.Command, args []string) (err error) { + // Populate registry.ImageEngine + err = preRunE(cmd, args) + if err != nil { + return + } + + // Populate registry.ContainerEngine + containerEngine, err = registry.NewContainerEngine(cmd, args) + return +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCmd, + Parent: imageCmd, + }) + + flags := inspectCmd.Flags() + flags.BoolVarP(&inspectOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVarP(&inspectOpts.Size, "size", "s", false, "Display total file size") + flags.StringVarP(&inspectOpts.Format, "format", "f", "", "Change the output format to a Go template") + + if registry.EngineOptions.EngineMode == entities.ABIMode { + // TODO: This is the same as V1. We could skip creating the flag altogether in V2... + _ = flags.MarkHidden("latest") + } +} + +const ( + inspectTypeContainer = "container" + inspectTypeImage = "image" + inspectAll = "all" +) + +func imageInspect(cmd *cobra.Command, args []string) error { + inspectType := inspectTypeImage + latestContainer := inspectOpts.Latest + + if len(args) == 0 && !latestContainer { + return errors.Errorf("container or image name must be specified: podman inspect [options [...]] name") + } + + if len(args) > 0 && latestContainer { + return errors.Errorf("you cannot provide additional arguments with --latest") + } + + if !util.StringInSlice(inspectType, []string{inspectTypeContainer, inspectTypeImage, inspectAll}) { + return errors.Errorf("the only recognized types are %q, %q, and %q", inspectTypeContainer, inspectTypeImage, inspectAll) + } + + outputFormat := inspectOpts.Format + if strings.Contains(outputFormat, "{{.Id}}") { + outputFormat = strings.Replace(outputFormat, "{{.Id}}", formats.IDString, -1) + } + // These fields were renamed, so we need to provide backward compat for + // the old names. + if strings.Contains(outputFormat, ".Src") { + outputFormat = strings.Replace(outputFormat, ".Src", ".Source", -1) + } + if strings.Contains(outputFormat, ".Dst") { + outputFormat = strings.Replace(outputFormat, ".Dst", ".Destination", -1) + } + if strings.Contains(outputFormat, ".ImageID") { + outputFormat = strings.Replace(outputFormat, ".ImageID", ".Image", -1) + } + _ = outputFormat + // if latestContainer { + // lc, err := ctnrRuntime.GetLatestContainer() + // if err != nil { + // return err + // } + // args = append(args, lc.ID()) + // inspectType = inspectTypeContainer + // } + + // inspectedObjects, iterateErr := iterateInput(getContext(), c.Size, args, runtime, inspectType) + // if iterateErr != nil { + // return iterateErr + // } + // + // var out formats.Writer + // if outputFormat != "" && outputFormat != formats.JSONString { + // // template + // out = formats.StdoutTemplateArray{Output: inspectedObjects, Template: outputFormat} + // } else { + // // default is json output + // out = formats.JSONStructArray{Output: inspectedObjects} + // } + // + // return out.Out() + return nil +} diff --git a/cmd/podmanV2/images/list.go b/cmd/podmanV2/images/list.go new file mode 100644 index 000000000..9a5b47299 --- /dev/null +++ b/cmd/podmanV2/images/list.go @@ -0,0 +1,243 @@ +package images + +import ( + "errors" + "fmt" + "os" + "sort" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/report" + "github.com/containers/libpod/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type listFlagType struct { + format string + history bool + noHeading bool + noTrunc bool + quiet bool + sort string + readOnly bool + digests bool +} + +var ( + // Command: podman image _list_ + listCmd = &cobra.Command{ + Use: "list [flag] [IMAGE]", + Aliases: []string{"ls"}, + Args: cobra.MaximumNArgs(1), + Short: "List images in local storage", + Long: "Lists images previously pulled to the system or created on the system.", + RunE: images, + Example: `podman image list --format json + podman image list --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}" + podman image list --filter dangling=true`, + } + + // Options to pull data + listOptions = entities.ImageListOptions{} + + // Options for presenting data + listFlag = listFlagType{} + + sortFields = entities.NewStringSet( + "created", + "id", + "repository", + "size", + "tag") +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: listCmd, + Parent: imageCmd, + }) + imageListFlagSet(listCmd.Flags()) +} + +func imageListFlagSet(flags *pflag.FlagSet) { + flags.BoolVarP(&listOptions.All, "all", "a", false, "Show all images (default hides intermediate images)") + flags.StringSliceVarP(&listOptions.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") + flags.StringVar(&listFlag.format, "format", "", "Change the output format to JSON or a Go template") + flags.BoolVar(&listFlag.digests, "digests", false, "Show digests") + flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings") + flags.BoolVar(&listFlag.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&listFlag.noTrunc, "notruncate", false, "Do not truncate output") + flags.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Display only image IDs") + flags.StringVar(&listFlag.sort, "sort", "created", "Sort by "+sortFields.String()) + flags.BoolVarP(&listFlag.history, "history", "", false, "Display the image name history") +} + +func images(cmd *cobra.Command, args []string) error { + if len(listOptions.Filter) > 0 && len(args) > 0 { + return errors.New("cannot specify an image and a filter(s)") + } + + if len(listOptions.Filter) < 1 && len(args) > 0 { + listOptions.Filter = append(listOptions.Filter, "reference="+args[0]) + } + + if cmd.Flag("sort").Changed && !sortFields.Contains(listFlag.sort) { + return fmt.Errorf("\"%s\" is not a valid field for sorting. Choose from: %s", + listFlag.sort, sortFields.String()) + } + + summaries, err := registry.ImageEngine().List(registry.GetContext(), listOptions) + if err != nil { + return err + } + + imageS := summaries + sort.Slice(imageS, sortFunc(listFlag.sort, imageS)) + + if cmd.Flag("format").Changed && listFlag.format == "json" { + return writeJSON(imageS) + } else { + return writeTemplate(imageS, err) + } +} + +func writeJSON(imageS []*entities.ImageSummary) error { + type image struct { + entities.ImageSummary + Created string + } + + imgs := make([]image, 0, len(imageS)) + for _, e := range imageS { + var h image + h.ImageSummary = *e + h.Created = time.Unix(e.Created, 0).Format(time.RFC3339) + h.RepoTags = nil + + imgs = append(imgs, h) + } + + json := jsoniter.ConfigCompatibleWithStandardLibrary + enc := json.NewEncoder(os.Stdout) + return enc.Encode(imgs) +} + +func writeTemplate(imageS []*entities.ImageSummary, err error) error { + type image struct { + entities.ImageSummary + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + } + + imgs := make([]image, 0, len(imageS)) + for _, e := range imageS { + for _, tag := range e.RepoTags { + var h image + h.ImageSummary = *e + h.Repository, h.Tag = tokenRepoTag(tag) + imgs = append(imgs, h) + } + if e.IsReadOnly() { + listFlag.readOnly = true + } + } + + hdr, row := imageListFormat(listFlag) + format := hdr + "{{range . }}" + row + "{{end}}" + + tmpl := template.Must(template.New("report").Funcs(report.PodmanTemplateFuncs()).Parse(format)) + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer w.Flush() + return tmpl.Execute(w, imgs) +} + +func tokenRepoTag(tag string) (string, string) { + tokens := strings.SplitN(tag, ":", 2) + switch len(tokens) { + case 0: + return tag, "" + case 1: + return tokens[0], "" + case 2: + return tokens[0], tokens[1] + default: + return "<N/A>", "" + } +} + +func sortFunc(key string, data []*entities.ImageSummary) func(i, j int) bool { + switch key { + case "id": + return func(i, j int) bool { + return data[i].ID < data[j].ID + } + case "repository": + return func(i, j int) bool { + return data[i].RepoTags[0] < data[j].RepoTags[0] + } + case "size": + return func(i, j int) bool { + return data[i].Size < data[j].Size + } + case "tag": + return func(i, j int) bool { + return data[i].RepoTags[0] < data[j].RepoTags[0] + } + default: + // case "created": + return func(i, j int) bool { + return data[i].Created >= data[j].Created + } + } +} + +func imageListFormat(flags listFlagType) (string, string) { + if flags.quiet { + return "", "{{slice .ID 0 12}}\n" + } + + // Defaults + hdr := "REPOSITORY\tTAG" + row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}" + + if flags.digests { + hdr += "\tDIGEST" + row += "\t{{.Digest}}" + } + + hdr += "\tIMAGE ID" + if flags.noTrunc { + row += "\tsha256:{{.ID}}" + } else { + row += "\t{{slice .ID 0 12}}" + } + + hdr += "\tCREATED\tSIZE" + row += "\t{{humanDuration .Created}}\t{{humanSize .Size}}" + + if flags.history { + hdr += "\tHISTORY" + row += "\t{{if .History}}{{join .History \", \"}}{{else}}<none>{{end}}" + } + + if flags.readOnly { + hdr += "\tReadOnly" + row += "\t{{.ReadOnly}}" + } + + if flags.noHeading { + hdr = "" + } else { + hdr += "\n" + } + + row += "\n" + return hdr, row +} diff --git a/cmd/podmanV2/images/prune.go b/cmd/podmanV2/images/prune.go new file mode 100644 index 000000000..6577c458e --- /dev/null +++ b/cmd/podmanV2/images/prune.go @@ -0,0 +1,86 @@ +package images + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + pruneDescription = `Removes all unnamed images from local storage. + + If an image is not being used by a container, it will be removed from the system.` + pruneCmd = &cobra.Command{ + Use: "prune", + Args: cobra.NoArgs, + Short: "Remove unused images", + Long: pruneDescription, + RunE: prune, + Example: `podman image prune`, + } + + pruneOpts = entities.ImagePruneOptions{} + force bool + filter = []string{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pruneCmd, + Parent: imageCmd, + }) + + flags := pruneCmd.Flags() + flags.BoolVarP(&pruneOpts.All, "all", "a", false, "Remove all unused images, not just dangling ones") + flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation") + flags.StringArrayVar(&filter, "filter", []string{}, "Provide filter values (e.g. 'label=<key>=<value>')") + +} + +func prune(cmd *cobra.Command, args []string) error { + if !force { + reader := bufio.NewReader(os.Stdin) + fmt.Printf(` +WARNING! This will remove all dangling images. +Are you sure you want to continue? [y/N] `) + answer, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "error reading input") + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + + // TODO Remove once filter refactor is finished and url.Values rules :) + for _, f := range filter { + t := strings.SplitN(f, "=", 2) + pruneOpts.Filters.Add(t[0], t[1]) + } + + results, err := registry.ImageEngine().Prune(registry.GetContext(), pruneOpts) + if err != nil { + return err + } + + for _, i := range results.Report.Id { + fmt.Println(i) + } + + for _, e := range results.Report.Err { + fmt.Fprint(os.Stderr, e.Error()+"\n") + } + + if results.Size > 0 { + fmt.Fprintf(os.Stdout, "Size: %d\n", results.Size) + } + + return nil +} diff --git a/cmd/podmanV2/images/rm.go b/cmd/podmanV2/images/rm.go new file mode 100644 index 000000000..bb5880de3 --- /dev/null +++ b/cmd/podmanV2/images/rm.go @@ -0,0 +1,70 @@ +package images + +import ( + "fmt" + "os" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + rmDescription = "Removes one or more previously pulled or locally created images." + rmCmd = &cobra.Command{ + Use: "rm [flags] IMAGE [IMAGE...]", + Short: "Removes one or more images from local storage", + Long: rmDescription, + PreRunE: preRunE, + RunE: rm, + Example: `podman image rm imageID + podman image rm --force alpine + podman image rm c4dfb1609ee2 93fd78260bd1 c0ed59d05ff7`, + } + + imageOpts = entities.ImageDeleteOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCmd, + Parent: imageCmd, + }) + + flags := rmCmd.Flags() + flags.BoolVarP(&imageOpts.All, "all", "a", false, "Remove all images") + flags.BoolVarP(&imageOpts.Force, "force", "f", false, "Force Removal of the image") +} + +func rm(cmd *cobra.Command, args []string) error { + + if len(args) < 1 && !imageOpts.All { + return errors.Errorf("image name or ID must be specified") + } + if len(args) > 0 && imageOpts.All { + return errors.Errorf("when using the --all switch, you may not pass any images names or IDs") + } + + report, err := registry.ImageEngine().Delete(registry.GetContext(), args, imageOpts) + if err != nil { + switch { + case report != nil && report.ImageNotFound != nil: + fmt.Fprintln(os.Stderr, err.Error()) + registry.SetExitCode(2) + case report != nil && report.ImageInUse != nil: + fmt.Fprintln(os.Stderr, err.Error()) + default: + return err + } + } + + for _, u := range report.Untagged { + fmt.Println("Untagged: " + u) + } + for _, d := range report.Deleted { + fmt.Println("Deleted: " + d) + } + return nil +} diff --git a/cmd/podmanV2/images/rmi.go b/cmd/podmanV2/images/rmi.go new file mode 100644 index 000000000..7f9297bc9 --- /dev/null +++ b/cmd/podmanV2/images/rmi.go @@ -0,0 +1,30 @@ +package images + +import ( + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + rmiCmd = &cobra.Command{ + Use: strings.Replace(rmCmd.Use, "rm ", "rmi ", 1), + Args: rmCmd.Args, + Short: rmCmd.Short, + Long: rmCmd.Long, + PreRunE: rmCmd.PreRunE, + RunE: rmCmd.RunE, + Example: strings.Replace(rmCmd.Example, "podman image rm", "podman rmi", -1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmiCmd, + }) + rmiCmd.SetHelpTemplate(registry.HelpTemplate()) + rmiCmd.SetUsageTemplate(registry.UsageTemplate()) +} diff --git a/cmd/podmanV2/main.go b/cmd/podmanV2/main.go new file mode 100644 index 000000000..bd9fbb25e --- /dev/null +++ b/cmd/podmanV2/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "os" + "reflect" + "runtime" + "strings" + + _ "github.com/containers/libpod/cmd/podmanV2/containers" + _ "github.com/containers/libpod/cmd/podmanV2/images" + _ "github.com/containers/libpod/cmd/podmanV2/networks" + _ "github.com/containers/libpod/cmd/podmanV2/pods" + "github.com/containers/libpod/cmd/podmanV2/registry" + _ "github.com/containers/libpod/cmd/podmanV2/volumes" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/sirupsen/logrus" +) + +func init() { + if err := libpod.SetXdgDirs(); err != nil { + logrus.Errorf(err.Error()) + os.Exit(1) + } + + switch runtime.GOOS { + case "darwin": + fallthrough + case "windows": + registry.EngineOptions.EngineMode = entities.TunnelMode + case "linux": + registry.EngineOptions.EngineMode = entities.ABIMode + default: + logrus.Errorf("%s is not a supported OS", runtime.GOOS) + os.Exit(1) + } + + // TODO: Is there a Cobra way to "peek" at os.Args? + for _, v := range os.Args { + if strings.HasPrefix(v, "--remote") { + registry.EngineOptions.EngineMode = entities.TunnelMode + } + } +} + +func main() { + for _, c := range registry.Commands { + if Contains(registry.EngineOptions.EngineMode, c.Mode) { + parent := rootCmd + if c.Parent != nil { + parent = c.Parent + } + parent.AddCommand(c.Command) + } + } + + Execute() + os.Exit(0) +} + +func Contains(item interface{}, slice interface{}) bool { + s := reflect.ValueOf(slice) + + switch s.Kind() { + case reflect.Array: + fallthrough + case reflect.Slice: + break + default: + return false + } + + for i := 0; i < s.Len(); i++ { + if s.Index(i).Interface() == item { + return true + } + } + return false +} diff --git a/cmd/podmanV2/networks/network.go b/cmd/podmanV2/networks/network.go new file mode 100644 index 000000000..fc92d2321 --- /dev/null +++ b/cmd/podmanV2/networks/network.go @@ -0,0 +1,33 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _network_ + cmd = &cobra.Command{ + Use: "network", + Short: "Manage networks", + Long: "Manage networks", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: cmd, + }) + cmd.SetHelpTemplate(registry.HelpTemplate()) + cmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewContainerEngine(cmd, args) + return err +} diff --git a/cmd/podmanV2/parse/parse.go b/cmd/podmanV2/parse/parse.go new file mode 100644 index 000000000..10d2146fa --- /dev/null +++ b/cmd/podmanV2/parse/parse.go @@ -0,0 +1,233 @@ +//nolint +// most of these validate and parse functions have been taken from projectatomic/docker +// and modified for cri-o +package parse + +import ( + "bufio" + "fmt" + "net" + "net/url" + "os" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + Protocol_TCP Protocol = 0 + Protocol_UDP Protocol = 1 +) + +type Protocol int32 + +// PortMapping specifies the port mapping configurations of a sandbox. +type PortMapping struct { + // Protocol of the port mapping. + Protocol Protocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=runtime.Protocol" json:"protocol,omitempty"` + // Port number within the container. Default: 0 (not specified). + ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` + // Port number on the host. Default: 0 (not specified). + HostPort int32 `protobuf:"varint,3,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` + // Host IP. + HostIp string `protobuf:"bytes,4,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"` +} + +// Note: for flags that are in the form <number><unit>, use the RAMInBytes function +// from the units package in docker/go-units/size.go + +var ( + whiteSpaces = " \t" + alphaRegexp = regexp.MustCompile(`[a-zA-Z]`) + domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`) +) + +// validateExtraHost validates that the specified string is a valid extrahost and returns it. +// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6). +// for add-host flag +func ValidateExtraHost(val string) (string, error) { //nolint + // allow for IPv6 addresses in extra hosts by only splitting on first ":" + arr := strings.SplitN(val, ":", 2) + if len(arr) != 2 || len(arr[0]) == 0 { + return "", fmt.Errorf("bad format for add-host: %q", val) + } + if _, err := validateIPAddress(arr[1]); err != nil { + return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1]) + } + return val, nil +} + +// validateIPAddress validates an Ip address. +// for dns, ip, and ip6 flags also +func validateIPAddress(val string) (string, error) { + var ip = net.ParseIP(strings.TrimSpace(val)) + if ip != nil { + return ip.String(), nil + } + return "", fmt.Errorf("%s is not an ip address", val) +} + +func ValidateDomain(val string) (string, error) { + if alphaRegexp.FindString(val) == "" { + return "", fmt.Errorf("%s is not a valid domain", val) + } + ns := domainRegexp.FindSubmatch([]byte(val)) + if len(ns) > 0 && len(ns[1]) < 255 { + return string(ns[1]), nil + } + return "", fmt.Errorf("%s is not a valid domain", val) +} + +// GetAllLabels retrieves all labels given a potential label file and a number +// of labels provided from the command line. +func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) { + labels := make(map[string]string) + for _, file := range labelFile { + // Use of parseEnvFile still seems safe, as it's missing the + // extra parsing logic of parseEnv. + // There's an argument that we SHOULD be doing that parsing for + // all environment variables, even those sourced from files, but + // that would require a substantial rework. + if err := parseEnvFile(labels, file); err != nil { + // FIXME: parseEnvFile is using parseEnv, so we need to add extra + // logic for labels. + return nil, err + } + } + for _, label := range inputLabels { + split := strings.SplitN(label, "=", 2) + if split[0] == "" { + return nil, errors.Errorf("invalid label format: %q", label) + } + value := "" + if len(split) > 1 { + value = split[1] + } + labels[split[0]] = value + } + return labels, nil +} + +func parseEnv(env map[string]string, line string) error { + data := strings.SplitN(line, "=", 2) + + // catch invalid variables such as "=" or "=A" + if data[0] == "" { + return errors.Errorf("invalid environment variable: %q", line) + } + + // trim the front of a variable, but nothing else + name := strings.TrimLeft(data[0], whiteSpaces) + if strings.ContainsAny(name, whiteSpaces) { + return errors.Errorf("name %q has white spaces, poorly formatted name", name) + } + + if len(data) > 1 { + env[name] = data[1] + } else { + if strings.HasSuffix(name, "*") { + name = strings.TrimSuffix(name, "*") + for _, e := range os.Environ() { + part := strings.SplitN(e, "=", 2) + if len(part) < 2 { + continue + } + if strings.HasPrefix(part[0], name) { + env[part[0]] = part[1] + } + } + } else { + // if only a pass-through variable is given, clean it up. + if val, ok := os.LookupEnv(name); ok { + env[name] = val + } + } + } + return nil +} + +// parseEnvFile reads a file with environment variables enumerated by lines +func parseEnvFile(env map[string]string, filename string) error { + fh, err := os.Open(filename) + if err != nil { + return err + } + defer fh.Close() + + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + // trim the line from all leading whitespace first + line := strings.TrimLeft(scanner.Text(), whiteSpaces) + // line is not empty, and not starting with '#' + if len(line) > 0 && !strings.HasPrefix(line, "#") { + if err := parseEnv(env, line); err != nil { + return err + } + } + } + return scanner.Err() +} + +// ValidateFileName returns an error if filename contains ":" +// as it is currently not supported +func ValidateFileName(filename string) error { + if strings.Contains(filename, ":") { + return errors.Errorf("invalid filename (should not contain ':') %q", filename) + } + return nil +} + +// ValidURL checks a string urlStr is a url or not +func ValidURL(urlStr string) error { + _, err := url.ParseRequestURI(urlStr) + if err != nil { + return errors.Wrapf(err, "invalid url path: %q", urlStr) + } + return nil +} + +// checkAllLatestAndCIDFile checks that --all and --latest are used correctly. +// If cidfile is set, also check for the --cidfile flag. +func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { + argLen := len(args) + if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { + if !cidfile { + return errors.New("unable to lookup values for 'latest' or 'all'") + } else if c.Flags().Lookup("cidfile") == nil { + return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") + } + } + + specifiedAll, _ := c.Flags().GetBool("all") + specifiedLatest, _ := c.Flags().GetBool("latest") + specifiedCIDFile := false + if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { + specifiedCIDFile = true + } + + if specifiedCIDFile && (specifiedAll || specifiedLatest) { + return errors.Errorf("--all, --latest and --cidfile cannot be used together") + } else if specifiedAll && specifiedLatest { + return errors.Errorf("--all and --latest cannot be used together") + } + + if ignoreArgLen { + return nil + } + if (argLen > 0) && (specifiedAll || specifiedLatest) { + return errors.Errorf("no arguments are needed with --all or --latest") + } else if cidfile && (argLen > 0) && (specifiedAll || specifiedLatest || specifiedCIDFile) { + return errors.Errorf("no arguments are needed with --all, --latest or --cidfile") + } + + if specifiedCIDFile { + return nil + } + + if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { + return errors.Errorf("you must provide at least one name or id") + } + return nil +} diff --git a/cmd/podmanV2/parse/parse_test.go b/cmd/podmanV2/parse/parse_test.go new file mode 100644 index 000000000..a6ddc2be9 --- /dev/null +++ b/cmd/podmanV2/parse/parse_test.go @@ -0,0 +1,152 @@ +//nolint +// most of these validate and parse functions have been taken from projectatomic/docker +// and modified for cri-o +package parse + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + Var1 = []string{"ONE=1", "TWO=2"} +) + +func createTmpFile(content []byte) (string, error) { + tmpfile, err := ioutil.TempFile(os.TempDir(), "unittest") + if err != nil { + return "", err + } + + if _, err := tmpfile.Write(content); err != nil { + return "", err + + } + if err := tmpfile.Close(); err != nil { + return "", err + } + return tmpfile.Name(), nil +} + +func TestValidateExtraHost(t *testing.T) { + type args struct { + val string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + //2001:0db8:85a3:0000:0000:8a2e:0370:7334 + {name: "good-ipv4", args: args{val: "foobar:192.168.1.1"}, want: "foobar:192.168.1.1", wantErr: false}, + {name: "bad-ipv4", args: args{val: "foobar:999.999.999.99"}, want: "", wantErr: true}, + {name: "bad-ipv4", args: args{val: "foobar:999.999.999"}, want: "", wantErr: true}, + {name: "noname-ipv4", args: args{val: "192.168.1.1"}, want: "", wantErr: true}, + {name: "noname-ipv4", args: args{val: ":192.168.1.1"}, want: "", wantErr: true}, + {name: "noip", args: args{val: "foobar:"}, want: "", wantErr: true}, + {name: "noip", args: args{val: "foobar"}, want: "", wantErr: true}, + {name: "good-ipv6", args: args{val: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334", wantErr: false}, + {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, + {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334.0000.0000.000"}, want: "", wantErr: true}, + {name: "noname-ipv6", args: args{val: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, + {name: "noname-ipv6", args: args{val: ":2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ValidateExtraHost(tt.args.val) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateExtraHost() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ValidateExtraHost() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_validateIPAddress(t *testing.T) { + type args struct { + val string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {name: "ipv4-good", args: args{val: "192.168.1.1"}, want: "192.168.1.1", wantErr: false}, + {name: "ipv4-bad", args: args{val: "192.168.1.1.1"}, want: "", wantErr: true}, + {name: "ipv4-bad", args: args{val: "192."}, want: "", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateIPAddress(tt.args.val) + if (err != nil) != tt.wantErr { + t.Errorf("validateIPAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("validateIPAddress() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValidateFileName(t *testing.T) { + type args struct { + filename string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "good", args: args{filename: "/some/rand/path"}, wantErr: false}, + {name: "good", args: args{filename: "some/rand/path"}, wantErr: false}, + {name: "good", args: args{filename: "/"}, wantErr: false}, + {name: "bad", args: args{filename: "/:"}, wantErr: true}, + {name: "bad", args: args{filename: ":/"}, wantErr: true}, + {name: "bad", args: args{filename: "/some/rand:/path"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateFileName(tt.args.filename); (err != nil) != tt.wantErr { + t.Errorf("ValidateFileName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllLabels(t *testing.T) { + fileLabels := []string{} + labels, _ := GetAllLabels(fileLabels, Var1) + assert.Equal(t, len(labels), 2) +} + +func TestGetAllLabelsBadKeyValue(t *testing.T) { + inLabels := []string{"=badValue", "="} + fileLabels := []string{} + _, err := GetAllLabels(fileLabels, inLabels) + assert.Error(t, err, assert.AnError) +} + +func TestGetAllLabelsBadLabelFile(t *testing.T) { + fileLabels := []string{"/foobar5001/be"} + _, err := GetAllLabels(fileLabels, Var1) + assert.Error(t, err, assert.AnError) +} + +func TestGetAllLabelsFile(t *testing.T) { + content := []byte("THREE=3") + tFile, err := createTmpFile(content) + defer os.Remove(tFile) + assert.NoError(t, err) + fileLabels := []string{tFile} + result, _ := GetAllLabels(fileLabels, Var1) + assert.Equal(t, len(result), 3) +} diff --git a/cmd/podmanV2/pods/create.go b/cmd/podmanV2/pods/create.go new file mode 100644 index 000000000..ab8957ee3 --- /dev/null +++ b/cmd/podmanV2/pods/create.go @@ -0,0 +1,132 @@ +package pods + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podmanV2/common" + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/errorhandling" + "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + podCreateDescription = `After creating the pod, the pod ID is printed to stdout. + + You can then start it at any time with the podman pod start <pod_id> command. The pod will be created with the initial state 'created'.` + + createCommand = &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "Create a new empty pod", + Long: podCreateDescription, + RunE: create, + } +) + +var ( + createOptions entities.PodCreateOptions + labels, labelFile []string + podIDFile string + share string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: createCommand, + Parent: podCmd, + }) + flags := createCommand.Flags() + flags.SetInterspersed(false) + flags.AddFlagSet(common.GetNetFlags()) + flags.StringVar(&createOptions.CGroupParent, "cgroup-parent", "", "Set parent cgroup for the pod") + flags.BoolVar(&createOptions.Infra, "infra", true, "Create an infra container associated with the pod to share namespaces with") + flags.StringVar(&createOptions.InfraImage, "infra-image", define.DefaultInfraImage, "The image of the infra container to associate with the pod") + flags.StringVar(&createOptions.InfraCommand, "infra-command", define.DefaultInfraCommand, "The command to run on the infra container when the pod is started") + flags.StringSliceVar(&labelFile, "label-file", []string{}, "Read in a line delimited file of labels") + flags.StringSliceVarP(&labels, "label", "l", []string{}, "Set metadata on pod (default [])") + flags.StringVarP(&createOptions.Name, "name", "n", "", "Assign a name to the pod") + flags.StringVarP(&createOptions.Hostname, "hostname", "", "", "Set a hostname to the pod") + flags.StringVar(&podIDFile, "pod-id-file", "", "Write the pod ID to the file") + flags.StringVar(&share, "share", common.DefaultKernelNamespaces, "A comma delimited list of kernel namespaces the pod will share") +} + +func create(cmd *cobra.Command, args []string) error { + var ( + err error + podIdFile *os.File + ) + createOptions.Labels, err = parse.GetAllLabels(labelFile, labels) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + + if !createOptions.Infra && cmd.Flag("share").Changed && share != "none" && share != "" { + return errors.Errorf("You cannot share kernel namespaces on the pod level without an infra container") + } + createOptions.Share = strings.Split(share, ",") + if cmd.Flag("pod-id-file").Changed { + podIdFile, err = util.OpenExclusiveFile(podIDFile) + if err != nil && os.IsExist(err) { + return errors.Errorf("pod id file exists. Ensure another pod is not using it or delete %s", podIDFile) + } + if err != nil { + return errors.Errorf("error opening pod-id-file %s", podIDFile) + } + defer errorhandling.CloseQuiet(podIdFile) + defer errorhandling.SyncQuiet(podIdFile) + } + + createOptions.Net, err = common.NetFlagsToNetOptions(cmd) + if err != nil { + return err + } + netInput, err := cmd.Flags().GetString("network") + if err != nil { + return err + } + n := specgen.Namespace{} + switch netInput { + case "bridge": + n.NSMode = specgen.Bridge + case "host": + n.NSMode = specgen.Host + case "slip4netns": + n.NSMode = specgen.Slirp + default: + if strings.HasPrefix(netInput, "container:") { //nolint + split := strings.Split(netInput, ":") + if len(split) != 2 { + return errors.Errorf("invalid network paramater: %q", netInput) + } + n.NSMode = specgen.FromContainer + n.Value = split[1] + } else if strings.HasPrefix(netInput, "ns:") { + return errors.New("the ns: network option is not supported for pods") + } else { + n.NSMode = specgen.Bridge + createOptions.Net.CNINetworks = strings.Split(netInput, ",") + } + } + if len(createOptions.Net.PublishPorts) > 0 { + if !createOptions.Infra { + return errors.Errorf("you must have an infra container to publish port bindings to the host") + } + } + + response, err := registry.ContainerEngine().PodCreate(context.Background(), createOptions) + if err != nil { + return err + } + fmt.Println(response.Id) + return nil +} diff --git a/cmd/podmanV2/pods/exists.go b/cmd/podmanV2/pods/exists.go new file mode 100644 index 000000000..e37f2ebd7 --- /dev/null +++ b/cmd/podmanV2/pods/exists.go @@ -0,0 +1,43 @@ +package pods + +import ( + "context" + "os" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podExistsDescription = `If the named pod exists in local storage, podman pod exists exits with 0, otherwise the exit code will be 1.` + + existsCommand = &cobra.Command{ + Use: "exists POD", + Short: "Check if a pod exists in local storage", + Long: podExistsDescription, + RunE: exists, + Args: cobra.ExactArgs(1), + Example: `podman pod exists podID + podman pod exists mypod || podman pod create --name mypod`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: existsCommand, + Parent: podCmd, + }) +} + +func exists(cmd *cobra.Command, args []string) error { + response, err := registry.ContainerEngine().PodExists(context.Background(), args[0]) + if err != nil { + return err + } + if !response.Value { + os.Exit(1) + } + return nil +} diff --git a/cmd/podmanV2/pods/kill.go b/cmd/podmanV2/pods/kill.go new file mode 100644 index 000000000..06cca916c --- /dev/null +++ b/cmd/podmanV2/pods/kill.go @@ -0,0 +1,68 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podKillDescription = `Signals are sent to the main process of each container inside the specified pod. + + The default signal is SIGKILL, or any signal specified with option --signal.` + killCommand = &cobra.Command{ + Use: "kill [flags] POD [POD...]", + Short: "Send the specified signal or SIGKILL to containers in pod", + Long: podKillDescription, + RunE: kill, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod kill podID + podman pod kill --signal TERM mywebserver + podman pod kill --latest`, + } +) + +var ( + killOpts entities.PodKillOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: killCommand, + Parent: podCmd, + }) + flags := killCommand.Flags() + flags.BoolVarP(&killOpts.All, "all", "a", false, "Kill all containers in all pods") + flags.BoolVarP(&killOpts.Latest, "latest", "l", false, "Act on the latest pod podman is aware of") + flags.StringVarP(&killOpts.Signal, "signal", "s", "KILL", "Signal to send to the containers in the pod") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } + +} +func kill(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + responses, err := registry.ContainerEngine().PodKill(context.Background(), args, killOpts) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if len(r.Errs) == 0 { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Errs...) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/pods/pause.go b/cmd/podmanV2/pods/pause.go new file mode 100644 index 000000000..dc86e534d --- /dev/null +++ b/cmd/podmanV2/pods/pause.go @@ -0,0 +1,66 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podPauseDescription = `The pod name or ID can be used. + + All running containers within each specified pod will then be paused.` + pauseCommand = &cobra.Command{ + Use: "pause [flags] POD [POD...]", + Short: "Pause one or more pods", + Long: podPauseDescription, + RunE: pause, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod pause podID1 podID2 + podman pod pause --latest + podman pod pause --all`, + } +) + +var ( + pauseOptions entities.PodPauseOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pauseCommand, + Parent: podCmd, + }) + flags := pauseCommand.Flags() + flags.BoolVarP(&pauseOptions.All, "all", "a", false, "Pause all running pods") + flags.BoolVarP(&pauseOptions.Latest, "latest", "l", false, "Act on the latest pod podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} +func pause(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + responses, err := registry.ContainerEngine().PodPause(context.Background(), args, pauseOptions) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if len(r.Errs) == 0 { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Errs...) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/pods/pod.go b/cmd/podmanV2/pods/pod.go new file mode 100644 index 000000000..81c0d33e1 --- /dev/null +++ b/cmd/podmanV2/pods/pod.go @@ -0,0 +1,33 @@ +package pods + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _pod_ + podCmd = &cobra.Command{ + Use: "pod", + Short: "Manage pods", + Long: "Manage pods", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: podCmd, + }) + podCmd.SetHelpTemplate(registry.HelpTemplate()) + podCmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewContainerEngine(cmd, args) + return err +} diff --git a/cmd/podmanV2/pods/ps.go b/cmd/podmanV2/pods/ps.go new file mode 100644 index 000000000..d4c625b2e --- /dev/null +++ b/cmd/podmanV2/pods/ps.go @@ -0,0 +1,32 @@ +package pods + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + psDescription = "List all pods on system including their names, ids and current state." + + // Command: podman pod _ps_ + psCmd = &cobra.Command{ + Use: "ps", + Aliases: []string{"ls", "list"}, + Short: "list pods", + Long: psDescription, + RunE: pods, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: psCmd, + Parent: podCmd, + }) +} + +func pods(cmd *cobra.Command, args []string) error { + return nil +} diff --git a/cmd/podmanV2/pods/restart.go b/cmd/podmanV2/pods/restart.go new file mode 100644 index 000000000..1c8709704 --- /dev/null +++ b/cmd/podmanV2/pods/restart.go @@ -0,0 +1,68 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podRestartDescription = `The pod ID or name can be used. + + All of the containers within each of the specified pods will be restarted. If a container in a pod is not currently running it will be started.` + restartCommand = &cobra.Command{ + Use: "restart [flags] POD [POD...]", + Short: "Restart one or more pods", + Long: podRestartDescription, + RunE: restart, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod restart podID1 podID2 + podman pod restart --latest + podman pod restart --all`, + } +) + +var ( + restartOptions = entities.PodRestartOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: restartCommand, + Parent: podCmd, + }) + + flags := restartCommand.Flags() + flags.BoolVarP(&restartOptions.All, "all", "a", false, "Restart all running pods") + flags.BoolVarP(&restartOptions.Latest, "latest", "l", false, "Restart the latest pod podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func restart(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + responses, err := registry.ContainerEngine().PodRestart(context.Background(), args, restartOptions) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if len(r.Errs) == 0 { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Errs...) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/pods/rm.go b/cmd/podmanV2/pods/rm.go new file mode 100644 index 000000000..b43dd2d6c --- /dev/null +++ b/cmd/podmanV2/pods/rm.go @@ -0,0 +1,71 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podRmDescription = fmt.Sprintf(`podman rm will remove one or more stopped pods and their containers from the host. + + The pod name or ID can be used. A pod with containers will not be removed without --force. If --force is specified, all containers will be stopped, then removed.`) + rmCommand = &cobra.Command{ + Use: "rm [flags] POD [POD...]", + Short: "Remove one or more pods", + Long: podRmDescription, + RunE: rm, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod rm mywebserverpod + podman pod rm -f 860a4b23 + podman pod rm -f -a`, + } +) + +var ( + rmOptions = entities.PodRmOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCommand, + Parent: podCmd, + }) + + flags := rmCommand.Flags() + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Restart all running pods") + flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Force removal of a running pod by first stopping all containers, then removing all containers in the pod. The default is false") + flags.BoolVarP(&rmOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified pod is missing") + flags.BoolVarP(&rmOptions.Latest, "latest", "l", false, "Restart the latest pod podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("ignore") + } +} + +func rm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + responses, err := registry.ContainerEngine().PodRm(context.Background(), args, rmOptions) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/pods/start.go b/cmd/podmanV2/pods/start.go new file mode 100644 index 000000000..11ac312f9 --- /dev/null +++ b/cmd/podmanV2/pods/start.go @@ -0,0 +1,68 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podStartDescription = `The pod name or ID can be used. + + All containers defined in the pod will be started.` + startCommand = &cobra.Command{ + Use: "start [flags] POD [POD...]", + Short: "Start one or more pods", + Long: podStartDescription, + RunE: start, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod start podID + podman pod start --latest + podman pod start --all`, + } +) + +var ( + startOptions = entities.PodStartOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: startCommand, + Parent: podCmd, + }) + + flags := startCommand.Flags() + flags.BoolVarP(&startOptions.All, "all", "a", false, "Restart all running pods") + flags.BoolVarP(&startOptions.Latest, "latest", "l", false, "Restart the latest pod podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func start(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + responses, err := registry.ContainerEngine().PodStart(context.Background(), args, startOptions) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if len(r.Errs) == 0 { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Errs...) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/pods/stop.go b/cmd/podmanV2/pods/stop.go new file mode 100644 index 000000000..2b61850e2 --- /dev/null +++ b/cmd/podmanV2/pods/stop.go @@ -0,0 +1,78 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podStopDescription = `The pod name or ID can be used. + + This command will stop all running containers in each of the specified pods.` + + stopCommand = &cobra.Command{ + Use: "stop [flags] POD [POD...]", + Short: "Stop one or more pods", + Long: podStopDescription, + RunE: stop, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod stop mywebserverpod + podman pod stop --latest + podman pod stop --timeout 0 490eb 3557fb`, + } +) + +var ( + stopOptions = entities.PodStopOptions{ + Timeout: -1, + } + timeout uint +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: stopCommand, + Parent: podCmd, + }) + flags := stopCommand.Flags() + flags.BoolVarP(&stopOptions.All, "all", "a", false, "Stop all running pods") + flags.BoolVarP(&stopOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified pod is missing") + flags.BoolVarP(&stopOptions.Latest, "latest", "l", false, "Stop the latest pod podman is aware of") + flags.UintVarP(&timeout, "timeout", "t", 0, "Seconds to wait for pod stop before killing the container") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("ignore") + + } +} + +func stop(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if cmd.Flag("timeout").Changed { + stopOptions.Timeout = int(timeout) + } + responses, err := registry.ContainerEngine().PodStop(context.Background(), args, stopOptions) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if len(r.Errs) == 0 { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Errs...) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/pods/unpause.go b/cmd/podmanV2/pods/unpause.go new file mode 100644 index 000000000..2de7b964f --- /dev/null +++ b/cmd/podmanV2/pods/unpause.go @@ -0,0 +1,66 @@ +package pods + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podUnpauseDescription = `The podman unpause command will unpause all "paused" containers assigned to the pod. + + The pod name or ID can be used.` + unpauseCommand = &cobra.Command{ + Use: "unpause [flags] POD [POD...]", + Short: "Unpause one or more pods", + Long: podUnpauseDescription, + RunE: unpause, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman pod unpause podID1 podID2 + podman pod unpause --all + podman pod unpause --latest`, + } +) + +var ( + unpauseOptions entities.PodunpauseOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: unpauseCommand, + Parent: podCmd, + }) + flags := unpauseCommand.Flags() + flags.BoolVarP(&unpauseOptions.All, "all", "a", false, "Pause all running pods") + flags.BoolVarP(&unpauseOptions.Latest, "latest", "l", false, "Act on the latest pod podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} +func unpause(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + responses, err := registry.ContainerEngine().PodUnpause(context.Background(), args, unpauseOptions) + if err != nil { + return err + } + // in the cli, first we print out all the successful attempts + for _, r := range responses { + if len(r.Errs) == 0 { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Errs...) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/registry/registry.go b/cmd/podmanV2/registry/registry.go new file mode 100644 index 000000000..5cdb8a840 --- /dev/null +++ b/cmd/podmanV2/registry/registry.go @@ -0,0 +1,137 @@ +package registry + +import ( + "context" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type CobraFuncs func(cmd *cobra.Command, args []string) error + +type CliCommand struct { + Mode []entities.EngineMode + Command *cobra.Command + Parent *cobra.Command +} + +var ( + Commands []CliCommand + + imageEngine entities.ImageEngine + containerEngine entities.ContainerEngine + cliCtx context.Context + + EngineOptions entities.EngineOptions + + ExitCode = define.ExecErrorCodeGeneric +) + +func SetExitCode(code int) { + ExitCode = code +} + +func GetExitCode() int { + return ExitCode +} + +// HelpTemplate returns the help template for podman commands +// This uses the short and long options. +// command should not use this. +func HelpTemplate() string { + return `{{.Short}} + +Description: + {{.Long}} + +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` +} + +// UsageTemplate returns the usage template for podman commands +// This blocks the displaying of the global options. The main podman +// command should not use this. +func UsageTemplate() string { + return `Usage(v2):{{if (and .Runnable (not .HasAvailableSubCommands))}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: + {{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} +{{end}} +` +} + +func ImageEngine() entities.ImageEngine { + return imageEngine +} + +// NewImageEngine is a wrapper for building an ImageEngine to be used for PreRunE functions +func NewImageEngine(cmd *cobra.Command, args []string) (entities.ImageEngine, error) { + if imageEngine == nil { + EngineOptions.FlagSet = cmd.Flags() + engine, err := infra.NewImageEngine(EngineOptions) + if err != nil { + return nil, err + } + imageEngine = engine + } + return imageEngine, nil +} + +func ContainerEngine() entities.ContainerEngine { + return containerEngine +} + +// NewContainerEngine is a wrapper for building an ContainerEngine to be used for PreRunE functions +func NewContainerEngine(cmd *cobra.Command, args []string) (entities.ContainerEngine, error) { + if containerEngine == nil { + EngineOptions.FlagSet = cmd.Flags() + engine, err := infra.NewContainerEngine(EngineOptions) + if err != nil { + return nil, err + } + containerEngine = engine + } + return containerEngine, nil +} + +func SubCommandExists(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return errors.Errorf("unrecognized command `%[1]s %[2]s`\nTry '%[1]s --help' for more information.", cmd.CommandPath(), args[0]) + } + return errors.Errorf("missing command '%[1]s COMMAND'\nTry '%[1]s --help' for more information.", cmd.CommandPath()) +} + +type podmanContextKey string + +var podmanFactsKey = podmanContextKey("engineOptions") + +func NewOptions(ctx context.Context, facts *entities.EngineOptions) context.Context { + return context.WithValue(ctx, podmanFactsKey, facts) +} + +func Options(cmd *cobra.Command) (*entities.EngineOptions, error) { + if f, ok := cmd.Context().Value(podmanFactsKey).(*entities.EngineOptions); ok { + return f, errors.New("Command Context ") + } + return nil, nil +} + +func GetContext() context.Context { + if cliCtx == nil { + cliCtx = context.TODO() + } + return cliCtx +} diff --git a/cmd/podmanV2/registry/remote.go b/cmd/podmanV2/registry/remote.go new file mode 100644 index 000000000..32a231ac4 --- /dev/null +++ b/cmd/podmanV2/registry/remote.go @@ -0,0 +1,9 @@ +package registry + +import ( + "github.com/containers/libpod/pkg/domain/entities" +) + +func IsRemote() bool { + return EngineOptions.EngineMode == entities.TunnelMode +} diff --git a/cmd/podmanV2/report/templates.go b/cmd/podmanV2/report/templates.go new file mode 100644 index 000000000..f3bc06405 --- /dev/null +++ b/cmd/podmanV2/report/templates.go @@ -0,0 +1,70 @@ +package report + +import ( + "strings" + "text/template" + "time" + "unicode" + + "github.com/docker/go-units" +) + +var defaultFuncMap = template.FuncMap{ + "ellipsis": func(s string, length int) string { + if len(s) > length { + return s[:length-3] + "..." + } + return s + }, + "humanDuration": func(t int64) string { + return units.HumanDuration(time.Since(time.Unix(t, 0))) + " ago" + }, + "humanSize": func(sz int64) string { + s := units.HumanSizeWithPrecision(float64(sz), 3) + i := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:i+1] + " " + s[i+1:] + }, + "join": strings.Join, + "lower": strings.ToLower, + "rfc3339": func(t int64) string { + return time.Unix(t, 0).Format(time.RFC3339) + }, + "replace": strings.Replace, + "split": strings.Split, + "title": strings.Title, + "upper": strings.ToUpper, + // TODO: Remove after Go 1.14 port + "slice": func(s string, i, j int) string { + if i > j || len(s) < i { + return s + } + if len(s) < j { + return s[i:] + } + return s[i:j] + }, +} + +func ReportHeader(columns ...string) []byte { + hdr := make([]string, len(columns)) + for i, h := range columns { + hdr[i] = strings.ToUpper(h) + } + return []byte(strings.Join(hdr, "\t") + "\n") +} + +func AppendFuncMap(funcMap template.FuncMap) template.FuncMap { + merged := PodmanTemplateFuncs() + for k, v := range funcMap { + merged[k] = v + } + return merged +} + +func PodmanTemplateFuncs() template.FuncMap { + merged := make(template.FuncMap) + for k, v := range defaultFuncMap { + merged[k] = v + } + return merged +} diff --git a/cmd/podmanV2/root.go b/cmd/podmanV2/root.go new file mode 100644 index 000000000..cb4cb4e00 --- /dev/null +++ b/cmd/podmanV2/root.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "log/syslog" + "os" + "path" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/version" + "github.com/sirupsen/logrus" + logrusSyslog "github.com/sirupsen/logrus/hooks/syslog" + "github.com/spf13/cobra" +) + +var ( + rootCmd = &cobra.Command{ + Use: path.Base(os.Args[0]), + Long: "Manage pods, containers and images", + SilenceUsage: true, + SilenceErrors: true, + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + Version: version.Version, + } + + logLevels = entities.NewStringSet("debug", "info", "warn", "error", "fatal", "panic") + logLevel = "error" + useSyslog bool +) + +func init() { + // Override default --help information of `--version` global flag} + var dummyVersion bool + // TODO had to disable shorthand -v for version due to -v rm with volume + rootCmd.PersistentFlags().BoolVar(&dummyVersion, "version", false, "Version of Podman") + rootCmd.PersistentFlags().StringVarP(®istry.EngineOptions.Uri, "remote", "r", "", "URL to access Podman service") + rootCmd.PersistentFlags().StringSliceVar(®istry.EngineOptions.Identities, "identity", []string{}, "path to SSH identity file") + rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "error", fmt.Sprintf("Log messages above specified level (%s)", logLevels.String())) + rootCmd.PersistentFlags().BoolVar(&useSyslog, "syslog", false, "Output logging information to syslog as well as the console (default false)") + + cobra.OnInitialize( + logging, + syslogHook, + ) +} + +func preRunE(cmd *cobra.Command, args []string) error { + cmd.SetHelpTemplate(registry.HelpTemplate()) + cmd.SetUsageTemplate(registry.UsageTemplate()) + return nil +} + +func logging() { + if !logLevels.Contains(logLevel) { + fmt.Fprintf(os.Stderr, "Log Level \"%s\" is not supported, choose from: %s\n", logLevel, logLevels.String()) + os.Exit(1) + } + + level, err := logrus.ParseLevel(logLevel) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + logrus.SetLevel(level) + + if logrus.IsLevelEnabled(logrus.InfoLevel) { + logrus.Infof("%s filtering at log level %s", os.Args[0], logrus.GetLevel()) + } +} + +func syslogHook() { + if useSyslog { + hook, err := logrusSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "") + if err != nil { + logrus.WithError(err).Error("Failed to initialize syslog hook") + } + if err == nil { + logrus.AddHook(hook) + } + } +} + +func Execute() { + o := registry.NewOptions(rootCmd.Context(), ®istry.EngineOptions) + if err := rootCmd.ExecuteContext(o); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err.Error()) + } else if registry.GetExitCode() == define.ExecErrorCodeGeneric { + // The exitCode modified from define.ExecErrorCodeGeneric, + // indicates an application + // running inside of a container failed, as opposed to the + // podman command failed. Must exit with that exit code + // otherwise command exited correctly. + registry.SetExitCode(0) + } + os.Exit(registry.GetExitCode()) +} diff --git a/cmd/podmanV2/system/system.go b/cmd/podmanV2/system/system.go new file mode 100644 index 000000000..30ed328e8 --- /dev/null +++ b/cmd/podmanV2/system/system.go @@ -0,0 +1,33 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _system_ + cmd = &cobra.Command{ + Use: "system", + Short: "Manage podman", + Long: "Manage podman", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: cmd, + }) + cmd.SetHelpTemplate(registry.HelpTemplate()) + cmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewContainerEngine(cmd, args) + return err +} diff --git a/cmd/podmanV2/utils/error.go b/cmd/podmanV2/utils/error.go new file mode 100644 index 000000000..3464f0779 --- /dev/null +++ b/cmd/podmanV2/utils/error.go @@ -0,0 +1,16 @@ +package utils + +import "fmt" + +type OutputErrors []error + +func (o OutputErrors) PrintErrors() (lastError error) { + if len(o) == 0 { + return + } + lastError = o[len(o)-1] + for e := 0; e < len(o)-1; e++ { + fmt.Println(o[e]) + } + return +} diff --git a/cmd/podmanV2/volumes/create.go b/cmd/podmanV2/volumes/create.go new file mode 100644 index 000000000..91181dd03 --- /dev/null +++ b/cmd/podmanV2/volumes/create.go @@ -0,0 +1,72 @@ +package volumes + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + createDescription = `If using the default driver, "local", the volume will be created on the host in the volumes directory under container storage.` + + createCommand = &cobra.Command{ + Use: "create [flags] [NAME]", + Short: "Create a new volume", + Long: createDescription, + RunE: create, + Example: `podman volume create myvol + podman volume create + podman volume create --label foo=bar myvol`, + } +) + +var ( + createOpts = entities.VolumeCreateOptions{} + opts = struct { + Label []string + Opts []string + }{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: createCommand, + Parent: volumeCmd, + }) + flags := createCommand.Flags() + flags.StringVar(&createOpts.Driver, "driver", "", "Specify volume driver name (default local)") + flags.StringSliceVarP(&opts.Label, "label", "l", []string{}, "Set metadata for a volume (default [])") + flags.StringArrayVarP(&opts.Opts, "opt", "o", []string{}, "Set driver specific options (default [])") +} + +func create(cmd *cobra.Command, args []string) error { + var ( + err error + ) + if len(args) > 1 { + return errors.Errorf("too many arguments, create takes at most 1 argument") + } + if len(args) > 0 { + createOpts.Name = args[0] + } + createOpts.Label, err = parse.GetAllLabels([]string{}, opts.Label) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + createOpts.Options, err = parse.GetAllLabels([]string{}, opts.Opts) + if err != nil { + return errors.Wrapf(err, "unable to process options") + } + response, err := registry.ContainerEngine().VolumeCreate(context.Background(), createOpts) + if err != nil { + return err + } + fmt.Println(response.IdOrName) + return nil +} diff --git a/cmd/podmanV2/volumes/inspect.go b/cmd/podmanV2/volumes/inspect.go new file mode 100644 index 000000000..4d9720432 --- /dev/null +++ b/cmd/podmanV2/volumes/inspect.go @@ -0,0 +1,74 @@ +package volumes + +import ( + "encoding/json" + "fmt" + "html/template" + "os" + + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +var ( + volumeInspectDescription = `Display detailed information on one or more volumes. + + Use a Go template to change the format from JSON.` + inspectCommand = &cobra.Command{ + Use: "inspect [flags] VOLUME [VOLUME...]", + Short: "Display detailed information on one or more volumes", + Long: volumeInspectDescription, + RunE: inspect, + Example: `podman volume inspect myvol + podman volume inspect --all + podman volume inspect --format "{{.Driver}} {{.Scope}}" myvol`, + } +) + +var ( + inspectOpts = entities.VolumeInspectOptions{} + inspectFormat string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCommand, + Parent: volumeCmd, + }) + flags := inspectCommand.Flags() + flags.BoolVarP(&inspectOpts.All, "all", "a", false, "Inspect all volumes") + flags.StringVarP(&inspectFormat, "format", "f", "json", "Format volume output using Go template") +} + +func inspect(cmd *cobra.Command, args []string) error { + if (inspectOpts.All && len(args) > 0) || (!inspectOpts.All && len(args) < 1) { + return errors.New("provide one or more volume names or use --all") + } + responses, err := registry.ContainerEngine().VolumeInspect(context.Background(), args, inspectOpts) + if err != nil { + return err + } + switch inspectFormat { + case "", formats.JSONString: + jsonOut, err := json.MarshalIndent(responses, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshalling inspect JSON") + } + fmt.Println(string(jsonOut)) + default: + tmpl, err := template.New("volumeInspect").Parse(inspectFormat) + if err != nil { + return err + } + if err := tmpl.Execute(os.Stdout, responses); err != nil { + return err + } + } + return nil + +} diff --git a/cmd/podmanV2/volumes/list.go b/cmd/podmanV2/volumes/list.go new file mode 100644 index 000000000..c38f78c73 --- /dev/null +++ b/cmd/podmanV2/volumes/list.go @@ -0,0 +1,98 @@ +package volumes + +import ( + "context" + "html/template" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +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.` + lsCommand = &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Args: cobra.NoArgs, + Short: "List volumes", + Long: volumeLsDescription, + RunE: list, + } +) + +var ( + // Temporary struct to hold cli values. + cliOpts = struct { + Filter []string + Format string + Quiet bool + }{} + lsOpts = entities.VolumeListOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: lsCommand, + Parent: volumeCmd, + }) + flags := lsCommand.Flags() + flags.StringSliceVarP(&cliOpts.Filter, "filter", "f", []string{}, "Filter volume output") + flags.StringVar(&cliOpts.Format, "format", "{{.Driver}}\t{{.Name}}\n", "Format volume output using Go template") + flags.BoolVarP(&cliOpts.Quiet, "quiet", "q", false, "Print volume output in quiet mode") +} + +func list(cmd *cobra.Command, args []string) error { + var w io.Writer = os.Stdout + if cliOpts.Quiet && cmd.Flag("format").Changed { + return errors.New("quiet and format flags cannot be used together") + } + for _, f := range cliOpts.Filter { + 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) + } + lsOpts.Filter[filterSplit[0]] = append(lsOpts.Filter[filterSplit[0]], filterSplit[1:]...) + } + responses, err := registry.ContainerEngine().VolumeList(context.Background(), lsOpts) + if err != nil { + return err + } + // "\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" + cliOpts.Format = strings.Replace(cliOpts.Format, `\t`, "\t", -1) + if cliOpts.Quiet { + cliOpts.Format = "{{.Name}}\n" + } + headers := "DRIVER\tVOLUME NAME\n" + row := cliOpts.Format + if !strings.HasSuffix(cliOpts.Format, "\n") { + row += "\n" + } + format := "{{range . }}" + row + "{{end}}" + if !cliOpts.Quiet && !cmd.Flag("format").Changed { + w = tabwriter.NewWriter(os.Stdout, 12, 2, 2, ' ', 0) + format = headers + format + } + tmpl, err := template.New("listVolume").Parse(format) + if err != nil { + return err + } + if err := tmpl.Execute(w, responses); err != nil { + return err + } + if flusher, ok := w.(interface{ Flush() error }); ok { + return flusher.Flush() + } + return nil +} diff --git a/cmd/podmanV2/volumes/prune.go b/cmd/podmanV2/volumes/prune.go new file mode 100644 index 000000000..148065f56 --- /dev/null +++ b/cmd/podmanV2/volumes/prune.go @@ -0,0 +1,74 @@ +package volumes + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + volumePruneDescription = `Volumes that are not currently owned by a container will be removed. + + The command prompts for confirmation which can be overridden with the --force flag. + Note all data will be destroyed.` + pruneCommand = &cobra.Command{ + Use: "prune", + Args: cobra.NoArgs, + Short: "Remove all unused volumes", + Long: volumePruneDescription, + RunE: prune, + } +) + +var ( + pruneOptions entities.VolumePruneOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pruneCommand, + Parent: volumeCmd, + }) + flags := pruneCommand.Flags() + flags.BoolVarP(&pruneOptions.Force, "force", "f", false, "Do not prompt for confirmation") +} + +func prune(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + // Prompt for confirmation if --force is not set + if !pruneOptions.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] ") + answer, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "error reading input") + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + responses, err := registry.ContainerEngine().VolumePrune(context.Background(), pruneOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/volumes/rm.go b/cmd/podmanV2/volumes/rm.go new file mode 100644 index 000000000..b019285d8 --- /dev/null +++ b/cmd/podmanV2/volumes/rm.go @@ -0,0 +1,64 @@ +package volumes + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + volumeRmDescription = `Remove one or more existing volumes. + + By default only volumes that are not being used by any containers will be removed. To remove the volumes anyways, use the --force flag.` + rmCommand = &cobra.Command{ + Use: "rm [flags] VOLUME [VOLUME...]", + Aliases: []string{"remove"}, + Short: "Remove one or more volumes", + Long: volumeRmDescription, + RunE: rm, + Example: `podman volume rm myvol1 myvol2 + podman volume rm --all + podman volume rm --force myvol`, + } +) + +var ( + rmOptions = entities.VolumeRmOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCommand, + Parent: volumeCmd, + }) + flags := rmCommand.Flags() + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all volumes") + flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Remove a volume by force, even if it is being used by a container") +} + +func rm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if (len(args) > 0 && rmOptions.All) || (len(args) < 1 && !rmOptions.All) { + return errors.New("choose either one or more volumes or all") + } + responses, err := registry.ContainerEngine().VolumeRm(context.Background(), args, rmOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/volumes/volume.go b/cmd/podmanV2/volumes/volume.go new file mode 100644 index 000000000..84abe3d24 --- /dev/null +++ b/cmd/podmanV2/volumes/volume.go @@ -0,0 +1,33 @@ +package volumes + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _volume_ + volumeCmd = &cobra.Command{ + Use: "volume", + Short: "Manage volumes", + Long: "Volumes are created in and can be shared between containers", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: volumeCmd, + }) + volumeCmd.SetHelpTemplate(registry.HelpTemplate()) + volumeCmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewContainerEngine(cmd, args) + return err +} |