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