github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/top.go (about)

     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  /*
    18     Portions from:
    19     - https://github.com/moby/moby/blob/v20.10.6/api/types/container/container_top.go
    20     - https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go
    21     Copyright (C) The Moby authors.
    22     Licensed under the Apache License, Version 2.0
    23     NOTICE: https://github.com/moby/moby/blob/v20.10.6/NOTICE
    24  */
    25  
    26  package container
    27  
    28  import (
    29  	"context"
    30  	"fmt"
    31  	"regexp"
    32  	"strconv"
    33  	"strings"
    34  
    35  	"github.com/containerd/containerd"
    36  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    37  	"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
    38  )
    39  
    40  // ContainerTopOKBody is from https://github.com/moby/moby/blob/v20.10.6/api/types/container/container_top.go
    41  //
    42  // ContainerTopOKBody OK response to ContainerTop operation
    43  type ContainerTopOKBody struct { //nolint:revive
    44  
    45  	// Each process running in the container, where each is process
    46  	// is an array of values corresponding to the titles.
    47  	//
    48  	// Required: true
    49  	Processes [][]string `json:"Processes"`
    50  
    51  	// The ps column titles
    52  	// Required: true
    53  	Titles []string `json:"Titles"`
    54  }
    55  
    56  // Top performs the equivalent of running `top` inside of container(s)
    57  func Top(ctx context.Context, client *containerd.Client, containers []string, opt types.ContainerTopOptions) error {
    58  	walker := &containerwalker.ContainerWalker{
    59  		Client: client,
    60  		OnFound: func(ctx context.Context, found containerwalker.Found) error {
    61  			if found.MatchCount > 1 {
    62  				return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
    63  			}
    64  			return containerTop(ctx, opt.Stdout, client, found.Container.ID(), strings.Join(containers[1:], " "))
    65  		},
    66  	}
    67  
    68  	n, err := walker.Walk(ctx, containers[0])
    69  	if err != nil {
    70  		return err
    71  	} else if n == 0 {
    72  		return fmt.Errorf("no such container %s", containers[0])
    73  	}
    74  	return nil
    75  }
    76  
    77  // appendProcess2ProcList is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L49-L55
    78  func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) {
    79  	// Make sure number of fields equals number of header titles
    80  	// merging "overhanging" fields
    81  	process := fields[:len(procList.Titles)-1]
    82  	process = append(process, strings.Join(fields[len(procList.Titles)-1:], " "))
    83  	procList.Processes = append(procList.Processes, process)
    84  }
    85  
    86  // psPidsArg is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L119-L131
    87  //
    88  // psPidsArg converts a slice of PIDs to a string consisting
    89  // of comma-separated list of PIDs prepended by "-q".
    90  // For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3".
    91  func psPidsArg(pids []uint32) string {
    92  	b := []byte{'-', 'q'}
    93  	for i, p := range pids {
    94  		b = strconv.AppendUint(b, uint64(p), 10)
    95  		if i < len(pids)-1 {
    96  			b = append(b, ',')
    97  		}
    98  	}
    99  	return string(b)
   100  }
   101  
   102  // validatePSArgs is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L19-L35
   103  func validatePSArgs(psArgs string) error {
   104  	// NOTE: \\s does not detect unicode whitespaces.
   105  	// So we use fieldsASCII instead of strings.Fields in parsePSOutput.
   106  	// See https://github.com/docker/docker/pull/24358
   107  	// nolint: gosimple
   108  	re := regexp.MustCompile(`\s+(\S*)=\s*(PID\S*)`)
   109  	for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
   110  		if len(group) >= 3 {
   111  			k := group[1]
   112  			v := group[2]
   113  			if k != "pid" {
   114  				return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v)
   115  			}
   116  		}
   117  	}
   118  	return nil
   119  }
   120  
   121  // fieldsASCII is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L37-L47
   122  //
   123  // fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces
   124  func fieldsASCII(s string) []string {
   125  	fn := func(r rune) bool {
   126  		switch r {
   127  		case '\t', '\n', '\f', '\r', ' ':
   128  			return true
   129  		}
   130  		return false
   131  	}
   132  	return strings.FieldsFunc(s, fn)
   133  }
   134  
   135  // hasPid is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L57-L64
   136  func hasPid(procs []uint32, pid int) bool {
   137  	for _, p := range procs {
   138  		if int(p) == pid {
   139  			return true
   140  		}
   141  	}
   142  	return false
   143  }
   144  
   145  // parsePSOutput is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L66-L117
   146  func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) {
   147  	procList := &ContainerTopOKBody{}
   148  
   149  	lines := strings.Split(string(output), "\n")
   150  	procList.Titles = fieldsASCII(lines[0])
   151  
   152  	pidIndex := -1
   153  	for i, name := range procList.Titles {
   154  		if name == "PID" {
   155  			pidIndex = i
   156  			break
   157  		}
   158  	}
   159  	if pidIndex == -1 {
   160  		return nil, fmt.Errorf("couldn't find PID field in ps output")
   161  	}
   162  
   163  	// loop through the output and extract the PID from each line
   164  	// fixing #30580, be able to display thread line also when "m" option used
   165  	// in "docker top" client command
   166  	preContainedPidFlag := false
   167  	for _, line := range lines[1:] {
   168  		if len(line) == 0 {
   169  			continue
   170  		}
   171  		fields := fieldsASCII(line)
   172  
   173  		var (
   174  			p   int
   175  			err error
   176  		)
   177  
   178  		if fields[pidIndex] == "-" {
   179  			if preContainedPidFlag {
   180  				appendProcess2ProcList(procList, fields)
   181  			}
   182  			continue
   183  		}
   184  		p, err = strconv.Atoi(fields[pidIndex])
   185  		if err != nil {
   186  			return nil, fmt.Errorf("unexpected pid '%s': %s", fields[pidIndex], err)
   187  		}
   188  
   189  		if hasPid(procs, p) {
   190  			preContainedPidFlag = true
   191  			appendProcess2ProcList(procList, fields)
   192  			continue
   193  		}
   194  		preContainedPidFlag = false
   195  	}
   196  	return procList, nil
   197  }