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