From 500a2d508bc8babe8234f259ab51d4908daf9378 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Thu, 26 Mar 2020 11:13:45 +0100 Subject: podmanV2: implement top Implement the `top` command for podmanV2. Signed-off-by: Valentin Rothberg --- cmd/podmanV2/containers/top.go | 91 +++++++++++++++++++++++++++++++++ pkg/bindings/containers/containers.go | 37 +++++++++++++- pkg/bindings/test/containers_test.go | 32 +++++++++++- pkg/domain/entities/containers.go | 15 ++++++ pkg/domain/entities/engine_container.go | 3 ++ pkg/domain/infra/abi/containers.go | 22 ++++++++ pkg/domain/infra/tunnel/containers.go | 16 ++++++ 7 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 cmd/podmanV2/containers/top.go diff --git a/cmd/podmanV2/containers/top.go b/cmd/podmanV2/containers/top.go new file mode 100644 index 000000000..a86c12e2a --- /dev/null +++ b/cmd/podmanV2/containers/top.go @@ -0,0 +1,91 @@ +package containers + +import ( + "context" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/psgo" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + topDescription = fmt.Sprintf(`Similar to system "top" command. + + Specify format descriptors to alter the output. + + Running "podman top -l pid pcpu seccomp" will print the process ID, the CPU percentage and the seccomp mode of each process of the latest container. + Format Descriptors: + %s`, strings.Join(psgo.ListDescriptors(), ",")) + + topOptions = entities.TopOptions{} + + topCommand = &cobra.Command{ + Use: "top [flags] CONTAINER [FORMAT-DESCRIPTORS|ARGS]", + Short: "Display the running processes of a container", + Long: topDescription, + PersistentPreRunE: preRunE, + RunE: top, + Args: cobra.ArbitraryArgs, + Example: `podman top ctrID +podman top --latest +podman top ctrID pid seccomp args %C +podman top ctrID -eo user,pid,comm`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: topCommand, + }) + + topCommand.SetHelpTemplate(registry.HelpTemplate()) + topCommand.SetUsageTemplate(registry.UsageTemplate()) + + flags := topCommand.Flags() + flags.SetInterspersed(false) + flags.BoolVar(&topOptions.ListDescriptors, "list-descriptors", false, "") + flags.BoolVarP(&topOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + + _ = flags.MarkHidden("list-descriptors") // meant only for bash completion + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func top(cmd *cobra.Command, args []string) error { + if topOptions.ListDescriptors { + fmt.Println(strings.Join(psgo.ListDescriptors(), "\n")) + return nil + } + + if len(args) < 1 && !topOptions.Latest { + return errors.Errorf("you must provide the name or id of a running container") + } + + if topOptions.Latest { + topOptions.Descriptors = args + } else { + topOptions.NameOrID = args[0] + topOptions.Descriptors = args[1:] + } + + topResponse, err := registry.ContainerEngine().ContainerTop(context.Background(), topOptions) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) + for _, proc := range topResponse.Value { + if _, err := fmt.Fprintln(w, proc); err != nil { + return err + } + } + return w.Flush() +} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index c2f10345f..bad1294f4 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -5,8 +5,10 @@ import ( "net/http" "net/url" "strconv" + "strings" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers" lpapiv2 "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings" ) @@ -193,7 +195,40 @@ func Start(ctx context.Context, nameOrID string, detachKeys *string) error { } func Stats() {} -func Top() {} + +// Top gathers statistics about the running processes in a container. The nameOrID can be a container name +// or a partial/full ID. The descriptors allow for specifying which data to collect from the process. +func Top(ctx context.Context, nameOrID string, descriptors []string) ([]string, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + + if len(descriptors) > 0 { + // flatten the slice into one string + params.Set("ps_args", strings.Join(descriptors, ",")) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/top", params, nameOrID) + if err != nil { + return nil, err + } + + body := handlers.ContainerTopOKBody{} + if err = response.Process(&body); err != nil { + return nil, err + } + + // handlers.ContainerTopOKBody{} returns a slice of slices where each cell in the top table is an item. + // In libpod land, we're just using a slice with cells being split by tabs, which allows for an idiomatic + // usage of the tabwriter. + topOutput := []string{strings.Join(body.Titles, "\t")} + for _, out := range body.Processes { + topOutput = append(topOutput, strings.Join(out, "\t")) + } + + return topOutput, err +} // Unpause resumes the given paused container. The nameOrID can be a container name // or a partial/full ID. diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index 55c739865..9dd9cb707 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -34,7 +34,7 @@ var _ = Describe("Podman containers ", func() { AfterEach(func() { s.Kill() - //bt.cleanup() + bt.cleanup() }) It("podman pause a bogus container", func() { @@ -380,4 +380,34 @@ var _ = Describe("Podman containers ", func() { _, err = time.Parse(time.RFC1123Z, o) Expect(err).To(BeNil()) }) + + It("podman top", func() { + var name = "top" + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + + // By name + output, err := containers.Top(bt.conn, name, nil) + Expect(err).To(BeNil()) + + // By id + output, err = containers.Top(bt.conn, cid, nil) + Expect(err).To(BeNil()) + + // With descriptors + output, err = containers.Top(bt.conn, cid, []string{"user,pid,hpid"}) + Expect(err).To(BeNil()) + header := strings.Split(output[0], "\t") + for _, d := range []string{"USER", "PID", "HPID"} { + Expect(d).To(BeElementOf(header)) + } + + // With bogus ID + _, err = containers.Top(bt.conn, "IdoNotExist", nil) + Expect(err).ToNot(BeNil()) + + // With bogus descriptors + _, err = containers.Top(bt.conn, cid, []string{"Me,Neither"}) + Expect(err).To(BeNil()) + }) }) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 545452948..fbc0247ab 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -22,6 +22,11 @@ type BoolReport struct { Value bool } +// StringSliceReport wraps a string slice. +type StringSliceReport struct { + Value []string +} + type PauseUnPauseOptions struct { All bool } @@ -44,6 +49,16 @@ type StopReport struct { Id string } +type TopOptions struct { + // CLI flags. + ListDescriptors bool + Latest bool + + // Options for the API. + Descriptors []string + NameOrID string +} + type KillOptions struct { All bool Latest bool diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 0217910fc..21a674c43 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -14,6 +14,8 @@ type ContainerEngine interface { ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) + ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error) + PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) PodKill(ctx context.Context, namesOrIds []string, options PodKillOptions) ([]*PodKillReport, error) PodPause(ctx context.Context, namesOrIds []string, options PodPauseOptions) ([]*PodPauseReport, error) @@ -22,6 +24,7 @@ type ContainerEngine interface { PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) + VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts VolumeInspectOptions) ([]*VolumeInspectReport, error) VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 2e1a5aa82..3965c5f75 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -255,3 +255,25 @@ func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []st } return reports, nil } + +func (ic *ContainerEngine) ContainerTop(ctx context.Context, options entities.TopOptions) (*entities.StringSliceReport, error) { + var ( + container *libpod.Container + err error + ) + + // Look up the container. + if options.Latest { + container, err = ic.Libpod.GetLatestContainer() + } else { + container, err = ic.Libpod.LookupContainer(options.NameOrID) + } + if err != nil { + return nil, errors.Wrap(err, "unable to lookup requested container") + } + + // Run Top. + report := &entities.StringSliceReport{} + report.Value, err = container.Top(options.Descriptors) + return report, err +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 367ea6e36..3db38ea5c 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -5,6 +5,7 @@ import ( "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" ) func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { @@ -156,3 +157,18 @@ func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []st } return reports, nil } + +func (ic *ContainerEngine) ContainerTop(ctx context.Context, options entities.TopOptions) (*entities.StringSliceReport, error) { + switch { + case options.Latest: + return nil, errors.New("latest is not supported") + case options.NameOrID == "": + return nil, errors.New("NameOrID must be specified") + } + + topOutput, err := containers.Top(ic.ClientCxt, options.NameOrID, options.Descriptors) + if err != nil { + return nil, err + } + return &entities.StringSliceReport{Value: topOutput}, nil +} -- cgit v1.2.3-54-g00ecf