From 9294ba1360a64a54ac916cb138b38a4697d1dc22 Mon Sep 17 00:00:00 2001 From: Michael Wan Date: Thu, 15 Mar 2018 08:12:30 -0400 Subject: [PATCH] feature: finish top interface Signed-off-by: Michael Wan --- apis/server/container_bridge.go | 9 ++++- cli/top.go | 28 +++++++++++----- ctrd/container.go | 25 ++++++++++++++ daemon/mgr/container.go | 36 ++++++++++++++++++++ daemon/mgr/container_utils.go | 54 ++++++++++++++++++++++++++++++ daemon/mgr/container_utils_test.go | 51 ++++++++++++++++++++++++++++ test/cli_top_test.go | 36 ++++++++++++++++++++ 7 files changed, 229 insertions(+), 10 deletions(-) diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index da1326c39..ae1a74e41 100644 --- a/apis/server/container_bridge.go +++ b/apis/server/container_bridge.go @@ -334,7 +334,14 @@ func (s *Server) upgradeContainer(ctx context.Context, rw http.ResponseWriter, r } func (s *Server) topContainer(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { - return nil + name := mux.Vars(req)["name"] + + procList, err := s.ContainerMgr.Top(ctx, name, req.Form.Get("ps_args")) + if err != nil { + return err + } + + return EncodeResponse(rw, http.StatusOK, procList) } func (s *Server) logsContainer(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { diff --git a/cli/top.go b/cli/top.go index e68190d34..3b7368d1d 100644 --- a/cli/top.go +++ b/cli/top.go @@ -3,15 +3,16 @@ package main import ( "context" "fmt" - - //"github.com/alibaba/pouch/apis/types" - //"github.com/alibaba/pouch/pkg/reference" + "os" + "strings" + "text/tabwriter" "github.com/spf13/cobra" ) // topDescription -var topDescription = "" +var topDescription = "top comand is to display the running processes of a container." + + "Your can add options just like using Linux ps command." // TopCommand use to implement 'top' command, it displays all processes in a container. type TopCommand struct { @@ -23,10 +24,10 @@ type TopCommand struct { func (top *TopCommand) Init(c *Cli) { top.cli = c top.cmd = &cobra.Command{ - Use: "top CONTAINER", + Use: "top CONTAINER [ps OPTIONS]", Short: "Display the running processes of a container", Long: topDescription, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return top.runTop(args) }, @@ -43,16 +44,25 @@ func (top *TopCommand) runTop(args []string) error { arguments := args[1:] - resp, err := apiClient.ContainerTop(ctx, container, arguments) + procList, err := apiClient.ContainerTop(ctx, container, arguments) if err != nil { return fmt.Errorf("failed to execute top command in container %s: %v", container, err) } - fmt.Println(resp) + w := tabwriter.NewWriter(os.Stdout, 1, 8, 4, ' ', 0) + fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) + + for _, ps := range procList.Processes { + fmt.Fprintln(w, strings.Join(ps, "\t")) + } + w.Flush() return nil } // topExamples shows examples in top command, and is used in auto-generated cli docs. func topExamples() string { - return `` + return `$ pouch top 44f675 + UID PID PPID C STIME TTY TIME CMD + root 28725 28714 0 3月14 ? 00:00:00 sh + ` } diff --git a/ctrd/container.go b/ctrd/container.go index a0cbfedb2..1efe82ed7 100644 --- a/ctrd/container.go +++ b/ctrd/container.go @@ -444,3 +444,28 @@ func (c *Client) UpdateResources(ctx context.Context, id string, resources types return pack.task.Update(ctx, containerd.WithResources(r)) } + +// GetPidsForContainer returns s list of process IDs running in a container. +func (c *Client) GetPidsForContainer(ctx context.Context, id string) ([]int, error) { + if !c.lock.Trylock(id) { + return nil, errtypes.ErrLockfailed + } + defer c.lock.Unlock(id) + + var pids []int + + pack, err := c.watch.get(id) + if err != nil { + return nil, err + } + + processes, err := pack.task.Pids(ctx) + if err != nil { + return nil, err + } + + for _, ps := range processes { + pids = append(pids, int(ps.Pid)) + } + return pids, nil +} diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index ff52460df..1dfadbf7e 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "os/exec" "path" "path/filepath" "strings" @@ -75,6 +76,9 @@ type ContainerMgr interface { // Upgrade upgrades a container with new image and args. Upgrade(ctx context.Context, name string, config *types.ContainerUpgradeConfig) error + + // Top lists the processes running inside of the given container + Top(ctx context.Context, name string, psArgs string) (*types.ContainerProcessList, error) } // ContainerManager is the default implement of interface ContainerMgr. @@ -806,6 +810,38 @@ func (mgr *ContainerManager) Upgrade(ctx context.Context, name string, config *t return nil } +// Top lists the processes running inside of the given container +func (mgr *ContainerManager) Top(ctx context.Context, name string, psArgs string) (*types.ContainerProcessList, error) { + if psArgs == "" { + psArgs = "-ef" + } + c, err := mgr.container(name) + if err != nil { + return nil, err + } + + if !c.IsRunning() { + return nil, fmt.Errorf("container is not running, can not execute top command") + } + + pids, err := mgr.Client.GetPidsForContainer(ctx, c.ID()) + if err != nil { + return nil, errors.Wrapf(err, "failed to get pids of container") + } + + output, err := exec.Command("ps", strings.Split(psArgs, " ")...).Output() + if err != nil { + return nil, errors.Wrapf(err, "error running ps") + } + + procList, err := parsePSOutput(output, pids) + if err != nil { + return nil, errors.Wrapf(err, "parsePSOutput failed") + } + + return procList, nil +} + func (mgr *ContainerManager) openContainerIO(id string, attach *AttachConfig) (*containerio.IO, error) { return mgr.openIO(id, attach, false) } diff --git a/daemon/mgr/container_utils.go b/daemon/mgr/container_utils.go index 616e7fe43..dc4823416 100644 --- a/daemon/mgr/container_utils.go +++ b/daemon/mgr/container_utils.go @@ -2,8 +2,10 @@ package mgr import ( "fmt" + "strconv" "strings" + "github.com/alibaba/pouch/apis/types" "github.com/alibaba/pouch/pkg/errtypes" "github.com/alibaba/pouch/pkg/meta" "github.com/alibaba/pouch/pkg/randomid" @@ -126,3 +128,55 @@ func parseSecurityOpt(meta *ContainerMeta, securityOpt string) error { } return nil } + +// fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces +func fieldsASCII(s string) []string { + fn := func(r rune) bool { + switch r { + case '\t', '\n', '\f', '\r', ' ': + return true + } + return false + } + return strings.FieldsFunc(s, fn) +} + +func parsePSOutput(output []byte, pids []int) (*types.ContainerProcessList, error) { + procList := &types.ContainerProcessList{} + + lines := strings.Split(string(output), "\n") + procList.Titles = fieldsASCII(lines[0]) + + pidIndex := -1 + for i, name := range procList.Titles { + if name == "PID" { + pidIndex = i + } + } + if pidIndex == -1 { + return nil, fmt.Errorf("Couldn't find PID field in ps output") + } + + // loop through the output and extract the PID from each line + for _, line := range lines[1:] { + if len(line) == 0 { + continue + } + fields := fieldsASCII(line) + p, err := strconv.Atoi(fields[pidIndex]) + if err != nil { + return nil, fmt.Errorf("Unexpected pid '%s': %s", fields[pidIndex], err) + } + + for _, pid := range pids { + if pid == p { + // Make sure number of fields equals number of header titles + // merging "overhanging" fields + process := fields[:len(procList.Titles)-1] + process = append(process, strings.Join(fields[len(procList.Titles)-1:], " ")) + procList.Processes = append(procList.Processes, process) + } + } + } + return procList, nil +} diff --git a/daemon/mgr/container_utils_test.go b/daemon/mgr/container_utils_test.go index 30d27bcc2..15e3fbe58 100644 --- a/daemon/mgr/container_utils_test.go +++ b/daemon/mgr/container_utils_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/alibaba/pouch/apis/types" "github.com/alibaba/pouch/pkg/collect" "github.com/alibaba/pouch/pkg/meta" @@ -136,3 +137,53 @@ func Test_parseSecurityOpt(t *testing.T) { }) } } + +func Test_parsePSOutput(t *testing.T) { + type args struct { + output []byte + pids []int + } + tests := []struct { + name string + args args + want *types.ContainerProcessList + wantErr bool + }{ + // TODO: Add test cases. + { + name: "testParsePSOutputOk", + args: args{ + output: []byte("UID PID PPID C STIME TTY TIME CMD\nroot 1 0 0 3月12 ? 00:00:14 /usr/lib/systemd/systemd --switched-root --system --deserialize 21"), + pids: []int{1}, + }, + want: &types.ContainerProcessList{ + Processes: [][]string{ + {"root", "1", "0", "0", "3月12", "?", "00:00:14", "/usr/lib/systemd/systemd --switched-root --system --deserialize 21"}, + }, + Titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"}, + }, + wantErr: false, + }, + { + name: "testParsePSOutputWithNoPID", + args: args{ + output: []byte("UID PPID C STIME TTY TIME CMD\nroot 0 0 3月12 ? 00:00:14 /usr/lib/systemd/systemd --switched-root --system --deserialize 21"), + pids: []int{1}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parsePSOutput(tt.args.output, tt.args.pids) + if (err != nil) != tt.wantErr { + t.Errorf("parsePSOutput() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parsePSOutput() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/cli_top_test.go b/test/cli_top_test.go index 3ee885aef..7168ccc27 100644 --- a/test/cli_top_test.go +++ b/test/cli_top_test.go @@ -1,6 +1,8 @@ package main import ( + "strings" + "github.com/alibaba/pouch/test/command" "github.com/alibaba/pouch/test/environment" @@ -27,3 +29,37 @@ func (suite *PouchTopSuite) SetupSuite(c *check.C) { // TearDownTest does cleanup work in the end of each test. func (suite *PouchTopSuite) TearDownTest(c *check.C) { } + +// TestTopStoppedContainer is to verify the correctness of top a stopped container. +func (suite *PouchTopSuite) TestTopStoppedContainer(c *check.C) { + name := "TestTopStoppedContainer" + + command.PouchRun("create", "-m", "300M", "--name", name, busyboxImage).Assert(c, icmd.Success) + + res := command.PouchRun("top", name) + c.Assert(res.Error, check.NotNil) + + expectString := "container is not running, can not execute top command" + if out := res.Combined(); !strings.Contains(out, expectString) { + c.Fatalf("unexpected output %s expected %s", out, expectString) + } + + command.PouchRun("rm", "-f", name).Assert(c, icmd.Success) +} + +// TestTopContainer is to verify the correctness of pouch top command. +func (suite *PouchTopSuite) TestTopContainer(c *check.C) { + name := "TestTopContainer" + + command.PouchRun("run", "-m", "300M", "--name", name, busyboxImage).Assert(c, icmd.Success) + + res := command.PouchRun("top", name) + c.Assert(res.Error, check.IsNil) + + expectString := "UID PID PPID C STIME TTY TIME CMD" + if out := res.Combined(); !strings.Contains(out, expectString) { + c.Fatalf("unexpected output %s expected %s", out, expectString) + } + + command.PouchRun("rm", "-f", name).Assert(c, icmd.Success) +}