Skip to content

Commit

Permalink
feature: finish top interface
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Wan <zirenwan@gmail.com>
  • Loading branch information
HusterWan committed Mar 15, 2018
1 parent 3764a1e commit ee31158
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 11 deletions.
9 changes: 8 additions & 1 deletion apis/server/container_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,5 +334,12 @@ 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)
}
1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func main() {
cli.AddCommand(base, &UpdateCommand{})
cli.AddCommand(base, &LogoutCommand{})
cli.AddCommand(base, &UpgradeCommand{})
cli.AddCommand(base, &TopCommand{})

// add generate doc command
cli.AddCommand(base, &GenDocCommand{})
Expand Down
28 changes: 19 additions & 9 deletions cli/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
},
Expand All @@ -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
`
}
25 changes: 25 additions & 0 deletions ctrd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
36 changes: 36 additions & 0 deletions daemon/mgr/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -801,6 +805,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)
}
Expand Down
54 changes: 54 additions & 0 deletions daemon/mgr/container_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
52 changes: 51 additions & 1 deletion daemon/mgr/container_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"reflect"
"testing"

"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/pkg/collect"
"github.com/alibaba/pouch/pkg/meta"

"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -136,3 +136,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)
}
})
}
}
36 changes: 36 additions & 0 deletions test/cli_top_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"strings"

"github.com/alibaba/pouch/test/command"
"github.com/alibaba/pouch/test/environment"

Expand All @@ -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)
}

0 comments on commit ee31158

Please sign in to comment.