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

     1  package service
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/docker/cli/internal/test"
    11  	"github.com/docker/cli/internal/test/builders"
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/docker/docker/api/types/swarm"
    14  	"github.com/docker/docker/api/types/versions"
    15  	"gotest.tools/v3/assert"
    16  	is "gotest.tools/v3/assert/cmp"
    17  	"gotest.tools/v3/golden"
    18  )
    19  
    20  func TestServiceListOrder(t *testing.T) {
    21  	cli := test.NewFakeCli(&fakeClient{
    22  		serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
    23  			return []swarm.Service{
    24  				newService("a57dbe8", "service-1-foo"),
    25  				newService("a57dbdd", "service-10-foo"),
    26  				newService("aaaaaaa", "service-2-foo"),
    27  			}, nil
    28  		},
    29  	})
    30  	cmd := newListCommand(cli)
    31  	cmd.SetArgs([]string{})
    32  	assert.Check(t, cmd.Flags().Set("format", "{{.Name}}"))
    33  	assert.NilError(t, cmd.Execute())
    34  	golden.Assert(t, cli.OutBuffer().String(), "service-list-sort.golden")
    35  }
    36  
    37  // TestServiceListServiceStatus tests that the ServiceStatus struct is correctly
    38  // propagated. For older API versions, the ServiceStatus is calculated locally,
    39  // based on the tasks that are present in the swarm, and the nodes that they are
    40  // running on.
    41  // If the list command is ran with `--quiet` option, no attempt should be done to
    42  // propagate the ServiceStatus struct if not present, and it should be set to an
    43  // empty struct.
    44  func TestServiceListServiceStatus(t *testing.T) {
    45  	type listResponse struct {
    46  		ID       string
    47  		Replicas string
    48  	}
    49  
    50  	type testCase struct {
    51  		doc       string
    52  		withQuiet bool
    53  		opts      clusterOpts
    54  		cluster   *cluster
    55  		expected  []listResponse
    56  	}
    57  
    58  	tests := []testCase{
    59  		{
    60  			// Getting no nodes, services or tasks back from the daemon should
    61  			// not cause any problems
    62  			doc:      "empty cluster",
    63  			cluster:  &cluster{}, // force an empty cluster
    64  			expected: []listResponse{},
    65  		},
    66  		{
    67  			// Services are running, but no active nodes were found. On API v1.40
    68  			// and below, this will cause looking up the "running" tasks to fail,
    69  			// as well as looking up "desired" tasks for global services.
    70  			doc: "API v1.40 no active nodes",
    71  			opts: clusterOpts{
    72  				apiVersion:   "1.40",
    73  				activeNodes:  0,
    74  				runningTasks: 2,
    75  				desiredTasks: 4,
    76  			},
    77  			expected: []listResponse{
    78  				{ID: "replicated", Replicas: "0/4"},
    79  				{ID: "global", Replicas: "0/0"},
    80  				{ID: "none-id", Replicas: "0/0"},
    81  			},
    82  		},
    83  		{
    84  			doc: "API v1.40 3 active nodes, 1 task running",
    85  			opts: clusterOpts{
    86  				apiVersion:   "1.40",
    87  				activeNodes:  3,
    88  				runningTasks: 1,
    89  				desiredTasks: 2,
    90  			},
    91  			expected: []listResponse{
    92  				{ID: "replicated", Replicas: "1/2"},
    93  				{ID: "global", Replicas: "1/3"},
    94  				{ID: "none-id", Replicas: "0/0"},
    95  			},
    96  		},
    97  		{
    98  			doc: "API v1.40 3 active nodes, all tasks running",
    99  			opts: clusterOpts{
   100  				apiVersion:   "1.40",
   101  				activeNodes:  3,
   102  				runningTasks: 3,
   103  				desiredTasks: 3,
   104  			},
   105  			expected: []listResponse{
   106  				{ID: "replicated", Replicas: "3/3"},
   107  				{ID: "global", Replicas: "3/3"},
   108  				{ID: "none-id", Replicas: "0/0"},
   109  			},
   110  		},
   111  
   112  		{
   113  			// Services are running, but no active nodes were found. On API v1.41
   114  			// and up, the ServiceStatus is sent by the daemon, so this should not
   115  			// affect the results.
   116  			doc: "API v1.41 no active nodes",
   117  			opts: clusterOpts{
   118  				apiVersion:   "1.41",
   119  				activeNodes:  0,
   120  				runningTasks: 2,
   121  				desiredTasks: 4,
   122  			},
   123  			expected: []listResponse{
   124  				{ID: "replicated", Replicas: "2/4"},
   125  				{ID: "global", Replicas: "0/0"},
   126  				{ID: "none-id", Replicas: "0/0"},
   127  			},
   128  		},
   129  		{
   130  			doc: "API v1.41 3 active nodes, 1 task running",
   131  			opts: clusterOpts{
   132  				apiVersion:   "1.41",
   133  				activeNodes:  3,
   134  				runningTasks: 1,
   135  				desiredTasks: 2,
   136  			},
   137  			expected: []listResponse{
   138  				{ID: "replicated", Replicas: "1/2"},
   139  				{ID: "global", Replicas: "1/3"},
   140  				{ID: "none-id", Replicas: "0/0"},
   141  			},
   142  		},
   143  		{
   144  			doc: "API v1.41 3 active nodes, all tasks running",
   145  			opts: clusterOpts{
   146  				apiVersion:   "1.41",
   147  				activeNodes:  3,
   148  				runningTasks: 3,
   149  				desiredTasks: 3,
   150  			},
   151  			expected: []listResponse{
   152  				{ID: "replicated", Replicas: "3/3"},
   153  				{ID: "global", Replicas: "3/3"},
   154  				{ID: "none-id", Replicas: "0/0"},
   155  			},
   156  		},
   157  	}
   158  
   159  	matrix := make([]testCase, 0)
   160  	for _, quiet := range []bool{false, true} {
   161  		for _, tc := range tests {
   162  			if quiet {
   163  				tc.withQuiet = quiet
   164  				tc.doc += " with quiet"
   165  			}
   166  			matrix = append(matrix, tc)
   167  		}
   168  	}
   169  
   170  	for _, tc := range matrix {
   171  		tc := tc
   172  		t.Run(tc.doc, func(t *testing.T) {
   173  			if tc.cluster == nil {
   174  				tc.cluster = generateCluster(t, tc.opts)
   175  			}
   176  			cli := test.NewFakeCli(&fakeClient{
   177  				serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
   178  					if !options.Status || versions.LessThan(tc.opts.apiVersion, "1.41") {
   179  						// Don't return "ServiceStatus" if not requested, or on older API versions
   180  						for i := range tc.cluster.services {
   181  							tc.cluster.services[i].ServiceStatus = nil
   182  						}
   183  					}
   184  					return tc.cluster.services, nil
   185  				},
   186  				taskListFunc: func(context.Context, types.TaskListOptions) ([]swarm.Task, error) {
   187  					return tc.cluster.tasks, nil
   188  				},
   189  				nodeListFunc: func(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
   190  					return tc.cluster.nodes, nil
   191  				},
   192  			})
   193  			cmd := newListCommand(cli)
   194  			cmd.SetArgs([]string{})
   195  			if tc.withQuiet {
   196  				cmd.SetArgs([]string{"--quiet"})
   197  			}
   198  			_ = cmd.Flags().Set("format", "{{ json .}}")
   199  			assert.NilError(t, cmd.Execute())
   200  
   201  			lines := strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n")
   202  			jsonArr := fmt.Sprintf("[%s]", strings.Join(lines, ","))
   203  			results := make([]listResponse, 0)
   204  			assert.NilError(t, json.Unmarshal([]byte(jsonArr), &results))
   205  
   206  			if tc.withQuiet {
   207  				// With "quiet" enabled, ServiceStatus should not be propagated
   208  				for i := range tc.expected {
   209  					tc.expected[i].Replicas = "0/0"
   210  				}
   211  			}
   212  			assert.Check(t, is.DeepEqual(tc.expected, results), "%+v", results)
   213  		})
   214  	}
   215  }
   216  
   217  type clusterOpts struct {
   218  	apiVersion   string
   219  	activeNodes  uint64
   220  	desiredTasks uint64
   221  	runningTasks uint64
   222  }
   223  
   224  type cluster struct {
   225  	services []swarm.Service
   226  	tasks    []swarm.Task
   227  	nodes    []swarm.Node
   228  }
   229  
   230  func generateCluster(t *testing.T, opts clusterOpts) *cluster {
   231  	t.Helper()
   232  	c := cluster{
   233  		services: generateServices(t, opts),
   234  		nodes:    generateNodes(t, opts.activeNodes),
   235  	}
   236  	c.tasks = generateTasks(t, c.services, c.nodes, opts)
   237  	return &c
   238  }
   239  
   240  func generateServices(t *testing.T, opts clusterOpts) []swarm.Service {
   241  	t.Helper()
   242  
   243  	// Can't have more global tasks than nodes
   244  	globalTasks := opts.runningTasks
   245  	if globalTasks > opts.activeNodes {
   246  		globalTasks = opts.activeNodes
   247  	}
   248  
   249  	return []swarm.Service{
   250  		*builders.Service(
   251  			builders.ServiceID("replicated"),
   252  			builders.ServiceName("01-replicated-service"),
   253  			builders.ReplicatedService(opts.desiredTasks),
   254  			builders.ServiceStatus(opts.desiredTasks, opts.runningTasks),
   255  		),
   256  		*builders.Service(
   257  			builders.ServiceID("global"),
   258  			builders.ServiceName("02-global-service"),
   259  			builders.GlobalService(),
   260  			builders.ServiceStatus(opts.activeNodes, globalTasks),
   261  		),
   262  		*builders.Service(
   263  			builders.ServiceID("none-id"),
   264  			builders.ServiceName("03-none-service"),
   265  		),
   266  	}
   267  }
   268  
   269  func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, opts clusterOpts) []swarm.Task {
   270  	t.Helper()
   271  	tasks := make([]swarm.Task, 0)
   272  
   273  	for _, s := range services {
   274  		if s.Spec.Mode.Replicated == nil && s.Spec.Mode.Global == nil {
   275  			continue
   276  		}
   277  		var runningTasks, failedTasks, desiredTasks uint64
   278  
   279  		// Set the number of desired tasks to generate, based on the service's mode
   280  		if s.Spec.Mode.Replicated != nil {
   281  			desiredTasks = *s.Spec.Mode.Replicated.Replicas
   282  		} else if s.Spec.Mode.Global != nil {
   283  			desiredTasks = opts.activeNodes
   284  		}
   285  
   286  		for _, n := range nodes {
   287  			if runningTasks < opts.runningTasks && n.Status.State != swarm.NodeStateDown {
   288  				tasks = append(tasks, swarm.Task{
   289  					NodeID:       n.ID,
   290  					ServiceID:    s.ID,
   291  					Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
   292  					DesiredState: swarm.TaskStateRunning,
   293  				})
   294  				runningTasks++
   295  			}
   296  
   297  			// If the number of "running" tasks is lower than the desired number
   298  			// of tasks of the service, fill in the remaining number of tasks
   299  			// with failed tasks. These tasks have a desired "running" state,
   300  			// and thus will be included when calculating the "desired" tasks
   301  			// for services.
   302  			if failedTasks < (desiredTasks - opts.runningTasks) {
   303  				tasks = append(tasks, swarm.Task{
   304  					NodeID:       n.ID,
   305  					ServiceID:    s.ID,
   306  					Status:       swarm.TaskStatus{State: swarm.TaskStateFailed},
   307  					DesiredState: swarm.TaskStateRunning,
   308  				})
   309  				failedTasks++
   310  			}
   311  
   312  			// Also add tasks with DesiredState: Shutdown. These should not be
   313  			// counted as running or desired tasks.
   314  			tasks = append(tasks, swarm.Task{
   315  				NodeID:       n.ID,
   316  				ServiceID:    s.ID,
   317  				Status:       swarm.TaskStatus{State: swarm.TaskStateShutdown},
   318  				DesiredState: swarm.TaskStateShutdown,
   319  			})
   320  		}
   321  	}
   322  	return tasks
   323  }
   324  
   325  // generateNodes generates a "nodes" endpoint API response with the requested
   326  // number of "ready" nodes. In addition, a "down" node is generated.
   327  func generateNodes(t *testing.T, activeNodes uint64) []swarm.Node {
   328  	t.Helper()
   329  	nodes := make([]swarm.Node, 0)
   330  	var i uint64
   331  	for i = 0; i < activeNodes; i++ {
   332  		nodes = append(nodes, swarm.Node{
   333  			ID:     fmt.Sprintf("node-ready-%d", i),
   334  			Status: swarm.NodeStatus{State: swarm.NodeStateReady},
   335  		})
   336  		nodes = append(nodes, swarm.Node{
   337  			ID:     fmt.Sprintf("node-down-%d", i),
   338  			Status: swarm.NodeStatus{State: swarm.NodeStateDown},
   339  		})
   340  	}
   341  	return nodes
   342  }