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 }