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