github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/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(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(dockerCli command.Cli, opts listOptions) error {
    48  	var (
    49  		apiClient = dockerCli.Client()
    50  		ctx       = context.Background()
    51  		err       error
    52  	)
    53  
    54  	listOpts := types.ServiceListOptions{
    55  		Filters: opts.filter.Value(),
    56  		// When not running "quiet", also get service status (number of running
    57  		// and desired tasks). Note that this is only supported on API v1.41 and
    58  		// up; older API versions ignore this option, and we will have to collect
    59  		// the information manually below.
    60  		Status: !opts.quiet,
    61  	}
    62  
    63  	services, err := apiClient.ServiceList(ctx, listOpts)
    64  	if err != nil {
    65  		return err
    66  	}
    67  
    68  	if listOpts.Status {
    69  		// Now that a request was made, we know what API version was used (either
    70  		// through configuration, or after client and daemon negotiated a version).
    71  		// If API version v1.41 or up was used; the daemon should already have done
    72  		// the legwork for us, and we don't have to calculate the number of desired
    73  		// and running tasks. On older API versions, we need to do some extra requests
    74  		// to get that information.
    75  		//
    76  		// So theoretically, this step can be skipped based on API version, however,
    77  		// some of our unit tests don't set the API version, and there may be other
    78  		// situations where the client uses the "default" version. To account for
    79  		// these situations, we do a quick check for services that do not have
    80  		// a ServiceStatus set, and perform a lookup for those.
    81  		services, err = AppendServiceStatus(ctx, apiClient, services)
    82  		if err != nil {
    83  			return err
    84  		}
    85  	}
    86  
    87  	format := opts.format
    88  	if len(format) == 0 {
    89  		if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
    90  			format = dockerCli.ConfigFile().ServicesFormat
    91  		} else {
    92  			format = formatter.TableFormatKey
    93  		}
    94  	}
    95  
    96  	servicesCtx := formatter.Context{
    97  		Output: dockerCli.Out(),
    98  		Format: NewListFormat(format, opts.quiet),
    99  	}
   100  	return ListFormatWrite(servicesCtx, services)
   101  }
   102  
   103  // AppendServiceStatus propagates the ServiceStatus field for "services".
   104  //
   105  // If API version v1.41 or up is used, this information is already set by the
   106  // daemon. On older API versions, we need to do some extra requests to get
   107  // that information. Theoretically, this function can be skipped based on API
   108  // version, however, some of our unit tests don't set the API version, and
   109  // there may be other situations where the client uses the "default" version.
   110  // To take these situations into account, we do a quick check for services
   111  // that don't have ServiceStatus set, and perform a lookup for those.
   112  func AppendServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) {
   113  	status := map[string]*swarm.ServiceStatus{}
   114  	taskFilter := filters.NewArgs()
   115  	for i, s := range services {
   116  		// there is no need in this switch to check for job modes. jobs are not
   117  		// supported until after ServiceStatus was introduced.
   118  		switch {
   119  		case s.ServiceStatus != nil:
   120  			// Server already returned service-status, so we don't
   121  			// have to look-up tasks for this service.
   122  			continue
   123  		case s.Spec.Mode.Replicated != nil:
   124  			// For replicated services, set the desired number of tasks;
   125  			// that way we can present this information in case we're unable
   126  			// to get a list of tasks from the server.
   127  			services[i].ServiceStatus = &swarm.ServiceStatus{DesiredTasks: *s.Spec.Mode.Replicated.Replicas}
   128  			status[s.ID] = &swarm.ServiceStatus{}
   129  			taskFilter.Add("service", s.ID)
   130  		case s.Spec.Mode.Global != nil:
   131  			// No such thing as number of desired tasks for global services
   132  			services[i].ServiceStatus = &swarm.ServiceStatus{}
   133  			status[s.ID] = &swarm.ServiceStatus{}
   134  			taskFilter.Add("service", s.ID)
   135  		default:
   136  			// Unknown task type
   137  		}
   138  	}
   139  	if len(status) == 0 {
   140  		// All services have their ServiceStatus set, so we're done
   141  		return services, nil
   142  	}
   143  
   144  	tasks, err := c.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	if len(tasks) == 0 {
   149  		return services, nil
   150  	}
   151  	activeNodes, err := getActiveNodes(ctx, c)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	for _, task := range tasks {
   157  		if status[task.ServiceID] == nil {
   158  			// This should not happen in practice; either all services have
   159  			// a ServiceStatus set, or none of them.
   160  			continue
   161  		}
   162  		// TODO: this should only be needed for "global" services. Replicated
   163  		// services have `Spec.Mode.Replicated.Replicas`, which should give this value.
   164  		if task.DesiredState != swarm.TaskStateShutdown {
   165  			status[task.ServiceID].DesiredTasks++
   166  		}
   167  		if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
   168  			status[task.ServiceID].RunningTasks++
   169  		}
   170  	}
   171  
   172  	for i, service := range services {
   173  		if s := status[service.ID]; s != nil {
   174  			services[i].ServiceStatus = s
   175  		}
   176  	}
   177  	return services, nil
   178  }
   179  
   180  func getActiveNodes(ctx context.Context, c client.NodeAPIClient) (map[string]struct{}, error) {
   181  	nodes, err := c.NodeList(ctx, types.NodeListOptions{})
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	activeNodes := make(map[string]struct{})
   186  	for _, n := range nodes {
   187  		if n.Status.State != swarm.NodeStateDown {
   188  			activeNodes[n.ID] = struct{}{}
   189  		}
   190  	}
   191  	return activeNodes, nil
   192  }