github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/compose/convert/service_test.go (about)

     1  package convert
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	composetypes "github.com/docker/cli/cli/compose/types"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	"github.com/docker/docker/api/types/swarm"
    14  	"github.com/docker/docker/client"
    15  	"github.com/pkg/errors"
    16  	"gotest.tools/v3/assert"
    17  	is "gotest.tools/v3/assert/cmp"
    18  )
    19  
    20  func TestConvertRestartPolicyFromNone(t *testing.T) {
    21  	policy, err := convertRestartPolicy("no", nil)
    22  	assert.NilError(t, err)
    23  	assert.Check(t, is.DeepEqual((*swarm.RestartPolicy)(nil), policy))
    24  }
    25  
    26  func TestConvertRestartPolicyFromUnknown(t *testing.T) {
    27  	_, err := convertRestartPolicy("unknown", nil)
    28  	assert.Error(t, err, "unknown restart policy: unknown")
    29  }
    30  
    31  func TestConvertRestartPolicyFromAlways(t *testing.T) {
    32  	policy, err := convertRestartPolicy("always", nil)
    33  	expected := &swarm.RestartPolicy{
    34  		Condition: swarm.RestartPolicyConditionAny,
    35  	}
    36  	assert.NilError(t, err)
    37  	assert.Check(t, is.DeepEqual(expected, policy))
    38  }
    39  
    40  func TestConvertRestartPolicyFromFailure(t *testing.T) {
    41  	policy, err := convertRestartPolicy("on-failure:4", nil)
    42  	attempts := uint64(4)
    43  	expected := &swarm.RestartPolicy{
    44  		Condition:   swarm.RestartPolicyConditionOnFailure,
    45  		MaxAttempts: &attempts,
    46  	}
    47  	assert.NilError(t, err)
    48  	assert.Check(t, is.DeepEqual(expected, policy))
    49  }
    50  
    51  func strPtr(val string) *string {
    52  	return &val
    53  }
    54  
    55  func TestConvertEnvironment(t *testing.T) {
    56  	source := map[string]*string{
    57  		"foo": strPtr("bar"),
    58  		"key": strPtr("value"),
    59  	}
    60  	env := convertEnvironment(source)
    61  	assert.Check(t, is.DeepEqual([]string{"foo=bar", "key=value"}, env))
    62  }
    63  
    64  func TestConvertExtraHosts(t *testing.T) {
    65  	source := composetypes.HostsList{
    66  		"zulu:127.0.0.2",
    67  		"alpha:127.0.0.1",
    68  		"zulu:ff02::1",
    69  	}
    70  	assert.Check(t, is.DeepEqual([]string{"127.0.0.2 zulu", "127.0.0.1 alpha", "ff02::1 zulu"}, convertExtraHosts(source)))
    71  }
    72  
    73  func TestConvertResourcesFull(t *testing.T) {
    74  	source := composetypes.Resources{
    75  		Limits: &composetypes.ResourceLimit{
    76  			NanoCPUs:    "0.003",
    77  			MemoryBytes: composetypes.UnitBytes(300000000),
    78  		},
    79  		Reservations: &composetypes.Resource{
    80  			NanoCPUs:    "0.002",
    81  			MemoryBytes: composetypes.UnitBytes(200000000),
    82  		},
    83  	}
    84  	resources, err := convertResources(source)
    85  	assert.NilError(t, err)
    86  
    87  	expected := &swarm.ResourceRequirements{
    88  		Limits: &swarm.Limit{
    89  			NanoCPUs:    3000000,
    90  			MemoryBytes: 300000000,
    91  		},
    92  		Reservations: &swarm.Resources{
    93  			NanoCPUs:    2000000,
    94  			MemoryBytes: 200000000,
    95  		},
    96  	}
    97  	assert.Check(t, is.DeepEqual(expected, resources))
    98  }
    99  
   100  func TestConvertResourcesOnlyMemory(t *testing.T) {
   101  	source := composetypes.Resources{
   102  		Limits: &composetypes.ResourceLimit{
   103  			MemoryBytes: composetypes.UnitBytes(300000000),
   104  		},
   105  		Reservations: &composetypes.Resource{
   106  			MemoryBytes: composetypes.UnitBytes(200000000),
   107  		},
   108  	}
   109  	resources, err := convertResources(source)
   110  	assert.NilError(t, err)
   111  
   112  	expected := &swarm.ResourceRequirements{
   113  		Limits: &swarm.Limit{
   114  			MemoryBytes: 300000000,
   115  		},
   116  		Reservations: &swarm.Resources{
   117  			MemoryBytes: 200000000,
   118  		},
   119  	}
   120  	assert.Check(t, is.DeepEqual(expected, resources))
   121  }
   122  
   123  func TestConvertHealthcheck(t *testing.T) {
   124  	retries := uint64(10)
   125  	timeout := composetypes.Duration(30 * time.Second)
   126  	interval := composetypes.Duration(2 * time.Millisecond)
   127  	startPeriod := composetypes.Duration(time.Minute)
   128  	startInterval := composetypes.Duration(1 * time.Second)
   129  
   130  	source := &composetypes.HealthCheckConfig{
   131  		Test:          []string{"EXEC", "touch", "/foo"},
   132  		Timeout:       &timeout,
   133  		Interval:      &interval,
   134  		Retries:       &retries,
   135  		StartPeriod:   &startPeriod,
   136  		StartInterval: &startInterval,
   137  	}
   138  	expected := &container.HealthConfig{
   139  		Test:          source.Test,
   140  		Timeout:       time.Duration(timeout),
   141  		Interval:      time.Duration(interval),
   142  		StartPeriod:   time.Duration(startPeriod),
   143  		StartInterval: time.Duration(startInterval),
   144  		Retries:       10,
   145  	}
   146  
   147  	healthcheck, err := convertHealthcheck(source)
   148  	assert.NilError(t, err)
   149  	assert.Check(t, is.DeepEqual(expected, healthcheck))
   150  }
   151  
   152  func TestConvertHealthcheckDisable(t *testing.T) {
   153  	source := &composetypes.HealthCheckConfig{Disable: true}
   154  	expected := &container.HealthConfig{
   155  		Test: []string{"NONE"},
   156  	}
   157  
   158  	healthcheck, err := convertHealthcheck(source)
   159  	assert.NilError(t, err)
   160  	assert.Check(t, is.DeepEqual(expected, healthcheck))
   161  }
   162  
   163  func TestConvertHealthcheckDisableWithTest(t *testing.T) {
   164  	source := &composetypes.HealthCheckConfig{
   165  		Disable: true,
   166  		Test:    []string{"EXEC", "touch"},
   167  	}
   168  	_, err := convertHealthcheck(source)
   169  	assert.Error(t, err, "test and disable can't be set at the same time")
   170  }
   171  
   172  func TestConvertEndpointSpec(t *testing.T) {
   173  	source := []composetypes.ServicePortConfig{
   174  		{
   175  			Protocol:  "udp",
   176  			Target:    53,
   177  			Published: 1053,
   178  			Mode:      "host",
   179  		},
   180  		{
   181  			Target:    8080,
   182  			Published: 80,
   183  		},
   184  	}
   185  	endpoint := convertEndpointSpec("vip", source)
   186  
   187  	expected := swarm.EndpointSpec{
   188  		Mode: swarm.ResolutionMode(strings.ToLower("vip")),
   189  		Ports: []swarm.PortConfig{
   190  			{
   191  				TargetPort:    8080,
   192  				PublishedPort: 80,
   193  			},
   194  			{
   195  				Protocol:      "udp",
   196  				TargetPort:    53,
   197  				PublishedPort: 1053,
   198  				PublishMode:   "host",
   199  			},
   200  		},
   201  	}
   202  
   203  	assert.Check(t, is.DeepEqual(expected, *endpoint))
   204  }
   205  
   206  func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
   207  	networkConfigs := networkMap{}
   208  
   209  	configs, err := convertServiceNetworks(
   210  		nil, networkConfigs, NewNamespace("foo"), "service")
   211  
   212  	expected := []swarm.NetworkAttachmentConfig{
   213  		{
   214  			Target:  "foo_default",
   215  			Aliases: []string{"service"},
   216  		},
   217  	}
   218  
   219  	assert.NilError(t, err)
   220  	assert.Check(t, is.DeepEqual(expected, configs))
   221  }
   222  
   223  func TestConvertServiceNetworks(t *testing.T) {
   224  	networkConfigs := networkMap{
   225  		"front": composetypes.NetworkConfig{
   226  			External: composetypes.External{External: true},
   227  			Name:     "fronttier",
   228  		},
   229  		"back": composetypes.NetworkConfig{},
   230  	}
   231  	networks := map[string]*composetypes.ServiceNetworkConfig{
   232  		"front": {
   233  			Aliases: []string{"something"},
   234  		},
   235  		"back": {
   236  			Aliases: []string{"other"},
   237  		},
   238  	}
   239  
   240  	configs, err := convertServiceNetworks(
   241  		networks, networkConfigs, NewNamespace("foo"), "service")
   242  
   243  	expected := []swarm.NetworkAttachmentConfig{
   244  		{
   245  			Target:  "foo_back",
   246  			Aliases: []string{"other", "service"},
   247  		},
   248  		{
   249  			Target:  "fronttier",
   250  			Aliases: []string{"something", "service"},
   251  		},
   252  	}
   253  
   254  	assert.NilError(t, err)
   255  	assert.Check(t, is.DeepEqual(expected, configs))
   256  }
   257  
   258  func TestConvertServiceNetworksCustomDefault(t *testing.T) {
   259  	networkConfigs := networkMap{
   260  		"default": composetypes.NetworkConfig{
   261  			External: composetypes.External{External: true},
   262  			Name:     "custom",
   263  		},
   264  	}
   265  	networks := map[string]*composetypes.ServiceNetworkConfig{}
   266  
   267  	configs, err := convertServiceNetworks(
   268  		networks, networkConfigs, NewNamespace("foo"), "service")
   269  
   270  	expected := []swarm.NetworkAttachmentConfig{
   271  		{
   272  			Target:  "custom",
   273  			Aliases: []string{"service"},
   274  		},
   275  	}
   276  
   277  	assert.NilError(t, err)
   278  	assert.Check(t, is.DeepEqual(expected, configs))
   279  }
   280  
   281  func TestConvertDNSConfigEmpty(t *testing.T) {
   282  	dnsConfig := convertDNSConfig(nil, nil)
   283  	assert.Check(t, is.DeepEqual((*swarm.DNSConfig)(nil), dnsConfig))
   284  }
   285  
   286  var (
   287  	nameservers = []string{"8.8.8.8", "9.9.9.9"}
   288  	search      = []string{"dc1.example.com", "dc2.example.com"}
   289  )
   290  
   291  func TestConvertDNSConfigAll(t *testing.T) {
   292  	dnsConfig := convertDNSConfig(nameservers, search)
   293  	assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
   294  		Nameservers: nameservers,
   295  		Search:      search,
   296  	}, dnsConfig))
   297  }
   298  
   299  func TestConvertDNSConfigNameservers(t *testing.T) {
   300  	dnsConfig := convertDNSConfig(nameservers, nil)
   301  	assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
   302  		Nameservers: nameservers,
   303  		Search:      nil,
   304  	}, dnsConfig))
   305  }
   306  
   307  func TestConvertDNSConfigSearch(t *testing.T) {
   308  	dnsConfig := convertDNSConfig(nil, search)
   309  	assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
   310  		Nameservers: nil,
   311  		Search:      search,
   312  	}, dnsConfig))
   313  }
   314  
   315  func TestConvertCredentialSpec(t *testing.T) {
   316  	tests := []struct {
   317  		name        string
   318  		in          composetypes.CredentialSpecConfig
   319  		out         *swarm.CredentialSpec
   320  		configs     []*swarm.ConfigReference
   321  		expectedErr string
   322  	}{
   323  		{
   324  			name: "empty",
   325  		},
   326  		{
   327  			name:        "config-and-file",
   328  			in:          composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json"},
   329  			expectedErr: `invalid credential spec: cannot specify both "Config" and "File"`,
   330  		},
   331  		{
   332  			name:        "config-and-registry",
   333  			in:          composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", Registry: "testing"},
   334  			expectedErr: `invalid credential spec: cannot specify both "Config" and "Registry"`,
   335  		},
   336  		{
   337  			name:        "file-and-registry",
   338  			in:          composetypes.CredentialSpecConfig{File: "somefile.json", Registry: "testing"},
   339  			expectedErr: `invalid credential spec: cannot specify both "File" and "Registry"`,
   340  		},
   341  		{
   342  			name:        "config-and-file-and-registry",
   343  			in:          composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json", Registry: "testing"},
   344  			expectedErr: `invalid credential spec: cannot specify both "Config", "File", and "Registry"`,
   345  		},
   346  		{
   347  			name:        "missing-config-reference",
   348  			in:          composetypes.CredentialSpecConfig{Config: "missing"},
   349  			expectedErr: "invalid credential spec: spec specifies config missing, but no such config can be found",
   350  			configs: []*swarm.ConfigReference{
   351  				{
   352  					ConfigName: "someName",
   353  					ConfigID:   "missing",
   354  				},
   355  			},
   356  		},
   357  		{
   358  			name: "namespaced-config",
   359  			in:   composetypes.CredentialSpecConfig{Config: "name"},
   360  			configs: []*swarm.ConfigReference{
   361  				{
   362  					ConfigName: "namespaced-config_name",
   363  					ConfigID:   "someID",
   364  				},
   365  			},
   366  			out: &swarm.CredentialSpec{Config: "someID"},
   367  		},
   368  		{
   369  			name: "config",
   370  			in:   composetypes.CredentialSpecConfig{Config: "someName"},
   371  			configs: []*swarm.ConfigReference{
   372  				{
   373  					ConfigName: "someOtherName",
   374  					ConfigID:   "someOtherID",
   375  				}, {
   376  					ConfigName: "someName",
   377  					ConfigID:   "someID",
   378  				},
   379  			},
   380  			out: &swarm.CredentialSpec{Config: "someID"},
   381  		},
   382  		{
   383  			name: "file",
   384  			in:   composetypes.CredentialSpecConfig{File: "somefile.json"},
   385  			out:  &swarm.CredentialSpec{File: "somefile.json"},
   386  		},
   387  		{
   388  			name: "registry",
   389  			in:   composetypes.CredentialSpecConfig{Registry: "testing"},
   390  			out:  &swarm.CredentialSpec{Registry: "testing"},
   391  		},
   392  	}
   393  
   394  	for _, tc := range tests {
   395  		tc := tc
   396  		t.Run(tc.name, func(t *testing.T) {
   397  			namespace := NewNamespace(tc.name)
   398  			swarmSpec, err := convertCredentialSpec(namespace, tc.in, tc.configs)
   399  
   400  			if tc.expectedErr != "" {
   401  				assert.Error(t, err, tc.expectedErr)
   402  			} else {
   403  				assert.NilError(t, err)
   404  			}
   405  			assert.DeepEqual(t, swarmSpec, tc.out)
   406  		})
   407  	}
   408  }
   409  
   410  func TestConvertUpdateConfigOrder(t *testing.T) {
   411  	// test default behavior
   412  	updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{})
   413  	assert.Check(t, is.Equal("", updateConfig.Order))
   414  
   415  	// test start-first
   416  	updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
   417  		Order: "start-first",
   418  	})
   419  	assert.Check(t, is.Equal(updateConfig.Order, "start-first"))
   420  
   421  	// test stop-first
   422  	updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
   423  		Order: "stop-first",
   424  	})
   425  	assert.Check(t, is.Equal(updateConfig.Order, "stop-first"))
   426  }
   427  
   428  func TestConvertFileObject(t *testing.T) {
   429  	namespace := NewNamespace("testing")
   430  	config := composetypes.FileReferenceConfig{
   431  		Source: "source",
   432  		Target: "target",
   433  		UID:    "user",
   434  		GID:    "group",
   435  		Mode:   uint32Ptr(0o644),
   436  	}
   437  	swarmRef, err := convertFileObject(namespace, config, lookupConfig)
   438  	assert.NilError(t, err)
   439  
   440  	expected := swarmReferenceObject{
   441  		Name: "testing_source",
   442  		File: swarmReferenceTarget{
   443  			Name: config.Target,
   444  			UID:  config.UID,
   445  			GID:  config.GID,
   446  			Mode: os.FileMode(0o644),
   447  		},
   448  	}
   449  	assert.Check(t, is.DeepEqual(expected, swarmRef))
   450  }
   451  
   452  func lookupConfig(key string) (composetypes.FileObjectConfig, error) {
   453  	if key != "source" {
   454  		return composetypes.FileObjectConfig{}, errors.New("bad key")
   455  	}
   456  	return composetypes.FileObjectConfig{}, nil
   457  }
   458  
   459  func TestConvertFileObjectDefaults(t *testing.T) {
   460  	namespace := NewNamespace("testing")
   461  	config := composetypes.FileReferenceConfig{Source: "source"}
   462  	swarmRef, err := convertFileObject(namespace, config, lookupConfig)
   463  	assert.NilError(t, err)
   464  
   465  	expected := swarmReferenceObject{
   466  		Name: "testing_source",
   467  		File: swarmReferenceTarget{
   468  			Name: config.Source,
   469  			UID:  "0",
   470  			GID:  "0",
   471  			Mode: os.FileMode(0o444),
   472  		},
   473  	}
   474  	assert.Check(t, is.DeepEqual(expected, swarmRef))
   475  }
   476  
   477  func TestServiceConvertsIsolation(t *testing.T) {
   478  	src := composetypes.ServiceConfig{
   479  		Isolation: "hyperv",
   480  	}
   481  	result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil)
   482  	assert.NilError(t, err)
   483  	assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation))
   484  }
   485  
   486  func TestConvertServiceSecrets(t *testing.T) {
   487  	namespace := Namespace{name: "foo"}
   488  	secrets := []composetypes.ServiceSecretConfig{
   489  		{Source: "foo_secret"},
   490  		{Source: "bar_secret"},
   491  	}
   492  	secretSpecs := map[string]composetypes.SecretConfig{
   493  		"foo_secret": {
   494  			Name: "foo_secret",
   495  		},
   496  		"bar_secret": {
   497  			Name: "bar_secret",
   498  		},
   499  	}
   500  	apiClient := &fakeClient{
   501  		secretListFunc: func(opts types.SecretListOptions) ([]swarm.Secret, error) {
   502  			assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret"))
   503  			assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret"))
   504  			return []swarm.Secret{
   505  				{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}},
   506  				{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}},
   507  			}, nil
   508  		},
   509  	}
   510  	ctx := context.Background()
   511  	refs, err := convertServiceSecrets(ctx, apiClient, namespace, secrets, secretSpecs)
   512  	assert.NilError(t, err)
   513  	expected := []*swarm.SecretReference{
   514  		{
   515  			SecretName: "bar_secret",
   516  			File: &swarm.SecretReferenceFileTarget{
   517  				Name: "bar_secret",
   518  				UID:  "0",
   519  				GID:  "0",
   520  				Mode: 0o444,
   521  			},
   522  		},
   523  		{
   524  			SecretName: "foo_secret",
   525  			File: &swarm.SecretReferenceFileTarget{
   526  				Name: "foo_secret",
   527  				UID:  "0",
   528  				GID:  "0",
   529  				Mode: 0o444,
   530  			},
   531  		},
   532  	}
   533  	assert.DeepEqual(t, expected, refs)
   534  }
   535  
   536  func TestConvertServiceConfigs(t *testing.T) {
   537  	namespace := Namespace{name: "foo"}
   538  	service := composetypes.ServiceConfig{
   539  		Configs: []composetypes.ServiceConfigObjConfig{
   540  			{Source: "foo_config"},
   541  			{Source: "bar_config"},
   542  		},
   543  		CredentialSpec: composetypes.CredentialSpecConfig{
   544  			Config: "baz_config",
   545  		},
   546  	}
   547  	configSpecs := map[string]composetypes.ConfigObjConfig{
   548  		"foo_config": {
   549  			Name: "foo_config",
   550  		},
   551  		"bar_config": {
   552  			Name: "bar_config",
   553  		},
   554  		"baz_config": {
   555  			Name: "baz_config",
   556  		},
   557  	}
   558  	apiClient := &fakeClient{
   559  		configListFunc: func(opts types.ConfigListOptions) ([]swarm.Config, error) {
   560  			assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config"))
   561  			assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config"))
   562  			assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config"))
   563  			return []swarm.Config{
   564  				{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}},
   565  				{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}},
   566  				{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}},
   567  			}, nil
   568  		},
   569  	}
   570  	ctx := context.Background()
   571  	refs, err := convertServiceConfigObjs(ctx, apiClient, namespace, service, configSpecs)
   572  	assert.NilError(t, err)
   573  	expected := []*swarm.ConfigReference{
   574  		{
   575  			ConfigName: "bar_config",
   576  			File: &swarm.ConfigReferenceFileTarget{
   577  				Name: "bar_config",
   578  				UID:  "0",
   579  				GID:  "0",
   580  				Mode: 0o444,
   581  			},
   582  		},
   583  		{
   584  			ConfigName: "baz_config",
   585  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
   586  		},
   587  		{
   588  			ConfigName: "foo_config",
   589  			File: &swarm.ConfigReferenceFileTarget{
   590  				Name: "foo_config",
   591  				UID:  "0",
   592  				GID:  "0",
   593  				Mode: 0o444,
   594  			},
   595  		},
   596  	}
   597  	assert.DeepEqual(t, expected, refs)
   598  }
   599  
   600  type fakeClient struct {
   601  	client.Client
   602  	secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error)
   603  	configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
   604  }
   605  
   606  func (c *fakeClient) SecretList(_ context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
   607  	if c.secretListFunc != nil {
   608  		return c.secretListFunc(options)
   609  	}
   610  	return []swarm.Secret{}, nil
   611  }
   612  
   613  func (c *fakeClient) ConfigList(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
   614  	if c.configListFunc != nil {
   615  		return c.configListFunc(options)
   616  	}
   617  	return []swarm.Config{}, nil
   618  }
   619  
   620  func TestConvertUpdateConfigParallelism(t *testing.T) {
   621  	parallel := uint64(4)
   622  
   623  	// test default behavior
   624  	updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{})
   625  	assert.Check(t, is.Equal(uint64(1), updateConfig.Parallelism))
   626  
   627  	// Non default value
   628  	updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
   629  		Parallelism: &parallel,
   630  	})
   631  	assert.Check(t, is.Equal(parallel, updateConfig.Parallelism))
   632  }
   633  
   634  func TestConvertServiceCapAddAndCapDrop(t *testing.T) {
   635  	tests := []struct {
   636  		title   string
   637  		in, out composetypes.ServiceConfig
   638  	}{
   639  		{
   640  			title: "default behavior",
   641  		},
   642  		{
   643  			title: "some values",
   644  			in: composetypes.ServiceConfig{
   645  				CapAdd:  []string{"SYS_NICE", "CAP_NET_ADMIN"},
   646  				CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
   647  			},
   648  			out: composetypes.ServiceConfig{
   649  				CapAdd:  []string{"CAP_NET_ADMIN", "CAP_SYS_NICE"},
   650  				CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID"},
   651  			},
   652  		},
   653  		{
   654  			title: "adding ALL capabilities",
   655  			in: composetypes.ServiceConfig{
   656  				CapAdd:  []string{"ALL", "CAP_NET_ADMIN"},
   657  				CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
   658  			},
   659  			out: composetypes.ServiceConfig{
   660  				CapAdd:  []string{"ALL"},
   661  				CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
   662  			},
   663  		},
   664  		{
   665  			title: "dropping ALL capabilities",
   666  			in: composetypes.ServiceConfig{
   667  				CapAdd:  []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
   668  				CapDrop: []string{"ALL", "CAP_NET_ADMIN", "CAP_FOO"},
   669  			},
   670  			out: composetypes.ServiceConfig{
   671  				CapAdd:  []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
   672  				CapDrop: []string{"ALL"},
   673  			},
   674  		},
   675  	}
   676  	for _, tc := range tests {
   677  		tc := tc
   678  		t.Run(tc.title, func(t *testing.T) {
   679  			result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil)
   680  			assert.NilError(t, err)
   681  			assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd))
   682  			assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop))
   683  		})
   684  	}
   685  }