diff options
-rw-r--r-- | cmd/podman/pod.go | 1 | ||||
-rw-r--r-- | cmd/podman/pod_top.go | 101 | ||||
-rw-r--r-- | docs/podman-pod-top.1.md | 92 | ||||
-rw-r--r-- | libpod/pod_top_linux.go | 55 | ||||
-rw-r--r-- | libpod/pod_top_unsupported.go | 8 | ||||
-rw-r--r-- | test/e2e/pod_top_test.go | 144 | ||||
-rw-r--r-- | vendor.conf | 2 |
7 files changed, 402 insertions, 1 deletions
diff --git a/cmd/podman/pod.go b/cmd/podman/pod.go index e691204d8..dde43f77c 100644 --- a/cmd/podman/pod.go +++ b/cmd/podman/pod.go @@ -20,6 +20,7 @@ Pods are a group of one or more containers sharing the same network, pid and ipc podStartCommand, podStatsCommand, podStopCommand, + podTopCommand, podUnpauseCommand, } podCommand = cli.Command{ diff --git a/cmd/podman/pod_top.go b/cmd/podman/pod_top.go new file mode 100644 index 000000000..703ec0a44 --- /dev/null +++ b/cmd/podman/pod_top.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + podTopFlags = []cli.Flag{ + LatestFlag, + cli.BoolFlag{ + Name: "list-descriptors", + Hidden: true, + }, + } + podTopDescription = fmt.Sprintf(`Display the running processes containers in a pod. Specify format descriptors +to alter the output. You may run "podman pod top -l pid pcpu seccomp" to print +the process ID, the CPU percentage and the seccomp mode of each process of +the latest pod. +%s +`, getDescriptorString()) + + podTopCommand = cli.Command{ + Name: "top", + Usage: "Display the running processes of containers in a pod", + Description: podTopDescription, + Flags: podTopFlags, + Action: podTopCmd, + ArgsUsage: "POD-NAME [format descriptors]", + SkipArgReorder: true, + } +) + +func podTopCmd(c *cli.Context) error { + var pod *libpod.Pod + var err error + args := c.Args() + + if c.Bool("list-descriptors") { + descriptors, err := libpod.GetContainerPidInformationDescriptors() + if err != nil { + return err + } + fmt.Println(strings.Join(descriptors, "\n")) + return nil + } + + if len(args) < 1 && !c.Bool("latest") { + return errors.Errorf("you must provide the name or id of a running pod") + } + if err := validateFlags(c, podTopFlags); err != nil { + return err + } + + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + var descriptors []string + if c.Bool("latest") { + descriptors = args + pod, err = runtime.GetLatestPod() + } else { + descriptors = args[1:] + pod, err = runtime.LookupPod(args[0]) + } + + if err != nil { + return errors.Wrapf(err, "unable to lookup requested container") + } + + podStatus, err := shared.GetPodStatus(pod) + if err != nil { + return err + } + if podStatus != "Running" { + return errors.Errorf("pod top can only be used on pods with at least one running container") + } + + psOutput, err := pod.GetPodPidInformation(descriptors) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) + for _, proc := range psOutput { + fmt.Fprintln(w, proc) + } + w.Flush() + return nil +} diff --git a/docs/podman-pod-top.1.md b/docs/podman-pod-top.1.md new file mode 100644 index 000000000..7a66b4534 --- /dev/null +++ b/docs/podman-pod-top.1.md @@ -0,0 +1,92 @@ +% podman-pod-top "1" + +## NAME +podman\-pod\-top - Display the running processes of containers in a pod + +## SYNOPSIS +**podman top** [*options*] *pod* [*format-descriptors*] + +## DESCRIPTION +Display the running process of containers in a pod. The *format-descriptors* are ps (1) compatible AIX format descriptors but extended to print additional information, such as the seccomp mode or the effective capabilities of a given process. + +## OPTIONS + +**--help, -h** + + Print usage statement + +**--latest, -l** + +Instead of providing the pod name or ID, use the last created pod. + +## FORMAT DESCRIPTORS + +The following descriptors are supported in addition to the AIX format descriptors mentioned in ps (1): + +**args, capbnd, capeff, capinh, capprm, comm, etime, group, hgroup, hpid, huser, label, nice, pcpu, pgid, pid, ppid, rgroup, ruser, seccomp, state, time, tty, user, vsz** + +**capbnd** + + Set of bounding capabilities. See capabilities (7) for more information. + +**capeff** + + Set of effective capabilities. See capabilities (7) for more information. + +**capinh** + + Set of inheritable capabilities. See capabilities (7) for more information. + +**capprm** + + Set of permitted capabilities. See capabilities (7) for more information. + +**hgroup** + + The corresponding effective group of a container process on the host. + +**hpid** + + The corresponding host PID of a container process. + +**huser** + + The corresponding effective user of a container process on the host. + +**label** + + Current security attributes of the process. + +**seccomp** + + Seccomp mode of the process (i.e., disabled, strict or filter). See seccomp (2) for more information. + +**state** + + Process state codes (e.g, **R** for *running*, **S** for *sleeping*). See proc(5) for more information. + +## EXAMPLES + +By default, `podman-top` prints data similar to `ps -ef`: + +``` + # podman pod top b031293491cc +USER PID PPID %CPU ELAPSED TTY TIME COMMAND +root 1 0 0.000 2h5m38.737137571s ? 0s top +root 8 0 0.000 2h5m15.737228361s ? 0s top +``` + +The output can be controlled by specifying format descriptors as arguments after the pod: + +``` + # podman pod top -l pid seccomp args %C +PID SECCOMP COMMAND %CPU +1 filter top 0.000 +1 filter /bin/sh 0.000 +``` + +## SEE ALSO +podman-pod(1), ps(1), seccomp(2), proc(5), capabilities(7) + +## HISTORY +August 2018, Originally compiled by Peter Hunt <pehunt@redhat.com> diff --git a/libpod/pod_top_linux.go b/libpod/pod_top_linux.go new file mode 100644 index 000000000..f49e28c9d --- /dev/null +++ b/libpod/pod_top_linux.go @@ -0,0 +1,55 @@ +// +build linux + +package libpod + +import ( + "strconv" + "strings" + + "github.com/containers/psgo" +) + +// GetPodPidInformation returns process-related data of all processes in +// the pod. The output data can be controlled via the `descriptors` +// argument which expects format descriptors and supports all AIXformat +// descriptors of ps (1) plus some additional ones to for instance inspect the +// set of effective capabilities. Eeach element in the returned string slice +// is a tab-separated string. +// +// For more details, please refer to github.com/containers/psgo. +func (p *Pod) GetPodPidInformation(descriptors []string) ([]string, error) { + p.lock.Lock() + defer p.lock.Unlock() + + pids := make([]string, 0) + ctrsInPod, err := p.allContainers() + if err != nil { + return nil, err + } + for _, c := range ctrsInPod { + c.lock.Lock() + + if err := c.syncContainer(); err != nil { + c.lock.Unlock() + return nil, err + } + if c.state.State == ContainerStateRunning { + pid := strconv.Itoa(c.state.PID) + pids = append(pids, pid) + } + c.lock.Unlock() + } + // TODO: psgo returns a [][]string to give users the ability to apply + // filters on the data. We need to change the API here and the + // varlink API to return a [][]string if we want to make use of + // filtering. + output, err := psgo.JoinNamespaceAndProcessInfoByPids(pids, descriptors) + if err != nil { + return nil, err + } + res := []string{} + for _, out := range output { + res = append(res, strings.Join(out, "\t")) + } + return res, nil +} diff --git a/libpod/pod_top_unsupported.go b/libpod/pod_top_unsupported.go new file mode 100644 index 000000000..d44470523 --- /dev/null +++ b/libpod/pod_top_unsupported.go @@ -0,0 +1,8 @@ +// +build !linux + +package libpod + +// GetPodPidInformation is exclusive to linux +func (p *Pod) GetPodPidInformation(descriptors []string) ([]string, error) { + return nil, ErrNotImplemented +} diff --git a/test/e2e/pod_top_test.go b/test/e2e/pod_top_test.go new file mode 100644 index 000000000..2b4aa540f --- /dev/null +++ b/test/e2e/pod_top_test.go @@ -0,0 +1,144 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman top", func() { + var ( + tempdir string + err error + podmanTest PodmanTest + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.CleanupPod() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + }) + + It("podman pod top without pod name or id", func() { + result := podmanTest.Podman([]string{"pod", "top"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(125)) + }) + + It("podman pod top on bogus pod", func() { + result := podmanTest.Podman([]string{"pod", "top", "1234"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(125)) + }) + + It("podman pod top on non-running pod", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podid := session.OutputToString() + + result := podmanTest.Podman([]string{"top", podid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(125)) + }) + + It("podman pod top on pod", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podid := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + result := podmanTest.Podman([]string{"pod", "top", "-l"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(len(result.OutputToStringArray())).To(BeNumerically(">", 1)) + }) + + It("podman pod top with options", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podid := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + result := podmanTest.Podman([]string{"pod", "top", podid, "pid", "%C", "args"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(len(result.OutputToStringArray())).To(BeNumerically(">", 1)) + }) + + It("podman pod top on pod invalid options", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podid := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + result := podmanTest.Podman([]string{"pod", "top", podid, "invalid"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(125)) + }) + + It("podman pod top on pod with containers in same pid namespace", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podid := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + cid := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, "--pid", fmt.Sprintf("container:%s", cid), ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + result := podmanTest.Podman([]string{"pod", "top", podid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(len(result.OutputToStringArray())).To(Equal(4)) + }) + + It("podman pod top on pod with containers in different namespace", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podid := session.OutputToString() + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "-d", "--pod", podid, ALPINE, "top", "-d", "2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + result := podmanTest.Podman([]string{"pod", "top", podid}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(len(result.OutputToStringArray())).To(Equal(4)) + }) +}) diff --git a/vendor.conf b/vendor.conf index 86278ae05..4a2e82e7a 100644 --- a/vendor.conf +++ b/vendor.conf @@ -12,7 +12,7 @@ github.com/containernetworking/cni v0.7.0-alpha1 github.com/containernetworking/plugins 1562a1e60ed101aacc5e08ed9dbeba8e9f3d4ec1 github.com/containers/image 134f99bed228d6297dc01d152804f6f09f185418 github.com/containers/storage 17c7d1fee5603ccf6dd97edc14162fc1510e7e23 -github.com/containers/psgo master +github.com/containers/psgo 5dde6da0bc8831b35243a847625bcf18183bd1ee github.com/coreos/go-systemd v14 github.com/cri-o/ocicni master github.com/cyphar/filepath-securejoin v0.2.1 |