Skip to content

Commit 02badc3

Browse files
authored
Merge pull request #223 from fahedouch/setup-nerdctl-top
Setup nerdctl top cmd
2 parents 04038e4 + ff8686b commit 02badc3

File tree

4 files changed

+357
-1
lines changed

4 files changed

+357
-1
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ It does not necessarily mean that the corresponding features are missing in cont
214214
- [:whale: nerdctl events](#whale-nerdctl-events)
215215
- [:whale: nerdctl info](#whale-nerdctl-info)
216216
- [:whale: nerdctl version](#whale-nerdctl-version)
217+
- [Stats management](#stats-management)
218+
- [:whale: nerdctl top](#whale-nerdctl-top)
217219
- [Shell completion](#shell-completion)
218220
- [:nerd_face: nerdctl completion bash](#nerd_face-nerdctl-completion-bash)
219221
- [Compose](#compose)
@@ -753,6 +755,14 @@ Usage: `nerdctl version [OPTIONS]`
753755

754756
Unimplemented `docker version` flags: `--format`
755757

758+
## Stats management
759+
### :whale: nerdctl top
760+
Display the running processes of a container.
761+
762+
763+
Usage: `nerdctl top CONTAINER [ps OPTIONS]`
764+
765+
756766
## Shell completion
757767

758768
### :nerd_face: nerdctl completion bash
@@ -849,7 +859,6 @@ Container management:
849859

850860
Stats:
851861
- `docker stats`
852-
- `docker top`
853862

854863
Image:
855864
- `docker export` and `docker import`

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ func newApp() *cli.App {
173173
versionCommand,
174174
// Inspect
175175
inspectCommand,
176+
// stats
177+
topCommand,
176178
// Management
177179
containerCommand,
178180
imageCommand,

top.go

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"fmt"
23+
"os/exec"
24+
"regexp"
25+
"strconv"
26+
"strings"
27+
"text/tabwriter"
28+
29+
"github.com/containerd/containerd"
30+
"github.com/containerd/nerdctl/pkg/idutil/containerwalker"
31+
"github.com/containerd/nerdctl/pkg/infoutil"
32+
"github.com/containerd/nerdctl/pkg/rootlessutil"
33+
"github.com/pkg/errors"
34+
"github.com/urfave/cli/v2"
35+
)
36+
37+
// ContainerTopOKBody OK response to ContainerTop operation
38+
type ContainerTopOKBody struct {
39+
40+
// Each process running in the container, where each is process
41+
// is an array of values corresponding to the titles.
42+
//
43+
// Required: true
44+
Processes [][]string `json:"Processes"`
45+
46+
// The ps column titles
47+
// Required: true
48+
Titles []string `json:"Titles"`
49+
}
50+
51+
var topCommand = &cli.Command{
52+
Name: "top",
53+
Usage: "Display the running processes of a container",
54+
ArgsUsage: "CONTAINER [ps OPTIONS]",
55+
Action: topAction,
56+
BashComplete: topBashComplete,
57+
}
58+
59+
func topAction(clicontext *cli.Context) error {
60+
61+
if clicontext.NArg() < 1 {
62+
return errors.Errorf("requires at least 1 argument")
63+
}
64+
65+
// NOTE: rootless container does not rely on cgroupv1.
66+
// more details about possible ways to resolve this concern: #223
67+
if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
68+
return fmt.Errorf("top is not supported for rootless container and cgroupv1")
69+
}
70+
71+
client, ctx, cancel, err := newClient(clicontext)
72+
if err != nil {
73+
return err
74+
}
75+
defer cancel()
76+
77+
walker := &containerwalker.ContainerWalker{
78+
Client: client,
79+
OnFound: func(ctx context.Context, found containerwalker.Found) error {
80+
if err := containerTop(ctx, clicontext, client, found.Container.ID(), strings.Join(clicontext.Args().Tail(), " ")); err != nil {
81+
return err
82+
}
83+
return nil
84+
},
85+
}
86+
87+
n, err := walker.Walk(ctx, clicontext.Args().First())
88+
if err != nil {
89+
return err
90+
} else if n == 0 {
91+
return errors.Errorf("no such container %s", clicontext.Args().First())
92+
}
93+
return nil
94+
}
95+
96+
//function from moby/moby/daemon/top_unix.go
97+
func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) {
98+
// Make sure number of fields equals number of header titles
99+
// merging "overhanging" fields
100+
process := fields[:len(procList.Titles)-1]
101+
process = append(process, strings.Join(fields[len(procList.Titles)-1:], " "))
102+
procList.Processes = append(procList.Processes, process)
103+
}
104+
105+
//function from moby/moby/daemon/top_unix.go
106+
// psPidsArg converts a slice of PIDs to a string consisting
107+
// of comma-separated list of PIDs prepended by "-q".
108+
// For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3".
109+
func psPidsArg(pids []uint32) string {
110+
b := []byte{'-', 'q'}
111+
for i, p := range pids {
112+
b = strconv.AppendUint(b, uint64(p), 10)
113+
if i < len(pids)-1 {
114+
b = append(b, ',')
115+
}
116+
}
117+
return string(b)
118+
}
119+
120+
//function from moby/moby/daemon/top_unix.go
121+
func validatePSArgs(psArgs string) error {
122+
// NOTE: \\s does not detect unicode whitespaces.
123+
// So we use fieldsASCII instead of strings.Fields in parsePSOutput.
124+
// See https://github.com/docker/docker/pull/24358
125+
// nolint: gosimple
126+
re := regexp.MustCompile("\\s+([^\\s]*)=\\s*(PID[^\\s]*)")
127+
for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
128+
if len(group) >= 3 {
129+
k := group[1]
130+
v := group[2]
131+
if k != "pid" {
132+
return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v)
133+
}
134+
}
135+
}
136+
return nil
137+
}
138+
139+
//function from moby/moby/daemon/top_unix.go
140+
// fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces
141+
func fieldsASCII(s string) []string {
142+
fn := func(r rune) bool {
143+
switch r {
144+
case '\t', '\n', '\f', '\r', ' ':
145+
return true
146+
}
147+
return false
148+
}
149+
return strings.FieldsFunc(s, fn)
150+
}
151+
152+
//function from moby/moby/daemon/top_unix.go
153+
func hasPid(procs []uint32, pid int) bool {
154+
for _, p := range procs {
155+
if int(p) == pid {
156+
return true
157+
}
158+
}
159+
return false
160+
}
161+
162+
//function from moby/moby/daemon/top_unix.go
163+
func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) {
164+
procList := &ContainerTopOKBody{}
165+
166+
lines := strings.Split(string(output), "\n")
167+
procList.Titles = fieldsASCII(lines[0])
168+
169+
pidIndex := -1
170+
for i, name := range procList.Titles {
171+
if name == "PID" {
172+
pidIndex = i
173+
break
174+
}
175+
}
176+
if pidIndex == -1 {
177+
return nil, fmt.Errorf("Couldn't find PID field in ps output")
178+
}
179+
180+
// loop through the output and extract the PID from each line
181+
// fixing #30580, be able to display thread line also when "m" option used
182+
// in "docker top" client command
183+
preContainedPidFlag := false
184+
for _, line := range lines[1:] {
185+
if len(line) == 0 {
186+
continue
187+
}
188+
fields := fieldsASCII(line)
189+
190+
var (
191+
p int
192+
err error
193+
)
194+
195+
if fields[pidIndex] == "-" {
196+
if preContainedPidFlag {
197+
appendProcess2ProcList(procList, fields)
198+
}
199+
continue
200+
}
201+
p, err = strconv.Atoi(fields[pidIndex])
202+
if err != nil {
203+
return nil, fmt.Errorf("Unexpected pid '%s': %s", fields[pidIndex], err)
204+
}
205+
206+
if hasPid(procs, p) {
207+
preContainedPidFlag = true
208+
appendProcess2ProcList(procList, fields)
209+
continue
210+
}
211+
preContainedPidFlag = false
212+
}
213+
return procList, nil
214+
}
215+
216+
// function inspired from moby/moby/daemon/top_unix.go
217+
// ContainerTop lists the processes running inside of the given
218+
// container by calling ps with the given args, or with the flags
219+
// "-ef" if no args are given. An error is returned if the container
220+
// is not found, or is not running, or if there are any problems
221+
// running ps, or parsing the output.
222+
func containerTop(ctx context.Context, clicontext *cli.Context, client *containerd.Client, id string, psArgs string) error {
223+
if psArgs == "" {
224+
psArgs = "-ef"
225+
}
226+
227+
if err := validatePSArgs(psArgs); err != nil {
228+
return err
229+
}
230+
231+
container, err := client.LoadContainer(ctx, id)
232+
if err != nil {
233+
return err
234+
}
235+
236+
task, err := container.Task(ctx, nil)
237+
if err != nil {
238+
return err
239+
}
240+
241+
status, err := task.Status(ctx)
242+
if err != nil {
243+
return err
244+
}
245+
246+
if status.Status != containerd.Running {
247+
return nil
248+
}
249+
250+
//TO DO handle restarting case: wait for container to restart and then launch top command
251+
252+
procs, err := task.Pids(ctx)
253+
if err != nil {
254+
return err
255+
}
256+
257+
psList := make([]uint32, 0, len(procs))
258+
for _, ps := range procs {
259+
psList = append(psList, ps.Pid)
260+
}
261+
262+
args := strings.Split(psArgs, " ")
263+
pids := psPidsArg(psList)
264+
output, err := exec.Command("ps", append(args, pids)...).Output()
265+
if err != nil {
266+
// some ps options (such as f) can't be used together with q,
267+
// so retry without it
268+
output, err = exec.Command("ps", args...).Output()
269+
if err != nil {
270+
if ee, ok := err.(*exec.ExitError); ok {
271+
// first line of stderr shows why ps failed
272+
line := bytes.SplitN(ee.Stderr, []byte{'\n'}, 2)
273+
if len(line) > 0 && len(line[0]) > 0 {
274+
return errors.New(string(line[0]))
275+
}
276+
}
277+
return nil
278+
}
279+
}
280+
procList, err := parsePSOutput(output, psList)
281+
if err != nil {
282+
return err
283+
}
284+
285+
w := tabwriter.NewWriter(clicontext.App.Writer, 20, 1, 3, ' ', 0)
286+
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))
287+
288+
for _, proc := range procList.Processes {
289+
fmt.Fprintln(w, strings.Join(proc, "\t"))
290+
}
291+
292+
return w.Flush()
293+
}
294+
295+
func topBashComplete(clicontext *cli.Context) {
296+
coco := parseCompletionContext(clicontext)
297+
if coco.boring || coco.flagTakesValue {
298+
defaultBashComplete(clicontext)
299+
return
300+
}
301+
// show container names (TODO: only running containers)
302+
bashCompleteContainerNames(clicontext, nil)
303+
}

top_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"testing"
21+
22+
"github.com/containerd/nerdctl/pkg/infoutil"
23+
"github.com/containerd/nerdctl/pkg/rootlessutil"
24+
"github.com/containerd/nerdctl/pkg/testutil"
25+
)
26+
27+
func TestTop(t *testing.T) {
28+
//more details https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178
29+
if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
30+
t.Skip("test skipped for rootless container with cgroup v1")
31+
}
32+
const (
33+
testContainerName = "nerdctl-test-top"
34+
)
35+
36+
base := testutil.NewBase(t)
37+
defer base.Cmd("rm", "-f", testContainerName).Run()
38+
39+
base.Cmd("run", "-d", "--name", testContainerName, testutil.AlpineImage, "sleep", "5").AssertOK()
40+
base.Cmd("top", testContainerName, "-o", "pid,user,cmd").AssertOK()
41+
42+
}

0 commit comments

Comments
 (0)