github.com/xeptore/docker-cli@v20.10.14+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.ErrorContains(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address")
   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(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
   508  	return s.listResult, nil
   509  }
   510  func (s secretAPIClientMock) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
   511  	return types.SecretCreateResponse{}, nil
   512  }
   513  func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error {
   514  	return nil
   515  }
   516  func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) {
   517  	return swarm.Secret{}, []byte{}, nil
   518  }
   519  func (s secretAPIClientMock) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
   520  	return nil
   521  }
   522  
   523  // TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update"
   524  // by combining "--secret-rm" and "--secret-add" for the same secret.
   525  func TestUpdateSecretUpdateInPlace(t *testing.T) {
   526  	apiClient := secretAPIClientMock{
   527  		listResult: []swarm.Secret{
   528  			{
   529  				ID:   "tn9qiblgnuuut11eufquw5dev",
   530  				Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}},
   531  			},
   532  		},
   533  	}
   534  
   535  	flags := newUpdateCommand(nil).Flags()
   536  	flags.Set("secret-add", "source=foo,target=foo2")
   537  	flags.Set("secret-rm", "foo")
   538  
   539  	secrets := []*swarm.SecretReference{
   540  		{
   541  			File: &swarm.SecretReferenceFileTarget{
   542  				Name: "foo",
   543  				UID:  "0",
   544  				GID:  "0",
   545  				Mode: 292,
   546  			},
   547  			SecretID:   "tn9qiblgnuuut11eufquw5dev",
   548  			SecretName: "foo",
   549  		},
   550  	}
   551  
   552  	updatedSecrets, err := getUpdatedSecrets(apiClient, flags, secrets)
   553  
   554  	assert.NilError(t, err)
   555  	assert.Assert(t, is.Len(updatedSecrets, 1))
   556  	assert.Check(t, is.Equal("tn9qiblgnuuut11eufquw5dev", updatedSecrets[0].SecretID))
   557  	assert.Check(t, is.Equal("foo", updatedSecrets[0].SecretName))
   558  	assert.Check(t, is.Equal("foo2", updatedSecrets[0].File.Name))
   559  }
   560  
   561  func TestUpdateReadOnly(t *testing.T) {
   562  	spec := &swarm.ServiceSpec{
   563  		TaskTemplate: swarm.TaskSpec{
   564  			ContainerSpec: &swarm.ContainerSpec{},
   565  		},
   566  	}
   567  	cspec := spec.TaskTemplate.ContainerSpec
   568  
   569  	// Update with --read-only=true, changed to true
   570  	flags := newUpdateCommand(nil).Flags()
   571  	flags.Set("read-only", "true")
   572  	updateService(context.TODO(), nil, flags, spec)
   573  	assert.Check(t, cspec.ReadOnly)
   574  
   575  	// Update without --read-only, no change
   576  	flags = newUpdateCommand(nil).Flags()
   577  	updateService(context.TODO(), nil, flags, spec)
   578  	assert.Check(t, cspec.ReadOnly)
   579  
   580  	// Update with --read-only=false, changed to false
   581  	flags = newUpdateCommand(nil).Flags()
   582  	flags.Set("read-only", "false")
   583  	updateService(context.TODO(), nil, flags, spec)
   584  	assert.Check(t, !cspec.ReadOnly)
   585  }
   586  
   587  func TestUpdateInit(t *testing.T) {
   588  	spec := &swarm.ServiceSpec{
   589  		TaskTemplate: swarm.TaskSpec{
   590  			ContainerSpec: &swarm.ContainerSpec{},
   591  		},
   592  	}
   593  	cspec := spec.TaskTemplate.ContainerSpec
   594  
   595  	// Update with --init=true
   596  	flags := newUpdateCommand(nil).Flags()
   597  	flags.Set("init", "true")
   598  	updateService(context.TODO(), nil, flags, spec)
   599  	assert.Check(t, is.Equal(true, *cspec.Init))
   600  
   601  	// Update without --init, no change
   602  	flags = newUpdateCommand(nil).Flags()
   603  	updateService(context.TODO(), nil, flags, spec)
   604  	assert.Check(t, is.Equal(true, *cspec.Init))
   605  
   606  	// Update with --init=false
   607  	flags = newUpdateCommand(nil).Flags()
   608  	flags.Set("init", "false")
   609  	updateService(context.TODO(), nil, flags, spec)
   610  	assert.Check(t, is.Equal(false, *cspec.Init))
   611  }
   612  
   613  func TestUpdateStopSignal(t *testing.T) {
   614  	spec := &swarm.ServiceSpec{
   615  		TaskTemplate: swarm.TaskSpec{
   616  			ContainerSpec: &swarm.ContainerSpec{},
   617  		},
   618  	}
   619  	cspec := spec.TaskTemplate.ContainerSpec
   620  
   621  	// Update with --stop-signal=SIGUSR1
   622  	flags := newUpdateCommand(nil).Flags()
   623  	flags.Set("stop-signal", "SIGUSR1")
   624  	updateService(context.TODO(), nil, flags, spec)
   625  	assert.Check(t, is.Equal("SIGUSR1", cspec.StopSignal))
   626  
   627  	// Update without --stop-signal, no change
   628  	flags = newUpdateCommand(nil).Flags()
   629  	updateService(context.TODO(), nil, flags, spec)
   630  	assert.Check(t, is.Equal("SIGUSR1", cspec.StopSignal))
   631  
   632  	// Update with --stop-signal=SIGWINCH
   633  	flags = newUpdateCommand(nil).Flags()
   634  	flags.Set("stop-signal", "SIGWINCH")
   635  	updateService(context.TODO(), nil, flags, spec)
   636  	assert.Check(t, is.Equal("SIGWINCH", cspec.StopSignal))
   637  }
   638  
   639  func TestUpdateIsolationValid(t *testing.T) {
   640  	flags := newUpdateCommand(nil).Flags()
   641  	err := flags.Set("isolation", "process")
   642  	assert.NilError(t, err)
   643  	spec := swarm.ServiceSpec{
   644  		TaskTemplate: swarm.TaskSpec{
   645  			ContainerSpec: &swarm.ContainerSpec{},
   646  		},
   647  	}
   648  	err = updateService(context.Background(), nil, flags, &spec)
   649  	assert.NilError(t, err)
   650  	assert.Check(t, is.Equal(container.IsolationProcess, spec.TaskTemplate.ContainerSpec.Isolation))
   651  }
   652  
   653  // TestUpdateLimitsReservations tests that limits and reservations are updated,
   654  // and that values are not updated are not reset to their default value
   655  func TestUpdateLimitsReservations(t *testing.T) {
   656  	// test that updating works if the service did not previously
   657  	// have limits set (https://github.com/moby/moby/issues/38363)
   658  	t.Run("update limits from scratch", func(t *testing.T) {
   659  		spec := swarm.ServiceSpec{
   660  			TaskTemplate: swarm.TaskSpec{
   661  				ContainerSpec: &swarm.ContainerSpec{},
   662  			},
   663  		}
   664  		flags := newUpdateCommand(nil).Flags()
   665  		err := flags.Set(flagLimitCPU, "2")
   666  		assert.NilError(t, err)
   667  		err = flags.Set(flagLimitMemory, "200M")
   668  		assert.NilError(t, err)
   669  		err = flags.Set(flagLimitPids, "100")
   670  		assert.NilError(t, err)
   671  		err = updateService(context.Background(), nil, flags, &spec)
   672  		assert.NilError(t, err)
   673  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   674  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   675  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   676  	})
   677  
   678  	// test that updating works if the service did not previously
   679  	// have reservations set (https://github.com/moby/moby/issues/38363)
   680  	t.Run("update reservations from scratch", func(t *testing.T) {
   681  		spec := swarm.ServiceSpec{
   682  			TaskTemplate: swarm.TaskSpec{
   683  				ContainerSpec: &swarm.ContainerSpec{},
   684  			},
   685  		}
   686  		flags := newUpdateCommand(nil).Flags()
   687  		err := flags.Set(flagReserveCPU, "2")
   688  		assert.NilError(t, err)
   689  		err = flags.Set(flagReserveMemory, "200M")
   690  		assert.NilError(t, err)
   691  		err = updateService(context.Background(), nil, flags, &spec)
   692  		assert.NilError(t, err)
   693  	})
   694  
   695  	spec := swarm.ServiceSpec{
   696  		TaskTemplate: swarm.TaskSpec{
   697  			ContainerSpec: &swarm.ContainerSpec{},
   698  			Resources: &swarm.ResourceRequirements{
   699  				Limits: &swarm.Limit{
   700  					NanoCPUs:    1000000000,
   701  					MemoryBytes: 104857600,
   702  					Pids:        100,
   703  				},
   704  				Reservations: &swarm.Resources{
   705  					NanoCPUs:    1000000000,
   706  					MemoryBytes: 104857600,
   707  				},
   708  			},
   709  		},
   710  	}
   711  
   712  	// Updating without flags set should not modify existing values
   713  	t.Run("update without flags set", func(t *testing.T) {
   714  		flags := newUpdateCommand(nil).Flags()
   715  		err := updateService(context.Background(), nil, flags, &spec)
   716  		assert.NilError(t, err)
   717  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(1000000000)))
   718  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(104857600)))
   719  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   720  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(1000000000)))
   721  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(104857600)))
   722  	})
   723  
   724  	// Updating CPU limit/reservation should not affect memory limit/reservation
   725  	// and pids-limt
   726  	t.Run("update cpu limit and reservation", func(t *testing.T) {
   727  		flags := newUpdateCommand(nil).Flags()
   728  		err := flags.Set(flagLimitCPU, "2")
   729  		assert.NilError(t, err)
   730  		err = flags.Set(flagReserveCPU, "2")
   731  		assert.NilError(t, err)
   732  		err = updateService(context.Background(), nil, flags, &spec)
   733  		assert.NilError(t, err)
   734  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   735  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(104857600)))
   736  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   737  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   738  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(104857600)))
   739  	})
   740  
   741  	// Updating Memory limit/reservation should not affect CPU limit/reservation
   742  	// and pids-limt
   743  	t.Run("update memory limit and reservation", func(t *testing.T) {
   744  		flags := newUpdateCommand(nil).Flags()
   745  		err := flags.Set(flagLimitMemory, "200M")
   746  		assert.NilError(t, err)
   747  		err = flags.Set(flagReserveMemory, "200M")
   748  		assert.NilError(t, err)
   749  		err = updateService(context.Background(), nil, flags, &spec)
   750  		assert.NilError(t, err)
   751  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   752  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   753  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(100)))
   754  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   755  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
   756  	})
   757  
   758  	// Updating PidsLimit should only modify PidsLimit, other values unchanged
   759  	t.Run("update pids limit", func(t *testing.T) {
   760  		flags := newUpdateCommand(nil).Flags()
   761  		err := flags.Set(flagLimitPids, "2")
   762  		assert.NilError(t, err)
   763  		err = updateService(context.Background(), nil, flags, &spec)
   764  		assert.NilError(t, err)
   765  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   766  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   767  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(2)))
   768  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   769  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
   770  	})
   771  
   772  	t.Run("update pids limit to default", func(t *testing.T) {
   773  		// Updating PidsLimit to 0 should work
   774  		flags := newUpdateCommand(nil).Flags()
   775  		err := flags.Set(flagLimitPids, "0")
   776  		assert.NilError(t, err)
   777  		err = updateService(context.Background(), nil, flags, &spec)
   778  		assert.NilError(t, err)
   779  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
   780  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
   781  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.Pids, int64(0)))
   782  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
   783  		assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
   784  	})
   785  }
   786  
   787  func TestUpdateIsolationInvalid(t *testing.T) {
   788  	// validation depends on daemon os / version so validation should be done on the daemon side
   789  	flags := newUpdateCommand(nil).Flags()
   790  	err := flags.Set("isolation", "test")
   791  	assert.NilError(t, err)
   792  	spec := swarm.ServiceSpec{
   793  		TaskTemplate: swarm.TaskSpec{
   794  			ContainerSpec: &swarm.ContainerSpec{},
   795  		},
   796  	}
   797  	err = updateService(context.Background(), nil, flags, &spec)
   798  	assert.NilError(t, err)
   799  	assert.Check(t, is.Equal(container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation))
   800  }
   801  
   802  func TestAddGenericResources(t *testing.T) {
   803  	task := &swarm.TaskSpec{}
   804  	flags := newUpdateCommand(nil).Flags()
   805  
   806  	assert.Check(t, addGenericResources(flags, task))
   807  
   808  	flags.Set(flagGenericResourcesAdd, "foo=1")
   809  	assert.Check(t, addGenericResources(flags, task))
   810  	assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 1))
   811  
   812  	// Checks that foo isn't added a 2nd time
   813  	flags = newUpdateCommand(nil).Flags()
   814  	flags.Set(flagGenericResourcesAdd, "bar=1")
   815  	assert.Check(t, addGenericResources(flags, task))
   816  	assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 2))
   817  }
   818  
   819  func TestRemoveGenericResources(t *testing.T) {
   820  	task := &swarm.TaskSpec{}
   821  	flags := newUpdateCommand(nil).Flags()
   822  
   823  	assert.Check(t, removeGenericResources(flags, task))
   824  
   825  	flags.Set(flagGenericResourcesRemove, "foo")
   826  	assert.Check(t, is.ErrorContains(removeGenericResources(flags, task), ""))
   827  
   828  	flags = newUpdateCommand(nil).Flags()
   829  	flags.Set(flagGenericResourcesAdd, "foo=1")
   830  	addGenericResources(flags, task)
   831  	flags = newUpdateCommand(nil).Flags()
   832  	flags.Set(flagGenericResourcesAdd, "bar=1")
   833  	addGenericResources(flags, task)
   834  
   835  	flags = newUpdateCommand(nil).Flags()
   836  	flags.Set(flagGenericResourcesRemove, "foo")
   837  	assert.Check(t, removeGenericResources(flags, task))
   838  	assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 1))
   839  }
   840  
   841  func TestUpdateNetworks(t *testing.T) {
   842  	ctx := context.Background()
   843  	nws := []types.NetworkResource{
   844  		{Name: "aaa-network", ID: "id555"},
   845  		{Name: "mmm-network", ID: "id999"},
   846  		{Name: "zzz-network", ID: "id111"},
   847  	}
   848  
   849  	client := &fakeClient{
   850  		networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
   851  			for _, network := range nws {
   852  				if network.ID == networkID || network.Name == networkID {
   853  					return network, nil
   854  				}
   855  			}
   856  			return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
   857  		},
   858  	}
   859  
   860  	svc := swarm.ServiceSpec{
   861  		TaskTemplate: swarm.TaskSpec{
   862  			ContainerSpec: &swarm.ContainerSpec{},
   863  			Networks: []swarm.NetworkAttachmentConfig{
   864  				{Target: "id999"},
   865  			},
   866  		},
   867  	}
   868  
   869  	flags := newUpdateCommand(nil).Flags()
   870  	err := flags.Set(flagNetworkAdd, "aaa-network")
   871  	assert.NilError(t, err)
   872  	err = updateService(ctx, client, flags, &svc)
   873  	assert.NilError(t, err)
   874  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
   875  
   876  	flags = newUpdateCommand(nil).Flags()
   877  	err = flags.Set(flagNetworkAdd, "aaa-network")
   878  	assert.NilError(t, err)
   879  	err = updateService(ctx, client, flags, &svc)
   880  	assert.Error(t, err, "service is already attached to network aaa-network")
   881  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
   882  
   883  	flags = newUpdateCommand(nil).Flags()
   884  	err = flags.Set(flagNetworkAdd, "id555")
   885  	assert.NilError(t, err)
   886  	err = updateService(ctx, client, flags, &svc)
   887  	assert.Error(t, err, "service is already attached to network id555")
   888  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
   889  
   890  	flags = newUpdateCommand(nil).Flags()
   891  	err = flags.Set(flagNetworkRemove, "id999")
   892  	assert.NilError(t, err)
   893  	err = updateService(ctx, client, flags, &svc)
   894  	assert.NilError(t, err)
   895  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks))
   896  
   897  	flags = newUpdateCommand(nil).Flags()
   898  	err = flags.Set(flagNetworkAdd, "mmm-network")
   899  	assert.NilError(t, err)
   900  	err = flags.Set(flagNetworkRemove, "aaa-network")
   901  	assert.NilError(t, err)
   902  	err = updateService(ctx, client, flags, &svc)
   903  	assert.NilError(t, err)
   904  	assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks))
   905  }
   906  
   907  func TestUpdateMaxReplicas(t *testing.T) {
   908  	ctx := context.Background()
   909  
   910  	svc := swarm.ServiceSpec{
   911  		TaskTemplate: swarm.TaskSpec{
   912  			ContainerSpec: &swarm.ContainerSpec{},
   913  			Placement: &swarm.Placement{
   914  				MaxReplicas: 1,
   915  			},
   916  		},
   917  	}
   918  
   919  	flags := newUpdateCommand(nil).Flags()
   920  	flags.Set(flagMaxReplicas, "2")
   921  	err := updateService(ctx, nil, flags, &svc)
   922  	assert.NilError(t, err)
   923  
   924  	assert.DeepEqual(t, svc.TaskTemplate.Placement, &swarm.Placement{MaxReplicas: uint64(2)})
   925  }
   926  
   927  func TestUpdateSysCtls(t *testing.T) {
   928  	ctx := context.Background()
   929  
   930  	tests := []struct {
   931  		name     string
   932  		spec     map[string]string
   933  		add      []string
   934  		rm       []string
   935  		expected map[string]string
   936  	}{
   937  		{
   938  			name:     "from scratch",
   939  			add:      []string{"sysctl.zet=value-99", "sysctl.alpha=value-1"},
   940  			expected: map[string]string{"sysctl.zet": "value-99", "sysctl.alpha": "value-1"},
   941  		},
   942  		{
   943  			name:     "append new",
   944  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   945  			add:      []string{"new.sysctl=newvalue"},
   946  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
   947  		},
   948  		{
   949  			name:     "append duplicate is a no-op",
   950  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   951  			add:      []string{"sysctl.one=value-1"},
   952  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   953  		},
   954  		{
   955  			name:     "remove and append existing 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  			rm:       []string{"sysctl.one=value-1"},
   959  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   960  		},
   961  		{
   962  			name:     "remove and append new should append",
   963  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   964  			add:      []string{"new.sysctl=newvalue"},
   965  			rm:       []string{"new.sysctl=newvalue"},
   966  			expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
   967  		},
   968  		{
   969  			name:     "update existing",
   970  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   971  			add:      []string{"sysctl.one=newvalue"},
   972  			expected: map[string]string{"sysctl.one": "newvalue", "sysctl.two": "value-2"},
   973  		},
   974  		{
   975  			name:     "update existing twice",
   976  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   977  			add:      []string{"sysctl.one=newvalue", "sysctl.one=evennewervalue"},
   978  			expected: map[string]string{"sysctl.one": "evennewervalue", "sysctl.two": "value-2"},
   979  		},
   980  		{
   981  			name:     "remove all",
   982  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   983  			rm:       []string{"sysctl.one=value-1", "sysctl.two=value-2"},
   984  			expected: map[string]string{},
   985  		},
   986  		{
   987  			name:     "remove by key",
   988  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   989  			rm:       []string{"sysctl.one"},
   990  			expected: map[string]string{"sysctl.two": "value-2"},
   991  		},
   992  		{
   993  			name:     "remove by key and different value",
   994  			spec:     map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
   995  			rm:       []string{"sysctl.one=anyvalueyoulike"},
   996  			expected: map[string]string{"sysctl.two": "value-2"},
   997  		},
   998  	}
   999  
  1000  	for _, tc := range tests {
  1001  		t.Run(tc.name, func(t *testing.T) {
  1002  			svc := swarm.ServiceSpec{
  1003  				TaskTemplate: swarm.TaskSpec{
  1004  					ContainerSpec: &swarm.ContainerSpec{Sysctls: tc.spec},
  1005  				},
  1006  			}
  1007  			flags := newUpdateCommand(nil).Flags()
  1008  			for _, v := range tc.add {
  1009  				assert.NilError(t, flags.Set(flagSysCtlAdd, v))
  1010  			}
  1011  			for _, v := range tc.rm {
  1012  				assert.NilError(t, flags.Set(flagSysCtlRemove, v))
  1013  			}
  1014  			err := updateService(ctx, &fakeClient{}, flags, &svc)
  1015  			assert.NilError(t, err)
  1016  			if !assert.Check(t, is.DeepEqual(svc.TaskTemplate.ContainerSpec.Sysctls, tc.expected)) {
  1017  				t.Logf("expected: %v", tc.expected)
  1018  				t.Logf("actual: %v", svc.TaskTemplate.ContainerSpec.Sysctls)
  1019  			}
  1020  		})
  1021  	}
  1022  }
  1023  
  1024  func TestUpdateGetUpdatedConfigs(t *testing.T) {
  1025  	// cannedConfigs is a set of configs that we'll use over and over in the
  1026  	// tests. it's a map of Name to Config
  1027  	cannedConfigs := map[string]*swarm.Config{
  1028  		"bar": {
  1029  			ID: "barID",
  1030  			Spec: swarm.ConfigSpec{
  1031  				Annotations: swarm.Annotations{
  1032  					Name: "bar",
  1033  				},
  1034  			},
  1035  		},
  1036  		"cred": {
  1037  			ID: "credID",
  1038  			Spec: swarm.ConfigSpec{
  1039  				Annotations: swarm.Annotations{
  1040  					Name: "cred",
  1041  				},
  1042  			},
  1043  		},
  1044  		"newCred": {
  1045  			ID: "newCredID",
  1046  			Spec: swarm.ConfigSpec{
  1047  				Annotations: swarm.Annotations{
  1048  					Name: "newCred",
  1049  				},
  1050  			},
  1051  		},
  1052  	}
  1053  	// cannedConfigRefs is the same thing, but with config references instead
  1054  	// instead of ID, however, it just maps an arbitrary string value. this is
  1055  	// so we could have multiple config refs using the same config
  1056  	cannedConfigRefs := map[string]*swarm.ConfigReference{
  1057  		"fooRef": {
  1058  			ConfigID:   "fooID",
  1059  			ConfigName: "foo",
  1060  			File: &swarm.ConfigReferenceFileTarget{
  1061  				Name: "foo",
  1062  				UID:  "0",
  1063  				GID:  "0",
  1064  				Mode: 0444,
  1065  			},
  1066  		},
  1067  		"barRef": {
  1068  			ConfigID:   "barID",
  1069  			ConfigName: "bar",
  1070  			File: &swarm.ConfigReferenceFileTarget{
  1071  				Name: "bar",
  1072  				UID:  "0",
  1073  				GID:  "0",
  1074  				Mode: 0444,
  1075  			},
  1076  		},
  1077  		"bazRef": {
  1078  			ConfigID:   "bazID",
  1079  			ConfigName: "baz",
  1080  			File: &swarm.ConfigReferenceFileTarget{
  1081  				Name: "baz",
  1082  				UID:  "0",
  1083  				GID:  "0",
  1084  				Mode: 0444,
  1085  			},
  1086  		},
  1087  		"credRef": {
  1088  			ConfigID:   "credID",
  1089  			ConfigName: "cred",
  1090  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1091  		},
  1092  		"newCredRef": {
  1093  			ConfigID:   "newCredID",
  1094  			ConfigName: "newCred",
  1095  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1096  		},
  1097  	}
  1098  
  1099  	type flagVal [2]string
  1100  	type test struct {
  1101  		// the name of the subtest
  1102  		name string
  1103  		// flags are the flags we'll be setting
  1104  		flags []flagVal
  1105  		// oldConfigs are the configs that would already be on the service
  1106  		// it is a slice of strings corresponding to the key of
  1107  		// cannedConfigRefs
  1108  		oldConfigs []string
  1109  		// oldCredSpec is the credentialSpec being carried over from the old
  1110  		// object
  1111  		oldCredSpec *swarm.CredentialSpec
  1112  		// lookupConfigs are the configs we're expecting to be listed. it is a
  1113  		// slice of strings corresponding to the key of cannedConfigs
  1114  		lookupConfigs []string
  1115  		// expected is the configs we should get as a result. it is a slice of
  1116  		// strings corresponding to the key in cannedConfigRefs
  1117  		expected []string
  1118  	}
  1119  
  1120  	testCases := []test{
  1121  		{
  1122  			name:       "no configs added or removed",
  1123  			oldConfigs: []string{"fooRef"},
  1124  			expected:   []string{"fooRef"},
  1125  		}, {
  1126  			name:          "add a config",
  1127  			flags:         []flagVal{{"config-add", "bar"}},
  1128  			oldConfigs:    []string{"fooRef"},
  1129  			lookupConfigs: []string{"bar"},
  1130  			expected:      []string{"fooRef", "barRef"},
  1131  		}, {
  1132  			name:       "remove a config",
  1133  			flags:      []flagVal{{"config-rm", "bar"}},
  1134  			oldConfigs: []string{"fooRef", "barRef"},
  1135  			expected:   []string{"fooRef"},
  1136  		}, {
  1137  			name:        "include an old credential spec",
  1138  			oldConfigs:  []string{"credRef"},
  1139  			oldCredSpec: &swarm.CredentialSpec{Config: "credID"},
  1140  			expected:    []string{"credRef"},
  1141  		}, {
  1142  			name:          "add a credential spec",
  1143  			oldConfigs:    []string{"fooRef"},
  1144  			flags:         []flagVal{{"credential-spec", "config://cred"}},
  1145  			lookupConfigs: []string{"cred"},
  1146  			expected:      []string{"fooRef", "credRef"},
  1147  		}, {
  1148  			name:          "change a credential spec",
  1149  			oldConfigs:    []string{"fooRef", "credRef"},
  1150  			oldCredSpec:   &swarm.CredentialSpec{Config: "credID"},
  1151  			flags:         []flagVal{{"credential-spec", "config://newCred"}},
  1152  			lookupConfigs: []string{"newCred"},
  1153  			expected:      []string{"fooRef", "newCredRef"},
  1154  		}, {
  1155  			name:          "credential spec no longer config",
  1156  			oldConfigs:    []string{"fooRef", "credRef"},
  1157  			oldCredSpec:   &swarm.CredentialSpec{Config: "credID"},
  1158  			flags:         []flagVal{{"credential-spec", "file://someFile"}},
  1159  			lookupConfigs: []string{},
  1160  			expected:      []string{"fooRef"},
  1161  		}, {
  1162  			name:          "credential spec becomes config",
  1163  			oldConfigs:    []string{"fooRef"},
  1164  			oldCredSpec:   &swarm.CredentialSpec{File: "someFile"},
  1165  			flags:         []flagVal{{"credential-spec", "config://cred"}},
  1166  			lookupConfigs: []string{"cred"},
  1167  			expected:      []string{"fooRef", "credRef"},
  1168  		}, {
  1169  			name:          "remove credential spec",
  1170  			oldConfigs:    []string{"fooRef", "credRef"},
  1171  			oldCredSpec:   &swarm.CredentialSpec{Config: "credID"},
  1172  			flags:         []flagVal{{"credential-spec", ""}},
  1173  			lookupConfigs: []string{},
  1174  			expected:      []string{"fooRef"},
  1175  		}, {
  1176  			name: "just frick my stuff up",
  1177  			// a more complicated test. add barRef, remove bazRef, keep fooRef,
  1178  			// change credentialSpec from credRef to newCredRef
  1179  			oldConfigs:  []string{"fooRef", "bazRef", "credRef"},
  1180  			oldCredSpec: &swarm.CredentialSpec{Config: "cred"},
  1181  			flags: []flagVal{
  1182  				{"config-add", "bar"},
  1183  				{"config-rm", "baz"},
  1184  				{"credential-spec", "config://newCred"},
  1185  			},
  1186  			lookupConfigs: []string{"bar", "newCred"},
  1187  			expected:      []string{"fooRef", "barRef", "newCredRef"},
  1188  		},
  1189  	}
  1190  
  1191  	for _, tc := range testCases {
  1192  		t.Run(tc.name, func(t *testing.T) {
  1193  			flags := newUpdateCommand(nil).Flags()
  1194  			for _, f := range tc.flags {
  1195  				flags.Set(f[0], f[1])
  1196  			}
  1197  
  1198  			// fakeConfigAPIClientList is actually defined in create_test.go,
  1199  			// but we'll use it here as well
  1200  			var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts types.ConfigListOptions) ([]swarm.Config, error) {
  1201  				names := opts.Filters.Get("name")
  1202  				assert.Equal(t, len(names), len(tc.lookupConfigs))
  1203  
  1204  				configs := []swarm.Config{}
  1205  				for _, lookup := range tc.lookupConfigs {
  1206  					assert.Assert(t, is.Contains(names, lookup))
  1207  					cfg, ok := cannedConfigs[lookup]
  1208  					assert.Assert(t, ok)
  1209  					configs = append(configs, *cfg)
  1210  				}
  1211  				return configs, nil
  1212  			}
  1213  
  1214  			// build the actual set of old configs and the container spec
  1215  			oldConfigs := []*swarm.ConfigReference{}
  1216  			for _, config := range tc.oldConfigs {
  1217  				cfg, ok := cannedConfigRefs[config]
  1218  				assert.Assert(t, ok)
  1219  				oldConfigs = append(oldConfigs, cfg)
  1220  			}
  1221  
  1222  			containerSpec := &swarm.ContainerSpec{
  1223  				Configs: oldConfigs,
  1224  				Privileges: &swarm.Privileges{
  1225  					CredentialSpec: tc.oldCredSpec,
  1226  				},
  1227  			}
  1228  
  1229  			finalConfigs, err := getUpdatedConfigs(fakeClient, flags, containerSpec)
  1230  			assert.NilError(t, err)
  1231  
  1232  			// ensure that the finalConfigs consists of all of the expected
  1233  			// configs
  1234  			assert.Equal(t, len(finalConfigs), len(tc.expected),
  1235  				"%v final configs, %v expected",
  1236  				len(finalConfigs), len(tc.expected),
  1237  			)
  1238  			for _, expected := range tc.expected {
  1239  				assert.Assert(t, is.Contains(finalConfigs, cannedConfigRefs[expected]))
  1240  			}
  1241  		})
  1242  	}
  1243  }
  1244  
  1245  func TestUpdateCredSpec(t *testing.T) {
  1246  	type testCase struct {
  1247  		// name is the name of the subtest
  1248  		name string
  1249  		// flagVal is the value we're setting flagCredentialSpec to
  1250  		flagVal string
  1251  		// spec is the existing serviceSpec with its configs
  1252  		spec *swarm.ContainerSpec
  1253  		// expected is the expected value of the credential spec after the
  1254  		// function. it may be nil
  1255  		expected *swarm.CredentialSpec
  1256  	}
  1257  
  1258  	testCases := []testCase{
  1259  		{
  1260  			name:     "add file credential spec",
  1261  			flagVal:  "file://somefile",
  1262  			spec:     &swarm.ContainerSpec{},
  1263  			expected: &swarm.CredentialSpec{File: "somefile"},
  1264  		}, {
  1265  			name:    "remove a file credential spec",
  1266  			flagVal: "",
  1267  			spec: &swarm.ContainerSpec{
  1268  				Privileges: &swarm.Privileges{
  1269  					CredentialSpec: &swarm.CredentialSpec{
  1270  						File: "someFile",
  1271  					},
  1272  				},
  1273  			},
  1274  			expected: nil,
  1275  		}, {
  1276  			name:     "remove when no CredentialSpec exists",
  1277  			flagVal:  "",
  1278  			spec:     &swarm.ContainerSpec{},
  1279  			expected: nil,
  1280  		}, {
  1281  			name:    "add a config credenital spec",
  1282  			flagVal: "config://someConfigName",
  1283  			spec: &swarm.ContainerSpec{
  1284  				Configs: []*swarm.ConfigReference{
  1285  					{
  1286  						ConfigName: "someConfigName",
  1287  						ConfigID:   "someConfigID",
  1288  						Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1289  					},
  1290  				},
  1291  			},
  1292  			expected: &swarm.CredentialSpec{
  1293  				Config: "someConfigID",
  1294  			},
  1295  		}, {
  1296  			name:    "remove a config credential spec",
  1297  			flagVal: "",
  1298  			spec: &swarm.ContainerSpec{
  1299  				Privileges: &swarm.Privileges{
  1300  					CredentialSpec: &swarm.CredentialSpec{
  1301  						Config: "someConfigID",
  1302  					},
  1303  				},
  1304  			},
  1305  			expected: nil,
  1306  		}, {
  1307  			name:    "update a config credential spec",
  1308  			flagVal: "config://someConfigName",
  1309  			spec: &swarm.ContainerSpec{
  1310  				Configs: []*swarm.ConfigReference{
  1311  					{
  1312  						ConfigName: "someConfigName",
  1313  						ConfigID:   "someConfigID",
  1314  						Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
  1315  					},
  1316  				},
  1317  				Privileges: &swarm.Privileges{
  1318  					CredentialSpec: &swarm.CredentialSpec{
  1319  						Config: "someDifferentConfigID",
  1320  					},
  1321  				},
  1322  			},
  1323  			expected: &swarm.CredentialSpec{
  1324  				Config: "someConfigID",
  1325  			},
  1326  		},
  1327  	}
  1328  
  1329  	for _, tc := range testCases {
  1330  		t.Run(tc.name, func(t *testing.T) {
  1331  			flags := newUpdateCommand(nil).Flags()
  1332  			flags.Set(flagCredentialSpec, tc.flagVal)
  1333  
  1334  			updateCredSpecConfig(flags, tc.spec)
  1335  			// handle the case where tc.spec.Privileges is nil
  1336  			if tc.expected == nil {
  1337  				assert.Assert(t, tc.spec.Privileges == nil || tc.spec.Privileges.CredentialSpec == nil)
  1338  				return
  1339  			}
  1340  
  1341  			assert.Assert(t, tc.spec.Privileges != nil)
  1342  			assert.DeepEqual(t, tc.spec.Privileges.CredentialSpec, tc.expected)
  1343  		})
  1344  	}
  1345  }
  1346  
  1347  func TestUpdateCaps(t *testing.T) {
  1348  	tests := []struct {
  1349  		// name is the name of the testcase
  1350  		name string
  1351  		// flagAdd is the value passed to --cap-add
  1352  		flagAdd []string
  1353  		// flagDrop is the value passed to --cap-drop
  1354  		flagDrop []string
  1355  		// spec is the original ContainerSpec, before being updated
  1356  		spec *swarm.ContainerSpec
  1357  		// expectedAdd is the set of requested caps the ContainerSpec should have once updated
  1358  		expectedAdd []string
  1359  		// expectedDrop is the set of dropped caps the ContainerSpec should have once updated
  1360  		expectedDrop []string
  1361  	}{
  1362  		{
  1363  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1364  			name: "Empty spec, no updates",
  1365  			spec: &swarm.ContainerSpec{},
  1366  		},
  1367  		{
  1368  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1369  			name: "No updates",
  1370  			spec: &swarm.ContainerSpec{
  1371  				CapabilityAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1372  				CapabilityDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1373  			},
  1374  			expectedAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1375  			expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1376  		},
  1377  		{
  1378  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1379  			name:     "Empty updates",
  1380  			flagAdd:  []string{},
  1381  			flagDrop: []string{},
  1382  			spec: &swarm.ContainerSpec{
  1383  				CapabilityAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1384  				CapabilityDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1385  			},
  1386  			expectedAdd:  []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
  1387  			expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1388  		},
  1389  		{
  1390  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1391  			name:     "Normalize cap-add only",
  1392  			flagAdd:  []string{},
  1393  			flagDrop: []string{},
  1394  			spec: &swarm.ContainerSpec{
  1395  				CapabilityAdd: []string{"ALL", "CAP_MOUNT", "CAP_NET_ADMIN"},
  1396  			},
  1397  			expectedAdd:  []string{"ALL"},
  1398  			expectedDrop: nil,
  1399  		},
  1400  		{
  1401  			// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
  1402  			name: "Normalize cap-drop only",
  1403  			spec: &swarm.ContainerSpec{
  1404  				CapabilityDrop: []string{"ALL", "CAP_MOUNT", "CAP_NET_ADMIN"},
  1405  			},
  1406  			expectedDrop: []string{"ALL"},
  1407  		},
  1408  		{
  1409  			name:         "Add new caps",
  1410  			flagAdd:      []string{"CAP_NET_ADMIN"},
  1411  			flagDrop:     []string{},
  1412  			spec:         &swarm.ContainerSpec{},
  1413  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1414  			expectedDrop: nil,
  1415  		},
  1416  		{
  1417  			name:         "Drop new caps",
  1418  			flagAdd:      []string{},
  1419  			flagDrop:     []string{"CAP_NET_ADMIN"},
  1420  			spec:         &swarm.ContainerSpec{},
  1421  			expectedAdd:  nil,
  1422  			expectedDrop: []string{"CAP_NET_ADMIN"},
  1423  		},
  1424  		{
  1425  			name:     "Add a previously dropped cap",
  1426  			flagAdd:  []string{"CAP_NET_ADMIN"},
  1427  			flagDrop: []string{},
  1428  			spec: &swarm.ContainerSpec{
  1429  				CapabilityDrop: []string{"CAP_NET_ADMIN"},
  1430  			},
  1431  			expectedAdd:  nil,
  1432  			expectedDrop: nil,
  1433  		},
  1434  		{
  1435  			name:     "Drop a previously requested cap, and add a new one",
  1436  			flagAdd:  []string{"CAP_CHOWN"},
  1437  			flagDrop: []string{"CAP_NET_ADMIN"},
  1438  			spec: &swarm.ContainerSpec{
  1439  				CapabilityAdd: []string{"CAP_NET_ADMIN"},
  1440  			},
  1441  			expectedAdd:  []string{"CAP_CHOWN"},
  1442  			expectedDrop: nil,
  1443  		},
  1444  		{
  1445  			name:    "Add caps to service that has ALL caps has no effect",
  1446  			flagAdd: []string{"CAP_NET_ADMIN"},
  1447  			spec: &swarm.ContainerSpec{
  1448  				CapabilityAdd: []string{"ALL"},
  1449  			},
  1450  			expectedAdd:  []string{"ALL"},
  1451  			expectedDrop: nil,
  1452  		},
  1453  		{
  1454  			name:     "Drop ALL caps, then add new caps to service that has ALL caps",
  1455  			flagAdd:  []string{"CAP_NET_ADMIN"},
  1456  			flagDrop: []string{"ALL"},
  1457  			spec: &swarm.ContainerSpec{
  1458  				CapabilityAdd: []string{"ALL"},
  1459  			},
  1460  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1461  			expectedDrop: nil,
  1462  		},
  1463  		{
  1464  			name:         "Add takes precedence on empty spec",
  1465  			flagAdd:      []string{"CAP_NET_ADMIN"},
  1466  			flagDrop:     []string{"CAP_NET_ADMIN"},
  1467  			spec:         &swarm.ContainerSpec{},
  1468  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1469  			expectedDrop: nil,
  1470  		},
  1471  		{
  1472  			name:     "Add takes precedence on existing spec",
  1473  			flagAdd:  []string{"CAP_NET_ADMIN"},
  1474  			flagDrop: []string{"CAP_NET_ADMIN"},
  1475  			spec: &swarm.ContainerSpec{
  1476  				CapabilityAdd:  []string{"CAP_NET_ADMIN"},
  1477  				CapabilityDrop: []string{"CAP_NET_ADMIN"},
  1478  			},
  1479  			expectedAdd:  []string{"CAP_NET_ADMIN"},
  1480  			expectedDrop: nil,
  1481  		},
  1482  		{
  1483  			name:     "Drop all, and add new caps",
  1484  			flagAdd:  []string{"CAP_CHOWN"},
  1485  			flagDrop: []string{"ALL"},
  1486  			spec: &swarm.ContainerSpec{
  1487  				CapabilityAdd:  []string{"CAP_NET_ADMIN", "CAP_MOUNT"},
  1488  				CapabilityDrop: []string{"CAP_NET_ADMIN", "CAP_MOUNT"},
  1489  			},
  1490  			expectedAdd:  []string{"CAP_CHOWN", "CAP_MOUNT", "CAP_NET_ADMIN"},
  1491  			expectedDrop: []string{"ALL"},
  1492  		},
  1493  		{
  1494  			name:     "Add all caps",
  1495  			flagAdd:  []string{"ALL"},
  1496  			flagDrop: []string{"CAP_NET_ADMIN", "CAP_SYS_ADMIN"},
  1497  			spec: &swarm.ContainerSpec{
  1498  				CapabilityAdd:  []string{"CAP_NET_ADMIN"},
  1499  				CapabilityDrop: []string{"CAP_CHOWN"},
  1500  			},
  1501  			expectedAdd:  []string{"ALL"},
  1502  			expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
  1503  		},
  1504  		{
  1505  			name:     "Drop all, and add all",
  1506  			flagAdd:  []string{"ALL"},
  1507  			flagDrop: []string{"ALL"},
  1508  			spec: &swarm.ContainerSpec{
  1509  				CapabilityAdd:  []string{"CAP_NET_ADMIN"},
  1510  				CapabilityDrop: []string{"CAP_CHOWN"},
  1511  			},
  1512  			expectedAdd:  []string{"ALL"},
  1513  			expectedDrop: []string{"CAP_CHOWN"},
  1514  		},
  1515  		{
  1516  			name:     "Caps are normalized and sorted",
  1517  			flagAdd:  []string{"bbb", "aaa", "cAp_bBb", "cAp_aAa"},
  1518  			flagDrop: []string{"zzz", "yyy", "cAp_yYy", "cAp_yYy"},
  1519  			spec: &swarm.ContainerSpec{
  1520  				CapabilityAdd:  []string{"ccc", "CAP_DDD"},
  1521  				CapabilityDrop: []string{"www", "CAP_XXX"},
  1522  			},
  1523  			expectedAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1524  			expectedDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1525  		},
  1526  		{
  1527  			name:     "Reset capabilities",
  1528  			flagAdd:  []string{"RESET"},
  1529  			flagDrop: []string{"RESET"},
  1530  			spec: &swarm.ContainerSpec{
  1531  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1532  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1533  			},
  1534  			expectedAdd:  nil,
  1535  			expectedDrop: nil,
  1536  		},
  1537  		{
  1538  			name:     "Reset capabilities, and update after",
  1539  			flagAdd:  []string{"RESET", "CAP_ADD_ONE", "CAP_FOO"},
  1540  			flagDrop: []string{"RESET", "CAP_DROP_ONE", "CAP_FOO"},
  1541  			spec: &swarm.ContainerSpec{
  1542  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1543  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1544  			},
  1545  			expectedAdd:  []string{"CAP_ADD_ONE", "CAP_FOO"},
  1546  			expectedDrop: []string{"CAP_DROP_ONE"},
  1547  		},
  1548  		{
  1549  			name:     "Reset capabilities, and add ALL",
  1550  			flagAdd:  []string{"RESET", "ALL"},
  1551  			flagDrop: []string{"RESET", "ALL"},
  1552  			spec: &swarm.ContainerSpec{
  1553  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1554  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1555  			},
  1556  			expectedAdd:  []string{"ALL"},
  1557  			expectedDrop: nil,
  1558  		},
  1559  		{
  1560  			name:     "Add ALL and RESET",
  1561  			flagAdd:  []string{"ALL", "RESET"},
  1562  			flagDrop: []string{"ALL", "RESET"},
  1563  			spec: &swarm.ContainerSpec{
  1564  				CapabilityAdd:  []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
  1565  				CapabilityDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
  1566  			},
  1567  			expectedAdd:  []string{"ALL"},
  1568  			expectedDrop: nil,
  1569  		},
  1570  	}
  1571  
  1572  	for _, tc := range tests {
  1573  		t.Run(tc.name, func(t *testing.T) {
  1574  			flags := newUpdateCommand(nil).Flags()
  1575  			for _, c := range tc.flagAdd {
  1576  				_ = flags.Set(flagCapAdd, c)
  1577  			}
  1578  			for _, c := range tc.flagDrop {
  1579  				_ = flags.Set(flagCapDrop, c)
  1580  			}
  1581  
  1582  			updateCapabilities(flags, tc.spec)
  1583  
  1584  			assert.DeepEqual(t, tc.spec.CapabilityAdd, tc.expectedAdd)
  1585  			assert.DeepEqual(t, tc.spec.CapabilityDrop, tc.expectedDrop)
  1586  		})
  1587  	}
  1588  }
  1589  
  1590  func TestUpdateUlimits(t *testing.T) {
  1591  	ctx := context.Background()
  1592  
  1593  	tests := []struct {
  1594  		name     string
  1595  		spec     []*units.Ulimit
  1596  		rm       []string
  1597  		add      []string
  1598  		expected []*units.Ulimit
  1599  	}{
  1600  		{
  1601  			name: "from scratch",
  1602  			add:  []string{"nofile=512:1024", "core=1024:1024"},
  1603  			expected: []*units.Ulimit{
  1604  				{Name: "core", Hard: 1024, Soft: 1024},
  1605  				{Name: "nofile", Hard: 1024, Soft: 512},
  1606  			},
  1607  		},
  1608  		{
  1609  			name: "append new",
  1610  			spec: []*units.Ulimit{
  1611  				{Name: "nofile", Hard: 1024, Soft: 512},
  1612  			},
  1613  			add: []string{"core=1024:1024"},
  1614  			expected: []*units.Ulimit{
  1615  				{Name: "core", Hard: 1024, Soft: 1024},
  1616  				{Name: "nofile", Hard: 1024, Soft: 512},
  1617  			},
  1618  		},
  1619  		{
  1620  			name: "remove and append new should append",
  1621  			spec: []*units.Ulimit{
  1622  				{Name: "core", Hard: 1024, Soft: 1024},
  1623  				{Name: "nofile", Hard: 1024, Soft: 512},
  1624  			},
  1625  			rm:  []string{"nofile=512:1024"},
  1626  			add: []string{"nofile=512:1024"},
  1627  			expected: []*units.Ulimit{
  1628  				{Name: "core", Hard: 1024, Soft: 1024},
  1629  				{Name: "nofile", Hard: 1024, Soft: 512},
  1630  			},
  1631  		},
  1632  		{
  1633  			name: "update existing",
  1634  			spec: []*units.Ulimit{
  1635  				{Name: "nofile", Hard: 2048, Soft: 1024},
  1636  			},
  1637  			add: []string{"nofile=512:1024"},
  1638  			expected: []*units.Ulimit{
  1639  				{Name: "nofile", Hard: 1024, Soft: 512},
  1640  			},
  1641  		},
  1642  		{
  1643  			name: "update existing twice",
  1644  			spec: []*units.Ulimit{
  1645  				{Name: "nofile", Hard: 2048, Soft: 1024},
  1646  			},
  1647  			add: []string{"nofile=256:512", "nofile=512:1024"},
  1648  			expected: []*units.Ulimit{
  1649  				{Name: "nofile", Hard: 1024, Soft: 512},
  1650  			},
  1651  		},
  1652  		{
  1653  			name: "remove all",
  1654  			spec: []*units.Ulimit{
  1655  				{Name: "core", Hard: 1024, Soft: 1024},
  1656  				{Name: "nofile", Hard: 1024, Soft: 512},
  1657  			},
  1658  			rm:       []string{"nofile=512:1024", "core=1024:1024"},
  1659  			expected: nil,
  1660  		},
  1661  		{
  1662  			name: "remove by key",
  1663  			spec: []*units.Ulimit{
  1664  				{Name: "core", Hard: 1024, Soft: 1024},
  1665  				{Name: "nofile", Hard: 1024, Soft: 512},
  1666  			},
  1667  			rm: []string{"core"},
  1668  			expected: []*units.Ulimit{
  1669  				{Name: "nofile", Hard: 1024, Soft: 512},
  1670  			},
  1671  		},
  1672  		{
  1673  			name: "remove by key and different value",
  1674  			spec: []*units.Ulimit{
  1675  				{Name: "core", Hard: 1024, Soft: 1024},
  1676  				{Name: "nofile", Hard: 1024, Soft: 512},
  1677  			},
  1678  			rm: []string{"core=1234:5678"},
  1679  			expected: []*units.Ulimit{
  1680  				{Name: "nofile", Hard: 1024, Soft: 512},
  1681  			},
  1682  		},
  1683  	}
  1684  
  1685  	for _, tc := range tests {
  1686  		tc := tc
  1687  		t.Run(tc.name, func(t *testing.T) {
  1688  			svc := swarm.ServiceSpec{
  1689  				TaskTemplate: swarm.TaskSpec{
  1690  					ContainerSpec: &swarm.ContainerSpec{Ulimits: tc.spec},
  1691  				},
  1692  			}
  1693  			flags := newUpdateCommand(nil).Flags()
  1694  			for _, v := range tc.add {
  1695  				assert.NilError(t, flags.Set(flagUlimitAdd, v))
  1696  			}
  1697  			for _, v := range tc.rm {
  1698  				assert.NilError(t, flags.Set(flagUlimitRemove, v))
  1699  			}
  1700  			err := updateService(ctx, &fakeClient{}, flags, &svc)
  1701  			assert.NilError(t, err)
  1702  			assert.DeepEqual(t, svc.TaskTemplate.ContainerSpec.Ulimits, tc.expected)
  1703  		})
  1704  	}
  1705  }