github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/service/list.go (about)

     1  package service
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/docker/cli/cli"
     7  	"github.com/docker/cli/cli/command"
     8  	"github.com/docker/cli/cli/command/completion"
     9  	"github.com/docker/cli/cli/command/formatter"
    10  	flagsHelper "github.com/docker/cli/cli/flags"
    11  	"github.com/docker/cli/opts"
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/docker/docker/api/types/filters"
    14  	"github.com/docker/docker/api/types/swarm"
    15  	"github.com/docker/docker/client"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  type listOptions struct {
    20  	quiet  bool
    21  	format string
    22  	filter opts.FilterOpt
    23  }
    24  
    25  func newListCommand(dockerCLI command.Cli) *cobra.Command {
    26  	options := listOptions{filter: opts.NewFilterOpt()}
    27  
    28  	cmd := &cobra.Command{
    29  		Use:     "ls [OPTIONS]",
    30  		Aliases: []string{"list"},
    31  		Short:   "List services",
    32  		Args:    cli.NoArgs,
    33  		RunE: func(cmd *cobra.Command, args []string) error {
    34  			return runList(cmd.Context(), dockerCLI, options)
    35  		},
    36  		ValidArgsFunction: completion.NoComplete,
    37  	}
    38  
    39  	flags := cmd.Flags()
    40  	flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs")
    41  	flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
    42  	flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
    43  
    44  	return cmd
    45  }
    46  
    47  func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
    48  	var (
    49  		apiClient = dockerCLI.Client()
    50  		err       error
    51  	)
    52  
    53  	listOpts := types.ServiceListOptions{
    54  		Filters: options.filter.Value(),
    55  		// When not running "quiet", also get service status (number of running
    56  		// and desired tasks). Note that this is only supported on API v1.41 and
    57  		// up; older API versions ignore this option, and we will have to collect
    58  		// the information manually below.
    59  		Status: !options.quiet,
    60  	}
    61  
    62  	services, err := apiClient.ServiceList(ctx, listOpts)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	if listOpts.Status {
    68  		// Now that a request was made, we know what API version was used (either
    69  		// through configuration, or after client and daemon negotiated a version).
    70  		// If API version v1.41 or up was used; the daemon should already have done
    71  		// the legwork for us, and we don't have to calculate the number of desired
    72  		// and running tasks. On older API versions, we need to do some extra requests
    73  		// to get that information.
    74  		//
    75  		// So theoretically, this step can be skipped based on API version, however,
    76  		// some of our unit tests don't set the API version, and there may be other
    77  		// situations where the client uses the "default" version. To account for
    78  		// these situations, we do a quick check for services that do not have
    79  		// a ServiceStatus set, and perform a lookup for those.
    80  		services, err = AppendServiceStatus(ctx, apiClient, services)
    81  		if err != nil {
    82  			return err
    83  		}
    84  	}
    85  
    86  	format := options.format
    87  	if len(format) == 0 {
    88  		if len(dockerCLI.ConfigFile().ServicesFormat) > 0 && !options.quiet {
    89  			format = dockerCLI.ConfigFile().ServicesFormat
    90  		} else {
    91  			format = formatter.TableFormatKey
    92  		}
    93  	}
    94  
    95  	servicesCtx := formatter.Context{
    96  		Output: dockerCLI.Out(),
    97  		Format: NewListFormat(format, options.quiet),
    98  	}
    99  	return ListFormatWrite(servicesCtx, services)
   100  }
   101  
   102  // AppendServiceStatus propagates the ServiceStatus field for "services".
   103  //
   104  // If API version v1.41 or up is used, this information is already set by the
   105  // daemon. On older API versions, we need to do some extra requests to get
   106  // that information. Theoretically, this function can be skipped based on API
   107  // version, however, some of our unit tests don't set the API version, and
   108  // there may be other situations where the client uses the "default" version.
   109  // To take these situations into account, we do a quick check for services
   110  // that don't have ServiceStatus set, and perform a lookup for those.
   111  func AppendServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) {
   112  	status := map[string]*swarm.ServiceStatus{}
   113  	taskFilter := filters.NewArgs()
   114  	for i, s := range services {
   115  		// there is no need in this switch to check for job modes. jobs are not
   116  		// supported until after ServiceStatus was introduced.
   117  		switch {
   118  		case s.ServiceStatus != nil:
   119  			// Server already returned service-status, so we don't
   120  			// have to look-up tasks for this service.
   121  			continue
   122  		case s.Spec.Mode.Replicated != nil:
   123  			// For replicated services, set the desired number of tasks;
   124  			// that way we can present this information in case we're unable
   125  			// to get a list of tasks from the server.
   126  			services[i].ServiceStatus = &swarm.ServiceStatus{DesiredTasks: *s.Spec.Mode.Replicated.Replicas}
   127  			status[s.ID] = &swarm.ServiceStatus{}
   128  			taskFilter.Add("service", s.ID)
   129  		case s.Spec.Mode.Global != nil:
   130  			// No such thing as number of desired tasks for global services
   131  			services[i].ServiceStatus = &swarm.ServiceStatus{}
   132  			status[s.ID] = &swarm.ServiceStatus{}
   133  			taskFilter.Add("service", s.ID)
   134  		default:
   135  			// Unknown task type
   136  		}
   137  	}
   138  	if len(status) == 0 {
   139  		// All services have their ServiceStatus set, so we're done
   140  		return services, nil
   141  	}
   142  
   143  	tasks, err := c.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	if len(tasks) == 0 {
   148  		return services, nil
   149  	}
   150  	activeNodes, err := getActiveNodes(ctx, c)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	for _, task := range tasks {
   156  		if status[task.ServiceID] == nil {
   157  			// This should not happen in practice; either all services have
   158  			// a ServiceStatus set, or none of them.
   159  			continue
   160  		}
   161  		// TODO: this should only be needed for "global" services. Replicated
   162  		// services have `Spec.Mode.Replicated.Replicas`, which should give this value.
   163  		if task.DesiredState != swarm.TaskStateShutdown {
   164  			status[task.ServiceID].DesiredTasks++
   165  		}
   166  		if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
   167  			status[task.ServiceID].RunningTasks++
   168  		}
   169  	}
   170  
   171  	for i, service := range services {
   172  		if s := status[service.ID]; s != nil {
   173  			services[i].ServiceStatus = s
   174  		}
   175  	}
   176  	return services, nil
   177  }
   178  
   179  func getActiveNodes(ctx context.Context, c client.NodeAPIClient) (map[string]struct{}, error) {
   180  	nodes, err := c.NodeList(ctx, types.NodeListOptions{})
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	activeNodes := make(map[string]struct{})
   185  	for _, n := range nodes {
   186  		if n.Status.State != swarm.NodeStateDown {
   187  			activeNodes[n.ID] = struct{}{}
   188  		}
   189  	}
   190  	return activeNodes, nil
   191  }