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