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 }