diff options
Diffstat (limited to 'cmd/podman/pods')
-rw-r--r-- | cmd/podman/pods/create.go | 2 | ||||
-rw-r--r-- | cmd/podman/pods/exists.go | 1 | ||||
-rw-r--r-- | cmd/podman/pods/ps.go | 101 | ||||
-rw-r--r-- | cmd/podman/pods/rm.go | 4 | ||||
-rw-r--r-- | cmd/podman/pods/stats.go | 189 |
5 files changed, 276 insertions, 21 deletions
diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index ff21166f3..0c0d07b3e 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -24,7 +24,7 @@ var ( createCommand = &cobra.Command{ Use: "create", - Args: cobra.NoArgs, + Args: common.NoArgs, Short: "Create a new empty pod", Long: podCreateDescription, RunE: create, diff --git a/cmd/podman/pods/exists.go b/cmd/podman/pods/exists.go index 5a94bf150..cf3e3eae5 100644 --- a/cmd/podman/pods/exists.go +++ b/cmd/podman/pods/exists.go @@ -19,6 +19,7 @@ var ( Args: cobra.ExactArgs(1), Example: `podman pod exists podID podman pod exists mypod || podman pod create --name mypod`, + DisableFlagsInUseLine: true, } ) diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 808980eff..8ae1f91a8 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -5,11 +5,13 @@ import ( "fmt" "io" "os" + "sort" "strings" "text/tabwriter" "text/template" "time" + "github.com/containers/libpod/cmd/podman/common" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" "github.com/docker/go-units" @@ -27,12 +29,13 @@ var ( Short: "list pods", Long: psDescription, RunE: pods, + Args: common.NoArgs, } ) var ( defaultHeaders string = "POD ID\tNAME\tSTATUS\tCREATED" - inputFilters string + inputFilters []string noTrunc bool psInput entities.PodPSOptions ) @@ -48,7 +51,7 @@ func init() { flags.BoolVar(&psInput.CtrIds, "ctr-ids", false, "Display the container UUIDs. If no-trunc is not set they will be truncated") flags.BoolVar(&psInput.CtrStatus, "ctr-status", false, "Display the container status") // TODO should we make this a [] ? - flags.StringVarP(&inputFilters, "filter", "f", "", "Filter output based on conditions given") + flags.StringSliceVarP(&inputFilters, "filter", "f", []string{}, "Filter output based on conditions given") flags.StringVar(&psInput.Format, "format", "", "Pretty-print pods to JSON or using a Go template") flags.BoolVarP(&psInput.Latest, "latest", "l", false, "Act on the latest pod podman is aware of") flags.BoolVar(&psInput.Namespace, "namespace", false, "Display namespace information of the pod") @@ -67,8 +70,13 @@ func pods(cmd *cobra.Command, args []string) error { row string lpr []ListPodReporter ) + + if psInput.Quiet && len(psInput.Format) > 0 { + return errors.New("quiet and format cannot be used together") + } if cmd.Flag("filter").Changed { - for _, f := range strings.Split(inputFilters, ",") { + psInput.Filters = make(map[string][]string) + for _, f := range inputFilters { split := strings.Split(f, "=") if len(split) < 2 { return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) @@ -81,6 +89,10 @@ func pods(cmd *cobra.Command, args []string) error { return err } + if err := sortPodPsOutput(psInput.Sort, responses); err != nil { + return err + } + if psInput.Format == "json" { b, err := json.MarshalIndent(responses, "", " ") if err != nil { @@ -95,11 +107,7 @@ func pods(cmd *cobra.Command, args []string) error { } headers, row := createPodPsOut() if psInput.Quiet { - if noTrunc { - row = "{{.Id}}\n" - } else { - row = "{{slice .Id 0 12}}\n" - } + row = "{{.Id}}\n" } if cmd.Flag("format").Changed { row = psInput.Format @@ -130,11 +138,7 @@ func pods(cmd *cobra.Command, args []string) error { func createPodPsOut() (string, string) { var row string headers := defaultHeaders - if noTrunc { - row += "{{.Id}}" - } else { - row += "{{slice .Id 0 12}}" - } + row += "{{.Id}}" row += "\t{{.Name}}\t{{.Status}}\t{{.Created}}" @@ -160,11 +164,7 @@ func createPodPsOut() (string, string) { } headers += "\tINFRA ID\n" - if noTrunc { - row += "\t{{.InfraId}}\n" - } else { - row += "\t{{slice .InfraId 0 12}}\n" - } + row += "\t{{.InfraId}}\n" return headers, row } @@ -184,6 +184,19 @@ func (l ListPodReporter) NumberOfContainers() int { return len(l.Containers) } +// ID is a wrapper to Id for compat, typos +func (l ListPodReporter) ID() string { + return l.Id() +} + +// Id returns the Pod id +func (l ListPodReporter) Id() string { + if noTrunc { + return l.ListPodsReport.Id + } + return l.ListPodsReport.Id[0:12] +} + // Added for backwards compatibility with podmanv1 func (l ListPodReporter) InfraID() string { return l.InfraId() @@ -192,6 +205,9 @@ func (l ListPodReporter) InfraID() string { // InfraId returns the infra container id for the pod // depending on trunc func (l ListPodReporter) InfraId() string { + if len(l.ListPodsReport.InfraId) == 0 { + return "" + } if noTrunc { return l.ListPodsReport.InfraId } @@ -225,3 +241,52 @@ func (l ListPodReporter) ContainerStatuses() string { } return strings.Join(statuses, ",") } + +func sortPodPsOutput(sortBy string, lprs []*entities.ListPodsReport) error { + switch sortBy { + case "created": + sort.Sort(podPsSortedCreated{lprs}) + case "id": + sort.Sort(podPsSortedId{lprs}) + case "name": + sort.Sort(podPsSortedName{lprs}) + case "number": + sort.Sort(podPsSortedNumber{lprs}) + case "status": + sort.Sort(podPsSortedStatus{lprs}) + default: + return errors.Errorf("invalid option for --sort, options are: id, names, or number") + } + return nil +} + +type lprSort []*entities.ListPodsReport + +func (a lprSort) Len() int { return len(a) } +func (a lprSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type podPsSortedCreated struct{ lprSort } + +func (a podPsSortedCreated) Less(i, j int) bool { + return a.lprSort[i].Created.After(a.lprSort[j].Created) +} + +type podPsSortedId struct{ lprSort } + +func (a podPsSortedId) Less(i, j int) bool { return a.lprSort[i].Id < a.lprSort[j].Id } + +type podPsSortedNumber struct{ lprSort } + +func (a podPsSortedNumber) Less(i, j int) bool { + return len(a.lprSort[i].Containers) < len(a.lprSort[j].Containers) +} + +type podPsSortedName struct{ lprSort } + +func (a podPsSortedName) Less(i, j int) bool { return a.lprSort[i].Name < a.lprSort[j].Name } + +type podPsSortedStatus struct{ lprSort } + +func (a podPsSortedStatus) Less(i, j int) bool { + return a.lprSort[i].Status < a.lprSort[j].Status +} diff --git a/cmd/podman/pods/rm.go b/cmd/podman/pods/rm.go index ea3a6476a..4b9882f8a 100644 --- a/cmd/podman/pods/rm.go +++ b/cmd/podman/pods/rm.go @@ -41,10 +41,10 @@ func init() { }) flags := rmCommand.Flags() - flags.BoolVarP(&rmOptions.All, "all", "a", false, "Restart all running pods") + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all running pods") flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Force removal of a running pod by first stopping all containers, then removing all containers in the pod. The default is false") flags.BoolVarP(&rmOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified pod is missing") - flags.BoolVarP(&rmOptions.Latest, "latest", "l", false, "Restart the latest pod podman is aware of") + flags.BoolVarP(&rmOptions.Latest, "latest", "l", false, "Remove the latest pod podman is aware of") if registry.IsRemote() { _ = flags.MarkHidden("latest") _ = flags.MarkHidden("ignore") diff --git a/cmd/podman/pods/stats.go b/cmd/podman/pods/stats.go new file mode 100644 index 000000000..7c3597d9a --- /dev/null +++ b/cmd/podman/pods/stats.go @@ -0,0 +1,189 @@ +package pods + +import ( + "context" + "fmt" + "os" + "reflect" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/buger/goterm" + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util/camelcase" + "github.com/spf13/cobra" +) + +type podStatsOptionsWrapper struct { + entities.PodStatsOptions + + // Format - pretty-print to JSON or a go template. + Format string + // NoReset - do not reset the screen when streaming. + NoReset bool + // NoStream - do not stream stats but write them once. + NoStream bool +} + +var ( + statsOptions = podStatsOptionsWrapper{} + statsDescription = `Display the containers' resource-usage statistics of one or more running pod` + // Command: podman pod _pod_ + statsCmd = &cobra.Command{ + Use: "stats [flags] [POD...]", + Short: "Display resource-usage statistics of pods", + Long: statsDescription, + RunE: stats, + Example: `podman pod stats + podman pod stats a69b23034235 named-pod + podman pod stats --latest + podman pod stats --all`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: statsCmd, + Parent: podCmd, + }) + + flags := statsCmd.Flags() + flags.BoolVarP(&statsOptions.All, "all", "a", false, "Provide stats for all pods") + flags.StringVar(&statsOptions.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template") + flags.BoolVarP(&statsOptions.Latest, "latest", "l", false, "Provide stats on the latest pod Podman is aware of") + flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen when streaming") + flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result") + + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func stats(cmd *cobra.Command, args []string) error { + // Validate input. + if err := entities.ValidatePodStatsOptions(args, &statsOptions.PodStatsOptions); err != nil { + return err + } + + format := statsOptions.Format + doJson := strings.ToLower(format) == formats.JSONString + header := getPodStatsHeader(format) + + for { + reports, err := registry.ContainerEngine().PodStats(context.Background(), args, statsOptions.PodStatsOptions) + if err != nil { + return err + } + // Print the stats in the requested format and configuration. + if doJson { + if err := printJSONPodStats(reports); err != nil { + return err + } + } else { + if !statsOptions.NoReset { + goterm.Clear() + goterm.MoveCursor(1, 1) + goterm.Flush() + } + if len(format) == 0 { + printPodStatsLines(reports) + } else if err := printFormattedPodStatsLines(format, reports, header); err != nil { + return err + } + } + if statsOptions.NoStream { + break + } + time.Sleep(time.Second) + } + + return nil +} + +func printJSONPodStats(stats []*entities.PodStatsReport) error { + b, err := json.MarshalIndent(&stats, "", " ") + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s\n", string(b)) + return nil +} + +func printPodStatsLines(stats []*entities.PodStatsReport) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + outFormat := "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" + fmt.Fprintf(w, outFormat, "POD", "CID", "NAME", "CPU %", "MEM USAGE/ LIMIT", "MEM %", "NET IO", "BLOCK IO", "PIDS") + for _, i := range stats { + if len(stats) == 0 { + fmt.Fprintf(w, outFormat, i.Pod, "--", "--", "--", "--", "--", "--", "--", "--") + } else { + fmt.Fprintf(w, outFormat, i.Pod, i.CID, i.Name, i.CPU, i.MemUsage, i.Mem, i.NetIO, i.BlockIO, i.PIDS) + } + } + w.Flush() +} + +func printFormattedPodStatsLines(format string, stats []*entities.PodStatsReport, headerNames map[string]string) error { + if len(stats) == 0 { + return nil + } + + // Use a tabwriter to align column format + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + // Spit out the header if "table" is present in the format + if strings.HasPrefix(format, "table") { + hformat := strings.Replace(strings.TrimSpace(format[5:]), " ", "\t", -1) + format = hformat + headerTmpl, err := template.New("header").Parse(hformat) + if err != nil { + return err + } + if err := headerTmpl.Execute(w, headerNames); err != nil { + return err + } + fmt.Fprintln(w, "") + } + + // Spit out the data rows now + dataTmpl, err := template.New("data").Parse(format) + if err != nil { + return err + } + for _, s := range stats { + if err := dataTmpl.Execute(w, s); err != nil { + return err + } + fmt.Fprintln(w, "") + } + // Flush the writer + return w.Flush() + +} + +// getPodStatsHeader returns the stats header for the specified options. +func getPodStatsHeader(format string) map[string]string { + headerNames := make(map[string]string) + if format == "" { + return headerNames + } + // Make a map of the field names for the headers + v := reflect.ValueOf(entities.PodStatsReport{}) + t := v.Type() + for i := 0; i < t.NumField(); i++ { + split := camelcase.Split(t.Field(i).Name) + value := strings.ToUpper(strings.Join(split, " ")) + switch value { + case "CPU", "MEM": + value += " %" + case "MEM USAGE": + value = "MEM USAGE / LIMIT" + } + headerNames[t.Field(i).Name] = value + } + return headerNames +} |