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

     1  package service
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"sort"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	mounttypes "github.com/docker/docker/api/types/mount"
    14  	"github.com/docker/docker/api/types/swarm"
    15  	"github.com/docker/go-units"
    16  	"gotest.tools/v3/assert"
    17  	is "gotest.tools/v3/assert/cmp"
    18  )
    19  
    20  func TestUpdateServiceArgs(t *testing.T) {
    21  	flags := newUpdateCommand(nil).Flags()
    22  	flags.Set("args", "the \"new args\"")
    23  
    24  	spec := &swarm.ServiceSpec{
    25  		TaskTemplate: swarm.TaskSpec{
    26  			ContainerSpec: &swarm.ContainerSpec{},
    27  		},
    28  	}
    29  	cspec := spec.TaskTemplate.ContainerSpec
    30  	cspec.Args = []string{"old", "args"}
    31  
    32  	updateService(context.TODO(), nil, flags, spec)
    33  	assert.Check(t, is.DeepEqual([]string{"the", "new args"}, cspec.Args))
    34  }
    35  
    36  func TestUpdateLabels(t *testing.T) {
    37  	flags := newUpdateCommand(nil).Flags()
    38  	flags.Set("label-add", "add-beats-remove=value")
    39  	flags.Set("label-add", "to-add=value")
    40  	flags.Set("label-add", "to-update=new-value")
    41  	flags.Set("label-add", "to-replace=new-value")
    42  	flags.Set("label-rm", "add-beats-remove")
    43  	flags.Set("label-rm", "to-remove")
    44  	flags.Set("label-rm", "to-replace")
    45  	flags.Set("label-rm", "no-such-label")
    46  
    47  	labels := map[string]string{
    48  		"to-keep":    "value",
    49  		"to-remove":  "value",
    50  		"to-replace": "value",
    51  		"to-update":  "value",
    52  	}
    53  
    54  	updateLabels(flags, &labels)
    55  	assert.DeepEqual(t, labels, map[string]string{
    56  		"add-beats-remove": "value",
    57  		"to-add":           "value",
    58  		"to-keep":          "value",
    59  		"to-replace":       "new-value",
    60  		"to-update":        "new-value",
    61  	})
    62  }
    63  
    64  func TestUpdateContainerLabels(t *testing.T) {
    65  	flags := newUpdateCommand(nil).Flags()
    66  	flags.Set("container-label-add", "add-beats-remove=value")
    67  	flags.Set("container-label-add", "to-add=value")
    68  	flags.Set("container-label-add", "to-update=new-value")
    69  	flags.Set("container-label-add", "to-replace=new-value")
    70  	flags.Set("container-label-rm", "add-beats-remove")
    71  	flags.Set("container-label-rm", "to-remove")
    72  	flags.Set("container-label-rm", "to-replace")
    73  	flags.Set("container-label-rm", "no-such-label")
    74  
    75  	labels := map[string]string{
    76  		"to-keep":    "value",
    77  		"to-remove":  "value",
    78  		"to-replace": "value",
    79  		"to-update":  "value",
    80  	}
    81  
    82  	updateContainerLabels(flags, &labels)
    83  	assert.DeepEqual(t, labels, map[string]string{
    84  		"add-beats-remove": "value",
    85  		"to-add":           "value",
    86  		"to-keep":          "value",
    87  		"to-replace":       "new-value",
    88  		"to-update":        "new-value",
    89  	})
    90  }
    91  
    92  func TestUpdatePlacementConstraints(t *testing.T) {
    93  	flags := newUpdateCommand(nil).Flags()
    94  	flags.Set("constraint-add", "node=toadd")
    95  	flags.Set("constraint-rm", "node!=toremove")
    96  
    97  	placement := &swarm.Placement{
    98  		Constraints: []string{"node!=toremove", "container=tokeep"},
    99  	}
   100  
   101  	updatePlacementConstraints(flags, placement)
   102  	assert.Assert(t, is.Len(placement.Constraints, 2))
   103  	assert.Check(t, is.Equal("container=tokeep", placement.Constraints[0]))
   104  	assert.Check(t, is.Equal("node=toadd", placement.Constraints[1]))
   105  }
   106  
   107  func TestUpdatePlacementPrefs(t *testing.T) {
   108  	flags := newUpdateCommand(nil).Flags()
   109  	flags.Set("placement-pref-add", "spread=node.labels.dc")
   110  	flags.Set("placement-pref-rm", "spread=node.labels.rack")
   111  
   112  	placement := &swarm.Placement{
   113  		Preferences: []swarm.PlacementPreference{
   114  			{
   115  				Spread: &swarm.SpreadOver{
   116  					SpreadDescriptor: "node.labels.rack",
   117  				},
   118  			},
   119  			{
   120  				Spread: &swarm.SpreadOver{
   121  					SpreadDescriptor: "node.labels.row",
   122  				},
   123  			},
   124  		},
   125  	}
   126  
   127  	updatePlacementPreferences(flags, placement)
   128  	assert.Assert(t, is.Len(placement.Preferences, 2))
   129  	assert.Check(t, is.Equal("node.labels.row", placement.Preferences[0].Spread.SpreadDescriptor))
   130  	assert.Check(t, is.Equal("node.labels.dc", placement.Preferences[1].Spread.SpreadDescriptor))
   131  }
   132  
   133  func TestUpdateEnvironment(t *testing.T) {
   134  	flags := newUpdateCommand(nil).Flags()
   135  	flags.Set("env-add", "toadd=newenv")
   136  	flags.Set("env-rm", "toremove")
   137  
   138  	envs := []string{"toremove=theenvtoremove", "tokeep=value"}
   139  
   140  	updateEnvironment(flags, &envs)
   141  	assert.Assert(t, is.Len(envs, 2))
   142  	// Order has been removed in updateEnvironment (map)
   143  	sort.Strings(envs)
   144  	assert.Check(t, is.Equal("toadd=newenv", envs[0]))
   145  	assert.Check(t, is.Equal("tokeep=value", envs[1]))
   146  }
   147  
   148  func TestUpdateEnvironmentWithDuplicateValues(t *testing.T) {
   149  	flags := newUpdateCommand(nil).Flags()
   150  	flags.Set("env-rm", "foo")
   151  	flags.Set("env-add", "foo=first")
   152  	flags.Set("env-add", "foo=second")
   153  
   154  	envs := []string{"foo=value"}
   155  
   156  	updateEnvironment(flags, &envs)
   157  	assert.Check(t, is.Len(envs, 1))
   158  	assert.Equal(t, envs[0], "foo=second")
   159  }
   160  
   161  func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) {
   162  	// Test case for #25404
   163  	flags := newUpdateCommand(nil).Flags()
   164  	flags.Set("env-add", "A=b")
   165  
   166  	envs := []string{"A=c"}
   167  
   168  	updateEnvironment(flags, &envs)
   169  	assert.Assert(t, is.Len(envs, 1))
   170  	assert.Check(t, is.Equal("A=b", envs[0]))
   171  }
   172  
   173  func TestUpdateGroups(t *testing.T) {
   174  	flags := newUpdateCommand(nil).Flags()
   175  	flags.Set("group-add", "wheel")
   176  	flags.Set("group-add", "docker")
   177  	flags.Set("group-rm", "root")
   178  	flags.Set("group-add", "foo")
   179  	flags.Set("group-rm", "docker")
   180  
   181  	groups := []string{"bar", "root"}
   182  
   183  	updateGroups(flags, &groups)
   184  	assert.Assert(t, is.Len(groups, 3))
   185  	assert.Check(t, is.Equal("bar", groups[0]))
   186  	assert.Check(t, is.Equal("foo", groups[1]))
   187  	assert.Check(t, is.Equal("wheel", groups[2]))
   188  }
   189  
   190  func TestUpdateDNSConfig(t *testing.T) {
   191  	flags := newUpdateCommand(nil).Flags()
   192  
   193  	// IPv4, with duplicates
   194  	flags.Set("dns-add", "1.1.1.1")
   195  	flags.Set("dns-add", "1.1.1.1")
   196  	flags.Set("dns-add", "2.2.2.2")
   197  	flags.Set("dns-rm", "3.3.3.3")
   198  	flags.Set("dns-rm", "2.2.2.2")
   199  	// IPv6
   200  	flags.Set("dns-add", "2001:db8:abc8::1")
   201  	// Invalid dns record
   202  	assert.Check(t, is.ErrorContains(flags.Set("dns-add", "x.y.z.w"), "IP address is not correctly formatted: x.y.z.w"))
   203  
   204  	// domains with duplicates
   205  	flags.Set("dns-search-add", "example.com")
   206  	flags.Set("dns-search-add", "example.com")
   207  	flags.Set("dns-search-add", "example.org")
   208  	flags.Set("dns-search-rm", "example.org")
   209  	// Invalid dns search domain
   210  	assert.ErrorContains(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain")
   211  
   212  	flags.Set("dns-option-add", "ndots:9")
   213  	flags.Set("dns-option-rm", "timeout:3")
   214  
   215  	config := &swarm.DNSConfig{
   216  		Nameservers: []string{"3.3.3.3", "5.5.5.5"},
   217  		Search:      []string{"localdomain"},
   218  		Options:     []string{"timeout:3"},
   219  	}
   220  
   221  	updateDNSConfig(flags, &config)
   222  
   223  	assert.Assert(t, is.Len(config.Nameservers, 3))
   224  	assert.Check(t, is.Equal("1.1.1.1", config.Nameservers[0]))
   225  	assert.Check(t, is.Equal("2001:db8:abc8::1", config.Nameservers[1]))
   226  	assert.Check(t, is.Equal("5.5.5.5", config.Nameservers[2]))
   227  
   228  	assert.Assert(t, is.Len(config.Search, 2))
   229  	assert.Check(t, is.Equal("example.com", config.Search[0]))
   230  	assert.Check(t, is.Equal("localdomain", config.Search[1]))
   231  
   232  	assert.Assert(t, is.Len(config.Options, 1))
   233  	assert.Check(t, is.Equal(config.Options[0], "ndots:9"))
   234  }
   235  
   236  func TestUpdateMounts(t *testing.T) {
   237  	flags := newUpdateCommand(nil).Flags()
   238  	flags.Set("mount-add", "type=volume,source=vol2,target=/toadd")
   239  	flags.Set("mount-rm", "/toremove")
   240  
   241  	mounts := []mounttypes.Mount{
   242  		{Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind},
   243  		{Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind},
   244  	}
   245  
   246  	updateMounts(flags, &mounts)
   247  	assert.Assert(t, is.Len(mounts, 2))
   248  	assert.Check(t, is.Equal("/toadd", mounts[0].Target))
   249  	assert.Check(t, is.Equal("/tokeep", mounts[1].Target))
   250  }
   251  
   252  func TestUpdateMountsWithDuplicateMounts(t *testing.T) {
   253  	flags := newUpdateCommand(nil).Flags()
   254  	flags.Set("mount-add", "type=volume,source=vol4,target=/toadd")
   255  
   256  	mounts := []mounttypes.Mount{
   257  		{Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind},
   258  		{Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind},
   259  		{Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind},
   260  	}
   261  
   262  	updateMounts(flags, &mounts)
   263  	assert.Assert(t, is.Len(mounts, 3))
   264  	assert.Check(t, is.Equal("/tokeep1", mounts[0].Target))
   265  	assert.Check(t, is.Equal("/tokeep2", mounts[1].Target))
   266  	assert.Check(t, is.Equal("/toadd", mounts[2].Target))
   267  }
   268  
   269  func TestUpdatePorts(t *testing.T) {
   270  	flags := newUpdateCommand(nil).Flags()
   271  	flags.Set("publish-add", "1000:1000")
   272  	flags.Set("publish-rm", "333/udp")
   273  
   274  	portConfigs := []swarm.PortConfig{
   275  		{TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP},
   276  		{TargetPort: 555},
   277  	}
   278  
   279  	err := updatePorts(flags, &portConfigs)
   280  	assert.NilError(t, err)
   281  	assert.Assert(t, is.Len(portConfigs, 2))
   282  	// Do a sort to have the order (might have changed by map)
   283  	targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)}
   284  	sort.Ints(targetPorts)
   285  	assert.Check(t, is.Equal(555, targetPorts[0]))
   286  	assert.Check(t, is.Equal(1000, targetPorts[1]))
   287  }
   288  
   289  func TestUpdatePortsDuplicate(t *testing.T) {
   290  	// Test case for #25375
   291  	flags := newUpdateCommand(nil).Flags()
   292  	flags.Set("publish-add", "80:80")
   293  
   294  	portConfigs := []swarm.PortConfig{
   295  		{
   296  			TargetPort:    80,
   297  			PublishedPort: 80,
   298  			Protocol:      swarm.PortConfigProtocolTCP,
   299  			PublishMode:   swarm.PortConfigPublishModeIngress,
   300  		},
   301  	}
   302  
   303  	err := updatePorts(flags, &portConfigs)
   304  	assert.NilError(t, err)
   305  	assert.Assert(t, is.Len(portConfigs, 1))
   306  	assert.Check(t, is.Equal(uint32(80), portConfigs[0].TargetPort))
   307  }
   308  
   309  func TestUpdateHealthcheckTable(t *testing.T) {
   310  	type test struct {
   311  		flags    [][2]string
   312  		initial  *container.HealthConfig
   313  		expected *container.HealthConfig
   314  		err      string
   315  	}
   316  	testCases := []test{
   317  		{
   318  			flags:    [][2]string{{"no-healthcheck", "true"}},
   319  			initial:  &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10},
   320  			expected: &container.HealthConfig{Test: []string{"NONE"}},
   321  		},
   322  		{
   323  			flags:    [][2]string{{"health-cmd", "cmd1"}},
   324  			initial:  &container.HealthConfig{Test: []string{"NONE"}},
   325  			expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}},
   326  		},
   327  		{
   328  			flags:    [][2]string{{"health-retries", "10"}},
   329  			initial:  &container.HealthConfig{Test: []string{"NONE"}},
   330  			expected: &container.HealthConfig{Retries: 10},
   331  		},
   332  		{
   333  			flags:    [][2]string{{"health-retries", "10"}},
   334  			initial:  &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
   335  			expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
   336  		},
   337  		{
   338  			flags:    [][2]string{{"health-interval", "1m"}},
   339  			initial:  &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
   340  			expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute},
   341  		},
   342  		{
   343  			flags:    [][2]string{{"health-cmd", ""}},
   344  			initial:  &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
   345  			expected: &container.HealthConfig{Retries: 10},
   346  		},
   347  		{
   348  			flags:    [][2]string{{"health-retries", "0"}},
   349  			initial:  &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
   350  			expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
   351  		},
   352  		{
   353  			flags:    [][2]string{{"health-start-period", "1m"}},
   354  			initial:  &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
   355  			expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, StartPeriod: time.Minute},
   356  		},
   357  		{
   358  			flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}},
   359  			err:   "--no-healthcheck conflicts with --health-* options",
   360  		},
   361  		{
   362  			flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}},
   363  			err:   "--no-healthcheck conflicts with --health-* options",
   364  		},
   365  		{
   366  			flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}},
   367  			err:   "--no-healthcheck conflicts with --health-* options",
   368  		},
   369  	}
   370  	for i, c := range testCases {
   371  		flags := newUpdateCommand(nil).Flags()
   372  		for _, flag := range c.flags {
   373  			flags.Set(flag[0], flag[1])
   374  		}
   375  		cspec := &swarm.ContainerSpec{
   376  			Healthcheck: c.initial,
   377  		}
   378  		err := updateHealthcheck(flags, cspec)
   379  		if c.err != "" {
   380  			assert.Error(t, err, c.err)
   381  		} else {
   382  			assert.NilError(t, err)
   383  			if !reflect.DeepEqual(cspec.Healthcheck, c.expected) {
   384  				t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck)
   385  			}
   386  		}
   387  	}
   388  }
   389  
   390  func TestUpdateHosts(t *testing.T) {
   391  	flags := newUpdateCommand(nil).Flags()
   392  	flags.Set("host-add", "example.net:2.2.2.2")
   393  	flags.Set("host-add", "ipv6.net:2001:db8:abc8::1")
   394  	// adding the special "host-gateway" target should work
   395  	flags.Set("host-add", "host.docker.internal:host-gateway")
   396  	// remove with ipv6 should work
   397  	flags.Set("host-rm", "example.net:2001:db8:abc8::1")
   398  	// just hostname should work as well
   399  	flags.Set("host-rm", "example.net")
   400  	// removing the special "host-gateway" target should work
   401  	flags.Set("host-rm", "gateway.docker.internal:host-gateway")
   402  	// bad format error
   403  	assert.ErrorContains(t, flags.Set("host-add", "$example.com$"), `bad format for add-host: "$example.com$"`)
   404  
   405  	hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net", "gateway.docker.internal:host-gateway"}
   406  	expected := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2.2.2.2 example.net", "2001:db8:abc8::1 ipv6.net", "host-gateway host.docker.internal"}
   407  
   408  	err := updateHosts(flags, &hosts)
   409  	assert.NilError(t, err)
   410  	assert.Check(t, is.DeepEqual(expected, hosts))
   411  }
   412  
   413  func TestUpdateHostsPreservesOrder(t *testing.T) {
   414  	flags := newUpdateCommand(nil).Flags()
   415  	flags.Set("host-add", "foobar:127.0.0.2")
   416  	flags.Set("host-add", "foobar:127.0.0.1")
   417  	flags.Set("host-add", "foobar:127.0.0.3")
   418  
   419  	hosts := []string{}
   420  	err := updateHosts(flags, &hosts)
   421  	assert.NilError(t, err)
   422  	assert.Check(t, is.DeepEqual([]string{"127.0.0.2 foobar", "127.0.0.1 foobar", "127.0.0.3 foobar"}, hosts))
   423  }
   424  
   425  func TestUpdateHostsReplaceEntry(t *testing.T) {
   426  	flags := newUpdateCommand(nil).Flags()
   427  	flags.Set("host-add", "foobar:127.0.0.4")
   428  	flags.Set("host-rm", "foobar:127.0.0.2")
   429  
   430  	hosts := []string{"127.0.0.2 foobar", "127.0.0.1 foobar", "127.0.0.3 foobar"}
   431  
   432  	err := updateHosts(flags, &hosts)
   433  	assert.NilError(t, err)
   434  	assert.Check(t, is.DeepEqual([]string{"127.0.0.1 foobar", "127.0.0.3 foobar", "127.0.0.4 foobar"}, hosts))
   435  }
   436  
   437  func TestUpdateHostsRemoveHost(t *testing.T) {
   438  	flags := newUpdateCommand(nil).Flags()
   439  	flags.Set("host-rm", "host1")
   440  
   441  	hosts := []string{"127.0.0.2 host3 host1 host2 host4", "127.0.0.1 host1 host4", "127.0.0.3 host1"}
   442  
   443  	err := updateHosts(flags, &hosts)
   444  	assert.NilError(t, err)
   445  
   446  	// Removing host `host1` should remove the entry from each line it appears in.
   447  	// If there are no other hosts in the entry, the entry itself should be removed.
   448  	assert.Check(t, is.DeepEqual([]string{"127.0.0.2 host3 host2 host4", "127.0.0.1 host4"}, hosts))
   449  }
   450  
   451  func TestUpdateHostsRemoveHostIP(t *testing.T) {
   452  	flags := newUpdateCommand(nil).Flags()
   453  	flags.Set("host-rm", "host1:127.0.0.1")
   454  
   455  	hosts := []string{"127.0.0.2 host3 host1 host2 host4", "127.0.0.1 host1 host4", "127.0.0.3 host1", "127.0.0.1 host1"}
   456  
   457  	err := updateHosts(flags, &hosts)
   458  	assert.NilError(t, err)
   459  
   460  	// Removing host `host1` should remove the entry from each line it appears in,
   461  	// but only if the IP-address matches. If there are no other hosts in the entry,
   462  	// the entry itself should be removed.
   463  	assert.Check(t, is.DeepEqual([]string{"127.0.0.2 host3 host1 host2 host4", "127.0.0.1 host4", "127.0.0.3 host1"}, hosts))
   464  }
   465  
   466  func TestUpdateHostsRemoveAll(t *testing.T) {
   467  	flags := newUpdateCommand(nil).Flags()
   468  	flags.Set("host-add", "host-three:127.0.0.4")
   469  	flags.Set("host-add", "host-one:127.0.0.5")
   470  	flags.Set("host-rm", "host-one")
   471  
   472  	hosts := []string{"127.0.0.1 host-one", "127.0.0.2 host-two", "127.0.0.3 host-one"}
   473  
   474  	err := updateHosts(flags, &hosts)
   475  	assert.NilError(t, err)
   476  	assert.Check(t, is.DeepEqual([]string{"127.0.0.2 host-two", "127.0.0.4 host-three", "127.0.0.5 host-one"}, hosts))
   477  }
   478  
   479  func TestUpdatePortsRmWithProtocol(t *testing.T) {
   480  	flags := newUpdateCommand(nil).Flags()
   481  	flags.Set("publish-add", "8081:81")
   482  	flags.Set("publish-add", "8082:82")
   483  	flags.Set("publish-rm", "80")
   484  	flags.Set("publish-rm", "81/tcp")
   485  	flags.Set("publish-rm", "82/udp")
   486  
   487  	portConfigs := []swarm.PortConfig{
   488  		{
   489  			TargetPort:    80,
   490  			PublishedPort: 8080,
   491  			Protocol:      swarm.PortConfigProtocolTCP,
   492  			PublishMode:   swarm.PortConfigPublishModeIngress,
   493  		},
   494  	}
   495  
   496  	err := updatePorts(flags, &portConfigs)
   497  	assert.NilError(t, err)
   498  	assert.Assert(t, is.Len(portConfigs, 2))
   499  	assert.Check(t, is.Equal(uint32(81), portConfigs[0].TargetPort))
   500  	assert.Check(t, is.Equal(uint32(82), portConfigs[1].TargetPort))
   501  }
   502  
   503  type secretAPIClientMock struct {
   504  	listResult []swarm.Secret
   505  }
   506  
   507  func (s secretAPIClientMock) SecretList(context.Context, types.SecretListOptions) ([]swarm.Secret, error) {
   508  	return s.listResult, nil
   509  }
   510  
   511  func (s secretAPIClientMock) SecretCreate(context.Context, swarm.SecretSpec) (types.SecretCreateResponse, error) {
   512  	return types.SecretCreateResponse{}, nil
   513  }
   514  
   515  func (s secretAPIClientMock) SecretRemove(context.Context, string) error {
   516  	return nil
   517  }
   518  
   519  func (s secretAPIClientMock) SecretInspectWithRaw(context.Context, string) (swarm.Secret, []byte, error) {
   520  	return swarm.Secret{}, []byte{}, nil
   521  }
   522  
   523  func (s secretAPIClientMock) SecretUpdate(context.Context, string, swarm.Version, swarm.SecretSpec) error {
   524  	return nil
   525  }
   526  
   527  // TestUpdateSecretUpdateInPlace tests the ability to update the "target" of a
   528  // secret with "docker service update" by combining "--secret-rm" and
   529  // "--secret-add" for the same secret.
   530  func TestUpdateSecretUpdateInPlace(t *testing.T) {
   531  	apiClient := secretAPIClientMock{
   532  		listResult: []swarm.Secret{
   533  			{
   534  				ID:   "tn9qiblgnuuut11eufquw5dev",
   535  				Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}},
   536  			},
   537  		},
   538  	}
   539  
   540  	flags := newUpdateCommand(nil).Flags()
   541  	flags.Set("secret-add", "source=foo,target=foo2")
   542  	flags.Set("secret-rm", "foo")
   543  
   544  	secrets := []*swarm.SecretReference{
   545  		{
   546  			File: &swarm.SecretReferenceFileTarget{
   547  				Name: "foo",
   548  				UID:  "0",
   549  				GID:  "0",
   550  				Mode: 292,
   551  			},
   552  			SecretID:   "tn9qiblgnuuut11eufquw5dev",
   553  			SecretName: "foo",
   554  		},
   555  	}
   556  
   557  	ctx := context.Background()
   558  	updatedSecrets, err := getUpdatedSecrets(ctx, apiClient, flags, secrets)
   559  
   560  	assert.NilError(t, err)
   561  	assert.Assert(t, is.Len(updatedSecrets, 1))
   562  	assert.Check(t, is.Equal("tn9qiblgnuuut11eufquw5dev", updatedSecrets[0].SecretID))
   563  	assert.Check(t, is.Equal("foo", updatedSecrets[0].SecretName))
   564  	assert.Check(t, is.Equal("foo2", updatedSecrets[0].File.Name))
   565  }
   566  
   567  func TestUpdateReadOnly(t *testing.T) {
   568  	spec := &swarm.ServiceSpec{
   569  		TaskTemplate: swarm.TaskSpec{
   570  			ContainerSpec: &swarm.ContainerSpec{},
   571  		},
   572  	}
   573  	cspec := spec.TaskTemplate.ContainerSpec
   574  
   575  	// Update with --read-only=true, changed to true
   576  	flags := newUpdateCommand(nil).Flags()
   577  	flags.Set("read-only", "true")
   578  	updateService(context.TODO(), nil, flags, spec)
   579  	assert.Check(t, cspec.ReadOnly)
   580  
   581  	// Update without --read-only, no change
   582  	flags = newUpdateCommand(nil).Flags()
   583  	updateService(context.TODO(), nil, flags, spec)
   584  	assert.Check(t, cspec.ReadOnly)
   585  
   586  	// Update with --read-only=false, changed to false
   587  	flags = newUpdateCommand(nil).Flags()
   588  	flags.Set("read-only", "false")
   589  	updateService(context.TODO(), nil, flags, spec)
   590  	assert.Check(t, !cspec.ReadOnly)
   591  }
   592  
   593  func TestUpdateInit(t *testing.T) {
   594  	spec := &swarm.ServiceSpec{
   595  		TaskTemplate: swarm.TaskSpec{
   596  			ContainerSpec: &swarm.ContainerSpec{},
   597  		},
   598  	}
   599  	cspec := spec.TaskTemplate.ContainerSpec
   600  
   601  	// Update with --init=true
   602  	flags := newUpdateCommand(nil).Flags()
   603  	flags.Set("init", "true")
   604  	updateService(context.TODO(), nil, flags, spec)
   605  	assert.Check(t, is.Equal(true, *cspec.Init))
   606  
   607  	// Update without --init, no change
   608  	flags = newUpdateCommand(nil).Flags()
   609  	updateService(context.TODO(), nil, flags, spec)
   610  	assert.Check(t, is.Equal(true, *cspec.Init))
   611  
   612  	// Update with --init=false
   613  	flags = newUpdateCommand(nil).Flags()
   614  	flags.Set("init", "false")
   615  	updateService(context.TODO(), nil, flags, spec)
   616  	assert.Check(t, is.Equal(false, *cspec.Init))
   617  }
   618  
   619  func TestUpdateStopSignal(t *testing.T) {
   620  	spec := &swarm.ServiceSpec{
   621  		TaskTemplate: swarm.TaskSpec{
   622  			ContainerSpec: &swarm.ContainerSpec{},
   623  		},
   624  	}
   625  	cspec := spec.TaskTemplate.ContainerSpec
   626  
   627  	// Update with --stop-signal=SIGUSR1
   628  	flags := newUpdateCommand(nil).Flags()
   629  	flags.Set("stop-signal", "SIGUSR1")
   630  	updateService(context.TODO(), nil, flags, spec)
   631  	assert.Check(t, is.Equal("SIGUSR1", cspec.StopSignal))
   632  
   633  	// Update without --stop-signal, no change
   634  	flags = newUpdateCommand(nil).Flags()
   635  	updateService(context.TODO(), nil, flags, spec)
   636  	assert.Check(t, is.Equal("SIGUSR1", cspec.StopSignal))
   637  
   638  	// Update with --stop-signal=SIGWINCH
   639  	flags = newUpdateCommand(nil).Flags()
   640  	flags.Set("stop-signal", "SIGWINCH")
   641  	updateService(context.TODO(), nil, flags, spec)
   642  	assert.Check(t, is.Equal("SIGWINCH", cspec.StopSignal))
   643  }
   644  
   645  func TestUpdateIsolationValid(t *testing.T) {
   646  	flags := newUpdateCommand(nil).Flags()
   647  	err := flags.Set("isolation", "process")
   648  	assert.NilError(t, err)
   649  	spec := swarm.ServiceSpec{
   650  		TaskTemplate: swarm.TaskSpec{
   651  			ContainerSpec: &swarm.ContainerSpec{},
   652  		},
   653  	}
   654  	err = updateService(context.Background(), nil, flags, &spec)
   655  	assert.NilError(t, err)
   656  	assert.Check(t, is.Equal(container.IsolationProcess, spec.TaskTemplate.ContainerSpec.Isolation))
   657  }
   658  
   659  // TestUpdateLimitsReservations tests that limits and reservations are updated,
   660  // and that values are not updated are not reset to their default value
   661  func TestUpdateLimitsReservations(t *testing.T) {
   662  	// test that updating works if the service did not previously
   663  	// have limits set (https://github.com/moby/moby/issues/38363)
   664  	t.Run("update limits from scratch", func(t *testing.T) {
   665  		spec := swarm.ServiceSpec{
   666  			TaskTemplate: swarm.TaskSpec{
   667  				ContainerSpec: &swarm.ContainerSpec{},
   668  			},
   669  		}
   670  		flags := newUpdateCommand(nil).Flags()
   671  		err := flags.Set(flagLimitCPU, "2")
   672  		assert.NilError(t, err)
   673  		err = flags.Set(flagLimitMemory, "200M")
   674  		assert.NilError(t, err)
   675  		err = flags.Set(flagLimitPids, "100")
   676  		assert.NilError(t, err)
   677  		err = updateService(context.Background(), nil, flags, &spec)
   678  		assert.NilError(t, err)
   679  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   680  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   681  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   682  	})
   683  
   684  	// test that updating works if the service did not previously
   685  	// have reservations set (https://github.com/moby/moby/issues/38363)
   686  	t.Run("update reservations from scratch", func(t *testing.T) {
   687  		spec := swarm.ServiceSpec{
   688  			TaskTemplate: swarm.TaskSpec{
   689  				ContainerSpec: &swarm.ContainerSpec{},
   690  			},
   691  		}
   692  		flags := newUpdateCommand(nil).Flags()
   693  		err := flags.Set(flagReserveCPU, "2")
   694  		assert.NilError(t, err)
   695  		err = flags.Set(flagReserveMemory, "200M")
   696  		assert.NilError(t, err)
   697  		err = updateService(context.Background(), nil, flags, &spec)
   698  		assert.NilError(t, err)
   699  	})
   700  
   701  	spec := swarm.ServiceSpec{
   702  		TaskTemplate: swarm.TaskSpec{
   703  			ContainerSpec: &swarm.ContainerSpec{},
   704  			Resources: &swarm.ResourceRequirements{
   705  				Limits: &swarm.Limit{
   706  					NanoCPUs:    1000000000,
   707  					MemoryBytes: 104857600,
   708  					Pids:        100,
   709  				},
   710  				Reservations: &swarm.Resources{
   711  					NanoCPUs:    1000000000,
   712  					MemoryBytes: 104857600,
   713  				},
   714  			},
   715  		},
   716  	}
   717  
   718  	// Updating without flags set should not modify existing values
   719  	t.Run("update without flags set", func(t *testing.T) {
   720  		flags := newUpdateCommand(nil).Flags()
   721  		err := updateService(context.Background(), nil, flags, &spec)
   722  		assert.NilError(t, err)
   723  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(1000000000)))
   724  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(104857600)))
   725  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   726  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(1000000000)))
   727  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(104857600)))
   728  	})
   729  
   730  	// Updating CPU limit/reservation should not affect memory limit/reservation
   731  	// and pids-limt
   732  	t.Run("update cpu limit and reservation", func(t *testing.T) {
   733  		flags := newUpdateCommand(nil).Flags()
   734  		err := flags.Set(flagLimitCPU, "2")
   735  		assert.NilError(t, err)
   736  		err = flags.Set(flagReserveCPU, "2")
   737  		assert.NilError(t, err)
   738  		err = updateService(context.Background(), nil, flags, &spec)
   739  		assert.NilError(t, err)
   740  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   741  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(104857600)))
   742  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   743  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   744  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(104857600)))
   745  	})
   746  
   747  	// Updating Memory limit/reservation should not affect CPU limit/reservation
   748  	// and pids-limt
   749  	t.Run("update memory limit and reservation", func(t *testing.T) {
   750  		flags := newUpdateCommand(nil).Flags()
   751  		err := flags.Set(flagLimitMemory, "200M")
   752  		assert.NilError(t, err)
   753  		err = flags.Set(flagReserveMemory, "200M")
   754  		assert.NilError(t, err)
   755  		err = updateService(context.Background(), nil, flags, &spec)
   756  		assert.NilError(t, err)
   757  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   758  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   759  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   760  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   761  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
   762  	})
   763  
   764  	// Updating PidsLimit should only modify PidsLimit, other values unchanged
   765  	t.Run("update pids limit", func(t *testing.T) {
   766  		flags := newUpdateCommand(nil).Flags()
   767  		err := flags.Set(flagLimitPids, "2")
   768  		assert.NilError(t, err)
   769  		err = updateService(context.Background(), nil, flags, &spec)
   770  		assert.NilError(t, err)
   771  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   772  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   773  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(2)))
   774  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   775  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
   776  	})
   777  
   778  	t.Run("update pids limit to default", func(t *testing.T) {
   779  		// Updating PidsLimit to 0 should work
   780  		flags := newUpdateCommand(nil).Flags()
   781  		err := flags.Set(flagLimitPids, "0")
   782  		assert.NilError(t, err)
   783  		err = updateService(context.Background(), nil, flags, &spec)
   784  		assert.NilError(t, err)
   785  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   786  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   787  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(0)))
   788  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   789  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
   790  	})
   791  }
   792  
   793  func TestUpdateIsolationInvalid(t *testing.T) {
   794  	// validation depends on daemon os / version so validation should be done on the daemon side
   795  	flags := newUpdateCommand(nil).Flags()
   796  	err := flags.Set("isolation", "test")
   797  	assert.NilError(t, err)
   798  	spec := swarm.ServiceSpec{
   799  		TaskTemplate: swarm.TaskSpec{
   800  			ContainerSpec: &swarm.ContainerSpec{},
   801  		},
   802  	}
   803  	err = updateService(context.Background(), nil, flags, &spec)
   804  	assert.NilError(t, err)
   805  	assert.Check(t, is.Equal(container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation))
   806  }
   807  
   808  func TestAddGenericResources(t *testing.T) {
   809  	task := &swarm.TaskSpec{}
   810  	flags := newUpdateCommand(nil).Flags()
   811  
   812  	assert.Check(t, addGenericResources(flags, task))
   813  
   814  	flags.Set(flagGenericResourcesAdd, "foo=1")
   815  	assert.Check(t, addGenericResources(flags, task))
   816  	assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 1))
   817  
   818  	// Checks that foo isn't added a 2nd time
   819  	flags = newUpdateCommand(nil).Flags()
   820  	flags.Set(flagGenericResourcesAdd, "bar=1")
   821  	assert.Check(t, addGenericResources(flags, task))
   822  	assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 2))
   823  }
   824  
   825  func TestRemoveGenericResources(t *testing.T) {
   826  	task := &swarm.TaskSpec{}
   827  	flags := newUpdateCommand(nil).Flags()
   828  
   829  	assert.Check(t, removeGenericResources(flags, task))
   830  
   831  	flags.Set(flagGenericResourcesRemove, "foo")
   832  	assert.Check(t, is.ErrorContains(removeGenericResources(flags, task), ""))
   833  
   834  	flags = newUpdateCommand(nil).Flags()
   835  	flags.Set(flagGenericResourcesAdd, "foo=1")
   836  	addGenericResources(flags, task)
   837  	flags = newUpdateCommand(nil).Flags()
   838  	flags.Set(flagGenericResourcesAdd, "bar=1")
   839  	addGenericResources(flags, task)
   840  
   841  	flags = newUpdateCommand(nil).Flags()
   842  	flags.Set(flagGenericResourcesRemove, "foo")
   843  	assert.Check(t, removeGenericResources(flags, task))
   844  	assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 1))
   845  }
   846  
   847  func TestUpdateNetworks(t *testing.T) {
   848  	ctx := context.Background()
   849  	nws := []types.NetworkResource{
   850  		{Name: "aaa-network", ID: "id555"},
   851  		{Name: "mmm-network", ID: "id999"},
   852  		{Name: "zzz-network", ID: "id111"},
   853  	}
   854  
   855  	client := &fakeClient{
   856  		networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
   857  			for _, network := range nws {
   858  				if network.ID == networkID || network.Name == networkID {
   859  					return network, nil
   860  				}
   861  			}
   862  			return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
   863  		},
   864  	}
   865  
   866  	svc := swarm.ServiceSpec{
   867  		TaskTemplate: swarm.TaskSpec{
   868  			ContainerSpec: &swarm.ContainerSpec{},
   869  			Networks: []swarm.NetworkAttachmentConfig{
   870  				{Target: "id999"},
   871  			},
   872  		},
   873  	}
   874  
   875  	flags := newUpdateCommand(nil).Flags()
   876  	err := flags.Set(flagNetworkAdd, "aaa-network")
   877  	assert.NilError(t, err)
   878  	err = updateService(ctx, client, flags, &svc)
   879  	assert.NilError(t, err)
   880  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
   881  
   882  	flags = newUpdateCommand(nil).Flags()
   883  	err = flags.Set(flagNetworkAdd, "aaa-network")
   884  	assert.NilError(t, err)
   885  	err = updateService(ctx, client, flags, &svc)
   886  	assert.Error(t, err, "service is already attached to network aaa-network")
   887  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
   888  
   889  	flags = newUpdateCommand(nil).Flags()
   890  	err = flags.Set(flagNetworkAdd, "id555")
   891  	assert.NilError(t, err)
   892  	err = updateService(ctx, client, flags, &svc)
   893  	assert.Error(t, err, "service is already attached to network id555")
   894  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
   895  
   896  	flags = newUpdateCommand(nil).Flags()
   897  	err = flags.Set(flagNetworkRemove, "id999")
   898  	assert.NilError(t, err)
   899  	err = updateService(ctx, client, flags, &svc)
   900  	assert.NilError(t, err)
   901  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks))
   902  
   903  	flags = newUpdateCommand(nil).Flags()
   904  	err = flags.Set(flagNetworkAdd, "mmm-network")
   905  	assert.NilError(t, err)
   906  	err = flags.Set(flagNetworkRemove, "aaa-network")
   907  	assert.NilError(t, err)
   908  	err = updateService(ctx, client, flags, &svc)
   909  	assert.NilError(t, err)
   910  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks))
   911  }
   912  
   913  func TestUpdateMaxReplicas(t *testing.T) {
   914  	ctx := context.Background()
   915  
   916  	svc := swarm.ServiceSpec{
   917  		TaskTemplate: swarm.TaskSpec{
   918  			ContainerSpec: &swarm.ContainerSpec{},
   919  			Placement: &swarm.Placement{
   920  				MaxReplicas: 1,
   921  			},
   922  		},
   923  	}
   924  
   925  	flags := newUpdateCommand(nil).Flags()
   926  	flags.Set(flagMaxReplicas, "2")
   927  	err := updateService(ctx, nil, flags, &svc)
   928  	assert.NilError(t, err)
   929  
   930  	assert.DeepEqual(t, svc.TaskTemplate.Placement, &swarm.Placement{MaxReplicas: uint64(2)})
   931  }
   932  
   933  func TestUpdateSysCtls(t *testing.T) {
   934  	ctx := context.Background()
   935  
   936  	tests := []struct {
   937  		name     string
   938  		spec     map[string]string
   939  		add      []string
   940  		rm       []string
   941  		expected map[string]string
   942  	}{
   943  		{
   944  			name:     "from scratch",
   945  			add:      []string{"sysctl.zet=value-99", "sysctl.alpha=value-1"},
   946  			expected: map[string]string{"sysctl.zet": "value-99", "sysctl.alpha": "value-1"},
   947  		},
   948  		{
   949  			name:     "append new",
   950  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   951  			add:      []string{"new.sysctl=newvalue"},
   952  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
   953  		},
   954  		{
   955  			name:     "append duplicate is a no-op",
   956  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   957  			add:      []string{"sysctl.one=value-1"},
   958  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   959  		},
   960  		{
   961  			name:     "remove and append existing is a no-op",
   962  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   963  			add:      []string{"sysctl.one=value-1"},
   964  			rm:       []string{"sysctl.one=value-1"},
   965  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   966  		},
   967  		{
   968  			name:     "remove and append new should append",
   969  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   970  			add:      []string{"new.sysctl=newvalue"},
   971  			rm:       []string{"new.sysctl=newvalue"},
   972  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
   973  		},
   974  		{
   975  			name:     "update existing",
   976  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   977  			add:      []string{"sysctl.one=newvalue"},
   978  			expected: map[string]string{"sysctl.one": "newvalue", "sysctl.two": "value-2"},
   979  		},
   980  		{
   981  			name:     "update existing twice",
   982  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   983  			add:      []string{"sysctl.one=newvalue", "sysctl.one=evennewervalue"},
   984  			expected: map[string]string{"sysctl.one": "evennewervalue", "sysctl.two": "value-2"},
   985  		},
   986  		{
   987  			name:     "remove all",
   988  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   989  			rm:       []string{"sysctl.one=value-1", "sysctl.two=value-2"},
   990  			expected: map[string]string{},
   991  		},
   992  		{
   993  			name:     "remove by key",
   994  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   995  			rm:       []string{"sysctl.one"},
   996  			expected: map[string]string{"sysctl.two": "value-2"},
   997  		},
   998  		{
   999  			name:     "remove by key and different value",
  1000  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
  1001  			rm:       []string{"sysctl.one=anyvalueyoulike"},
  1002  			expected: map[string]string{"sysctl.two": "value-2"},
  1003  		},
  1004  	}
  1005  
  1006  	for _, tc := range tests {
  1007  		t.Run(tc.name, func(t *testing.T) {
  1008  			svc := swarm.ServiceSpec{
  1009  				TaskTemplate: swarm.TaskSpec{
  1010  					ContainerSpec: &swarm.ContainerSpec{Sysctls: tc.spec},
  1011  				},
  1012  			}
  1013  			flags := newUpdateCommand(nil).Flags()
  1014  			for _, v := range tc.add {
  1015  				assert.NilError(t, flags.Set(flagSysCtlAdd, v))
  1016  			}
  1017  			for _, v := range tc.rm {
  1018  				assert.NilError(t, flags.Set(flagSysCtlRemove, v))
  1019  			}
  1020  			err := updateService(ctx, &fakeClient{}, flags, &svc)
  1021  			assert.NilError(t, err)
  1022  			if !assert.Check(t, is.DeepEqual(svc.TaskTemplate.ContainerSpec.Sysctls, tc.expected)) {
  1023  				t.Logf("expected: %v", tc.expected)
  1024  				t.Logf("actual: %v", svc.TaskTemplate.ContainerSpec.Sysctls)
  1025  			}
  1026  		})
  1027  	}
  1028  }
  1029  
  1030  func TestUpdateGetUpdatedConfigs(t *testing.T) {
  1031  	// cannedConfigs is a set of configs that we'll use over and over in the
  1032  	// tests. it's a map of Name to Config
  1033  	cannedConfigs := map[string]*swarm.Config{
  1034  		"bar": {
  1035  			ID: "barID",
  1036  			Spec: swarm.ConfigSpec{
  1037  				Annotations: swarm.Annotations{
  1038  					Name: "bar",
  1039  				},
  1040  			},
  1041  		},
  1042  		"cred": {
  1043  			ID: "credID",
  1044  			Spec: swarm.ConfigSpec{
  1045  				Annotations: swarm.Annotations{
  1046  					Name: "cred",
  1047  				},
  1048  			},
  1049  		},
  1050  		"newCred": {
  1051  			ID: "newCredID",
  1052  			Spec: swarm.ConfigSpec{
  1053  				Annotations: swarm.Annotations{
  1054  					Name: "newCred",
  1055  				},
  1056  			},
  1057  		},
  1058  	}
  1059  	// cannedConfigRefs is the same thing, but with config references instead
  1060  	// of ID, however, it just maps an arbitrary string value. this is
  1061  	// so we could have multiple config refs using the same config
  1062  	cannedConfigRefs := map[string]*swarm.ConfigReference{
  1063  		"fooRef": {
  1064  			ConfigID:   "fooID",
  1065  			ConfigName: "foo",
  1066  			File: &swarm.ConfigReferenceFileTarget{
  1067  				Name: "foo",
  1068  				UID:  "0",
  1069  				GID:  "0",
  1070  				Mode: 0o444,
  1071  			},
  1072  		},
  1073  		"barRef": {
  1074  			ConfigID:   "barID",
  1075  			ConfigName: "bar",
  1076  			File: &swarm.ConfigReferenceFileTarget{
  1077  				Name: "bar",
  1078  				UID:  "0",
  1079  				GID:  "0",
  1080  				Mode: 0o444,
  1081  			},
  1082  		},
  1083  		"bazRef": {
  1084  			ConfigID:   "bazID",
  1085  			ConfigName: "baz",
  1086  			File: &swarm.ConfigReferenceFileTarget{
  1087  				Name: "baz",
  1088  				UID:  "0",
  1089  				GID:  "0",
  1090  				Mode: 0o444,
  1091  			},
  1092  		},
  1093  		"credRef": {
  1094  			ConfigID:   "credID",
  1095  			ConfigName: "cred",
  1096  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1097  		},
  1098  		"newCredRef": {
  1099  			ConfigID:   "newCredID",
  1100  			ConfigName: "newCred",
  1101  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1102  		},
  1103  	}
  1104  
  1105  	type flagVal [2]string
  1106  	type test struct {
  1107  		// the name of the subtest
  1108  		name string
  1109  		// flags are the flags we'll be setting
  1110  		flags []flagVal
  1111  		// oldConfigs are the configs that would already be on the service
  1112  		// it is a slice of strings corresponding to the key of
  1113  		// cannedConfigRefs
  1114  		oldConfigs []string
  1115  		// oldCredSpec is the credentialSpec being carried over from the old
  1116  		// object
  1117  		oldCredSpec *swarm.CredentialSpec
  1118  		// lookupConfigs are the configs we're expecting to be listed. it is a
  1119  		// slice of strings corresponding to the key of cannedConfigs
  1120  		lookupConfigs []string
  1121  		// expected is the configs we should get as a result. it is a slice of
  1122  		// strings corresponding to the key in cannedConfigRefs
  1123  		expected []string
  1124  	}
  1125  
  1126  	testCases := []test{
  1127  		{
  1128  			name:       "no configs added or removed",
  1129  			oldConfigs: []string{"fooRef"},
  1130  			expected:   []string{"fooRef"},
  1131  		}, {
  1132  			name:          "add a config",
  1133  			flags:         []flagVal{{"config-add", "bar"}},
  1134  			oldConfigs:    []string{"fooRef"},
  1135  			lookupConfigs: []string{"bar"},
  1136  			expected:      []string{"fooRef", "barRef"},
  1137  		}, {
  1138  			name:       "remove a config",
  1139  			flags:      []flagVal{{"config-rm", "bar"}},
  1140  			oldConfigs: []string{"fooRef", "barRef"},
  1141  			expected:   []string{"fooRef"},
  1142  		}, {
  1143  			name:        "include an old credential spec",
  1144  			oldConfigs:  []string{"credRef"},
  1145  			oldCredSpec: &swarm.CredentialSpec{Config: "credID"},
  1146  			expected:    []string{"credRef"},
  1147  		}, {
  1148  			name:          "add a credential spec",
  1149  			oldConfigs:    []string{"fooRef"},
  1150  			flags:         []flagVal{{"credential-spec", "config://cred"}},
  1151  			lookupConfigs: []string{"cred"},
  1152  			expected:      []string{"fooRef", "credRef"},
  1153  		}, {
  1154  			name:          "change a credential spec",
  1155  			oldConfigs:    []string{"fooRef", "credRef"},
  1156  			oldCredSpec:   &swarm.CredentialSpec{Config: "credID"},
  1157  			flags:         []flagVal{{"credential-spec", "config://newCred"}},
  1158  			lookupConfigs: []string{"newCred"},
  1159  			expected:      []string{"fooRef", "newCredRef"},
  1160  		}, {
  1161  			name:          "credential spec no longer config",
  1162  			oldConfigs:    []string{"fooRef", "credRef"},
  1163  			oldCredSpec:   &swarm.CredentialSpec{Config: "credID"},
  1164  			flags:         []flagVal{{"credential-spec", "file://someFile"}},
  1165  			lookupConfigs: []string{},
  1166  			expected:      []string{"fooRef"},
  1167  		}, {
  1168  			name:          "credential spec becomes config",
  1169  			oldConfigs:    []string{"fooRef"},
  1170  			oldCredSpec:   &swarm.CredentialSpec{File: "someFile"},
  1171  			flags:         []flagVal{{"credential-spec", "config://cred"}},
  1172  			lookupConfigs: []string{"cred"},
  1173  			expected:      []string{"fooRef", "credRef"},
  1174  		}, {
  1175  			name:          "remove credential spec",
  1176  			oldConfigs:    []string{"fooRef", "credRef"},
  1177  			oldCredSpec:   &swarm.CredentialSpec{Config: "credID"},
  1178  			flags:         []flagVal{{"credential-spec", ""}},
  1179  			lookupConfigs: []string{},
  1180  			expected:      []string{"fooRef"},
  1181  		}, {
  1182  			name: "just frick my stuff up",
  1183  			// a more complicated test. add barRef, remove bazRef, keep fooRef,
  1184  			// change credentialSpec from credRef to newCredRef
  1185  			oldConfigs:  []string{"fooRef", "bazRef", "credRef"},
  1186  			oldCredSpec: &swarm.CredentialSpec{Config: "cred"},
  1187  			flags: []flagVal{
  1188  				{"config-add", "bar"},
  1189  				{"config-rm", "baz"},
  1190  				{"credential-spec", "config://newCred"},
  1191  			},
  1192  			lookupConfigs: []string{"bar", "newCred"},
  1193  			expected:      []string{"fooRef", "barRef", "newCredRef"},
  1194  		},
  1195  	}
  1196  
  1197  	for _, tc := range testCases {
  1198  		t.Run(tc.name, func(t *testing.T) {
  1199  			flags := newUpdateCommand(nil).Flags()
  1200  			for _, f := range tc.flags {
  1201  				flags.Set(f[0], f[1])
  1202  			}
  1203  
  1204  			// fakeConfigAPIClientList is actually defined in create_test.go,
  1205  			// but we'll use it here as well
  1206  			var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts types.ConfigListOptions) ([]swarm.Config, error) {
  1207  				names := opts.Filters.Get("name")
  1208  				assert.Equal(t, len(names), len(tc.lookupConfigs))
  1209  
  1210  				configs := []swarm.Config{}
  1211  				for _, lookup := range tc.lookupConfigs {
  1212  					assert.Assert(t, is.Contains(names, lookup))
  1213  					cfg, ok := cannedConfigs[lookup]
  1214  					assert.Assert(t, ok)
  1215  					configs = append(configs, *cfg)
  1216  				}
  1217  				return configs, nil
  1218  			}
  1219  
  1220  			// build the actual set of old configs and the container spec
  1221  			oldConfigs := []*swarm.ConfigReference{}
  1222  			for _, config := range tc.oldConfigs {
  1223  				cfg, ok := cannedConfigRefs[config]
  1224  				assert.Assert(t, ok)
  1225  				oldConfigs = append(oldConfigs, cfg)
  1226  			}
  1227  
  1228  			containerSpec := &swarm.ContainerSpec{
  1229  				Configs: oldConfigs,
  1230  				Privileges: &swarm.Privileges{
  1231  					CredentialSpec: tc.oldCredSpec,
  1232  				},
  1233  			}
  1234  
  1235  			ctx := context.Background()
  1236  			finalConfigs, err := getUpdatedConfigs(ctx, fakeClient, flags, containerSpec)
  1237  			assert.NilError(t, err)
  1238  
  1239  			// ensure that the finalConfigs consists of all of the expected
  1240  			// configs
  1241  			assert.Equal(t, len(finalConfigs), len(tc.expected),
  1242  				"%v final configs, %v expected",
  1243  				len(finalConfigs), len(tc.expected),
  1244  			)
  1245  			for _, expected := range tc.expected {
  1246  				assert.Assert(t, is.Contains(finalConfigs, cannedConfigRefs[expected]))
  1247  			}
  1248  		})
  1249  	}
  1250  }
  1251  
  1252  func TestUpdateCredSpec(t *testing.T) {
  1253  	type testCase struct {
  1254  		// name is the name of the subtest
  1255  		name string
  1256  		// flagVal is the value we're setting flagCredentialSpec to
  1257  		flagVal string
  1258  		// spec is the existing serviceSpec with its configs
  1259  		spec *swarm.ContainerSpec
  1260  		// expected is the expected value of the credential spec after the
  1261  		// function. it may be nil
  1262  		expected *swarm.CredentialSpec
  1263  	}
  1264  
  1265  	testCases := []testCase{
  1266  		{
  1267  			name:     "add file credential spec",
  1268  			flagVal:  "file://somefile",
  1269  			spec:     &swarm.ContainerSpec{},
  1270  			expected: &swarm.CredentialSpec{File: "somefile"},
  1271  		}, {
  1272  			name:    "remove a file credential spec",
  1273  			flagVal: "",
  1274  			spec: &swarm.ContainerSpec{
  1275  				Privileges: &swarm.Privileges{
  1276  					CredentialSpec: &swarm.CredentialSpec{
  1277  						File: "someFile",
  1278  					},
  1279  				},
  1280  			},
  1281  			expected: nil,
  1282  		}, {
  1283  			name:     "remove when no CredentialSpec exists",
  1284  			flagVal:  "",
  1285  			spec:     &swarm.ContainerSpec{},
  1286  			expected: nil,
  1287  		}, {
  1288  			name:    "add a config credential spec",
  1289  			flagVal: "config://someConfigName",
  1290  			spec: &swarm.ContainerSpec{
  1291  				Configs: []*swarm.ConfigReference{
  1292  					{
  1293  						ConfigName: "someConfigName",
  1294  						ConfigID:   "someConfigID",
  1295  						Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1296  					},
  1297  				},
  1298  			},
  1299  			expected: &swarm.CredentialSpec{
  1300  				Config: "someConfigID",
  1301  			},
  1302  		}, {
  1303  			name:    "remove a config credential spec",
  1304  			flagVal: "",
  1305  			spec: &swarm.ContainerSpec{
  1306  				Privileges: &swarm.Privileges{
  1307  					CredentialSpec: &swarm.CredentialSpec{
  1308  						Config: "someConfigID",
  1309  					},
  1310  				},
  1311  			},
  1312  			expected: nil,
  1313  		}, {
  1314  			name:    "update a config credential spec",
  1315  			flagVal: "config://someConfigName",
  1316  			spec: &swarm.ContainerSpec{
  1317  				Configs: []*swarm.ConfigReference{
  1318  					{
  1319  						ConfigName: "someConfigName",
  1320  						ConfigID:   "someConfigID",
  1321  						Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1322  					},
  1323  				},
  1324  				Privileges: &swarm.Privileges{
  1325  					CredentialSpec: &swarm.CredentialSpec{
  1326  						Config: "someDifferentConfigID",
  1327  					},
  1328  				},
  1329  			},
  1330  			expected: &swarm.CredentialSpec{
  1331  				Config: "someConfigID",
  1332  			},
  1333  		},
  1334  	}
  1335  
  1336  	for _, tc := range testCases {
  1337  		t.Run(tc.name, func(t *testing.T) {
  1338  			flags := newUpdateCommand(nil).Flags()
  1339  			flags.Set(flagCredentialSpec, tc.flagVal)
  1340  
  1341  			updateCredSpecConfig(flags, tc.spec)
  1342  			// handle the case where tc.spec.Privileges is nil
  1343  			if tc.expected == nil {
  1344  				assert.Assert(t, tc.spec.Privileges == nil || tc.spec.Privileges.CredentialSpec == nil)
  1345  				return
  1346  			}
  1347  
  1348  			assert.Assert(t, tc.spec.Privileges != nil)
  1349  			assert.DeepEqual(t, tc.spec.Privileges.CredentialSpec, tc.expected)
  1350  		})
  1351  	}
  1352  }
  1353  
  1354  func TestUpdateCaps(t *testing.T) {
  1355  	tests := []struct {
  1356  		// name is the name of the testcase
  1357  		name string
  1358  		// flagAdd is the value passed to --cap-add
  1359  		flagAdd []string
  1360  		// flagDrop is the value passed to --cap-drop
  1361  		flagDrop []string
  1362  		// spec is the original ContainerSpec, before being updated
  1363  		spec *swarm.ContainerSpec
  1364  		// expectedAdd is the set of requested caps the ContainerSpec should have once updated
  1365  		expectedAdd []string
  1366  		// expectedDrop is the set of dropped caps the ContainerSpec should have once updated
  1367  		expectedDrop []string
  1368  	}{
  1369  		{
  1370  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1371  			name: "Empty spec, no updates",
  1372  			spec: &swarm.ContainerSpec{},
  1373  		},
  1374  		{
  1375  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1376  			name: "No updates",
  1377  			spec: &swarm.ContainerSpec{
  1378  				CapabilityAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1379  				CapabilityDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1380  			},
  1381  			expectedAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1382  			expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1383  		},
  1384  		{
  1385  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1386  			name:     "Empty updates",
  1387  			flagAdd:  []string{},
  1388  			flagDrop: []string{},
  1389  			spec: &swarm.ContainerSpec{
  1390  				CapabilityAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1391  				CapabilityDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1392  			},
  1393  			expectedAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1394  			expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1395  		},
  1396  		{
  1397  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1398  			name:     "Normalize cap-add only",
  1399  			flagAdd:  []string{},
  1400  			flagDrop: []string{},
  1401  			spec: &swarm.ContainerSpec{
  1402  				CapabilityAdd: []string{"ALL", "CAP_MOUNT", "CAP_NET_ADMIN"},
  1403  			},
  1404  			expectedAdd:  []string{"ALL"},
  1405  			expectedDrop: nil,
  1406  		},
  1407  		{
  1408  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1409  			name: "Normalize cap-drop only",
  1410  			spec: &swarm.ContainerSpec{
  1411  				CapabilityDrop: []string{"ALL", "CAP_MOUNT", "CAP_NET_ADMIN"},
  1412  			},
  1413  			expectedDrop: []string{"ALL"},
  1414  		},
  1415  		{
  1416  			name:         "Add new caps",
  1417  			flagAdd:      []string{"CAP_NET_ADMIN"},
  1418  			flagDrop:     []string{},
  1419  			spec:         &swarm.ContainerSpec{},
  1420  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1421  			expectedDrop: nil,
  1422  		},
  1423  		{
  1424  			name:         "Drop new caps",
  1425  			flagAdd:      []string{},
  1426  			flagDrop:     []string{"CAP_NET_ADMIN"},
  1427  			spec:         &swarm.ContainerSpec{},
  1428  			expectedAdd:  nil,
  1429  			expectedDrop: []string{"CAP_NET_ADMIN"},
  1430  		},
  1431  		{
  1432  			name:     "Add a previously dropped cap",
  1433  			flagAdd:  []string{"CAP_NET_ADMIN"},
  1434  			flagDrop: []string{},
  1435  			spec: &swarm.ContainerSpec{
  1436  				CapabilityDrop: []string{"CAP_NET_ADMIN"},
  1437  			},
  1438  			expectedAdd:  nil,
  1439  			expectedDrop: nil,
  1440  		},
  1441  		{
  1442  			name:     "Drop a previously requested cap, and add a new one",
  1443  			flagAdd:  []string{"CAP_CHOWN"},
  1444  			flagDrop: []string{"CAP_NET_ADMIN"},
  1445  			spec: &swarm.ContainerSpec{
  1446  				CapabilityAdd: []string{"CAP_NET_ADMIN"},
  1447  			},
  1448  			expectedAdd:  []string{"CAP_CHOWN"},
  1449  			expectedDrop: nil,
  1450  		},
  1451  		{
  1452  			name:    "Add caps to service that has ALL caps has no effect",
  1453  			flagAdd: []string{"CAP_NET_ADMIN"},
  1454  			spec: &swarm.ContainerSpec{
  1455  				CapabilityAdd: []string{"ALL"},
  1456  			},
  1457  			expectedAdd:  []string{"ALL"},
  1458  			expectedDrop: nil,
  1459  		},
  1460  		{
  1461  			name:     "Drop ALL caps, then add new caps to service that has ALL caps",
  1462  			flagAdd:  []string{"CAP_NET_ADMIN"},
  1463  			flagDrop: []string{"ALL"},
  1464  			spec: &swarm.ContainerSpec{
  1465  				CapabilityAdd: []string{"ALL"},
  1466  			},
  1467  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1468  			expectedDrop: nil,
  1469  		},
  1470  		{
  1471  			name:         "Add takes precedence on empty spec",
  1472  			flagAdd:      []string{"CAP_NET_ADMIN"},
  1473  			flagDrop:     []string{"CAP_NET_ADMIN"},
  1474  			spec:         &swarm.ContainerSpec{},
  1475  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1476  			expectedDrop: nil,
  1477  		},
  1478  		{
  1479  			name:     "Add takes precedence on existing spec",
  1480  			flagAdd:  []string{"CAP_NET_ADMIN"},
  1481  			flagDrop: []string{"CAP_NET_ADMIN"},
  1482  			spec: &swarm.ContainerSpec{
  1483  				CapabilityAdd:  []string{"CAP_NET_ADMIN"},
  1484  				CapabilityDrop: []string{"CAP_NET_ADMIN"},
  1485  			},
  1486  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1487  			expectedDrop: nil,
  1488  		},
  1489  		{
  1490  			name:     "Drop all, and add new caps",
  1491  			flagAdd:  []string{"CAP_CHOWN"},
  1492  			flagDrop: []string{"ALL"},
  1493  			spec: &swarm.ContainerSpec{
  1494  				CapabilityAdd:  []string{"CAP_NET_ADMIN", "CAP_MOUNT"},
  1495  				CapabilityDrop: []string{"CAP_NET_ADMIN", "CAP_MOUNT"},
  1496  			},
  1497  			expectedAdd:  []string{"CAP_CHOWN", "CAP_MOUNT", "CAP_NET_ADMIN"},
  1498  			expectedDrop: []string{"ALL"},
  1499  		},
  1500  		{
  1501  			name:     "Add all caps",
  1502  			flagAdd:  []string{"ALL"},
  1503  			flagDrop: []string{"CAP_NET_ADMIN", "CAP_SYS_ADMIN"},
  1504  			spec: &swarm.ContainerSpec{
  1505  				CapabilityAdd:  []string{"CAP_NET_ADMIN"},
  1506  				CapabilityDrop: []string{"CAP_CHOWN"},
  1507  			},
  1508  			expectedAdd:  []string{"ALL"},
  1509  			expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1510  		},
  1511  		{
  1512  			name:     "Drop all, and add all",
  1513  			flagAdd:  []string{"ALL"},
  1514  			flagDrop: []string{"ALL"},
  1515  			spec: &swarm.ContainerSpec{
  1516  				CapabilityAdd:  []string{"CAP_NET_ADMIN"},
  1517  				CapabilityDrop: []string{"CAP_CHOWN"},
  1518  			},
  1519  			expectedAdd:  []string{"ALL"},
  1520  			expectedDrop: []string{"CAP_CHOWN"},
  1521  		},
  1522  		{
  1523  			name:     "Caps are normalized and sorted",
  1524  			flagAdd:  []string{"bbb", "aaa", "cAp_bBb", "cAp_aAa"},
  1525  			flagDrop: []string{"zzz", "yyy", "cAp_yYy", "cAp_yYy"},
  1526  			spec: &swarm.ContainerSpec{
  1527  				CapabilityAdd:  []string{"ccc", "CAP_DDD"},
  1528  				CapabilityDrop: []string{"www", "CAP_XXX"},
  1529  			},
  1530  			expectedAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1531  			expectedDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1532  		},
  1533  		{
  1534  			name:     "Reset capabilities",
  1535  			flagAdd:  []string{"RESET"},
  1536  			flagDrop: []string{"RESET"},
  1537  			spec: &swarm.ContainerSpec{
  1538  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1539  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1540  			},
  1541  			expectedAdd:  nil,
  1542  			expectedDrop: nil,
  1543  		},
  1544  		{
  1545  			name:     "Reset capabilities, and update after",
  1546  			flagAdd:  []string{"RESET", "CAP_ADD_ONE", "CAP_FOO"},
  1547  			flagDrop: []string{"RESET", "CAP_DROP_ONE", "CAP_FOO"},
  1548  			spec: &swarm.ContainerSpec{
  1549  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1550  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1551  			},
  1552  			expectedAdd:  []string{"CAP_ADD_ONE", "CAP_FOO"},
  1553  			expectedDrop: []string{"CAP_DROP_ONE"},
  1554  		},
  1555  		{
  1556  			name:     "Reset capabilities, and add ALL",
  1557  			flagAdd:  []string{"RESET", "ALL"},
  1558  			flagDrop: []string{"RESET", "ALL"},
  1559  			spec: &swarm.ContainerSpec{
  1560  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1561  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1562  			},
  1563  			expectedAdd:  []string{"ALL"},
  1564  			expectedDrop: nil,
  1565  		},
  1566  		{
  1567  			name:     "Add ALL and RESET",
  1568  			flagAdd:  []string{"ALL", "RESET"},
  1569  			flagDrop: []string{"ALL", "RESET"},
  1570  			spec: &swarm.ContainerSpec{
  1571  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1572  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1573  			},
  1574  			expectedAdd:  []string{"ALL"},
  1575  			expectedDrop: nil,
  1576  		},
  1577  	}
  1578  
  1579  	for _, tc := range tests {
  1580  		t.Run(tc.name, func(t *testing.T) {
  1581  			flags := newUpdateCommand(nil).Flags()
  1582  			for _, c := range tc.flagAdd {
  1583  				_ = flags.Set(flagCapAdd, c)
  1584  			}
  1585  			for _, c := range tc.flagDrop {
  1586  				_ = flags.Set(flagCapDrop, c)
  1587  			}
  1588  
  1589  			updateCapabilities(flags, tc.spec)
  1590  
  1591  			assert.DeepEqual(t, tc.spec.CapabilityAdd, tc.expectedAdd)
  1592  			assert.DeepEqual(t, tc.spec.CapabilityDrop, tc.expectedDrop)
  1593  		})
  1594  	}
  1595  }
  1596  
  1597  func TestUpdateUlimits(t *testing.T) {
  1598  	ctx := context.Background()
  1599  
  1600  	tests := []struct {
  1601  		name     string
  1602  		spec     []*units.Ulimit
  1603  		rm       []string
  1604  		add      []string
  1605  		expected []*units.Ulimit
  1606  	}{
  1607  		{
  1608  			name: "from scratch",
  1609  			add:  []string{"nofile=512:1024", "core=1024:1024"},
  1610  			expected: []*units.Ulimit{
  1611  				{Name: "core", Hard: 1024, Soft: 1024},
  1612  				{Name: "nofile", Hard: 1024, Soft: 512},
  1613  			},
  1614  		},
  1615  		{
  1616  			name: "append new",
  1617  			spec: []*units.Ulimit{
  1618  				{Name: "nofile", Hard: 1024, Soft: 512},
  1619  			},
  1620  			add: []string{"core=1024:1024"},
  1621  			expected: []*units.Ulimit{
  1622  				{Name: "core", Hard: 1024, Soft: 1024},
  1623  				{Name: "nofile", Hard: 1024, Soft: 512},
  1624  			},
  1625  		},
  1626  		{
  1627  			name: "remove and append new should append",
  1628  			spec: []*units.Ulimit{
  1629  				{Name: "core", Hard: 1024, Soft: 1024},
  1630  				{Name: "nofile", Hard: 1024, Soft: 512},
  1631  			},
  1632  			rm:  []string{"nofile=512:1024"},
  1633  			add: []string{"nofile=512:1024"},
  1634  			expected: []*units.Ulimit{
  1635  				{Name: "core", Hard: 1024, Soft: 1024},
  1636  				{Name: "nofile", Hard: 1024, Soft: 512},
  1637  			},
  1638  		},
  1639  		{
  1640  			name: "update existing",
  1641  			spec: []*units.Ulimit{
  1642  				{Name: "nofile", Hard: 2048, Soft: 1024},
  1643  			},
  1644  			add: []string{"nofile=512:1024"},
  1645  			expected: []*units.Ulimit{
  1646  				{Name: "nofile", Hard: 1024, Soft: 512},
  1647  			},
  1648  		},
  1649  		{
  1650  			name: "update existing twice",
  1651  			spec: []*units.Ulimit{
  1652  				{Name: "nofile", Hard: 2048, Soft: 1024},
  1653  			},
  1654  			add: []string{"nofile=256:512", "nofile=512:1024"},
  1655  			expected: []*units.Ulimit{
  1656  				{Name: "nofile", Hard: 1024, Soft: 512},
  1657  			},
  1658  		},
  1659  		{
  1660  			name: "remove all",
  1661  			spec: []*units.Ulimit{
  1662  				{Name: "core", Hard: 1024, Soft: 1024},
  1663  				{Name: "nofile", Hard: 1024, Soft: 512},
  1664  			},
  1665  			rm:       []string{"nofile=512:1024", "core=1024:1024"},
  1666  			expected: nil,
  1667  		},
  1668  		{
  1669  			name: "remove by key",
  1670  			spec: []*units.Ulimit{
  1671  				{Name: "core", Hard: 1024, Soft: 1024},
  1672  				{Name: "nofile", Hard: 1024, Soft: 512},
  1673  			},
  1674  			rm: []string{"core"},
  1675  			expected: []*units.Ulimit{
  1676  				{Name: "nofile", Hard: 1024, Soft: 512},
  1677  			},
  1678  		},
  1679  		{
  1680  			name: "remove by key and different value",
  1681  			spec: []*units.Ulimit{
  1682  				{Name: "core", Hard: 1024, Soft: 1024},
  1683  				{Name: "nofile", Hard: 1024, Soft: 512},
  1684  			},
  1685  			rm: []string{"core=1234:5678"},
  1686  			expected: []*units.Ulimit{
  1687  				{Name: "nofile", Hard: 1024, Soft: 512},
  1688  			},
  1689  		},
  1690  	}
  1691  
  1692  	for _, tc := range tests {
  1693  		tc := tc
  1694  		t.Run(tc.name, func(t *testing.T) {
  1695  			svc := swarm.ServiceSpec{
  1696  				TaskTemplate: swarm.TaskSpec{
  1697  					ContainerSpec: &swarm.ContainerSpec{Ulimits: tc.spec},
  1698  				},
  1699  			}
  1700  			flags := newUpdateCommand(nil).Flags()
  1701  			for _, v := range tc.add {
  1702  				assert.NilError(t, flags.Set(flagUlimitAdd, v))
  1703  			}
  1704  			for _, v := range tc.rm {
  1705  				assert.NilError(t, flags.Set(flagUlimitRemove, v))
  1706  			}
  1707  			err := updateService(ctx, &fakeClient{}, flags, &svc)
  1708  			assert.NilError(t, err)
  1709  			assert.DeepEqual(t, svc.TaskTemplate.ContainerSpec.Ulimits, tc.expected)
  1710  		})
  1711  	}
  1712  }