diff options
Diffstat (limited to 'cmd')
97 files changed, 2774 insertions, 366 deletions
diff --git a/cmd/podman/README.md b/cmd/podman/README.md index c1b8f48a7..da92d0216 100644 --- a/cmd/podman/README.md +++ b/cmd/podman/README.md @@ -1,23 +1,16 @@ -# Adding a podman V2 commands +# Podman CLI -## Build podman V2 +The following is an example of how to add a new primary command (`manifest`) and a sub-command (`inspect`) to the Podman CLI. +This is example code, the production code has additional error checking and the business logic provided. -```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. +See items below for details on building, installing, contributing to Podman: + - [Readme](README.md) + - [Contributing](CONTRIBUTING.md) + - [Podman Usage](transfer.md) + - [Trouble Shooting](troubleshooting.md) + - [Code Of Conduct](CODE-OF-CONDUCT.md) -## Adding a new command `podman manifests` +## Adding a new command `podman manifest` ```shell script $ mkdir -p $GOPATH/src/github.com/containers/libpod/cmd/podmanV2/manifests ``` @@ -27,6 +20,7 @@ package manifests import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -36,11 +30,11 @@ var ( manifestCmd = &cobra.Command{ Use: "manifest", Short: "Manage manifests", + Args: cobra.ExactArgs(1), Long: "Manage manifests", - Example: "podman manifests IMAGE", + Example: "podman manifest IMAGE", TraverseChildren: true, - PersistentPreRunE: preRunE, - RunE: registry.SubCommandExists, // Report error if there is no sub command given + RunE: validate.SubCommandExists, // Report error if there is no sub command given } ) func init() { @@ -51,15 +45,6 @@ func init() { // 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: @@ -87,7 +72,11 @@ var ( 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", + Annotations: map[string]string{ + // Add this annotation if this command cannot be run rootless + // registry.ParentNSRequired: "", + }, + Example: "podman manifest inspect DEADBEEF", } ) @@ -98,6 +87,7 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, // The definition for this command Command: inspectCmd, + // The parent command to proceed this command on the CLI Parent: manifestCmd, }) @@ -106,8 +96,22 @@ func init() { // 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 + // Business logic using registry.ImageEngine() // Do not pull from libpod directly use the domain objects and types return nil } ``` + +## Helper functions + +The complete set can be found in the `validate` package, here are some examples: + + - `cobra.Command{ Args: validate.NoArgs }` used when the command does not accept errors + - `cobra.Command{ Args: validate.IdOrLatestArgs }` used to ensure either a list of ids given or the --latest flag + - `cobra.Command{ RunE: validate.SubCommandExists }` used to validate a subcommand given to a command + - `validate.ChoiceValue` used to create a `pflag.Value` that validate user input against a provided slice of values. For example: + ```go + flags := cobraCommand.Flags() + created := validate.ChoiceValue(&opts.Sort, "command", "created", "id", "image", "names", "runningfor", "size", "status") + flags.Var(created, "sort", "Sort output by: "+created.Choices()) + ``` diff --git a/cmd/podman/auto-update.go b/cmd/podman/auto-update.go new file mode 100644 index 000000000..758cbbc6f --- /dev/null +++ b/cmd/podman/auto-update.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/errorhandling" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + autoUpdateDescription = `Auto update containers according to their auto-update policy. + + Auto-update policies are specified with the "io.containers.autoupdate" label. + Note that this command is experimental.` + autoUpdateCommand = &cobra.Command{ + Use: "auto-update [flags]", + Short: "Auto update containers according to their auto-update policy", + Long: autoUpdateDescription, + RunE: autoUpdate, + Example: `podman auto-update`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: autoUpdateCommand, + }) +} + +func autoUpdate(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + // Backwards compat. System tests expext this error string. + return errors.Errorf("`%s` takes no arguments", cmd.CommandPath()) + } + report, failures := registry.ContainerEngine().AutoUpdate(registry.GetContext()) + if report != nil { + for _, unit := range report.Units { + fmt.Println(unit) + } + } + return errorhandling.JoinErrors(failures) +} diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index a0aed984c..7086dc839 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -3,7 +3,7 @@ package common import ( "fmt" - buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/auth" "github.com/containers/libpod/cmd/podman/registry" "github.com/spf13/pflag" ) @@ -26,7 +26,7 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { ) createFlags.StringVar( &cf.Authfile, - "authfile", buildahcli.GetDefaultAuthFile(), + "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override", ) createFlags.StringVar( @@ -49,9 +49,7 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "cap-drop", []string{}, "Drop capabilities from the container", ) - cgroupNS := "" - createFlags.StringVar( - &cgroupNS, + createFlags.String( "cgroupns", containerConfig.CgroupNS(), "cgroup namespace to use", ) @@ -155,13 +153,10 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "device-write-iops", []string{}, "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)", ) - createFlags.StringVar( - &cf.Entrypoint, - "entrypoint", "", + createFlags.String("entrypoint", "", "Overwrite the default ENTRYPOINT of the image", ) - createFlags.StringArrayVarP( - &cf.env, + createFlags.StringArrayP( "env", "e", containerConfig.Env(), "Set environment variables in container", ) @@ -248,9 +243,7 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "interactive", "i", false, "Keep STDIN open even if not attached", ) - ipcNS := "" - createFlags.StringVar( - &ipcNS, + createFlags.String( "ipc", containerConfig.IPCNS(), "IPC namespace to use", ) @@ -331,9 +324,7 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "use `OS` instead of the running OS for choosing images", ) // markFlagHidden(createFlags, "override-os") - pid := "" - createFlags.StringVar( - &pid, + createFlags.String( "pid", containerConfig.PidNS(), "PID namespace to use", ) @@ -397,9 +388,7 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "security-opt", containerConfig.SecurityOptions(), "Security Options", ) - shmSize := "" - createFlags.StringVar( - &shmSize, + createFlags.String( "shm-size", containerConfig.ShmSize(), "Size of /dev/shm "+sizeWithUnitFormat, ) @@ -464,15 +453,11 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])", ) - userNS := "" - createFlags.StringVar( - &userNS, + createFlags.String( "userns", containerConfig.Containers.UserNS, "User namespace to use", ) - utsNS := "" - createFlags.StringVar( - &utsNS, + createFlags.String( "uts", containerConfig.Containers.UTSNS, "UTS namespace to use", ) diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 2f08bb6a6..8b38e3b47 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -31,8 +31,8 @@ type ContainerCLIOpts struct { DeviceReadIOPs []string DeviceWriteBPs []string DeviceWriteIOPs []string - Entrypoint string - env []string + Entrypoint *string + Env []string EnvHost bool EnvFile []string Expose []string diff --git a/cmd/podman/common/ports.go b/cmd/podman/common/ports.go index a96bafabd..2092bbe53 100644 --- a/cmd/podman/common/ports.go +++ b/cmd/podman/common/ports.go @@ -9,10 +9,10 @@ func verifyExpose(expose []string) error { // add the expose ports from the user (--expose) // can be single or a range for _, expose := range expose { - //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + // support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] _, port := nat.SplitProtoPort(expose) - //parse the start and end port and create a sequence of ports to expose - //if expose a port, the start and end port are the same + // parse the start and end port and create a sequence of ports to expose + // if expose a port, the start and end port are the same _, _, err := nat.ParsePortRange(port) if err != nil { return errors.Wrapf(err, "invalid range format for --expose: %s", expose) diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index f8c58f1a4..ff7c39de2 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -26,6 +26,16 @@ func getCPULimits(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) cpu := &specs.LinuxCPU{} hasLimits := false + const cpuPeriod = 100000 + + if c.CPUS > 0 { + quota := int64(c.CPUS * cpuPeriod) + period := uint64(cpuPeriod) + + cpu.Period = &period + cpu.Quota = "a + hasLimits = true + } if c.CPUShares > 0 { cpu.Shares = &c.CPUShares hasLimits = true @@ -142,6 +152,10 @@ func getMemoryLimits(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []strin return nil, errors.Wrapf(err, "invalid value for memory") } memory.Limit = &ml + if c.MemorySwap == "" { + limit := 2 * ml + memory.Swap = &(limit) + } hasLimits = true } if m := c.MemoryReservation; len(m) > 0 { @@ -192,7 +206,6 @@ func getMemoryLimits(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []strin func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error { var ( err error - //namespaces map[string]string ) // validate flags as needed @@ -234,9 +247,15 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string // We are not handling the Expose flag yet. // s.PortsExpose = c.Expose s.PortMappings = c.Net.PublishPorts - s.PublishImagePorts = c.PublishAll + s.PublishExposedPorts = c.PublishAll s.Pod = c.Pod + expose, err := createExpose(c.Expose) + if err != nil { + return err + } + s.Expose = expose + for k, v := range map[string]*specgen.Namespace{ c.IPC: &s.IpcNS, c.PID: &s.PidNS, @@ -316,15 +335,12 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string env = envLib.Join(env, fileEnv) } - // env overrides any previous variables - if cmdLineEnv := c.env; len(cmdLineEnv) > 0 { - parsedEnv, err := envLib.ParseSlice(cmdLineEnv) - if err != nil { - return err - } - env = envLib.Join(env, parsedEnv) + parsedEnv, err := envLib.ParseSlice(c.Env) + if err != nil { + return err } - s.Env = env + + s.Env = envLib.Join(env, parsedEnv) // LABEL VARIABLES labels, err := parse.GetAllLabels(c.LabelFile, c.Label) @@ -364,20 +380,20 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.WorkDir = workDir entrypoint := []string{} userCommand := []string{} - if ep := c.Entrypoint; len(ep) > 0 { - // Check if entrypoint specified is json - if err := json.Unmarshal([]byte(c.Entrypoint), &entrypoint); err != nil { - entrypoint = append(entrypoint, ep) + if c.Entrypoint != nil { + if ep := *c.Entrypoint; len(ep) > 0 { + // Check if entrypoint specified is json + if err := json.Unmarshal([]byte(*c.Entrypoint), &entrypoint); err != nil { + entrypoint = append(entrypoint, ep) + } } + s.Entrypoint = entrypoint } - var command []string - s.Entrypoint = entrypoint - // Build the command // If we have an entry point, it goes first - if len(entrypoint) > 0 { + if c.Entrypoint != nil { command = entrypoint } if len(inputCommand) > 0 { @@ -386,9 +402,12 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string userCommand = append(userCommand, inputCommand...) } - if len(inputCommand) > 0 { + switch { + case len(inputCommand) > 0: s.Command = userCommand - } else { + case c.Entrypoint != nil: + s.Command = []string{} + default: s.Command = command } @@ -400,6 +419,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } s.ShmSize = &shmSize } + s.CNINetworks = c.Net.CNINetworks s.HostAdd = c.Net.AddHosts s.UseImageResolvConf = c.Net.UseImageResolvConf s.DNSServers = c.Net.DNSServers @@ -481,11 +501,12 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.CapDrop = c.CapDrop s.Privileged = c.Privileged s.ReadOnlyFilesystem = c.ReadOnly + s.ConmonPidFile = c.ConmonPIDFile // TODO // ouitside of specgen and oci though // defaults to true, check spec/storage - //s.readon = c.ReadOnlyTmpFS + // s.readon = c.ReadOnlyTmpFS // TODO convert to map? // check if key=value and convert sysmap := make(map[string]string) @@ -511,10 +532,13 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string case "label": // TODO selinux opts and label opts are the same thing s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) + s.Annotations[define.InspectAnnotationLabel] = con[1] case "apparmor": s.ContainerSecurityConfig.ApparmorProfile = con[1] + s.Annotations[define.InspectAnnotationApparmor] = con[1] case "seccomp": s.SeccompProfilePath = con[1] + s.Annotations[define.InspectAnnotationSeccomp] = con[1] default: return fmt.Errorf("invalid --security-opt 2: %q", opt) } @@ -528,7 +552,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string // Only add read-only tmpfs mounts in case that we are read-only and the // read-only tmpfs flag has been set. - mounts, volumes, err := parseVolumes(c.Volume, c.Mount, c.TmpFS, (c.ReadOnlyTmpFS && c.ReadOnly)) + mounts, volumes, err := parseVolumes(c.Volume, c.Mount, c.TmpFS, c.ReadOnlyTmpFS && c.ReadOnly) if err != nil { return err } @@ -536,12 +560,12 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.Volumes = volumes // TODO any idea why this was done - //devices := rtc.Containers.Devices + // devices := rtc.Containers.Devices // TODO conflict on populate? // - //if c.Changed("device") { + // if c.Changed("device") { // devices = append(devices, c.StringSlice("device")...) - //} + // } for _, dev := range c.Devices { s.Devices = append(s.Devices, specs.LinuxDevice{Path: dev}) @@ -553,7 +577,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string // initpath s.Stdin = c.Interactive // quiet - //DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), + // DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), // Rlimits/Ulimits for _, u := range c.Ulimit { @@ -573,10 +597,10 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.Rlimits = append(s.Rlimits, rl) } - //Tmpfs: c.StringArray("tmpfs"), + // Tmpfs: c.StringArray("tmpfs"), // TODO how to handle this? - //Syslog: c.Bool("syslog"), + // Syslog: c.Bool("syslog"), logOpts := make(map[string]string) for _, o := range c.LogOptions { @@ -597,12 +621,34 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.Name = c.Name s.OOMScoreAdj = &c.OOMScoreAdj - s.RestartPolicy = c.Restart + if c.Restart != "" { + splitRestart := strings.Split(c.Restart, ":") + switch len(splitRestart) { + case 1: + // No retries specified + case 2: + if strings.ToLower(splitRestart[0]) != "on-failure" { + return errors.Errorf("restart policy retries can only be specified with on-failure restart policy") + } + retries, err := strconv.Atoi(splitRestart[1]) + if err != nil { + return errors.Wrapf(err, "error parsing restart policy retry count") + } + if retries < 0 { + return errors.Errorf("must specify restart policy retry count as a number greater than 0") + } + var retriesUint uint = uint(retries) + s.RestartRetries = &retriesUint + default: + return errors.Errorf("invalid restart policy: may specify retries at most once") + } + s.RestartPolicy = splitRestart[0] + } s.Remove = c.Rm s.StopTimeout = &c.StopTimeout // TODO where should we do this? - //func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) { + // func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) { return nil } diff --git a/cmd/podman/common/types.go b/cmd/podman/common/types.go deleted file mode 100644 index 2427ae975..000000000 --- a/cmd/podman/common/types.go +++ /dev/null @@ -1,3 +0,0 @@ -package common - -var DefaultKernelNamespaces = "cgroup,ipc,net,uts" diff --git a/cmd/podman/common/util.go b/cmd/podman/common/util.go index 5b99b8398..a3626b4e4 100644 --- a/cmd/podman/common/util.go +++ b/cmd/podman/common/util.go @@ -1,54 +1,201 @@ package common import ( - "fmt" + "net" "strconv" + "strings" - "github.com/spf13/cobra" - - "github.com/cri-o/ocicni/pkg/ocicni" - "github.com/docker/go-connections/nat" + "github.com/containers/libpod/pkg/specgen" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) -// 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() +// createExpose parses user-provided exposed port definitions and converts them +// into SpecGen format. +// TODO: The SpecGen format should really handle ranges more sanely - we could +// be massively inflating what is sent over the wire with a large range. +func createExpose(expose []string) (map[uint16]string, error) { + toReturn := make(map[uint16]string) + + for _, e := range expose { + // Check for protocol + proto := "tcp" + splitProto := strings.Split(e, "/") + if len(splitProto) > 2 { + return nil, errors.Errorf("invalid expose format - protocol can only be specified once") + } else if len(splitProto) == 2 { + proto = splitProto[1] + } + + // Check for a range + start, len, err := parseAndValidateRange(splitProto[0]) + if err != nil { + return nil, err + } + + var index uint16 + for index = 0; index < len; index++ { + portNum := start + index + protocols, ok := toReturn[portNum] + if !ok { + toReturn[portNum] = proto } else { - hostPort, err = strconv.Atoi(i.HostPort) - if err != nil { - return nil, errors.Wrapf(err, "unable to convert host port to integer") - } + newProto := strings.Join(append(strings.Split(protocols, ","), strings.Split(proto, ",")...), ",") + toReturn[portNum] = newProto } + } + } + + return toReturn, nil +} - pm.HostPort = int32(hostPort) - pm.Protocol = containerPb.Proto() - portBindings = append(portBindings, pm) +// createPortBindings iterates ports mappings into SpecGen format. +func createPortBindings(ports []string) ([]specgen.PortMapping, error) { + // --publish is formatted as follows: + // [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol] + toReturn := make([]specgen.PortMapping, 0, len(ports)) + + for _, p := range ports { + var ( + ctrPort string + proto, hostIP, hostPort *string + ) + + splitProto := strings.Split(p, "/") + switch len(splitProto) { + case 1: + // No protocol was provided + case 2: + proto = &(splitProto[1]) + default: + return nil, errors.Errorf("invalid port format - protocol can only be specified once") } + + splitPort := strings.Split(splitProto[0], ":") + switch len(splitPort) { + case 1: + ctrPort = splitPort[0] + case 2: + hostPort = &(splitPort[0]) + ctrPort = splitPort[1] + case 3: + hostIP = &(splitPort[0]) + hostPort = &(splitPort[1]) + ctrPort = splitPort[2] + default: + return nil, errors.Errorf("invalid port format - format is [[hostIP:]hostPort:]containerPort") + } + + newPort, err := parseSplitPort(hostIP, hostPort, ctrPort, proto) + if err != nil { + return nil, err + } + + toReturn = append(toReturn, newPort) } - return portBindings, nil + + return toReturn, nil } -// NoArgs returns an error if any args are included. -func NoArgs(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - return fmt.Errorf("`%s` takes no arguments", cmd.CommandPath()) +// parseSplitPort parses individual components of the --publish flag to produce +// a single port mapping in SpecGen format. +func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (specgen.PortMapping, error) { + newPort := specgen.PortMapping{} + if ctrPort == "" { + return newPort, errors.Errorf("must provide a non-empty container port to publish") + } + ctrStart, ctrLen, err := parseAndValidateRange(ctrPort) + if err != nil { + return newPort, errors.Wrapf(err, "error parsing container port") + } + newPort.ContainerPort = ctrStart + newPort.Range = ctrLen + + if protocol != nil { + if *protocol == "" { + return newPort, errors.Errorf("must provide a non-empty protocol to publish") + } + newPort.Protocol = *protocol + } + if hostIP != nil { + if *hostIP == "" { + return newPort, errors.Errorf("must provide a non-empty container host IP to publish") + } + testIP := net.ParseIP(*hostIP) + if testIP == nil { + return newPort, errors.Errorf("cannot parse %q as an IP address", *hostIP) + } + newPort.HostIP = testIP.String() + } + if hostPort != nil { + if *hostPort == "" { + return newPort, errors.Errorf("must provide a non-empty container host port to publish") + } + hostStart, hostLen, err := parseAndValidateRange(*hostPort) + if err != nil { + return newPort, errors.Wrapf(err, "error parsing host port") + } + if hostLen != ctrLen { + return newPort, errors.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen) + } + newPort.HostPort = hostStart + } + + hport := newPort.HostPort + if hport == 0 { + hport = newPort.ContainerPort + } + logrus.Debugf("Adding port mapping from %d to %d length %d protocol %q", hport, newPort.ContainerPort, newPort.Range, newPort.Protocol) + + return newPort, nil +} + +// Parse and validate a port range. +// Returns start port, length of range, error. +func parseAndValidateRange(portRange string) (uint16, uint16, error) { + splitRange := strings.Split(portRange, "-") + if len(splitRange) > 2 { + return 0, 0, errors.Errorf("invalid port format - port ranges are formatted as startPort-stopPort") + } + + if splitRange[0] == "" { + return 0, 0, errors.Errorf("port numbers cannot be negative") + } + + startPort, err := parseAndValidatePort(splitRange[0]) + if err != nil { + return 0, 0, err + } + + var rangeLen uint16 = 1 + if len(splitRange) == 2 { + if splitRange[1] == "" { + return 0, 0, errors.Errorf("must provide ending number for port range") + } + endPort, err := parseAndValidatePort(splitRange[1]) + if err != nil { + return 0, 0, err + } + if endPort <= startPort { + return 0, 0, errors.Errorf("the end port of a range must be higher than the start port - %d is not higher than %d", endPort, startPort) + } + // Our range is the total number of ports + // involved, so we need to add 1 (8080:8081 is + // 2 ports, for example, not 1) + rangeLen = endPort - startPort + 1 + } + + return startPort, rangeLen, nil +} + +// Turn a single string into a valid U16 port. +func parseAndValidatePort(port string) (uint16, error) { + num, err := strconv.Atoi(port) + if err != nil { + return 0, errors.Wrapf(err, "cannot parse %q as a port number", port) + } + if num < 1 || num > 65535 { + return 0, errors.Errorf("port numbers must be between 1 and 65535 (inclusive), got %d", num) } - return nil + return uint16(num), nil } diff --git a/cmd/podman/containers/attach.go b/cmd/podman/containers/attach.go index 78b52ad1b..119b47d3f 100644 --- a/cmd/podman/containers/attach.go +++ b/cmd/podman/containers/attach.go @@ -4,6 +4,7 @@ import ( "os" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -17,12 +18,7 @@ var ( Short: "Attach to a running container", Long: attachDescription, RunE: attach, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) > 1 || (len(args) == 0 && !cmd.Flag("latest").Changed) { - return errors.Errorf("attach requires the name or id of one running container or the latest flag") - } - return nil - }, + Args: validate.IdOrLatestArgs, Example: `podman attach ctrID podman attach 1234 podman attach --no-stdin foobar`, @@ -33,6 +29,7 @@ var ( Short: attachCommand.Short, Long: attachCommand.Long, RunE: attachCommand.RunE, + Args: validate.IdOrLatestArgs, Example: `podman container attach ctrID podman container attach 1234 podman container attach --no-stdin foobar`, @@ -71,11 +68,18 @@ func init() { } func attach(cmd *cobra.Command, args []string) error { + if len(args) > 1 || (len(args) == 0 && !attachOpts.Latest) { + return errors.Errorf("attach requires the name or id of one running container or the latest flag") + } + var name string + if len(args) > 0 { + name = args[0] + } attachOpts.Stdin = os.Stdin if attachOpts.NoStdin { attachOpts.Stdin = nil } attachOpts.Stdout = os.Stdout attachOpts.Stderr = os.Stderr - return registry.ContainerEngine().ContainerAttach(registry.GetContext(), args[0], attachOpts) + return registry.ContainerEngine().ContainerAttach(registry.GetContext(), name, attachOpts) } diff --git a/cmd/podman/containers/commit.go b/cmd/podman/containers/commit.go index 137e486eb..b3c3d7626 100644 --- a/cmd/podman/containers/commit.go +++ b/cmd/podman/containers/commit.go @@ -30,6 +30,7 @@ var ( } containerCommitCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), Use: commitCommand.Use, Short: commitCommand.Short, Long: commitCommand.Long, diff --git a/cmd/podman/containers/container.go b/cmd/podman/containers/container.go index 97b73cdd0..a102318fb 100644 --- a/cmd/podman/containers/container.go +++ b/cmd/podman/containers/container.go @@ -2,6 +2,7 @@ package containers import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/util" "github.com/spf13/cobra" @@ -17,7 +18,7 @@ var ( Short: "Manage containers", Long: "Manage containers", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } containerConfig = util.DefaultContainerConfig() diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index da550b606..2ecdda2e0 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -1,8 +1,10 @@ package containers import ( + "context" "fmt" "os" + "strings" "github.com/containers/common/pkg/config" "github.com/containers/libpod/cmd/podman/common" @@ -33,6 +35,7 @@ var ( } containerCreateCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), Use: createCommand.Use, Short: createCommand.Short, Long: createCommand.Long, @@ -52,6 +55,11 @@ func createFlags(flags *pflag.FlagSet) { flags.AddFlagSet(common.GetCreateFlags(&cliVals)) flags.AddFlagSet(common.GetNetFlags()) flags.SetNormalizeFunc(common.AliasFlags) + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("env-host") + _ = flags.MarkHidden("http-proxy") + } } func init() { @@ -59,7 +67,7 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: createCommand, }) - //common.GetCreateFlags(createCommand) + // common.GetCreateFlags(createCommand) flags := createCommand.Flags() createFlags(flags) @@ -105,6 +113,10 @@ func create(cmd *cobra.Command, args []string) error { return err } + if _, err := createPodIfNecessary(s); err != nil { + return err + } + report, err := registry.ContainerEngine().ContainerCreate(registry.GetContext(), s) if err != nil { return err @@ -159,6 +171,17 @@ func createInit(c *cobra.Command) error { if c.Flag("cgroupns").Changed { cliVals.CGroupsNS = c.Flag("cgroupns").Value.String() } + if c.Flag("entrypoint").Changed { + val := c.Flag("entrypoint").Value.String() + cliVals.Entrypoint = &val + } + if c.Flags().Changed("env") { + env, err := c.Flags().GetStringArray("env") + if err != nil { + return errors.Wrapf(err, "retrieve env flag") + } + cliVals.Env = env + } // Docker-compatibility: the "-h" flag for run/create is reserved for // the hostname (see https://github.com/containers/libpod/issues/1367). @@ -180,8 +203,10 @@ func pullImage(imageName string) error { return errors.New("unable to find a name and tag match for busybox in repotags: no such image") } _, pullErr := registry.ImageEngine().Pull(registry.GetContext(), imageName, entities.ImagePullOptions{ - Authfile: cliVals.Authfile, - Quiet: cliVals.Quiet, + Authfile: cliVals.Authfile, + Quiet: cliVals.Quiet, + OverrideArch: cliVals.OverrideArch, + OverrideOS: cliVals.OverrideOS, }) if pullErr != nil { return pullErr @@ -203,3 +228,25 @@ func openCidFile(cidfile string) (*os.File, error) { } return cidFile, nil } + +// createPodIfNecessary automatically creates a pod when requested. if the pod name +// has the form new:ID, the pod ID is created and the name in the spec generator is replaced +// with ID. +func createPodIfNecessary(s *specgen.SpecGenerator) (*entities.PodCreateReport, error) { + if !strings.HasPrefix(s.Pod, "new:") { + return nil, nil + } + podName := strings.Replace(s.Pod, "new:", "", 1) + if len(podName) < 1 { + return nil, errors.Errorf("new pod name must be at least one character") + } + createOptions := entities.PodCreateOptions{ + Name: podName, + Infra: true, + Net: &entities.NetOptions{ + PublishPorts: s.PortMappings, + }, + } + s.Pod = podName + return registry.ContainerEngine().PodCreate(context.Background(), createOptions) +} diff --git a/cmd/podman/containers/diff.go b/cmd/podman/containers/diff.go index 046dac53e..59b788010 100644 --- a/cmd/podman/containers/diff.go +++ b/cmd/podman/containers/diff.go @@ -3,6 +3,7 @@ package containers import ( "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/cmd/podman/report" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -12,7 +13,7 @@ var ( // podman container _diff_ diffCmd = &cobra.Command{ Use: "diff [flags] CONTAINER", - Args: registry.IdOrLatestArgs, + Args: validate.IdOrLatestArgs, Short: "Inspect changes on container's file systems", Long: `Displays changes on a container filesystem. The container will be compared to its parent layer.`, RunE: diff, diff --git a/cmd/podman/containers/exec.go b/cmd/podman/containers/exec.go index 2bff8ae33..0992b3862 100644 --- a/cmd/podman/containers/exec.go +++ b/cmd/podman/containers/exec.go @@ -16,20 +16,22 @@ var ( execDescription = `Execute the specified command inside a running container. ` execCommand = &cobra.Command{ - Use: "exec [flags] CONTAINER [COMMAND [ARG...]]", - Short: "Run a process in a running container", - Long: execDescription, - RunE: exec, + Use: "exec [flags] CONTAINER [COMMAND [ARG...]]", + Short: "Run a process in a running container", + Long: execDescription, + RunE: exec, + DisableFlagsInUseLine: true, Example: `podman exec -it ctrID ls podman exec -it -w /tmp myCtr pwd podman exec --user root ctrID ls`, } containerExecCommand = &cobra.Command{ - Use: execCommand.Use, - Short: execCommand.Short, - Long: execCommand.Long, - RunE: execCommand.RunE, + Use: execCommand.Use, + Short: execCommand.Short, + Long: execCommand.Long, + RunE: execCommand.RunE, + DisableFlagsInUseLine: true, Example: `podman container exec -it ctrID ls podman container exec -it -w /tmp myCtr pwd podman container exec --user root ctrID ls`, @@ -79,6 +81,10 @@ func init() { func exec(cmd *cobra.Command, args []string) error { var nameOrId string + + if len(args) == 0 && !execOpts.Latest { + return errors.New("exec requires the name or ID of a container or the --latest flag") + } execOpts.Cmd = args if !execOpts.Latest { execOpts.Cmd = args[1:] diff --git a/cmd/podman/containers/export.go b/cmd/podman/containers/export.go index fb5bd468f..bbb6a6bc9 100644 --- a/cmd/podman/containers/export.go +++ b/cmd/podman/containers/export.go @@ -28,6 +28,7 @@ var ( } containerExportCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), Use: exportCommand.Use, Short: exportCommand.Short, Long: exportCommand.Long, diff --git a/cmd/podman/containers/kill.go b/cmd/podman/containers/kill.go index 8b4a384fe..ef85aad7d 100644 --- a/cmd/podman/containers/kill.go +++ b/cmd/podman/containers/kill.go @@ -30,6 +30,9 @@ var ( } containerKillCommand = &cobra.Command{ + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, Use: killCommand.Use, Short: killCommand.Short, Long: killCommand.Long, diff --git a/cmd/podman/containers/list.go b/cmd/podman/containers/list.go index 22fa15b7e..c200a49aa 100644 --- a/cmd/podman/containers/list.go +++ b/cmd/podman/containers/list.go @@ -1,8 +1,8 @@ package containers import ( - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -12,7 +12,7 @@ var ( listCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, - Args: common.NoArgs, + Args: validate.NoArgs, Short: "List containers", Long: "Prints out information about the containers", RunE: ps, diff --git a/cmd/podman/containers/logs.go b/cmd/podman/containers/logs.go index 5dec71fdd..2b8c3ed5f 100644 --- a/cmd/podman/containers/logs.go +++ b/cmd/podman/containers/logs.go @@ -27,7 +27,7 @@ var ( ` logsCommand = &cobra.Command{ Use: "logs [flags] CONTAINER [CONTAINER...]", - Short: "Fetch the logs of one or more container", + Short: "Fetch the logs of one or more containers", Long: logsDescription, RunE: logs, Example: `podman logs ctrID diff --git a/cmd/podman/containers/port.go b/cmd/podman/containers/port.go index 2e3386aa9..ec0ddf838 100644 --- a/cmd/podman/containers/port.go +++ b/cmd/podman/containers/port.go @@ -11,6 +11,7 @@ import ( "github.com/cri-o/ocicni/pkg/ocicni" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -28,23 +29,50 @@ var ( podman port ctrID 80/tcp podman port --latest 80`, } + + containerPortCommand = &cobra.Command{ + Use: "port [flags] CONTAINER [PORT]", + Short: portCommand.Short, + Long: portDescription, + RunE: portCommand.RunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, true, false) + }, + Example: `podman container port --all + podman container port --latest 80`, + } ) var ( portOpts entities.ContainerPortOptions ) +func portFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&portOpts.All, "all", "a", false, "Display port information for all containers") + flags.BoolVarP(&portOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode}, Command: portCommand, }) + flags := portCommand.Flags() - flags.BoolVarP(&portOpts.All, "all", "a", false, "Display port information for all containers") - flags.BoolVarP(&portOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") - if registry.IsRemote() { - _ = flags.MarkHidden("latest") - } + portFlags(flags) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: containerPortCommand, + Parent: containerCmd, + }) + + containerPortflags := containerPortCommand.Flags() + portFlags(containerPortflags) + } func port(cmd *cobra.Command, args []string) error { diff --git a/cmd/podman/containers/prune.go b/cmd/podman/containers/prune.go index df627259c..38168a6e4 100644 --- a/cmd/podman/containers/prune.go +++ b/cmd/podman/containers/prune.go @@ -18,10 +18,10 @@ import ( var ( pruneDescription = fmt.Sprintf(`podman container prune - Removes all stopped | exited containers`) + Removes all non running containers`) pruneCommand = &cobra.Command{ Use: "prune [flags]", - Short: "Remove all stopped | exited containers", + Short: "Remove all non running containers", Long: pruneDescription, RunE: prune, Example: `podman container prune`, @@ -43,7 +43,6 @@ func init() { func prune(cmd *cobra.Command, args []string) error { var ( - errs utils.OutputErrors pruneOptions = entities.ContainerPruneOptions{} ) if len(args) > 0 { @@ -51,7 +50,7 @@ func prune(cmd *cobra.Command, args []string) error { } if !force { reader := bufio.NewReader(os.Stdin) - fmt.Println("WARNING! This will remove all stopped containers.") + fmt.Println("WARNING! This will remove all non running containers.") fmt.Print("Are you sure you want to continue? [y/N] ") answer, err := reader.ReadString('\n') if err != nil { @@ -76,11 +75,5 @@ func prune(cmd *cobra.Command, args []string) error { if err != nil { return err } - for k := range responses.ID { - fmt.Println(k) - } - for _, v := range responses.Err { - errs = append(errs, v) - } - return errs.PrintErrors() + return utils.PrintContainerPruneResults(responses) } diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 44f50bab2..4d12d2534 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -12,8 +12,8 @@ import ( tm "github.com/buger/goterm" "github.com/containers/buildah/pkg/formats" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/go-units" @@ -26,7 +26,7 @@ var ( psDescription = "Prints out information about the containers" psCommand = &cobra.Command{ Use: "ps", - Args: common.NoArgs, + Args: validate.NoArgs, Short: "List containers", Long: psDescription, RunE: ps, @@ -41,7 +41,7 @@ var ( } filters []string noTrunc bool - defaultHeaders string = "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES" + defaultHeaders = "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES" ) func init() { @@ -64,9 +64,12 @@ func listFlagSet(flags *pflag.FlagSet) { flags.BoolVarP(&listOpts.Pod, "pod", "p", false, "Print the ID and name of the pod the containers are associated with") flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Print the numeric IDs of the containers only") flags.BoolVarP(&listOpts.Size, "size", "s", false, "Display the total file sizes") - flags.StringVar(&listOpts.Sort, "sort", "created", "Sort output by command, created, id, image, names, runningfor, size, or status") flags.BoolVar(&listOpts.Sync, "sync", false, "Sync container state with OCI runtime") flags.UintVarP(&listOpts.Watch, "watch", "w", 0, "Watch the ps output on an interval in seconds") + + sort := validate.ChoiceValue(&listOpts.Sort, "command", "created", "id", "image", "names", "runningfor", "size", "status") + flags.Var(sort, "sort", "Sort output by: "+sort.Choices()) + if registry.IsRemote() { _ = flags.MarkHidden("latest") } @@ -175,7 +178,7 @@ func ps(cmd *cobra.Command, args []string) error { headers, format := createPsOut() if cmd.Flag("format").Changed { - format = listOpts.Format + format = strings.TrimPrefix(listOpts.Format, "table ") if !strings.HasPrefix(format, "\n") { format += "\n" } @@ -203,7 +206,7 @@ func ps(cmd *cobra.Command, args []string) error { return err } if err := tmpl.Execute(w, responses); err != nil { - return nil + return err } if err := w.Flush(); err != nil { return err @@ -352,7 +355,7 @@ func portsToString(ports []ocicni.PortMapping) string { if len(ports) == 0 { return "" } - //Sort the ports, so grouping continuous ports become easy. + // Sort the ports, so grouping continuous ports become easy. sort.Slice(ports, func(i, j int) bool { return comparePorts(ports[i], ports[j]) }) diff --git a/cmd/podman/containers/rm.go b/cmd/podman/containers/rm.go index 3021853a9..2a0f9cc6a 100644 --- a/cmd/podman/containers/rm.go +++ b/cmd/podman/containers/rm.go @@ -35,9 +35,12 @@ var ( containerRmCommand = &cobra.Command{ Use: rmCommand.Use, - Short: rmCommand.Use, + Short: rmCommand.Short, Long: rmCommand.Long, RunE: rmCommand.RunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, true) + }, Example: `podman container rm imageID podman container rm mywebserver myflaskserver 860a4b23 podman container rm --force --all diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index e3fe4cd0b..5f3ea9ef4 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -20,6 +20,7 @@ import ( var ( runDescription = "Runs a command in a new container from the given image" runCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), Use: "run [flags] IMAGE [COMMAND [ARG...]]", Short: "Run a command in a new container", Long: runDescription, @@ -30,6 +31,7 @@ var ( } containerRunCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), Use: runCommand.Use, Short: runCommand.Short, Long: runCommand.Long, @@ -58,6 +60,8 @@ func runFlags(flags *pflag.FlagSet) { flags.BoolVar(&runRmi, "rmi", false, "Remove container image unless used by other containers") if registry.IsRemote() { _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("env-host") + _ = flags.MarkHidden("http-proxy") } } func init() { @@ -144,6 +148,10 @@ func run(cmd *cobra.Command, args []string) error { } runOpts.Spec = s + if _, err := createPodIfNecessary(s); err != nil { + return err + } + report, err := registry.ContainerEngine().ContainerRun(registry.GetContext(), runOpts) // report.ExitCode is set by ContainerRun even it it returns an error if report != nil { @@ -164,9 +172,9 @@ func run(cmd *cobra.Command, args []string) error { return nil } if runRmi { - _, err := registry.ImageEngine().Remove(registry.GetContext(), []string{args[0]}, entities.ImageRemoveOptions{}) - if err != nil { - logrus.Errorf("%s", errors.Wrapf(err, "failed removing image")) + _, rmErrors := registry.ImageEngine().Remove(registry.GetContext(), []string{args[0]}, entities.ImageRemoveOptions{}) + if len(rmErrors) > 0 { + logrus.Errorf("%s", errors.Wrapf(errorhandling.JoinErrors(rmErrors), "failed removing image")) } } return nil diff --git a/cmd/podman/containers/runlabel.go b/cmd/podman/containers/runlabel.go new file mode 100644 index 000000000..11fa362b8 --- /dev/null +++ b/cmd/podman/containers/runlabel.go @@ -0,0 +1,80 @@ +package containers + +import ( + "context" + "os" + + "github.com/containers/common/pkg/auth" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// runlabelOptionsWrapper allows for combining API-only with CLI-only options +// and to convert between them. +type runlabelOptionsWrapper struct { + entities.ContainerRunlabelOptions + TLSVerifyCLI bool +} + +var ( + runlabelOptions = runlabelOptionsWrapper{} + runlabelDescription = "Executes a command as described by a container image label." + runlabelCommand = &cobra.Command{ + Use: "runlabel [flags] LABEL IMAGE [ARG...]", + Short: "Execute the command described by an image label", + Long: runlabelDescription, + RunE: runlabel, + Args: cobra.MinimumNArgs(2), + Example: `podman container runlabel run imageID + podman container runlabel --pull install imageID arg1 arg2 + podman container runlabel --display run myImage`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: runlabelCommand, + Parent: containerCmd, + }) + + flags := rmCommand.Flags() + flags.StringVar(&runlabelOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&runlabelOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") + flags.StringVar(&runlabelOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.BoolVar(&runlabelOptions.Display, "display", false, "Preview the command that the label would run") + flags.StringVarP(&runlabelOptions.Name, "name", "n", "", "Assign a name to the container") + flags.StringVar(&runlabelOptions.Optional1, "opt1", "", "Optional parameter to pass for install") + flags.StringVar(&runlabelOptions.Optional2, "opt2", "", "Optional parameter to pass for install") + flags.StringVar(&runlabelOptions.Optional3, "opt3", "", "Optional parameter to pass for install") + flags.BoolP("pull", "p", false, "Pull the image if it does not exist locally prior to executing the label contents") + flags.BoolVarP(&runlabelOptions.Quiet, "quiet", "q", false, "Suppress output information when installing images") + flags.BoolVar(&runlabelOptions.Replace, "replace", false, "Replace existing container with a new one from the image") + flags.StringVar(&runlabelOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") + flags.BoolVar(&runlabelOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + // Hide the optional flags. + _ = flags.MarkHidden("opt1") + _ = flags.MarkHidden("opt2") + _ = flags.MarkHidden("opt3") + + if err := flags.MarkDeprecated("pull", "podman will pull if not found in local storage"); err != nil { + logrus.Error("unable to mark pull flag deprecated") + } +} + +func runlabel(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("tls-verify") { + runlabelOptions.SkipTLSVerify = types.NewOptionalBool(!runlabelOptions.TLSVerifyCLI) + } + if runlabelOptions.Authfile != "" { + if _, err := os.Stat(runlabelOptions.Authfile); err != nil { + return errors.Wrapf(err, "error getting authfile %s", runlabelOptions.Authfile) + } + } + return registry.ContainerEngine().ContainerRunlabel(context.Background(), args[0], args[1], args[2:], runlabelOptions.ContainerRunlabelOptions) +} diff --git a/cmd/podman/containers/start.go b/cmd/podman/containers/start.go index 381bf8e26..ce78d24ed 100644 --- a/cmd/podman/containers/start.go +++ b/cmd/podman/containers/start.go @@ -99,7 +99,7 @@ func start(cmd *cobra.Command, args []string) error { for _, r := range responses { if r.Err == nil { - fmt.Println(r.Id) + fmt.Println(r.RawInput) } else { errs = append(errs, r.Err) } diff --git a/cmd/podman/containers/stats.go b/cmd/podman/containers/stats.go new file mode 100644 index 000000000..5b7f52cc7 --- /dev/null +++ b/cmd/podman/containers/stats.go @@ -0,0 +1,243 @@ +package containers + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + "text/template" + + tm "github.com/buger/goterm" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/utils" + "github.com/docker/go-units" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + statsDescription = "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers." + statsCommand = &cobra.Command{ + Use: "stats [flags] [CONTAINER...]", + Short: "Display a live stream of container resource usage statistics", + Long: statsDescription, + RunE: stats, + Args: checkStatOptions, + Example: `podman stats --all --no-stream + podman stats ctrID + podman stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`, + } + + containerStatsCommand = &cobra.Command{ + Use: statsCommand.Use, + Short: statsCommand.Short, + Long: statsCommand.Long, + RunE: statsCommand.RunE, + Args: checkStatOptions, + Example: `podman container stats --all --no-stream + podman container stats ctrID + podman container stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`, + } +) + +var ( + statsOptions entities.ContainerStatsOptions + defaultStatsRow = "{{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}\n" + defaultStatsHeader = "ID\tNAME\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET IO\tBLOCK IO\tPIDS\n" +) + +func statFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&statsOptions.All, "all", "a", false, "Show all containers. Only running containers are shown by default. The default is false") + flags.StringVar(&statsOptions.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template") + flags.BoolVarP(&statsOptions.Latest, "latest", "l", false, "Act on the latest container Podman is aware of") + flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals") + flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: statsCommand, + }) + flags := statsCommand.Flags() + statFlags(flags) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: containerStatsCommand, + Parent: containerCmd, + }) + + containerStatsFlags := containerStatsCommand.Flags() + statFlags(containerStatsFlags) +} + +// stats is different in that it will assume running containers if +// no input is given, so we need to validate differently +func checkStatOptions(cmd *cobra.Command, args []string) error { + opts := 0 + if statsOptions.All { + opts += 1 + } + if statsOptions.Latest { + opts += 1 + } + if len(args) > 0 { + opts += 1 + } + if opts > 1 { + return errors.Errorf("--all, --latest and containers cannot be used together") + } + return nil +} + +func stats(cmd *cobra.Command, args []string) error { + if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return err + } + if !unified { + return errors.New("stats is not supported in rootless mode without cgroups v2") + } + } + statsOptions.StatChan = make(chan []*define.ContainerStats, 1) + go func() { + for reports := range statsOptions.StatChan { + if err := outputStats(reports); err != nil { + logrus.Error(err) + } + } + }() + return registry.ContainerEngine().ContainerStats(registry.Context(), args, statsOptions) +} + +func outputStats(reports []*define.ContainerStats) error { + if len(statsOptions.Format) < 1 && !statsOptions.NoReset { + tm.Clear() + tm.MoveCursor(1, 1) + tm.Flush() + } + var stats []*containerStats + for _, r := range reports { + stats = append(stats, &containerStats{r}) + } + if statsOptions.Format == "json" { + return outputJSON(stats) + } + format := defaultStatsRow + if len(statsOptions.Format) > 0 { + format = statsOptions.Format + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + } + format = "{{range . }}" + format + "{{end}}" + if len(statsOptions.Format) < 1 { + format = defaultStatsHeader + format + } + tmpl, err := template.New("stats").Parse(format) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + if err := tmpl.Execute(w, stats); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err + } + return nil +} + +type containerStats struct { + *define.ContainerStats +} + +func (s *containerStats) ID() string { + return s.ContainerID[0:12] +} + +func (s *containerStats) CPUPerc() string { + return floatToPercentString(s.CPU) +} + +func (s *containerStats) MemPerc() string { + return floatToPercentString(s.ContainerStats.MemPerc) +} + +func (s *containerStats) NetIO() string { + return combineHumanValues(s.NetInput, s.NetOutput) +} + +func (s *containerStats) BlockIO() string { + return combineHumanValues(s.BlockInput, s.BlockOutput) +} + +func (s *containerStats) PIDS() string { + if s.PIDs == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%d", s.PIDs) +} +func (s *containerStats) MemUsage() string { + return combineHumanValues(s.ContainerStats.MemUsage, s.ContainerStats.MemLimit) +} + +func floatToPercentString(f float64) string { + strippedFloat, err := utils.RemoveScientificNotationFromFloat(f) + if err != nil || strippedFloat == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%.2f", strippedFloat) + "%" +} + +func combineHumanValues(a, b uint64) string { + if a == 0 && b == 0 { + return "-- / --" + } + return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) +} + +func outputJSON(stats []*containerStats) error { + type jstat struct { + Id string `json:"id"` + Name string `json:"name"` + CpuPercent string `json:"cpu_percent"` + MemUsage string `json:"mem_usage"` + MemPerc string `json:"mem_percent"` + NetIO string `json:"net_io"` + BlockIO string `json:"block_io"` + Pids string `json:"pids"` + } + var jstats []jstat + for _, j := range stats { + jstats = append(jstats, jstat{ + Id: j.ID(), + Name: j.Name, + CpuPercent: j.CPUPerc(), + MemUsage: j.MemPerc(), + MemPerc: j.MemUsage(), + NetIO: j.NetIO(), + BlockIO: j.BlockIO(), + Pids: j.PIDS(), + }) + } + b, err := json.MarshalIndent(jstats, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} diff --git a/cmd/podman/containers/stop.go b/cmd/podman/containers/stop.go index 4a451134a..22c487961 100644 --- a/cmd/podman/containers/stop.go +++ b/cmd/podman/containers/stop.go @@ -34,6 +34,9 @@ var ( Short: stopCommand.Short, Long: stopCommand.Long, RunE: stopCommand.RunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, true) + }, Example: `podman container stop ctrID podman container stop --latest podman container stop --time 2 mywebserver 6e534f14da9d`, diff --git a/cmd/podman/containers/unmount.go b/cmd/podman/containers/unmount.go index a4550abbd..d0ca202fe 100644 --- a/cmd/podman/containers/unmount.go +++ b/cmd/podman/containers/unmount.go @@ -27,6 +27,9 @@ var ( Args: func(cmd *cobra.Command, args []string) error { return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) }, + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, Example: `podman umount ctrID podman umount ctrID1 ctrID2 ctrID3 podman umount --all`, @@ -37,6 +40,9 @@ var ( Short: umountCommand.Short, Long: umountCommand.Long, RunE: umountCommand.RunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, Example: `podman container umount ctrID podman container umount ctrID1 ctrID2 ctrID3 podman container umount --all`, diff --git a/cmd/podman/containers/unpause.go b/cmd/podman/containers/unpause.go index adf8d12ee..7ea8e13c1 100644 --- a/cmd/podman/containers/unpause.go +++ b/cmd/podman/containers/unpause.go @@ -49,7 +49,7 @@ func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, - Command: unpauseCommand, + Command: containerUnpauseCommand, Parent: containerCmd, }) diff --git a/cmd/podman/containers/wait.go b/cmd/podman/containers/wait.go index da746361d..1f4d4159b 100644 --- a/cmd/podman/containers/wait.go +++ b/cmd/podman/containers/wait.go @@ -7,6 +7,7 @@ import ( "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -22,7 +23,7 @@ var ( Short: "Block on one or more containers", Long: waitDescription, RunE: wait, - Args: registry.IdOrLatestArgs, + Args: validate.IdOrLatestArgs, Example: `podman wait --latest podman wait --interval 5000 ctrID podman wait ctrID1 ctrID2`, @@ -33,6 +34,7 @@ var ( Short: waitCommand.Short, Long: waitCommand.Long, RunE: waitCommand.RunE, + Args: validate.IdOrLatestArgs, Example: `podman container wait --latest podman container wait --interval 5000 ctrID podman container wait ctrID1 ctrID2`, diff --git a/cmd/podman/diff.go b/cmd/podman/diff.go index ec94c0918..1ff2fce40 100644 --- a/cmd/podman/diff.go +++ b/cmd/podman/diff.go @@ -6,6 +6,7 @@ import ( "github.com/containers/libpod/cmd/podman/containers" "github.com/containers/libpod/cmd/podman/images" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -17,7 +18,7 @@ var ( diffDescription = `Displays changes on a container or image's filesystem. The container or image will be compared to its parent layer.` diffCmd = &cobra.Command{ Use: "diff [flags] {CONTAINER_ID | IMAGE_ID}", - Args: registry.IdOrLatestArgs, + Args: validate.IdOrLatestArgs, Short: "Display the changes of object's file system", Long: diffDescription, TraverseChildren: true, diff --git a/cmd/podman/generate/generate.go b/cmd/podman/generate/generate.go index f04ef58a5..7803c0c78 100644 --- a/cmd/podman/generate/generate.go +++ b/cmd/podman/generate/generate.go @@ -2,6 +2,7 @@ package pods import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/util" "github.com/spf13/cobra" @@ -14,14 +15,14 @@ var ( Short: "Generate structured data based on containers and pods.", Long: "Generate structured data (e.g., Kubernetes yaml or systemd units) based on containers and pods.", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } containerConfig = util.DefaultContainerConfig() ) func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode}, + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: generateCmd, }) } diff --git a/cmd/podman/generate/kube.go b/cmd/podman/generate/kube.go new file mode 100644 index 000000000..86a9cc686 --- /dev/null +++ b/cmd/podman/generate/kube.go @@ -0,0 +1,68 @@ +package pods + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + kubeOptions = entities.GenerateKubeOptions{} + kubeFile = "" + kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from a Podman container or pod. + +Whether the input is for a container or pod, Podman will always generate the specification as a pod.` + + kubeCmd = &cobra.Command{ + Use: "kube [flags] CONTAINER | POD", + Short: "Generate Kubernetes YAML from a container or pod.", + Long: kubeDescription, + RunE: kube, + Args: cobra.ExactArgs(1), + Example: `podman generate kube ctrID + podman generate kube podID + podman generate kube --service podID`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: kubeCmd, + Parent: generateCmd, + }) + flags := kubeCmd.Flags() + flags.BoolVarP(&kubeOptions.Service, "service", "s", false, "Generate YAML for a Kubernetes service object") + flags.StringVarP(&kubeFile, "filename", "f", "", "Write output to the specified path") + flags.SetNormalizeFunc(utils.AliasFlags) +} + +func kube(cmd *cobra.Command, args []string) error { + report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args[0], kubeOptions) + if err != nil { + return err + } + + content, err := ioutil.ReadAll(report.Reader) + if err != nil { + return err + } + if cmd.Flags().Changed("filename") { + if _, err := os.Stat(kubeFile); err == nil { + return errors.Errorf("cannot write to %q", kubeFile) + } + if err := ioutil.WriteFile(kubeFile, content, 0644); err != nil { + return errors.Wrapf(err, "cannot write to %q", kubeFile) + } + return nil + } + + fmt.Println(string(content)) + return nil +} diff --git a/cmd/podman/generate/systemd.go b/cmd/podman/generate/systemd.go index 55d770249..20d9748d4 100644 --- a/cmd/podman/generate/systemd.go +++ b/cmd/podman/generate/systemd.go @@ -29,7 +29,7 @@ var ( func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Mode: []entities.EngineMode{entities.ABIMode}, Command: systemdCmd, Parent: generateCmd, }) diff --git a/cmd/podman/healthcheck/healthcheck.go b/cmd/podman/healthcheck/healthcheck.go index 794a94615..f48701624 100644 --- a/cmd/podman/healthcheck/healthcheck.go +++ b/cmd/podman/healthcheck/healthcheck.go @@ -2,6 +2,7 @@ package healthcheck import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -10,10 +11,10 @@ var ( // Command: healthcheck healthCmd = &cobra.Command{ Use: "healthcheck", - Short: "Manage Healthcheck", - Long: "Manage Healthcheck", + Short: "Manage health checks on containers", + Long: "Run health checks on containers", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } ) diff --git a/cmd/podman/build.go b/cmd/podman/images/build.go index 43a2f7ab5..06a7efd25 100644 --- a/cmd/podman/build.go +++ b/cmd/podman/images/build.go @@ -1,4 +1,4 @@ -package main +package images import ( "os" @@ -17,6 +17,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // buildFlagsWrapper are local to cmd/ as the build code is using Buildah-internal @@ -48,6 +49,17 @@ var ( podman build --layers --force-rm --tag imageName .`, } + imageBuildCmd = &cobra.Command{ + Args: buildCmd.Args, + Use: buildCmd.Use, + Short: buildCmd.Short, + Long: buildCmd.Long, + RunE: buildCmd.RunE, + Example: `podman image build . + podman image build --creds=username:password -t imageName -f Containerfile.simple . + podman image build --layers --force-rm --tag imageName .`, + } + buildOpts = buildFlagsWrapper{} ) @@ -66,8 +78,17 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: buildCmd, }) - flags := buildCmd.Flags() + buildFlags(buildCmd.Flags()) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageBuildCmd, + Parent: imageCmd, + }) + buildFlags(imageBuildCmd.Flags()) +} +func buildFlags(flags *pflag.FlagSet) { // Podman flags flags.BoolVarP(&buildOpts.SquashAll, "squash-all", "", false, "Squash all layers into a single layer") diff --git a/cmd/podman/images/diff.go b/cmd/podman/images/diff.go index 7cfacfc6c..c24f98369 100644 --- a/cmd/podman/images/diff.go +++ b/cmd/podman/images/diff.go @@ -6,6 +6,7 @@ import ( "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -28,9 +29,11 @@ func init() { Command: diffCmd, Parent: imageCmd, }) + diffFlags(diffCmd.Flags()) +} +func diffFlags(flags *pflag.FlagSet) { diffOpts = &entities.DiffOptions{} - flags := diffCmd.Flags() flags.BoolVar(&diffOpts.Archive, "archive", true, "Save the diff as a tar archive") _ = flags.MarkDeprecated("archive", "Provided for backwards compatibility, has no impact on output.") flags.StringVar(&diffOpts.Format, "format", "", "Change the output format") diff --git a/cmd/podman/images/history.go b/cmd/podman/images/history.go index ce153aa46..17a80557e 100644 --- a/cmd/podman/images/history.go +++ b/cmd/podman/images/history.go @@ -15,6 +15,7 @@ import ( "github.com/docker/go-units" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -32,6 +33,15 @@ var ( RunE: history, } + imageHistoryCmd = &cobra.Command{ + Args: historyCmd.Args, + Use: historyCmd.Use, + Short: historyCmd.Short, + Long: historyCmd.Long, + RunE: historyCmd.RunE, + Example: `podman image history imageID`, + } + opts = struct { human bool noTrunc bool @@ -45,8 +55,17 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: historyCmd, }) + historyFlags(historyCmd.Flags()) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageHistoryCmd, + Parent: imageCmd, + }) + historyFlags(imageHistoryCmd.Flags()) +} - flags := historyCmd.Flags() +func historyFlags(flags *pflag.FlagSet) { flags.StringVar(&opts.format, "format", "", "Change the output to JSON or a Go template") flags.BoolVarP(&opts.human, "human", "H", true, "Display sizes and dates in human readable format") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") diff --git a/cmd/podman/images/image.go b/cmd/podman/images/image.go index 604f49251..790c16c05 100644 --- a/cmd/podman/images/image.go +++ b/cmd/podman/images/image.go @@ -2,6 +2,7 @@ package images import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -16,7 +17,7 @@ var ( Short: "Manage images", Long: "Manage images", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } ) diff --git a/cmd/podman/images/import.go b/cmd/podman/images/import.go index 1c0568762..0e16128ce 100644 --- a/cmd/podman/images/import.go +++ b/cmd/podman/images/import.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -26,6 +27,17 @@ var ( cat ctr.tar | podman -q import --message "importing the ctr.tar tarball" - image-imported cat ctr.tar | podman import -`, } + + imageImportCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), + Use: importCommand.Use, + Short: importCommand.Short, + Long: importCommand.Long, + RunE: importCommand.RunE, + Example: `podman image import http://example.com/ctr.tar url-image + cat ctr.tar | podman -q image import --message "importing the ctr.tar tarball" - image-imported + cat ctr.tar | podman image import -`, + } ) var ( @@ -37,8 +49,17 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: importCommand, }) + importFlags(importCommand.Flags()) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageImportCommand, + Parent: imageCmd, + }) + importFlags(imageImportCommand.Flags()) +} - flags := importCommand.Flags() +func importFlags(flags *pflag.FlagSet) { flags.StringArrayVarP(&importOpts.Changes, "change", "c", []string{}, "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR") flags.StringVarP(&importOpts.Message, "message", "m", "", "Set commit message for imported image") flags.BoolVarP(&importOpts.Quiet, "quiet", "q", false, "Suppress output") diff --git a/cmd/podman/images/load.go b/cmd/podman/images/load.go index f49f95002..4bbffd432 100644 --- a/cmd/podman/images/load.go +++ b/cmd/podman/images/load.go @@ -15,6 +15,7 @@ import ( "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/crypto/ssh/terminal" ) @@ -27,6 +28,14 @@ var ( RunE: load, Args: cobra.MaximumNArgs(1), } + + imageLoadCommand = &cobra.Command{ + Args: cobra.MinimumNArgs(1), + Use: loadCommand.Use, + Short: loadCommand.Short, + Long: loadCommand.Long, + RunE: loadCommand.RunE, + } ) var ( @@ -38,8 +47,16 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: loadCommand, }) + loadFlags(loadCommand.Flags()) + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageLoadCommand, + Parent: imageCmd, + }) + loadFlags(imageLoadCommand.Flags()) +} - flags := loadCommand.Flags() +func loadFlags(flags *pflag.FlagSet) { flags.StringVarP(&loadOpts.Input, "input", "i", "", "Read from specified archive file (default: stdin)") flags.BoolVarP(&loadOpts.Quiet, "quiet", "q", false, "Suppress the output") flags.StringVar(&loadOpts.SignaturePolicy, "signature-policy", "", "Pathname of signature policy file") @@ -61,7 +78,6 @@ func load(cmd *cobra.Command, args []string) error { loadOpts.Tag = "latest" } if r, ok := ref.(reference.Named); ok { - fmt.Println(r.Name()) loadOpts.Name = r.Name() } } diff --git a/cmd/podman/images/prune.go b/cmd/podman/images/prune.go index eb9e4a7e4..7c9e3eb61 100644 --- a/cmd/podman/images/prune.go +++ b/cmd/podman/images/prune.go @@ -6,8 +6,9 @@ import ( "os" "strings" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -19,7 +20,7 @@ var ( If an image is not being used by a container, it will be removed from the system.` pruneCmd = &cobra.Command{ Use: "prune", - Args: common.NoArgs, + Args: validate.NoArgs, Short: "Remove unused images", Long: pruneDescription, RunE: prune, @@ -71,17 +72,5 @@ Are you sure you want to continue? [y/N] `) 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 + return utils.PrintImagePruneResults(results) } diff --git a/cmd/podman/images/pull.go b/cmd/podman/images/pull.go index fead5f7ed..9e883703f 100644 --- a/cmd/podman/images/pull.go +++ b/cmd/podman/images/pull.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/auth" "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" @@ -45,6 +45,7 @@ var ( Short: pullCmd.Short, Long: pullCmd.Long, RunE: pullCmd.RunE, + Args: cobra.ExactArgs(1), Example: `podman image pull imageName podman image pull fedora:latest`, } @@ -74,7 +75,7 @@ func init() { // pullFlags set the flags for the pull command. func pullFlags(flags *pflag.FlagSet) { flags.BoolVar(&pullOptions.AllTags, "all-tags", false, "All tagged images in the repository will be pulled") - flags.StringVar(&pullOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&pullOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&pullOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") flags.StringVar(&pullOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") flags.StringVar(&pullOptions.OverrideArch, "override-arch", "", "Use `ARCH` instead of the architecture of the machine for choosing images") diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go index 0b3502d61..dd536213f 100644 --- a/cmd/podman/images/push.go +++ b/cmd/podman/images/push.go @@ -3,7 +3,7 @@ package images import ( "os" - buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/auth" "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" @@ -70,7 +70,7 @@ func init() { // pushFlags set the flags for the push command. func pushFlags(flags *pflag.FlagSet) { - flags.StringVar(&pushOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&pushOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&pushOptions.CertDir, "cert-dir", "", "Path to a directory containing TLS certificates and keys") flags.BoolVar(&pushOptions.Compress, "compress", false, "Compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)") flags.StringVar(&pushOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") diff --git a/cmd/podman/images/rm.go b/cmd/podman/images/rm.go index 1cf5fa365..4b9920532 100644 --- a/cmd/podman/images/rm.go +++ b/cmd/podman/images/rm.go @@ -5,6 +5,7 @@ import ( "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/errorhandling" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -48,7 +49,9 @@ func rm(cmd *cobra.Command, args []string) error { return errors.Errorf("when using the --all switch, you may not pass any images names or IDs") } - report, err := registry.ImageEngine().Remove(registry.GetContext(), args, imageOpts) + // Note: certain image-removal errors are non-fatal. Hence, the report + // might be set even if err != nil. + report, rmErrors := registry.ImageEngine().Remove(registry.GetContext(), args, imageOpts) if report != nil { for _, u := range report.Untagged { fmt.Println("Untagged: " + u) @@ -62,5 +65,5 @@ func rm(cmd *cobra.Command, args []string) error { registry.SetExitCode(report.ExitCode) } - return err + return errorhandling.JoinErrors(rmErrors) } diff --git a/cmd/podman/images/save.go b/cmd/podman/images/save.go index 8f7832074..56953e41c 100644 --- a/cmd/podman/images/save.go +++ b/cmd/podman/images/save.go @@ -13,6 +13,7 @@ import ( "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/crypto/ssh/terminal" ) @@ -43,6 +44,16 @@ var ( podman save --format docker-dir -o ubuntu-dir ubuntu podman save > alpine-all.tar alpine:latest`, } + imageSaveCommand = &cobra.Command{ + Args: saveCommand.Args, + Use: saveCommand.Use, + Short: saveCommand.Short, + Long: saveCommand.Long, + RunE: saveCommand.RunE, + Example: `podman image save --quiet -o myimage.tar imageID + podman image save --format docker-dir -o ubuntu-dir ubuntu + podman image save > alpine-all.tar alpine:latest`, + } ) var ( @@ -54,7 +65,17 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: saveCommand, }) - flags := saveCommand.Flags() + saveFlags(saveCommand.Flags()) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageSaveCommand, + Parent: imageCmd, + }) + saveFlags(imageSaveCommand.Flags()) +} + +func saveFlags(flags *pflag.FlagSet) { flags.BoolVar(&saveOpts.Compress, "compress", false, "Compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)") flags.StringVar(&saveOpts.Format, "format", define.V2s2Archive, "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-archive, docker-dir (directory with v2s2 manifest type)") flags.StringVarP(&saveOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)") diff --git a/cmd/podman/images/search.go b/cmd/podman/images/search.go index a8abfb339..640d497c3 100644 --- a/cmd/podman/images/search.go +++ b/cmd/podman/images/search.go @@ -5,8 +5,8 @@ import ( "reflect" "strings" - buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/formats" + "github.com/containers/common/pkg/auth" "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" @@ -87,7 +87,7 @@ func searchFlags(flags *pflag.FlagSet) { flags.StringVar(&searchOptions.Format, "format", "", "Change the output format to a Go template") flags.IntVar(&searchOptions.Limit, "limit", 0, "Limit the number of results") flags.BoolVar(&searchOptions.NoTrunc, "no-trunc", false, "Do not truncate the output") - flags.StringVar(&searchOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&searchOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.BoolVar(&searchOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") if registry.IsRemote() { _ = flags.MarkHidden("authfile") @@ -105,6 +105,10 @@ func imageSearch(cmd *cobra.Command, args []string) error { return errors.Errorf("search requires exactly one argument") } + if searchOptions.Limit > 100 { + return errors.Errorf("Limit %d is outside the range of [1, 100]", searchOptions.Limit) + } + // TLS verification in c/image is controlled via a `types.OptionalBool` // which allows for distinguishing among set-true, set-false, unspecified // which is important to implement a sane way of dealing with defaults of diff --git a/cmd/podman/images/tag.go b/cmd/podman/images/tag.go index 411313a9b..dae3416c4 100644 --- a/cmd/podman/images/tag.go +++ b/cmd/podman/images/tag.go @@ -18,6 +18,17 @@ var ( podman tag imageID:latest myNewImage:newTag podman tag httpd myregistryhost:5000/fedora/httpd:v2`, } + + imageTagCommand = &cobra.Command{ + Args: tagCommand.Args, + Use: tagCommand.Use, + Short: tagCommand.Short, + Long: tagCommand.Long, + RunE: tagCommand.RunE, + Example: `podman image tag 0e3bbc2 fedora:latest + podman image tag imageID:latest myNewImage:newTag + podman image tag httpd myregistryhost:5000/fedora/httpd:v2`, + } ) func init() { @@ -25,6 +36,11 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: tagCommand, }) + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageTagCommand, + Parent: imageCmd, + }) } func tag(cmd *cobra.Command, args []string) error { diff --git a/cmd/podman/images/trust.go b/cmd/podman/images/trust.go new file mode 100644 index 000000000..88a567871 --- /dev/null +++ b/cmd/podman/images/trust.go @@ -0,0 +1,27 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + trustDescription = `Manages which registries you trust as a source of container images based on their location. + The location is determined by the transport and the registry host of the image. Using this container image docker://quay.io/podman/stable as an example, docker is the transport and quay.io is the registry host.` + trustCmd = &cobra.Command{ + Use: "trust", + Short: "Manage container image trust policy", + Long: trustDescription, + RunE: validate.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: trustCmd, + Parent: imageCmd, + }) +} diff --git a/cmd/podman/images/trust_set.go b/cmd/podman/images/trust_set.go new file mode 100644 index 000000000..5868f5546 --- /dev/null +++ b/cmd/podman/images/trust_set.go @@ -0,0 +1,56 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + setTrustDescription = "Set default trust policy or add a new trust policy for a registry" + setTrustCommand = &cobra.Command{ + Use: "set [flags] REGISTRY", + Short: "Set default trust policy or a new trust policy for a registry", + Long: setTrustDescription, + Example: "", + RunE: setTrust, + Args: cobra.ExactArgs(1), + } +) + +var ( + setOptions entities.SetTrustOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: setTrustCommand, + Parent: trustCmd, + }) + setFlags := setTrustCommand.Flags() + setFlags.StringVar(&setOptions.PolicyPath, "policypath", "", "") + _ = setFlags.MarkHidden("policypath") + setFlags.StringSliceVarP(&setOptions.PubKeysFile, "pubkeysfile", "f", []string{}, `Path of installed public key(s) to trust for TARGET. +Absolute path to keys is added to policy.json. May +used multiple times to define multiple public keys. +File(s) must exist before using this command`) + setFlags.StringVarP(&setOptions.Type, "type", "t", "signedBy", "Trust type, accept values: signedBy(default), accept, reject") +} + +func setTrust(cmd *cobra.Command, args []string) error { + validTrustTypes := []string{"accept", "insecureAcceptAnything", "reject", "signedBy"} + + valid, err := image.IsValidImageURI(args[0]) + if err != nil || !valid { + return errors.Wrapf(err, "invalid image uri %s", args[0]) + } + + if !util.StringInSlice(setOptions.Type, validTrustTypes) { + return errors.Errorf("invalid choice: %s (choose from 'accept', 'reject', 'signedBy')", setOptions.Type) + } + return registry.ImageEngine().SetTrust(registry.Context(), args, setOptions) +} diff --git a/cmd/podman/images/trust_show.go b/cmd/podman/images/trust_show.go new file mode 100644 index 000000000..23ee6c709 --- /dev/null +++ b/cmd/podman/images/trust_show.go @@ -0,0 +1,77 @@ +package images + +import ( + "fmt" + "os" + "text/tabwriter" + "text/template" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + showTrustDescription = "Display trust policy for the system" + showTrustCommand = &cobra.Command{ + Use: "show [flags] [REGISTRY]", + Short: "Display trust policy for the system", + Long: showTrustDescription, + RunE: showTrust, + Example: "", + } +) + +var ( + showTrustOptions entities.ShowTrustOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: showTrustCommand, + Parent: trustCmd, + }) + showFlags := showTrustCommand.Flags() + showFlags.BoolVarP(&showTrustOptions.JSON, "json", "j", false, "Output as json") + showFlags.StringVar(&showTrustOptions.PolicyPath, "policypath", "", "") + showFlags.BoolVar(&showTrustOptions.Raw, "raw", false, "Output raw policy file") + _ = showFlags.MarkHidden("policypath") + showFlags.StringVar(&showTrustOptions.RegistryPath, "registrypath", "", "") + _ = showFlags.MarkHidden("registrypath") + +} + +func showTrust(cmd *cobra.Command, args []string) error { + report, err := registry.ImageEngine().ShowTrust(registry.Context(), args, showTrustOptions) + if err != nil { + return err + } + if showTrustOptions.Raw { + fmt.Println(report.Raw) + return nil + } + if showTrustOptions.JSON { + b, err := json.MarshalIndent(report.Policies, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil + } + + row := "{{.RepoName}}\t{{.Type}}\t{{.GPGId}}\t{{.SignatureStore}}\n" + format := "{{range . }}" + row + "{{end}}" + tmpl, err := template.New("listContainers").Parse(format) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + if err := tmpl.Execute(w, report.Policies); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err + } + return nil +} diff --git a/cmd/podman/images/untag.go b/cmd/podman/images/untag.go index 3218844b7..266a3f115 100644 --- a/cmd/podman/images/untag.go +++ b/cmd/podman/images/untag.go @@ -17,6 +17,17 @@ var ( podman untag imageID:latest otherImageName:latest podman untag httpd myregistryhost:5000/fedora/httpd:v2`, } + + imageUntagCommand = &cobra.Command{ + Args: untagCommand.Args, + Use: untagCommand.Use, + Short: untagCommand.Short, + Long: untagCommand.Long, + RunE: untagCommand.RunE, + Example: `podman image untag 0e3bbc2 + podman image untag imageID:latest otherImageName:latest + podman image untag httpd myregistryhost:5000/fedora/httpd:v2`, + } ) func init() { @@ -24,6 +35,11 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: untagCommand, }) + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageUntagCommand, + Parent: imageCmd, + }) } func untag(cmd *cobra.Command, args []string) error { diff --git a/cmd/podman/login.go b/cmd/podman/login.go index 1843a764d..92f13d0e7 100644 --- a/cmd/podman/login.go +++ b/cmd/podman/login.go @@ -8,6 +8,7 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/registries" "github.com/spf13/cobra" ) @@ -19,11 +20,11 @@ type loginOptionsWrapper struct { var ( loginOptions = loginOptionsWrapper{} loginCommand = &cobra.Command{ - Use: "login [flags] REGISTRY", + Use: "login [flags] [REGISTRY]", Short: "Login to a container registry", Long: "Login to a container registry on a specified server.", RunE: login, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Example: `podman login quay.io podman login --username ... --password ... quay.io podman login --authfile dir/auth.json quay.io`, @@ -45,9 +46,9 @@ func init() { // Podman flags. flags.BoolVarP(&loginOptions.tlsVerify, "tls-verify", "", false, "Require HTTPS and verify certificates when contacting registries") - flags.BoolVarP(&loginOptions.GetLoginSet, "get-login", "", false, "Return the current login user for the registry") loginOptions.Stdin = os.Stdin loginOptions.Stdout = os.Stdout + loginOptions.AcceptUnspecifiedRegistry = true } // Implementation of podman-login. @@ -62,7 +63,8 @@ func login(cmd *cobra.Command, args []string) error { AuthFilePath: loginOptions.AuthFile, DockerCertPath: loginOptions.CertDir, DockerInsecureSkipTLSVerify: skipTLS, + SystemRegistriesConfPath: registries.SystemRegistriesConfPath(), } - - return auth.Login(context.Background(), &sysCtx, &loginOptions.LoginOptions, args[0]) + loginOptions.GetLoginSet = cmd.Flag("get-login").Changed + return auth.Login(context.Background(), &sysCtx, &loginOptions.LoginOptions, args) } diff --git a/cmd/podman/logout.go b/cmd/podman/logout.go index 77bdc92b4..c016de8ae 100644 --- a/cmd/podman/logout.go +++ b/cmd/podman/logout.go @@ -7,14 +7,14 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" - "github.com/pkg/errors" + "github.com/containers/libpod/pkg/registries" "github.com/spf13/cobra" ) var ( logoutOptions = auth.LogoutOptions{} logoutCommand = &cobra.Command{ - Use: "logout [flags] REGISTRY", + Use: "logout [flags] [REGISTRY]", Short: "Logout of a container registry", Long: "Remove the cached username and password for the registry.", RunE: logout, @@ -37,21 +37,15 @@ func init() { // Flags from the auth package. flags.AddFlagSet(auth.GetLogoutFlags(&logoutOptions)) - logoutOptions.Stdin = os.Stdin logoutOptions.Stdout = os.Stdout + logoutOptions.AcceptUnspecifiedRegistry = true } // Implementation of podman-logout. func logout(cmd *cobra.Command, args []string) error { - sysCtx := types.SystemContext{AuthFilePath: logoutOptions.AuthFile} - - registry := "" - if len(args) > 0 { - if logoutOptions.All { - return errors.New("--all takes no arguments") - } - registry = args[0] + sysCtx := types.SystemContext{ + AuthFilePath: logoutOptions.AuthFile, + SystemRegistriesConfPath: registries.SystemRegistriesConfPath(), } - - return auth.Logout(&sysCtx, &logoutOptions, registry) + return auth.Logout(&sysCtx, &logoutOptions, args) } diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 481214a38..76ec7bc8e 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" _ "github.com/containers/libpod/cmd/podman/containers" @@ -8,11 +9,15 @@ import ( _ "github.com/containers/libpod/cmd/podman/healthcheck" _ "github.com/containers/libpod/cmd/podman/images" _ "github.com/containers/libpod/cmd/podman/manifest" + _ "github.com/containers/libpod/cmd/podman/networks" + _ "github.com/containers/libpod/cmd/podman/play" _ "github.com/containers/libpod/cmd/podman/pods" "github.com/containers/libpod/cmd/podman/registry" _ "github.com/containers/libpod/cmd/podman/system" _ "github.com/containers/libpod/cmd/podman/volumes" + "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage/pkg/reexec" + "github.com/spf13/cobra" ) func main() { @@ -26,6 +31,14 @@ func main() { for _, c := range registry.Commands { for _, m := range c.Mode { if cfg.EngineMode == m { + // Command cannot be run rootless + _, found := c.Command.Annotations[registry.ParentNSRequired] + if rootless.IsRootless() && found { + c.Command.RunE = func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("cannot `%s` in rootless mode", cmd.CommandPath()) + } + } + parent := rootCmd if c.Parent != nil { parent = c.Parent diff --git a/cmd/podman/manifest/annotate.go b/cmd/podman/manifest/annotate.go new file mode 100644 index 000000000..82ee1ffda --- /dev/null +++ b/cmd/podman/manifest/annotate.go @@ -0,0 +1,56 @@ +package manifest + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + manifestAnnotateOpts = entities.ManifestAnnotateOptions{} + annotateCmd = &cobra.Command{ + Use: "annotate [flags] LIST IMAGE", + Short: "Add or update information about an entry in a manifest list or image index", + Long: "Adds or updates information about an entry in a manifest list or image index.", + RunE: annotate, + Example: `podman manifest annotate --annotation left=right mylist:v1.11 image:v1.11-amd64`, + Args: cobra.ExactArgs(2), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: annotateCmd, + Parent: manifestCmd, + }) + flags := annotateCmd.Flags() + flags.StringSliceVar(&manifestAnnotateOpts.Annotation, "annotation", nil, "set an `annotation` for the specified image") + flags.StringVar(&manifestAnnotateOpts.Arch, "arch", "", "override the `architecture` of the specified image") + flags.StringSliceVar(&manifestAnnotateOpts.Features, "features", nil, "override the `features` of the specified image") + flags.StringVar(&manifestAnnotateOpts.OS, "os", "", "override the `OS` of the specified image") + flags.StringSliceVar(&manifestAnnotateOpts.OSFeatures, "os-features", nil, "override the OS `features` of the specified image") + flags.StringVar(&manifestAnnotateOpts.OSVersion, "os-version", "", "override the OS `version` of the specified image") + flags.StringVar(&manifestAnnotateOpts.Variant, "variant", "", "override the `variant` of the specified image") +} + +func annotate(cmd *cobra.Command, args []string) error { + listImageSpec := args[0] + instanceSpec := args[1] + if listImageSpec == "" { + return errors.Errorf(`invalid image name "%s"`, listImageSpec) + } + if instanceSpec == "" { + return errors.Errorf(`invalid image digest "%s"`, instanceSpec) + } + updatedListID, err := registry.ImageEngine().ManifestAnnotate(context.Background(), args, manifestAnnotateOpts) + if err != nil { + return errors.Wrapf(err, "error removing from manifest list %s", listImageSpec) + } + fmt.Printf("%s\n", updatedListID) + return nil +} diff --git a/cmd/podman/manifest/manifest.go b/cmd/podman/manifest/manifest.go index b9ac7ea68..d7f042a56 100644 --- a/cmd/podman/manifest/manifest.go +++ b/cmd/podman/manifest/manifest.go @@ -2,6 +2,7 @@ package manifest import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -13,9 +14,13 @@ var ( Short: "Manipulate manifest lists and image indexes", Long: manifestDescription, TraverseChildren: true, - RunE: registry.SubCommandExists, - Example: `podman manifest create localhost/list - podman manifest inspect localhost/list`, + RunE: validate.SubCommandExists, + Example: `podman manifest add mylist:v1.11 image:v1.11-amd64 + podman manifest create localhost/list + podman manifest inspect localhost/list + podman manifest annotate --annotation left=right mylist:v1.11 image:v1.11-amd64 + podman manifest push mylist:v1.11 quay.io/myimagelist + podman manifest remove mylist:v1.11 sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736`, } ) diff --git a/cmd/podman/manifest/push.go b/cmd/podman/manifest/push.go new file mode 100644 index 000000000..49c76f40b --- /dev/null +++ b/cmd/podman/manifest/push.go @@ -0,0 +1,66 @@ +package manifest + +import ( + "context" + + "github.com/containers/common/pkg/auth" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + manifestPushOpts = entities.ManifestPushOptions{} + pushCmd = &cobra.Command{ + Use: "push [flags] SOURCE DESTINATION", + Short: "Push a manifest list or image index to a registry", + Long: "Pushes manifest lists and image indexes to registries.", + RunE: push, + Example: `podman manifest push mylist:v1.11 quay.io/myimagelist`, + Args: cobra.ExactArgs(2), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pushCmd, + Parent: manifestCmd, + }) + flags := pushCmd.Flags() + flags.BoolVar(&manifestPushOpts.Purge, "purge", false, "remove the manifest list if push succeeds") + flags.BoolVar(&manifestPushOpts.All, "all", false, "also push the images in the list") + flags.StringVar(&manifestPushOpts.Authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&manifestPushOpts.CertDir, "cert-dir", "", "use certificates at the specified path to access the registry") + flags.StringVar(&manifestPushOpts.Creds, "creds", "", "use `[username[:password]]` for accessing the registry") + flags.StringVar(&manifestPushOpts.DigestFile, "digestfile", "", "after copying the image, write the digest of the resulting digest to the file") + flags.StringVarP(&manifestPushOpts.Format, "format", "f", "", "manifest type (oci or v2s2) to attempt to use when pushing the manifest list (default is manifest type of source)") + flags.BoolVarP(&manifestPushOpts.RemoveSignatures, "remove-signatures", "", false, "don't copy signatures when pushing images") + flags.StringVar(&manifestPushOpts.SignBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`") + flags.BoolVar(&manifestPushOpts.TlsVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry") + flags.BoolVarP(&manifestPushOpts.Quiet, "quiet", "q", false, "don't output progress information when pushing lists") + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("cert-dir") + _ = flags.MarkHidden("tls-verify") + } +} + +func push(cmd *cobra.Command, args []string) error { + if err := auth.CheckAuthFile(manifestPushOpts.Authfile); err != nil { + return err + } + listImageSpec := args[0] + destSpec := args[1] + if listImageSpec == "" { + return errors.Errorf(`invalid image name "%s"`, listImageSpec) + } + if destSpec == "" { + return errors.Errorf(`invalid destination "%s"`, destSpec) + } + if err := registry.ImageEngine().ManifestPush(context.Background(), args, manifestPushOpts); err != nil { + return errors.Wrapf(err, "error pushing manifest %s to %s", listImageSpec, destSpec) + } + return nil +} diff --git a/cmd/podman/manifest/remove.go b/cmd/podman/manifest/remove.go new file mode 100644 index 000000000..4d345efc0 --- /dev/null +++ b/cmd/podman/manifest/remove.go @@ -0,0 +1,47 @@ +package manifest + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + removeCmd = &cobra.Command{ + Use: "remove [flags] LIST IMAGE", + Short: "Remove an entry from a manifest list or image index", + Long: "Removes an image from a manifest list or image index.", + RunE: remove, + Example: `podman manifest remove mylist:v1.11 sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736`, + Args: cobra.ExactArgs(2), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: removeCmd, + Parent: manifestCmd, + }) +} + +func remove(cmd *cobra.Command, args []string) error { + listImageSpec := args[0] + instanceSpec := args[1] + if listImageSpec == "" { + return errors.Errorf(`invalid image name "%s"`, listImageSpec) + } + if instanceSpec == "" { + return errors.Errorf(`invalid image digest "%s"`, instanceSpec) + } + updatedListID, err := registry.ImageEngine().ManifestRemove(context.Background(), args) + if err != nil { + return errors.Wrapf(err, "error removing from manifest list %s", listImageSpec) + } + fmt.Printf("%s\n", updatedListID) + return nil +} diff --git a/cmd/podman/networks/create.go b/cmd/podman/networks/create.go new file mode 100644 index 000000000..2bb75ea9e --- /dev/null +++ b/cmd/podman/networks/create.go @@ -0,0 +1,81 @@ +package network + +import ( + "fmt" + "net" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/network" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + networkCreateDescription = `create CNI networks for containers and pods` + networkCreateCommand = &cobra.Command{ + Use: "create [flags] [NETWORK]", + Short: "network create", + Long: networkCreateDescription, + RunE: networkCreate, + Example: `podman network create podman1`, + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, + } +) + +var ( + networkCreateOptions entities.NetworkCreateOptions +) + +func networkCreateFlags(flags *pflag.FlagSet) { + flags.StringVarP(&networkCreateOptions.Driver, "driver", "d", "bridge", "driver to manage the network") + flags.IPVar(&networkCreateOptions.Gateway, "gateway", nil, "IPv4 or IPv6 gateway for the subnet") + flags.BoolVar(&networkCreateOptions.Internal, "internal", false, "restrict external access from this network") + flags.IPNetVar(&networkCreateOptions.Range, "ip-range", net.IPNet{}, "allocate container IP from range") + flags.StringVar(&networkCreateOptions.MacVLAN, "macvlan", "", "create a Macvlan connection based on this device") + // TODO not supported yet + //flags.StringVar(&networkCreateOptions.IPamDriver, "ipam-driver", "", "IP Address Management Driver") + // TODO enable when IPv6 is working + //flags.BoolVar(&networkCreateOptions.IPV6, "IPv6", false, "enable IPv6 networking") + flags.IPNetVar(&networkCreateOptions.Subnet, "subnet", net.IPNet{}, "subnet in CIDR format") + flags.BoolVar(&networkCreateOptions.DisableDNS, "disable-dns", false, "disable dns plugin") +} +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: networkCreateCommand, + Parent: networkCmd, + }) + flags := networkCreateCommand.Flags() + networkCreateFlags(flags) + +} + +func networkCreate(cmd *cobra.Command, args []string) error { + var ( + name string + ) + if err := network.IsSupportedDriver(networkCreateOptions.Driver); err != nil { + return err + } + if len(args) > 1 { + return errors.Errorf("only one network can be created at a time") + } + if len(args) > 0 && !libpod.NameRegex.MatchString(args[0]) { + return libpod.RegexError + } + + if len(args) > 0 { + name = args[0] + } + response, err := registry.ContainerEngine().NetworkCreate(registry.Context(), name, networkCreateOptions) + if err != nil { + return err + } + fmt.Println(response.Filename) + return nil +} diff --git a/cmd/podman/networks/inspect.go b/cmd/podman/networks/inspect.go new file mode 100644 index 000000000..0bc73579a --- /dev/null +++ b/cmd/podman/networks/inspect.go @@ -0,0 +1,46 @@ +package network + +import ( + "encoding/json" + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + networkinspectDescription = `Inspect network` + networkinspectCommand = &cobra.Command{ + Use: "inspect NETWORK [NETWORK...] [flags] ", + Short: "network inspect", + Long: networkinspectDescription, + RunE: networkInspect, + Example: `podman network inspect podman`, + Args: cobra.MinimumNArgs(1), + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: networkinspectCommand, + Parent: networkCmd, + }) +} + +func networkInspect(cmd *cobra.Command, args []string) error { + responses, err := registry.ContainerEngine().NetworkInspect(registry.Context(), args, entities.NetworkInspectOptions{}) + if err != nil { + return err + } + b, err := json.MarshalIndent(responses, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} diff --git a/cmd/podman/networks/list.go b/cmd/podman/networks/list.go new file mode 100644 index 000000000..e27062255 --- /dev/null +++ b/cmd/podman/networks/list.go @@ -0,0 +1,133 @@ +package network + +import ( + "encoding/json" + "fmt" + "html/template" + "io" + "os" + "strings" + + "github.com/containers/libpod/cmd/podman/validate" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/network" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + networklistDescription = `List networks` + networklistCommand = &cobra.Command{ + Use: "ls", + Args: validate.NoArgs, + Short: "network list", + Long: networklistDescription, + RunE: networkList, + Example: `podman network list`, + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, + } +) + +var ( + networkListOptions entities.NetworkListOptions + headers string = "NAME\tVERSION\tPLUGINS\n" + defaultListRow string = "{{.Name}}\t{{.Version}}\t{{.Plugins}}\n" +) + +func networkListFlags(flags *pflag.FlagSet) { + // TODO enable filters based on something + //flags.StringSliceVarP(&networklistCommand.Filter, "filter", "f", []string{}, "Pause all running containers") + flags.StringVarP(&networkListOptions.Format, "format", "f", "", "Pretty-print containers to JSON or using a Go template") + flags.BoolVarP(&networkListOptions.Quiet, "quiet", "q", false, "display only names") +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: networklistCommand, + Parent: networkCmd, + }) + flags := networklistCommand.Flags() + networkListFlags(flags) +} + +func networkList(cmd *cobra.Command, args []string) error { + var ( + w io.Writer = os.Stdout + nlprs []NetworkListPrintReports + ) + + responses, err := registry.ContainerEngine().NetworkList(registry.Context(), networkListOptions) + if err != nil { + return err + } + + // quiet means we only print the network names + if networkListOptions.Quiet { + return quietOut(responses) + } + + if networkListOptions.Format == "json" { + return jsonOut(responses) + } + + for _, r := range responses { + nlprs = append(nlprs, NetworkListPrintReports{r}) + } + + row := networkListOptions.Format + if len(row) < 1 { + row = defaultListRow + } + if !strings.HasSuffix(row, "\n") { + row += "\n" + } + + format := "{{range . }}" + row + "{{end}}" + if !cmd.Flag("format").Changed { + format = headers + format + } + tmpl, err := template.New("listNetworks").Parse(format) + if err != nil { + return err + } + if err := tmpl.Execute(w, nlprs); err != nil { + return err + } + if flusher, ok := w.(interface{ Flush() error }); ok { + return flusher.Flush() + } + return nil +} + +func quietOut(responses []*entities.NetworkListReport) error { + for _, r := range responses { + fmt.Println(r.Name) + } + return nil +} + +func jsonOut(responses []*entities.NetworkListReport) error { + b, err := json.MarshalIndent(responses, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} + +type NetworkListPrintReports struct { + *entities.NetworkListReport +} + +func (n NetworkListPrintReports) Version() string { + return n.CNIVersion +} + +func (n NetworkListPrintReports) Plugins() string { + return network.GetCNIPlugins(n.NetworkConfigList) +} diff --git a/cmd/podman/networks/network.go b/cmd/podman/networks/network.go index a0e412098..56dd390ea 100644 --- a/cmd/podman/networks/network.go +++ b/cmd/podman/networks/network.go @@ -1,28 +1,26 @@ -package images +package network import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) var ( // Command: podman _network_ - cmd = &cobra.Command{ + networkCmd = &cobra.Command{ Use: "network", Short: "Manage networks", Long: "Manage networks", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } ) -// TODO add the following to main.go to get networks back onto the -// command list. -//_ "github.com/containers/libpod/cmd/podman/networks" func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode}, - Command: cmd, + Command: networkCmd, }) } diff --git a/cmd/podman/networks/rm.go b/cmd/podman/networks/rm.go new file mode 100644 index 000000000..dc1eb9909 --- /dev/null +++ b/cmd/podman/networks/rm.go @@ -0,0 +1,63 @@ +package network + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + networkrmDescription = `Remove networks` + networkrmCommand = &cobra.Command{ + Use: "rm [flags] NETWORK [NETWORK...]", + Short: "network rm", + Long: networkrmDescription, + RunE: networkRm, + Example: `podman network rm podman`, + Args: cobra.MinimumNArgs(1), + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, + } +) + +var ( + networkRmOptions entities.NetworkRmOptions +) + +func networkRmFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&networkRmOptions.Force, "force", "f", false, "remove any containers using network") +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: networkrmCommand, + Parent: networkCmd, + }) + flags := networkrmCommand.Flags() + networkRmFlags(flags) +} + +func networkRm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + + responses, err := registry.ContainerEngine().NetworkRm(registry.Context(), args, networkRmOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Name) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podman/parse/common.go b/cmd/podman/parse/common.go index a5e9b4fc2..13f425b6d 100644 --- a/cmd/podman/parse/common.go +++ b/cmd/podman/parse/common.go @@ -30,13 +30,20 @@ func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool return errors.Errorf("--all and --latest cannot be used together") } + if (argLen > 0) && specifiedAll { + return errors.Errorf("no arguments are needed with --all") + } + 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 argLen > 0 { + if specifiedLatest { + return errors.Errorf("no arguments are needed with --latest") + } else if cidfile && (specifiedLatest || specifiedCIDFile) { + return errors.Errorf("no arguments are needed with --latest or --cidfile") + } } if specifiedCIDFile { diff --git a/cmd/podman/parse/net.go b/cmd/podman/parse/net.go index 03cda268c..f93c4ab1e 100644 --- a/cmd/podman/parse/net.go +++ b/cmd/podman/parse/net.go @@ -1,4 +1,4 @@ -//nolint +// nolint // most of these validate and parse functions have been taken from projectatomic/docker // and modified for cri-o package parse @@ -46,7 +46,7 @@ var ( // 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 +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 { diff --git a/cmd/podman/parse/net_test.go b/cmd/podman/parse/net_test.go index a6ddc2be9..51c8509df 100644 --- a/cmd/podman/parse/net_test.go +++ b/cmd/podman/parse/net_test.go @@ -1,4 +1,4 @@ -//nolint +// nolint // most of these validate and parse functions have been taken from projectatomic/docker // and modified for cri-o package parse @@ -41,7 +41,7 @@ func TestValidateExtraHost(t *testing.T) { want string wantErr bool }{ - //2001:0db8:85a3:0000:0000:8a2e:0370:7334 + // 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}, diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go new file mode 100644 index 000000000..2499b54b9 --- /dev/null +++ b/cmd/podman/play/kube.go @@ -0,0 +1,101 @@ +package pods + +import ( + "fmt" + "os" + + "github.com/containers/common/pkg/auth" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// playKubeOptionsWrapper allows for separating CLI-only fields from API-only +// fields. +type playKubeOptionsWrapper struct { + entities.PlayKubeOptions + + TLSVerifyCLI bool +} + +var ( + // https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/ + defaultSeccompRoot = "/var/lib/kubelet/seccomp" + kubeOptions = playKubeOptionsWrapper{} + kubeDescription = `Command reads in a structured file of Kubernetes YAML. + + It creates the pod and containers described in the YAML. The containers within the pod are then started and the ID of the new Pod is output.` + + kubeCmd = &cobra.Command{ + Use: "kube [flags] KUBEFILE", + Short: "Play a pod based on Kubernetes YAML.", + Long: kubeDescription, + RunE: kube, + Args: cobra.ExactArgs(1), + Example: `podman play kube nginx.yml + podman play kube --creds user:password --seccomp-profile-root /custom/path apache.yml`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: kubeCmd, + Parent: playCmd, + }) + + flags := kubeCmd.Flags() + flags.SetNormalizeFunc(utils.AliasFlags) + flags.StringVar(&kubeOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.StringVar(&kubeOptions.Network, "network", "", "Connect pod to CNI network(s)") + flags.BoolVarP(&kubeOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images") + if !registry.IsRemote() { + flags.StringVar(&kubeOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&kubeOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") + flags.BoolVar(&kubeOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + flags.StringVar(&kubeOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") + flags.StringVar(&kubeOptions.SeccompProfileRoot, "seccomp-profile-root", defaultSeccompRoot, "Directory path for seccomp profiles") + } +} + +func kube(cmd *cobra.Command, args []string) error { + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + kubeOptions.SkipTLSVerify = types.NewOptionalBool(!kubeOptions.TLSVerifyCLI) + } + if kubeOptions.Authfile != "" { + if _, err := os.Stat(kubeOptions.Authfile); err != nil { + return errors.Wrapf(err, "error getting authfile %s", kubeOptions.Authfile) + } + } + + report, err := registry.ContainerEngine().PlayKube(registry.GetContext(), args[0], kubeOptions.PlayKubeOptions) + if err != nil { + return err + } + + for _, l := range report.Logs { + fmt.Fprintf(os.Stderr, l) + } + + fmt.Printf("Pod:\n%s\n", report.Pod) + switch len(report.Containers) { + case 0: + return nil + case 1: + fmt.Printf("Container:\n") + default: + fmt.Printf("Containers:\n") + } + for _, ctr := range report.Containers { + fmt.Println(ctr) + } + + return nil +} diff --git a/cmd/podman/play/play.go b/cmd/podman/play/play.go new file mode 100644 index 000000000..b151e5f5d --- /dev/null +++ b/cmd/podman/play/play.go @@ -0,0 +1,26 @@ +package pods + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _play_ + playCmd = &cobra.Command{ + Use: "play", + Short: "Play a pod and its containers from a structured file.", + Long: "Play structured data (e.g., Kubernetes pod or service yaml) based on containers and pods.", + TraverseChildren: true, + RunE: validate.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: playCmd, + }) +} diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index 0c0d07b3e..e24cdef98 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -9,12 +9,16 @@ import ( "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/parse" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/errorhandling" + createconfig "github.com/containers/libpod/pkg/spec" "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -24,7 +28,7 @@ var ( createCommand = &cobra.Command{ Use: "create", - Args: common.NoArgs, + Args: validate.NoArgs, Short: "Create a new empty pod", Long: podCreateDescription, RunE: create, @@ -56,7 +60,15 @@ func init() { 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") + flags.StringVar(&share, "share", createconfig.DefaultKernelNamespaces, "A comma delimited list of kernel namespaces the pod will share") + flags.SetNormalizeFunc(aliasNetworkFlag) +} + +func aliasNetworkFlag(_ *pflag.FlagSet, name string) pflag.NormalizedName { + if name == "net" { + name = "network" + } + return pflag.NormalizedName(name) } func create(cmd *cobra.Command, args []string) error { @@ -70,6 +82,7 @@ func create(cmd *cobra.Command, args []string) error { } if !createOptions.Infra { + logrus.Debugf("Not creating an infra container") if cmd.Flag("infra-command").Changed { return errors.New("cannot set infra-command without an infra container") } @@ -103,32 +116,26 @@ func create(cmd *cobra.Command, args []string) error { 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 { + createOptions.Net.Network = specgen.Namespace{} + if cmd.Flag("network").Changed { + 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 "slirp4netns": + n.NSMode = specgen.Slirp + default: + // Container and NS mode are presently unsupported n.NSMode = specgen.Bridge createOptions.Net.CNINetworks = strings.Split(netInput, ",") } + createOptions.Net.Network = n } if len(createOptions.Net.PublishPorts) > 0 { if !createOptions.Infra { diff --git a/cmd/podman/pods/pod.go b/cmd/podman/pods/pod.go index e86b8aba4..ed265ef90 100644 --- a/cmd/podman/pods/pod.go +++ b/cmd/podman/pods/pod.go @@ -2,6 +2,7 @@ package pods import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/util" "github.com/spf13/cobra" @@ -15,9 +16,9 @@ var ( podCmd = &cobra.Command{ Use: "pod", Short: "Manage pods", - Long: "Manage pods", + Long: "Pods are a group of one or more containers sharing the same network, pid and ipc namespaces.", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } containerConfig = util.DefaultContainerConfig() ) diff --git a/cmd/podman/pods/prune.go b/cmd/podman/pods/prune.go index 091e3375b..bc15d8035 100644 --- a/cmd/podman/pods/prune.go +++ b/cmd/podman/pods/prune.go @@ -41,9 +41,6 @@ func init() { } func prune(cmd *cobra.Command, args []string) error { - var ( - errs utils.OutputErrors - ) if len(args) > 0 { return errors.Errorf("`%s` takes no arguments", cmd.CommandPath()) } @@ -60,16 +57,8 @@ func prune(cmd *cobra.Command, args []string) error { } } responses, err := registry.ContainerEngine().PodPrune(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() + return utils.PrintPodPruneResults(responses) } diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 8ae1f91a8..5703bd172 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -11,8 +11,8 @@ import ( "text/template" "time" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/docker/go-units" "github.com/pkg/errors" @@ -26,15 +26,15 @@ var ( psCmd = &cobra.Command{ Use: "ps", Aliases: []string{"ls", "list"}, - Short: "list pods", + Short: "List pods", Long: psDescription, RunE: pods, - Args: common.NoArgs, + Args: validate.NoArgs, } ) var ( - defaultHeaders string = "POD ID\tNAME\tSTATUS\tCREATED" + defaultHeaders = "POD ID\tNAME\tSTATUS\tCREATED" inputFilters []string noTrunc bool psInput entities.PodPSOptions diff --git a/cmd/podman/pods/stats.go b/cmd/podman/pods/stats.go index 7c3597d9a..d3950fdbc 100644 --- a/cmd/podman/pods/stats.go +++ b/cmd/podman/pods/stats.go @@ -35,7 +35,7 @@ var ( // Command: podman pod _pod_ statsCmd = &cobra.Command{ Use: "stats [flags] [POD...]", - Short: "Display resource-usage statistics of pods", + Short: "Display a live stream of resource usage statistics for the containers in one or more pods", Long: statsDescription, RunE: stats, Example: `podman pod stats diff --git a/cmd/podman/pods/top.go b/cmd/podman/pods/top.go index ad602f4ea..9cf2bd525 100644 --- a/cmd/podman/pods/top.go +++ b/cmd/podman/pods/top.go @@ -25,7 +25,7 @@ var ( topCommand = &cobra.Command{ Use: "top [flags] POD [FORMAT-DESCRIPTORS|ARGS]", - Short: "Display the running processes in a pod", + Short: "Display the running processes of containers in a pod", Long: topDescription, RunE: top, Args: cobra.ArbitraryArgs, diff --git a/cmd/podman/registry/config.go b/cmd/podman/registry/config.go index fc6eb538e..49d5bca74 100644 --- a/cmd/podman/registry/config.go +++ b/cmd/podman/registry/config.go @@ -22,6 +22,7 @@ const ( var ( podmanOptions entities.PodmanConfig podmanSync sync.Once + abiSupport = false ) // PodmanConfig returns an entities.PodmanConfig built up from @@ -39,23 +40,31 @@ func newPodmanConfig() { var mode entities.EngineMode switch runtime.GOOS { - case "darwin": - fallthrough - case "windows": + case "darwin", "windows": mode = entities.TunnelMode case "linux": - mode = entities.ABIMode + // Some linux clients might only be compiled without ABI + // support (e.g., podman-remote). + if abiSupport { + mode = entities.ABIMode + } else { + mode = entities.TunnelMode + } default: fmt.Fprintf(os.Stderr, "%s is not a supported OS", runtime.GOOS) os.Exit(1) } - // cobra.Execute() may not be called yet, so we peek at os.Args. - for _, v := range os.Args { - // Prefix checking works because of how default EngineMode's - // have been defined. - if strings.HasPrefix(v, "--remote") { - mode = entities.TunnelMode + // Check if need to fallback to the tunnel mode if --remote is used. + if abiSupport && mode == entities.ABIMode { + // cobra.Execute() may not be called yet, so we peek at os.Args. + for _, v := range os.Args { + // Prefix checking works because of how default EngineMode's + // have been defined. + if strings.HasPrefix(v, "--remote") { + mode = entities.TunnelMode + break + } } } diff --git a/cmd/podman/registry/config_abi.go b/cmd/podman/registry/config_abi.go new file mode 100644 index 000000000..55430e1bf --- /dev/null +++ b/cmd/podman/registry/config_abi.go @@ -0,0 +1,7 @@ +// +build ABISupport + +package registry + +func init() { + abiSupport = true +} diff --git a/cmd/podman/registry/config_tunnel.go b/cmd/podman/registry/config_tunnel.go new file mode 100644 index 000000000..29e744dac --- /dev/null +++ b/cmd/podman/registry/config_tunnel.go @@ -0,0 +1,7 @@ +// +build !ABISupport + +package registry + +func init() { + abiSupport = false +} diff --git a/cmd/podman/registry/registry.go b/cmd/podman/registry/registry.go index 993b14cbc..71ee2bed0 100644 --- a/cmd/podman/registry/registry.go +++ b/cmd/podman/registry/registry.go @@ -2,16 +2,18 @@ package registry import ( "context" - "fmt" + "path/filepath" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/infra" - "github.com/pkg/errors" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/util" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -// DefaultAPIAddress is the default address of the REST socket -const DefaultAPIAddress = "unix:/run/podman/podman.sock" +// DefaultRootAPIAddress is the default address of the REST socket +const DefaultRootAPIAddress = "unix:/run/podman/podman.sock" // DefaultVarlinkAddress is the default address of the varlink socket const DefaultVarlinkAddress = "unix:/run/podman/io.podman" @@ -77,21 +79,6 @@ func NewContainerEngine(cmd *cobra.Command, args []string) (entities.ContainerEn 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()) -} - -// IdOrLatestArgs used to validate a nameOrId was provided or the "--latest" flag -func IdOrLatestArgs(cmd *cobra.Command, args []string) error { - if len(args) > 1 || (len(args) == 0 && !cmd.Flag("latest").Changed) { - return fmt.Errorf("%s requires a name, id or the '--latest' flag", cmd.Name()) - } - return nil -} - type PodmanOptionsKey struct{} func Context() context.Context { @@ -115,3 +102,15 @@ func GetContextWithOptions() context.Context { func GetContext() context.Context { return Context() } + +func DefaultAPIAddress() string { + if rootless.IsRootless() { + xdg, err := util.GetRuntimeDir() + if err != nil { + logrus.Warnf("Failed to get rootless runtime dir for DefaultAPIAddress: %s", err) + return DefaultRootAPIAddress + } + return "unix:" + filepath.Join(xdg, "podman", "podman.sock") + } + return DefaultRootAPIAddress +} diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 8baed42cf..7d6f6f823 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/tracing" @@ -60,7 +61,7 @@ var ( SilenceErrors: true, TraverseChildren: true, PersistentPreRunE: persistentPreRunE, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, PersistentPostRunE: persistentPostRunE, Version: version.Version, } @@ -207,11 +208,11 @@ func syslogHook() { func rootFlags(opts *entities.PodmanConfig, flags *pflag.FlagSet) { // V2 flags - flags.StringVarP(&opts.Uri, "remote", "r", "", "URL to access Podman service") + flags.StringVarP(&opts.Uri, "remote", "r", registry.DefaultAPIAddress(), "URL to access Podman service") flags.StringSliceVar(&opts.Identities, "identity", []string{}, "path to SSH identity file") cfg := opts.Config - flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, opts.CGroupUsage) + flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") flags.StringVar(&opts.CpuProfile, "cpu-profile", "", "Path for the cpu profiling results") flags.StringVar(&opts.ConmonPath, "conmon", "", "Path of the conmon binary") flags.StringVar(&cfg.Engine.NetworkCmdPath, "network-cmd-path", cfg.Engine.NetworkCmdPath, "Path to the command for configuring the network") diff --git a/cmd/podman/system/df.go b/cmd/podman/system/df.go new file mode 100644 index 000000000..7caa8e39a --- /dev/null +++ b/cmd/podman/system/df.go @@ -0,0 +1,282 @@ +package system + +import ( + "fmt" + "html/template" + "io" + "os" + "strings" + "text/tabwriter" + "time" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +var ( + dfSystemDescription = ` + podman system df + + Show podman disk usage + ` + dfSystemCommand = &cobra.Command{ + Use: "df", + Args: validate.NoArgs, + Short: "Show podman disk usage", + Long: dfSystemDescription, + RunE: df, + } +) + +var ( + dfOptions entities.SystemDfOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: dfSystemCommand, + Parent: systemCmd, + }) + flags := dfSystemCommand.Flags() + flags.BoolVarP(&dfOptions.Verbose, "verbose", "v", false, "Show detailed information on disk usage") + flags.StringVar(&dfOptions.Format, "format", "", "Pretty-print images using a Go template") +} + +func df(cmd *cobra.Command, args []string) error { + reports, err := registry.ContainerEngine().SystemDf(registry.Context(), dfOptions) + if err != nil { + return err + } + if dfOptions.Verbose { + return printVerbose(reports) + } + return printSummary(reports, dfOptions.Format) +} + +func printSummary(reports *entities.SystemDfReport, userFormat string) error { + + var ( + dfSummaries []*dfSummary + active int + size, reclaimable int64 + format string = "{{.Type}}\t{{.Total}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}\n" + w io.Writer = os.Stdout + ) + + // Images + if len(userFormat) > 0 { + format = userFormat + } + + for _, i := range reports.Images { + if i.Containers > 0 { + active += 1 + } + size += i.Size + if i.Containers < 1 { + reclaimable += i.Size + } + } + + imageSummary := dfSummary{ + Type: "Images", + Total: len(reports.Images), + Active: active, + size: size, + reclaimable: reclaimable, + } + dfSummaries = append(dfSummaries, &imageSummary) + + // Containers + + var ( + conActive int + conSize, conReclaimable int64 + ) + for _, c := range reports.Containers { + if c.Status == "running" { + conActive += 1 + } else { + conReclaimable += c.RWSize + } + conSize += c.RWSize + } + + containerSummary := dfSummary{ + Type: "Containers", + Total: len(reports.Containers), + Active: conActive, + size: conSize, + reclaimable: conReclaimable, + } + + dfSummaries = append(dfSummaries, &containerSummary) + + // Volumes + var ( + activeVolumes int + volumesSize, volumesReclaimable int64 + ) + + for _, v := range reports.Volumes { + activeVolumes += v.Links + volumesSize += v.Size + volumesReclaimable += v.Size + } + volumeSummary := dfSummary{ + Type: "Local Volumes", + Total: len(reports.Volumes), + Active: activeVolumes, + size: volumesSize, + reclaimable: volumesReclaimable, + } + + dfSummaries = append(dfSummaries, &volumeSummary) + + headers := "TYPE\tTOTAL\tACTIVE\tSIZE\tRECLAIMABLE\n" + format = "{{range . }}" + format + "{{end}}" + if len(userFormat) == 0 { + format = headers + format + } + return writeTemplate(w, format, dfSummaries) +} + +func printVerbose(reports *entities.SystemDfReport) error { + var ( + dfImages []*dfImage + dfContainers []*dfContainer + dfVolumes []*dfVolume + w io.Writer = os.Stdout + ) + + // Images + fmt.Print("\nImages space usage:\n\n") + // convert to dfImage for output + for _, d := range reports.Images { + dfImages = append(dfImages, &dfImage{SystemDfImageReport: d}) + } + imageHeaders := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE\tSHARED SIZE\tUNIQUE SIZE\tCONTAINERS\n" + imageRow := "{{.Repository}}\t{{.Tag}}\t{{.ImageID}}\t{{.Created}}\t{{.Size}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}\n" + format := imageHeaders + "{{range . }}" + imageRow + "{{end}}" + if err := writeTemplate(w, format, dfImages); err != nil { + return nil + } + + // Containers + fmt.Print("\nContainers space usage:\n\n") + + // convert to dfContainers for output + for _, d := range reports.Containers { + dfContainers = append(dfContainers, &dfContainer{SystemDfContainerReport: d}) + } + containerHeaders := "CONTAINER ID\tIMAGE\tCOMMAND\tLOCAL VOLUMES\tSIZE\tCREATED\tSTATUS\tNAMES\n" + containerRow := "{{.ContainerID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.Created}}\t{{.Status}}\t{{.Names}}\n" + format = containerHeaders + "{{range . }}" + containerRow + "{{end}}" + if err := writeTemplate(w, format, dfContainers); err != nil { + return nil + } + + // Volumes + fmt.Print("\nLocal Volumes space usage:\n\n") + + // convert to dfVolume for output + for _, d := range reports.Volumes { + dfVolumes = append(dfVolumes, &dfVolume{SystemDfVolumeReport: d}) + } + volumeHeaders := "VOLUME NAME\tLINKS\tSIZE\n" + volumeRow := "{{.VolumeName}}\t{{.Links}}\t{{.Size}}\n" + format = volumeHeaders + "{{range . }}" + volumeRow + "{{end}}" + return writeTemplate(w, format, dfVolumes) +} + +func writeTemplate(w io.Writer, format string, output interface{}) error { + tmpl, err := template.New("dfout").Parse(format) + if err != nil { + return err + } + w = tabwriter.NewWriter(w, 8, 2, 2, ' ', 0) //nolint + if err := tmpl.Execute(w, output); err != nil { + return err + } + if flusher, ok := w.(interface{ Flush() error }); ok { + return flusher.Flush() + } + return nil +} + +type dfImage struct { + *entities.SystemDfImageReport +} + +func (d *dfImage) ImageID() string { + return d.SystemDfImageReport.ImageID[0:12] +} + +func (d *dfImage) Created() string { + return units.HumanDuration(time.Since(d.SystemDfImageReport.Created)) +} + +func (d *dfImage) Size() string { + return units.HumanSize(float64(d.SystemDfImageReport.Size)) +} + +func (d *dfImage) SharedSize() string { + return units.HumanSize(float64(d.SystemDfImageReport.SharedSize)) +} + +func (d *dfImage) UniqueSize() string { + return units.HumanSize(float64(d.SystemDfImageReport.UniqueSize)) +} + +type dfContainer struct { + *entities.SystemDfContainerReport +} + +func (d *dfContainer) ContainerID() string { + return d.SystemDfContainerReport.ContainerID[0:12] +} + +func (d *dfContainer) Image() string { + return d.SystemDfContainerReport.Image[0:12] +} + +func (d *dfContainer) Command() string { + return strings.Join(d.SystemDfContainerReport.Command, " ") +} + +func (d *dfContainer) Size() string { + return units.HumanSize(float64(d.SystemDfContainerReport.Size)) +} + +func (d *dfContainer) Created() string { + return units.HumanDuration(time.Since(d.SystemDfContainerReport.Created)) +} + +type dfVolume struct { + *entities.SystemDfVolumeReport +} + +func (d *dfVolume) Size() string { + return units.HumanSize(float64(d.SystemDfVolumeReport.Size)) +} + +type dfSummary struct { + Type string + Total int + Active int + size int64 + reclaimable int64 +} + +func (d *dfSummary) Size() string { + return units.HumanSize(float64(d.size)) +} + +func (d *dfSummary) Reclaimable() string { + percent := int(float64(d.reclaimable)/float64(d.size)) * 100 + return fmt.Sprintf("%s (%d%%)", units.HumanSize(float64(d.reclaimable)), percent) +} diff --git a/cmd/podman/system/events.go b/cmd/podman/system/events.go index 31dd9aa77..6aae62dc0 100644 --- a/cmd/podman/system/events.go +++ b/cmd/podman/system/events.go @@ -7,8 +7,8 @@ import ( "os" "github.com/containers/buildah/pkg/formats" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -19,7 +19,7 @@ var ( eventsDescription = "Monitor podman events" eventsCommand = &cobra.Command{ Use: "events", - Args: common.NoArgs, + Args: validate.NoArgs, Short: "Show podman events", Long: eventsDescription, RunE: eventsCmd, diff --git a/cmd/podman/system/info.go b/cmd/podman/system/info.go index 143796938..dad63bcd4 100644 --- a/cmd/podman/system/info.go +++ b/cmd/podman/system/info.go @@ -5,11 +5,12 @@ import ( "os" "text/template" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/ghodss/yaml" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -19,12 +20,21 @@ var ( ` infoCommand = &cobra.Command{ Use: "info", - Args: common.NoArgs, + Args: validate.NoArgs, Long: infoDescription, Short: "Display podman system information", RunE: info, Example: `podman info`, } + + systemInfoCommand = &cobra.Command{ + Args: infoCommand.Args, + Use: infoCommand.Use, + Short: infoCommand.Short, + Long: infoCommand.Long, + RunE: infoCommand.RunE, + Example: `podman system info`, + } ) var ( @@ -37,7 +47,17 @@ func init() { Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: infoCommand, }) - flags := infoCommand.Flags() + infoFlags(infoCommand.Flags()) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: systemInfoCommand, + Parent: systemCmd, + }) + infoFlags(systemInfoCommand.Flags()) +} + +func infoFlags(flags *pflag.FlagSet) { flags.BoolVarP(&debug, "debug", "D", false, "Display additional debug information") flags.StringVarP(&inFormat, "format", "f", "", "Change the output format to JSON or a Go template") } diff --git a/cmd/podman/system/migrate.go b/cmd/podman/system/migrate.go new file mode 100644 index 000000000..13aa162c7 --- /dev/null +++ b/cmd/podman/system/migrate.go @@ -0,0 +1,63 @@ +package system + +import ( + "fmt" + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra" + "github.com/spf13/cobra" +) + +var ( + migrateDescription = ` + podman system migrate + + Migrate existing containers to a new version of Podman. +` + + migrateCommand = &cobra.Command{ + Use: "migrate", + Args: validate.NoArgs, + Short: "Migrate containers", + Long: migrateDescription, + Run: migrate, + } +) + +var ( + migrateOptions entities.SystemMigrateOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: migrateCommand, + Parent: systemCmd, + }) + + flags := migrateCommand.Flags() + flags.StringVar(&migrateOptions.NewRuntime, "new-runtime", "", "Specify a new runtime for all containers") +} + +func migrate(cmd *cobra.Command, args []string) { + // Shutdown all running engines, `renumber` will hijack repository + registry.ContainerEngine().Shutdown(registry.Context()) + registry.ImageEngine().Shutdown(registry.Context()) + + engine, err := infra.NewSystemEngine(entities.MigrateMode, registry.PodmanConfig()) + if err != nil { + fmt.Println(err) + os.Exit(125) + } + defer engine.Shutdown(registry.Context()) + + err = engine.Migrate(registry.Context(), cmd.Flags(), registry.PodmanConfig(), migrateOptions) + if err != nil { + fmt.Println(err) + os.Exit(125) + } + os.Exit(0) +} diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go new file mode 100644 index 000000000..cc9b473f1 --- /dev/null +++ b/cmd/podman/system/prune.go @@ -0,0 +1,103 @@ +package system + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + pruneOptions = entities.SystemPruneOptions{} + + pruneDescription = fmt.Sprintf(` + podman system prune + + Remove unused data +`) + + pruneCommand = &cobra.Command{ + Use: "prune [flags]", + Short: "Remove unused data", + Args: validate.NoArgs, + Long: pruneDescription, + RunE: prune, + Example: `podman system prune`, + } + force bool +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pruneCommand, + Parent: systemCmd, + }) + flags := pruneCommand.Flags() + flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation. The default is false") + flags.BoolVarP(&pruneOptions.All, "all", "a", false, "Remove all unused data") + flags.BoolVar(&pruneOptions.Volume, "volumes", false, "Prune volumes") + +} + +func prune(cmd *cobra.Command, args []string) error { + // Prompt for confirmation if --force is not set + if !force { + reader := bufio.NewReader(os.Stdin) + volumeString := "" + if pruneOptions.Volume { + volumeString = ` + - all volumes not used by at least one container` + } + fmt.Printf(` +WARNING! This will remove: + - all stopped containers%s + - all stopped pods + - all dangling images + - all build cache +Are you sure you want to continue? [y/N] `, volumeString) + answer, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "error reading input") + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + // TODO: support for filters in system prune + response, err := registry.ContainerEngine().SystemPrune(context.Background(), pruneOptions) + if err != nil { + return err + } + // Print pod prune results + fmt.Println("Deleted Pods") + err = utils.PrintPodPruneResults(response.PodPruneReport) + if err != nil { + return err + } + // Print container prune results + fmt.Println("Deleted Containers") + err = utils.PrintContainerPruneResults(response.ContainerPruneReport) + if err != nil { + return err + } + // Print Volume prune results + if pruneOptions.Volume { + fmt.Println("Deleted Volumes") + err = utils.PrintVolumePruneResults(response.VolumePruneReport) + if err != nil { + return err + } + } + // Print Images prune results + fmt.Println("Deleted Images") + return utils.PrintImagePruneResults(response.ImagePruneReport) +} diff --git a/cmd/podman/system/renumber.go b/cmd/podman/system/renumber.go new file mode 100644 index 000000000..5ee6b3be6 --- /dev/null +++ b/cmd/podman/system/renumber.go @@ -0,0 +1,57 @@ +package system + +import ( + "fmt" + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra" + "github.com/spf13/cobra" +) + +var ( + renumberDescription = ` + podman system renumber + + Migrate lock numbers to handle a change in maximum number of locks. + Mandatory after the number of locks in libpod.conf is changed. +` + + renumberCommand = &cobra.Command{ + Use: "renumber", + Args: validate.NoArgs, + Short: "Migrate lock numbers", + Long: renumberDescription, + Run: renumber, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: renumberCommand, + Parent: systemCmd, + }) + +} +func renumber(cmd *cobra.Command, args []string) { + // Shutdown all running engines, `renumber` will hijack all methods + registry.ContainerEngine().Shutdown(registry.Context()) + registry.ImageEngine().Shutdown(registry.Context()) + + engine, err := infra.NewSystemEngine(entities.RenumberMode, registry.PodmanConfig()) + if err != nil { + fmt.Println(err) + os.Exit(125) + } + defer engine.Shutdown(registry.Context()) + + err = engine.Renumber(registry.Context(), cmd.Flags(), registry.PodmanConfig()) + if err != nil { + fmt.Println(err) + os.Exit(125) + } + os.Exit(0) +} diff --git a/cmd/podman/system/reset.go b/cmd/podman/system/reset.go new file mode 100644 index 000000000..22ddc7529 --- /dev/null +++ b/cmd/podman/system/reset.go @@ -0,0 +1,82 @@ +package system + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + systemResetDescription = `Reset podman storage back to default state" + + All containers will be stopped and removed, and all images, volumes and container content will be removed. +` + systemResetCommand = &cobra.Command{ + Use: "reset", + Args: validate.NoArgs, + Short: "Reset podman storage", + Long: systemResetDescription, + Run: reset, + } +) + +var ( + systemResetOptions entities.SystemResetOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: systemResetCommand, + Parent: systemCmd, + }) + flags := systemResetCommand.Flags() + flags.BoolVarP(&systemResetOptions.Force, "force", "f", false, "Do not prompt for confirmation") +} + +func reset(cmd *cobra.Command, args []string) { + // Prompt for confirmation if --force is not set + if !systemResetOptions.Force { + reader := bufio.NewReader(os.Stdin) + fmt.Print(` +WARNING! This will remove: + - all containers + - all pods + - all images + - all build cache +Are you sure you want to continue? [y/N] `) + answer, err := reader.ReadString('\n') + if err != nil { + fmt.Println(errors.Wrapf(err, "error reading input")) + os.Exit(1) + } + if strings.ToLower(answer)[0] != 'y' { + os.Exit(0) + } + } + + // Shutdown all running engines, `reset` will hijack repository + registry.ContainerEngine().Shutdown(registry.Context()) + registry.ImageEngine().Shutdown(registry.Context()) + + engine, err := infra.NewSystemEngine(entities.ResetMode, registry.PodmanConfig()) + if err != nil { + fmt.Println(err) + os.Exit(125) + } + defer engine.Shutdown(registry.Context()) + + if err := engine.Reset(registry.Context(), systemResetOptions); err != nil { + fmt.Println(err) + os.Exit(125) + } + os.Exit(0) +} diff --git a/cmd/podman/system/service.go b/cmd/podman/system/service.go index f4b91dd78..552c72f79 100644 --- a/cmd/podman/system/service.go +++ b/cmd/podman/system/service.go @@ -139,6 +139,6 @@ func resolveApiURI(_url []string) (string, error) { case srvArgs.Varlink: return registry.DefaultVarlinkAddress, nil default: - return registry.DefaultAPIAddress, nil + return registry.DefaultRootAPIAddress, nil } } diff --git a/cmd/podman/system/system.go b/cmd/podman/system/system.go index 2d55e8c13..d9691ad2a 100644 --- a/cmd/podman/system/system.go +++ b/cmd/podman/system/system.go @@ -2,6 +2,7 @@ package system import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -16,7 +17,7 @@ var ( Short: "Manage podman", Long: "Manage podman", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } ) diff --git a/cmd/podman/system/version.go b/cmd/podman/system/version.go index b0f4eb528..065eef309 100644 --- a/cmd/podman/system/version.go +++ b/cmd/podman/system/version.go @@ -9,8 +9,8 @@ import ( "time" "github.com/containers/buildah/pkg/formats" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -20,7 +20,7 @@ import ( var ( versionCommand = &cobra.Command{ Use: "version", - Args: common.NoArgs, + Args: validate.NoArgs, Short: "Display the Podman Version Information", RunE: version, Annotations: map[string]string{ @@ -56,14 +56,14 @@ func version(cmd *cobra.Command, args []string) error { // TODO we need to discuss how to implement // this more. current endpoints dont have a // version endpoint. maybe we use info? - //if remote { + // if remote { // v.Server, err = getRemoteVersion(c) // if err != nil { // return err // } - //} else { + // } else { v.Server = v.Client - //} + // } versionOutputFormat := versionFormat if versionOutputFormat != "" { diff --git a/cmd/podman/utils/alias.go b/cmd/podman/utils/alias.go index 54b3c5e89..e484461c5 100644 --- a/cmd/podman/utils/alias.go +++ b/cmd/podman/utils/alias.go @@ -2,7 +2,7 @@ package utils import "github.com/spf13/pflag" -// AliasFlags is a function to handle backwards compatability with old flags +// AliasFlags is a function to handle backwards compatibility with old flags func AliasFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { switch name { case "healthcheck-command": diff --git a/cmd/podman/utils/utils.go b/cmd/podman/utils/utils.go index c7d105ba4..f4c704628 100644 --- a/cmd/podman/utils/utils.go +++ b/cmd/podman/utils/utils.go @@ -1,6 +1,11 @@ package utils -import "os" +import ( + "fmt" + "os" + + "github.com/containers/libpod/pkg/domain/entities" +) // IsDir returns true if the specified path refers to a directory. func IsDir(path string) bool { @@ -20,3 +25,51 @@ func FileExists(path string) bool { } return !file.IsDir() } + +func PrintPodPruneResults(podPruneReports []*entities.PodPruneReport) error { + var errs OutputErrors + for _, r := range podPruneReports { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} + +func PrintContainerPruneResults(containerPruneReport *entities.ContainerPruneReport) error { + var errs OutputErrors + for k := range containerPruneReport.ID { + fmt.Println(k) + } + for _, v := range containerPruneReport.Err { + errs = append(errs, v) + } + return errs.PrintErrors() +} + +func PrintVolumePruneResults(volumePruneReport []*entities.VolumePruneReport) error { + var errs OutputErrors + for _, r := range volumePruneReport { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} + +func PrintImagePruneResults(imagePruneReport *entities.ImagePruneReport) error { + for _, i := range imagePruneReport.Report.Id { + fmt.Println(i) + } + for _, e := range imagePruneReport.Report.Err { + fmt.Fprint(os.Stderr, e.Error()+"\n") + } + if imagePruneReport.Size > 0 { + fmt.Fprintf(os.Stdout, "Size: %d\n", imagePruneReport.Size) + } + return nil +} diff --git a/cmd/podman/validate/args.go b/cmd/podman/validate/args.go new file mode 100644 index 000000000..14b4d7897 --- /dev/null +++ b/cmd/podman/validate/args.go @@ -0,0 +1,32 @@ +package validate + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// NoArgs returns an error if any args are included. +func NoArgs(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("`%s` takes no arguments", cmd.CommandPath()) + } + return nil +} + +// SubCommandExists returns an error if no sub command is provided +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()) +} + +// IdOrLatestArgs used to validate a nameOrId was provided or the "--latest" flag +func IdOrLatestArgs(cmd *cobra.Command, args []string) error { + if len(args) > 1 || (len(args) == 0 && !cmd.Flag("latest").Changed) { + return fmt.Errorf("`%s` requires a name, id or the \"--latest\" flag", cmd.CommandPath()) + } + return nil +} diff --git a/cmd/podman/validate/choice.go b/cmd/podman/validate/choice.go new file mode 100644 index 000000000..572c5f4a5 --- /dev/null +++ b/cmd/podman/validate/choice.go @@ -0,0 +1,46 @@ +package validate + +import ( + "fmt" + "strings" +) + +// Honors cobra.Value interface +type choiceValue struct { + value *string + choices []string +} + +// ChoiceValue may be used in cobra FlagSet methods Var/VarP/VarPF() to select from a set of values +// +// Example: +// created := validate.ChoiceValue(&opts.Sort, "command", "created", "id", "image", "names", "runningfor", "size", "status") +// flags.Var(created, "sort", "Sort output by: "+created.Choices()) +func ChoiceValue(p *string, choices ...string) *choiceValue { + return &choiceValue{ + value: p, + choices: choices, + } +} + +func (c *choiceValue) String() string { + return *c.value +} + +func (c *choiceValue) Set(value string) error { + for _, v := range c.choices { + if v == value { + *c.value = value + return nil + } + } + return fmt.Errorf("%q is not a valid value. Choose from: %q", value, c.Choices()) +} + +func (c *choiceValue) Choices() string { + return strings.Join(c.choices, ", ") +} + +func (c *choiceValue) Type() string { + return "choice" +} diff --git a/cmd/podman/volumes/list.go b/cmd/podman/volumes/list.go index 8cc6fb301..72bf9f25b 100644 --- a/cmd/podman/volumes/list.go +++ b/cmd/podman/volumes/list.go @@ -9,8 +9,8 @@ import ( "strings" "text/tabwriter" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -25,7 +25,7 @@ and the output format can be changed to JSON or a user specified Go template.` lsCommand = &cobra.Command{ Use: "ls", Aliases: []string{"list"}, - Args: common.NoArgs, + Args: validate.NoArgs, Short: "List volumes", Long: volumeLsDescription, RunE: list, diff --git a/cmd/podman/volumes/prune.go b/cmd/podman/volumes/prune.go index 77138f4b7..57344385b 100644 --- a/cmd/podman/volumes/prune.go +++ b/cmd/podman/volumes/prune.go @@ -7,9 +7,9 @@ import ( "os" "strings" - "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -22,7 +22,7 @@ var ( Note all data will be destroyed.` pruneCommand = &cobra.Command{ Use: "prune", - Args: common.NoArgs, + Args: validate.NoArgs, Short: "Remove all unused volumes", Long: volumePruneDescription, RunE: prune, @@ -44,9 +44,6 @@ func init() { } 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) @@ -64,12 +61,5 @@ func prune(cmd *cobra.Command, args []string) error { 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() + return utils.PrintVolumePruneResults(responses) } diff --git a/cmd/podman/volumes/volume.go b/cmd/podman/volumes/volume.go index 4d74ff084..3e90d178c 100644 --- a/cmd/podman/volumes/volume.go +++ b/cmd/podman/volumes/volume.go @@ -2,6 +2,7 @@ package volumes import ( "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -16,7 +17,7 @@ var ( Short: "Manage volumes", Long: "Volumes are created in and can be shared between containers", TraverseChildren: true, - RunE: registry.SubCommandExists, + RunE: validate.SubCommandExists, } ) |