github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/compose_ps.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  package main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"text/tabwriter"
    24  	"time"
    25  
    26  	"github.com/containerd/containerd"
    27  	"github.com/containerd/containerd/runtime/restart"
    28  	"github.com/containerd/errdefs"
    29  	gocni "github.com/containerd/go-cni"
    30  	"github.com/containerd/log"
    31  	"github.com/containerd/nerdctl/pkg/clientutil"
    32  	"github.com/containerd/nerdctl/pkg/cmd/compose"
    33  	"github.com/containerd/nerdctl/pkg/containerutil"
    34  	"github.com/containerd/nerdctl/pkg/formatter"
    35  	"github.com/containerd/nerdctl/pkg/labels"
    36  	"github.com/containerd/nerdctl/pkg/portutil"
    37  	"github.com/spf13/cobra"
    38  	"golang.org/x/sync/errgroup"
    39  )
    40  
    41  func newComposePsCommand() *cobra.Command {
    42  	var composePsCommand = &cobra.Command{
    43  		Use:           "ps [flags] [SERVICE...]",
    44  		Short:         "List containers of services",
    45  		RunE:          composePsAction,
    46  		SilenceUsage:  true,
    47  		SilenceErrors: true,
    48  	}
    49  	composePsCommand.Flags().String("format", "table", "Format the output. Supported values: [table|json]")
    50  	composePsCommand.Flags().String("filter", "", "Filter matches containers based on given conditions")
    51  	composePsCommand.Flags().StringArray("status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
    52  	composePsCommand.Flags().BoolP("quiet", "q", false, "Only display container IDs")
    53  	composePsCommand.Flags().Bool("services", false, "Display services")
    54  	composePsCommand.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
    55  	return composePsCommand
    56  }
    57  
    58  type composeContainerPrintable struct {
    59  	ID       string
    60  	Name     string
    61  	Image    string
    62  	Command  string
    63  	Project  string
    64  	Service  string
    65  	State    string
    66  	Health   string // placeholder, lack containerd support.
    67  	ExitCode uint32
    68  	// `Publishers` stores docker-compatible ports and used for json output.
    69  	// `Ports` stores formatted ports and only used for console output.
    70  	Publishers []PortPublisher
    71  	Ports      string `json:"-"`
    72  }
    73  
    74  func composePsAction(cmd *cobra.Command, args []string) error {
    75  	globalOptions, err := processRootCmdFlags(cmd)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	format, err := cmd.Flags().GetString("format")
    80  	if err != nil {
    81  		return err
    82  	}
    83  	if format != "json" && format != "table" {
    84  		return fmt.Errorf("unsupported format %s, supported formats are: [table|json]", format)
    85  	}
    86  	status, err := cmd.Flags().GetStringArray("status")
    87  	if err != nil {
    88  		return err
    89  	}
    90  	quiet, err := cmd.Flags().GetBool("quiet")
    91  	if err != nil {
    92  		return err
    93  	}
    94  	displayServices, err := cmd.Flags().GetBool("services")
    95  	if err != nil {
    96  		return err
    97  	}
    98  	filter, err := cmd.Flags().GetString("filter")
    99  	if err != nil {
   100  		return err
   101  	}
   102  	if filter != "" {
   103  		splited := strings.SplitN(filter, "=", 2)
   104  		if len(splited) != 2 {
   105  			return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", filter)
   106  		}
   107  		// currently only the 'status' filter is supported
   108  		if splited[0] != "status" {
   109  			return fmt.Errorf("invalid filter '%s'", splited[0])
   110  		}
   111  		status = append(status, splited[1])
   112  	}
   113  
   114  	all, err := cmd.Flags().GetBool("all")
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	defer cancel()
   124  	options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr())
   129  	if err != nil {
   130  		return err
   131  	}
   132  	serviceNames, err := c.ServiceNames(args...)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	containers, err := c.Containers(ctx, serviceNames...)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	if !all {
   142  		var upContainers []containerd.Container
   143  		for _, container := range containers {
   144  			// cStatus := formatter.ContainerStatus(ctx, c)
   145  			cStatus, err := containerutil.ContainerStatus(ctx, container)
   146  			if err != nil {
   147  				continue
   148  			}
   149  			if cStatus.Status == containerd.Running {
   150  				upContainers = append(upContainers, container)
   151  			}
   152  		}
   153  		containers = upContainers
   154  	}
   155  
   156  	if len(status) != 0 {
   157  		var filterdContainers []containerd.Container
   158  		for _, container := range containers {
   159  			cStatus := statusForFilter(ctx, container)
   160  			for _, s := range status {
   161  				if cStatus == s {
   162  					filterdContainers = append(filterdContainers, container)
   163  				}
   164  			}
   165  		}
   166  		containers = filterdContainers
   167  	}
   168  
   169  	if quiet {
   170  		for _, c := range containers {
   171  			fmt.Fprintln(cmd.OutOrStdout(), c.ID())
   172  		}
   173  		return nil
   174  	}
   175  
   176  	containersPrintable := make([]composeContainerPrintable, len(containers))
   177  	eg, ctx := errgroup.WithContext(ctx)
   178  	for i, container := range containers {
   179  		i, container := i, container
   180  		eg.Go(func() error {
   181  			var p composeContainerPrintable
   182  			var err error
   183  			if format == "json" {
   184  				p, err = composeContainerPrintableJSON(ctx, container)
   185  			} else {
   186  				p, err = composeContainerPrintableTab(ctx, container)
   187  			}
   188  			if err != nil {
   189  				return err
   190  			}
   191  			containersPrintable[i] = p
   192  			return nil
   193  		})
   194  	}
   195  
   196  	if err := eg.Wait(); err != nil {
   197  		return err
   198  	}
   199  
   200  	if displayServices {
   201  		for _, p := range containersPrintable {
   202  			fmt.Fprintln(cmd.OutOrStdout(), p.Service)
   203  		}
   204  		return nil
   205  	}
   206  	if format == "json" {
   207  		outJSON, err := formatter.ToJSON(containersPrintable, "", "")
   208  		if err != nil {
   209  			return err
   210  		}
   211  		_, err = fmt.Fprint(cmd.OutOrStdout(), outJSON)
   212  		return err
   213  	}
   214  
   215  	w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)
   216  	fmt.Fprintln(w, "NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS")
   217  	for _, p := range containersPrintable {
   218  		if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
   219  			p.Name,
   220  			p.Image,
   221  			p.Command,
   222  			p.Service,
   223  			p.State,
   224  			p.Ports,
   225  		); err != nil {
   226  			return err
   227  		}
   228  	}
   229  
   230  	return w.Flush()
   231  }
   232  
   233  // composeContainerPrintableTab constructs composeContainerPrintable with fields
   234  // only for console output.
   235  func composeContainerPrintableTab(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) {
   236  	info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
   237  	if err != nil {
   238  		return composeContainerPrintable{}, err
   239  	}
   240  	spec, err := container.Spec(ctx)
   241  	if err != nil {
   242  		return composeContainerPrintable{}, err
   243  	}
   244  	status := formatter.ContainerStatus(ctx, container)
   245  	if status == "Up" {
   246  		status = "running" // corresponds to Docker Compose v2.0.1
   247  	}
   248  	image, err := container.Image(ctx)
   249  	if err != nil {
   250  		return composeContainerPrintable{}, err
   251  	}
   252  
   253  	return composeContainerPrintable{
   254  		Name:    info.Labels[labels.Name],
   255  		Image:   image.Metadata().Name,
   256  		Command: formatter.InspectContainerCommandTrunc(spec),
   257  		Service: info.Labels[labels.ComposeService],
   258  		State:   status,
   259  		Ports:   formatter.FormatPorts(info.Labels),
   260  	}, nil
   261  }
   262  
   263  // composeContainerPrintableTab constructs composeContainerPrintable with fields
   264  // only for json output and compatible docker output.
   265  func composeContainerPrintableJSON(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) {
   266  	info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
   267  	if err != nil {
   268  		return composeContainerPrintable{}, err
   269  	}
   270  	spec, err := container.Spec(ctx)
   271  	if err != nil {
   272  		return composeContainerPrintable{}, err
   273  	}
   274  
   275  	var (
   276  		state    string
   277  		exitCode uint32
   278  	)
   279  	status, err := containerutil.ContainerStatus(ctx, container)
   280  	if err == nil {
   281  		// show exitCode only when container is exited/stopped
   282  		if status.Status == containerd.Stopped {
   283  			state = "exited"
   284  			exitCode = status.ExitStatus
   285  		} else {
   286  			state = string(status.Status)
   287  		}
   288  	} else {
   289  		state = string(containerd.Unknown)
   290  	}
   291  	image, err := container.Image(ctx)
   292  	if err != nil {
   293  		return composeContainerPrintable{}, err
   294  	}
   295  
   296  	return composeContainerPrintable{
   297  		ID:         container.ID(),
   298  		Name:       info.Labels[labels.Name],
   299  		Image:      image.Metadata().Name,
   300  		Command:    formatter.InspectContainerCommand(spec, false, false),
   301  		Project:    info.Labels[labels.ComposeProject],
   302  		Service:    info.Labels[labels.ComposeService],
   303  		State:      state,
   304  		Health:     "",
   305  		ExitCode:   exitCode,
   306  		Publishers: formatPublishers(info.Labels),
   307  	}, nil
   308  }
   309  
   310  // PortPublisher hold status about published port
   311  // Use this to match the json output with docker compose
   312  // FYI: https://github.com/docker/compose/blob/v2.13.0/pkg/api/api.go#L305C27-L311
   313  type PortPublisher struct {
   314  	URL           string
   315  	TargetPort    int
   316  	PublishedPort int
   317  	Protocol      string
   318  }
   319  
   320  // formatPublishers parses and returns docker-compatible []PortPublisher from
   321  // label map. If an error happens, an empty slice is returned.
   322  func formatPublishers(labelMap map[string]string) []PortPublisher {
   323  	mapper := func(pm gocni.PortMapping) PortPublisher {
   324  		return PortPublisher{
   325  			URL:           pm.HostIP,
   326  			TargetPort:    int(pm.ContainerPort),
   327  			PublishedPort: int(pm.HostPort),
   328  			Protocol:      pm.Protocol,
   329  		}
   330  	}
   331  
   332  	var dockerPorts []PortPublisher
   333  	if portMappings, err := portutil.ParsePortsLabel(labelMap); err == nil {
   334  		for _, p := range portMappings {
   335  			dockerPorts = append(dockerPorts, mapper(p))
   336  		}
   337  	} else {
   338  		log.L.Error(err.Error())
   339  	}
   340  	return dockerPorts
   341  }
   342  
   343  // statusForFilter returns the status value to be matched with the 'status' filter
   344  func statusForFilter(ctx context.Context, c containerd.Container) string {
   345  	// Just in case, there is something wrong in server.
   346  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
   347  	defer cancel()
   348  
   349  	task, err := c.Task(ctx, nil)
   350  	if err != nil {
   351  		// NOTE: NotFound doesn't mean that container hasn't started.
   352  		// In docker/CRI-containerd plugin, the task will be deleted
   353  		// when it exits. So, the status will be "created" for this
   354  		// case.
   355  		if errdefs.IsNotFound(err) {
   356  			return string(containerd.Created)
   357  		}
   358  		return string(containerd.Unknown)
   359  	}
   360  
   361  	status, err := task.Status(ctx)
   362  	if err != nil {
   363  		return string(containerd.Unknown)
   364  	}
   365  	labels, err := c.Labels(ctx)
   366  	if err != nil {
   367  		return string(containerd.Unknown)
   368  	}
   369  
   370  	switch s := status.Status; s {
   371  	case containerd.Stopped:
   372  		if labels[restart.StatusLabel] == string(containerd.Running) && restart.Reconcile(status, labels) {
   373  			return "restarting"
   374  		}
   375  		return "exited"
   376  	default:
   377  		return string(s)
   378  	}
   379  }