Files
Paul Holzinger b5d1d89a37 Add shell completion with cobra
Allow automatic generation for shell completion scripts
with the internal cobra functions (requires v1.0.0+).

This should replace the handwritten completion scripts
and even adds support for fish. With this approach it is
less likley that completions and code are out of sync.

We can now create the scripts with
- podman completion bash
- podman completion zsh
- podman completion fish

To test the completion run:
source <(podman completion bash)

The same works for podman-remote and podman --remote and
it will complete your remote containers/images with
the correct endpoints values from --url/--connection.

The completion logic is written in go and provided by the
cobra library. The completion functions lives in
`cmd/podman/completion/completion.go`.

The unit test at cmd/podman/shell_completion_test.go checks
if each command and flag has an autocompletion function set.
This prevents that commands and flags have no shell completion set.

This commit does not replace the current autocompletion scripts.

Closes #6440

Signed-off-by: Paul Holzinger <paul.holzinger@web.de>
2020-11-12 11:38:31 +01:00

276 lines
7.6 KiB
Go

package containers
import (
"fmt"
"os"
"text/tabwriter"
"text/template"
tm "github.com/buger/goterm"
"github.com/containers/common/pkg/report"
"github.com/containers/podman/v2/cmd/podman/common"
"github.com/containers/podman/v2/cmd/podman/parse"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/validate"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/pkg/cgroups"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/rootless"
"github.com/containers/podman/v2/utils"
"github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
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 [options] [CONTAINER...]",
Short: "Display a live stream of container resource usage statistics",
Long: statsDescription,
RunE: stats,
Args: checkStatOptions,
ValidArgsFunction: common.AutocompleteContainersRunning,
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,
ValidArgsFunction: statsCommand.ValidArgsFunction,
Example: `podman container stats --all --no-stream
podman container stats ctrID
podman container stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`,
}
)
// statsOptionsCLI is used for storing CLI arguments. Some fields are later
// used in the backend.
type statsOptionsCLI struct {
All bool
Format string
Latest bool
NoReset bool
NoStream bool
}
var (
statsOptions statsOptionsCLI
)
func statFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&statsOptions.All, "all", "a", false, "Show all containers. Only running containers are shown by default. The default is false")
formatFlagName := "format"
flags.StringVar(&statsOptions.Format, formatFlagName, "", "Pretty-print container statistics to JSON or using a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
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")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: statsCommand,
})
statFlags(statsCommand)
validate.AddLatestFlag(statsCommand, &statsOptions.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: containerStatsCommand,
Parent: containerCmd,
})
statFlags(containerStatsCommand)
validate.AddLatestFlag(containerStatsCommand, &statsOptions.Latest)
}
// 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++
}
if statsOptions.Latest {
opts++
}
if len(args) > 0 {
opts++
}
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")
}
}
// Convert to the entities options. We should not leak CLI-only
// options into the backend and separate concerns.
opts := entities.ContainerStatsOptions{
Latest: statsOptions.Latest,
Stream: !statsOptions.NoStream,
}
statsChan, err := registry.ContainerEngine().ContainerStats(registry.Context(), args, opts)
if err != nil {
return err
}
for report := range statsChan {
if report.Error != nil {
return report.Error
}
if err := outputStats(report.Stats); err != nil {
logrus.Error(err)
}
}
return nil
}
func outputStats(reports []define.ContainerStats) error {
headers := report.Headers(define.ContainerStats{}, map[string]string{
"ID": "ID",
"CPUPerc": "CPU %",
"MemUsage": "MEM USAGE / LIMIT",
"MemPerc": "MEM %",
"NetIO": "NET IO",
"BlockIO": "BLOCK IO",
"PIDS": "PIDS",
})
if !statsOptions.NoReset {
tm.Clear()
tm.MoveCursor(1, 1)
tm.Flush()
}
stats := make([]containerStats, 0, len(reports))
for _, r := range reports {
stats = append(stats, containerStats{r})
}
if report.IsJSON(statsOptions.Format) {
return outputJSON(stats)
}
format := "{{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}\n"
if len(statsOptions.Format) > 0 {
format = report.NormalizeFormat(statsOptions.Format)
}
format = parse.EnforceRange(format)
tmpl, err := template.New("stats").Parse(format)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0)
defer w.Flush()
if len(statsOptions.Format) < 1 {
if err := tmpl.Execute(w, headers); err != nil {
return err
}
}
if err := tmpl.Execute(w, stats); 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"` // nolint
Name string `json:"name"`
CpuPercent string `json:"cpu_percent"` // nolint
MemUsage string `json:"mem_usage"`
MemPerc string `json:"mem_percent"`
NetIO string `json:"net_io"`
BlockIO string `json:"block_io"`
Pids string `json:"pids"`
}
jstats := make([]jstat, 0, len(stats))
for _, j := range stats {
jstats = append(jstats, jstat{
Id: j.ID(),
Name: j.Name,
CpuPercent: j.CPUPerc(),
MemUsage: j.MemUsage(),
MemPerc: j.MemPerc(),
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
}