github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/compose/convert/service_test.go (about) 1 package convert 2 3 import ( 4 "context" 5 "os" 6 "sort" 7 "strings" 8 "testing" 9 "time" 10 11 composetypes "github.com/docker/cli/cli/compose/types" 12 "github.com/docker/docker/api/types" 13 "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/api/types/swarm" 15 "github.com/docker/docker/client" 16 "github.com/pkg/errors" 17 "gotest.tools/v3/assert" 18 is "gotest.tools/v3/assert/cmp" 19 ) 20 21 func TestConvertRestartPolicyFromNone(t *testing.T) { 22 policy, err := convertRestartPolicy("no", nil) 23 assert.NilError(t, err) 24 assert.Check(t, is.DeepEqual((*swarm.RestartPolicy)(nil), policy)) 25 } 26 27 func TestConvertRestartPolicyFromUnknown(t *testing.T) { 28 _, err := convertRestartPolicy("unknown", nil) 29 assert.Error(t, err, "unknown restart policy: unknown") 30 } 31 32 func TestConvertRestartPolicyFromAlways(t *testing.T) { 33 policy, err := convertRestartPolicy("always", nil) 34 expected := &swarm.RestartPolicy{ 35 Condition: swarm.RestartPolicyConditionAny, 36 } 37 assert.NilError(t, err) 38 assert.Check(t, is.DeepEqual(expected, policy)) 39 } 40 41 func TestConvertRestartPolicyFromFailure(t *testing.T) { 42 policy, err := convertRestartPolicy("on-failure:4", nil) 43 attempts := uint64(4) 44 expected := &swarm.RestartPolicy{ 45 Condition: swarm.RestartPolicyConditionOnFailure, 46 MaxAttempts: &attempts, 47 } 48 assert.NilError(t, err) 49 assert.Check(t, is.DeepEqual(expected, policy)) 50 } 51 52 func strPtr(val string) *string { 53 return &val 54 } 55 56 func TestConvertEnvironment(t *testing.T) { 57 source := map[string]*string{ 58 "foo": strPtr("bar"), 59 "key": strPtr("value"), 60 } 61 env := convertEnvironment(source) 62 sort.Strings(env) 63 assert.Check(t, is.DeepEqual([]string{"foo=bar", "key=value"}, env)) 64 } 65 66 func TestConvertExtraHosts(t *testing.T) { 67 source := composetypes.HostsList{ 68 "zulu:127.0.0.2", 69 "alpha:127.0.0.1", 70 "zulu:ff02::1", 71 } 72 assert.Check(t, is.DeepEqual([]string{"127.0.0.2 zulu", "127.0.0.1 alpha", "ff02::1 zulu"}, convertExtraHosts(source))) 73 } 74 75 func TestConvertResourcesFull(t *testing.T) { 76 source := composetypes.Resources{ 77 Limits: &composetypes.ResourceLimit{ 78 NanoCPUs: "0.003", 79 MemoryBytes: composetypes.UnitBytes(300000000), 80 }, 81 Reservations: &composetypes.Resource{ 82 NanoCPUs: "0.002", 83 MemoryBytes: composetypes.UnitBytes(200000000), 84 }, 85 } 86 resources, err := convertResources(source) 87 assert.NilError(t, err) 88 89 expected := &swarm.ResourceRequirements{ 90 Limits: &swarm.Limit{ 91 NanoCPUs: 3000000, 92 MemoryBytes: 300000000, 93 }, 94 Reservations: &swarm.Resources{ 95 NanoCPUs: 2000000, 96 MemoryBytes: 200000000, 97 }, 98 } 99 assert.Check(t, is.DeepEqual(expected, resources)) 100 } 101 102 func TestConvertResourcesOnlyMemory(t *testing.T) { 103 source := composetypes.Resources{ 104 Limits: &composetypes.ResourceLimit{ 105 MemoryBytes: composetypes.UnitBytes(300000000), 106 }, 107 Reservations: &composetypes.Resource{ 108 MemoryBytes: composetypes.UnitBytes(200000000), 109 }, 110 } 111 resources, err := convertResources(source) 112 assert.NilError(t, err) 113 114 expected := &swarm.ResourceRequirements{ 115 Limits: &swarm.Limit{ 116 MemoryBytes: 300000000, 117 }, 118 Reservations: &swarm.Resources{ 119 MemoryBytes: 200000000, 120 }, 121 } 122 assert.Check(t, is.DeepEqual(expected, resources)) 123 } 124 125 func TestConvertHealthcheck(t *testing.T) { 126 retries := uint64(10) 127 timeout := composetypes.Duration(30 * time.Second) 128 interval := composetypes.Duration(2 * time.Millisecond) 129 source := &composetypes.HealthCheckConfig{ 130 Test: []string{"EXEC", "touch", "/foo"}, 131 Timeout: &timeout, 132 Interval: &interval, 133 Retries: &retries, 134 } 135 expected := &container.HealthConfig{ 136 Test: source.Test, 137 Timeout: time.Duration(timeout), 138 Interval: time.Duration(interval), 139 Retries: 10, 140 } 141 142 healthcheck, err := convertHealthcheck(source) 143 assert.NilError(t, err) 144 assert.Check(t, is.DeepEqual(expected, healthcheck)) 145 } 146 147 func TestConvertHealthcheckDisable(t *testing.T) { 148 source := &composetypes.HealthCheckConfig{Disable: true} 149 expected := &container.HealthConfig{ 150 Test: []string{"NONE"}, 151 } 152 153 healthcheck, err := convertHealthcheck(source) 154 assert.NilError(t, err) 155 assert.Check(t, is.DeepEqual(expected, healthcheck)) 156 } 157 158 func TestConvertHealthcheckDisableWithTest(t *testing.T) { 159 source := &composetypes.HealthCheckConfig{ 160 Disable: true, 161 Test: []string{"EXEC", "touch"}, 162 } 163 _, err := convertHealthcheck(source) 164 assert.Error(t, err, "test and disable can't be set at the same time") 165 } 166 167 func TestConvertEndpointSpec(t *testing.T) { 168 source := []composetypes.ServicePortConfig{ 169 { 170 Protocol: "udp", 171 Target: 53, 172 Published: 1053, 173 Mode: "host", 174 }, 175 { 176 Target: 8080, 177 Published: 80, 178 }, 179 } 180 endpoint := convertEndpointSpec("vip", source) 181 182 expected := swarm.EndpointSpec{ 183 Mode: swarm.ResolutionMode(strings.ToLower("vip")), 184 Ports: []swarm.PortConfig{ 185 { 186 TargetPort: 8080, 187 PublishedPort: 80, 188 }, 189 { 190 Protocol: "udp", 191 TargetPort: 53, 192 PublishedPort: 1053, 193 PublishMode: "host", 194 }, 195 }, 196 } 197 198 assert.Check(t, is.DeepEqual(expected, *endpoint)) 199 } 200 201 func TestConvertServiceNetworksOnlyDefault(t *testing.T) { 202 networkConfigs := networkMap{} 203 204 configs, err := convertServiceNetworks( 205 nil, networkConfigs, NewNamespace("foo"), "service") 206 207 expected := []swarm.NetworkAttachmentConfig{ 208 { 209 Target: "foo_default", 210 Aliases: []string{"service"}, 211 }, 212 } 213 214 assert.NilError(t, err) 215 assert.Check(t, is.DeepEqual(expected, configs)) 216 } 217 218 func TestConvertServiceNetworks(t *testing.T) { 219 networkConfigs := networkMap{ 220 "front": composetypes.NetworkConfig{ 221 External: composetypes.External{External: true}, 222 Name: "fronttier", 223 }, 224 "back": composetypes.NetworkConfig{}, 225 } 226 networks := map[string]*composetypes.ServiceNetworkConfig{ 227 "front": { 228 Aliases: []string{"something"}, 229 }, 230 "back": { 231 Aliases: []string{"other"}, 232 }, 233 } 234 235 configs, err := convertServiceNetworks( 236 networks, networkConfigs, NewNamespace("foo"), "service") 237 238 expected := []swarm.NetworkAttachmentConfig{ 239 { 240 Target: "foo_back", 241 Aliases: []string{"other", "service"}, 242 }, 243 { 244 Target: "fronttier", 245 Aliases: []string{"something", "service"}, 246 }, 247 } 248 249 assert.NilError(t, err) 250 assert.Check(t, is.DeepEqual(expected, configs)) 251 } 252 253 func TestConvertServiceNetworksCustomDefault(t *testing.T) { 254 networkConfigs := networkMap{ 255 "default": composetypes.NetworkConfig{ 256 External: composetypes.External{External: true}, 257 Name: "custom", 258 }, 259 } 260 networks := map[string]*composetypes.ServiceNetworkConfig{} 261 262 configs, err := convertServiceNetworks( 263 networks, networkConfigs, NewNamespace("foo"), "service") 264 265 expected := []swarm.NetworkAttachmentConfig{ 266 { 267 Target: "custom", 268 Aliases: []string{"service"}, 269 }, 270 } 271 272 assert.NilError(t, err) 273 assert.Check(t, is.DeepEqual(expected, configs)) 274 } 275 276 func TestConvertDNSConfigEmpty(t *testing.T) { 277 dnsConfig := convertDNSConfig(nil, nil) 278 assert.Check(t, is.DeepEqual((*swarm.DNSConfig)(nil), dnsConfig)) 279 } 280 281 var ( 282 nameservers = []string{"8.8.8.8", "9.9.9.9"} 283 search = []string{"dc1.example.com", "dc2.example.com"} 284 ) 285 286 func TestConvertDNSConfigAll(t *testing.T) { 287 dnsConfig := convertDNSConfig(nameservers, search) 288 assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ 289 Nameservers: nameservers, 290 Search: search, 291 }, dnsConfig)) 292 } 293 294 func TestConvertDNSConfigNameservers(t *testing.T) { 295 dnsConfig := convertDNSConfig(nameservers, nil) 296 assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ 297 Nameservers: nameservers, 298 Search: nil, 299 }, dnsConfig)) 300 } 301 302 func TestConvertDNSConfigSearch(t *testing.T) { 303 dnsConfig := convertDNSConfig(nil, search) 304 assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ 305 Nameservers: nil, 306 Search: search, 307 }, dnsConfig)) 308 } 309 310 func TestConvertCredentialSpec(t *testing.T) { 311 tests := []struct { 312 name string 313 in composetypes.CredentialSpecConfig 314 out *swarm.CredentialSpec 315 configs []*swarm.ConfigReference 316 expectedErr string 317 }{ 318 { 319 name: "empty", 320 }, 321 { 322 name: "config-and-file", 323 in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json"}, 324 expectedErr: `invalid credential spec: cannot specify both "Config" and "File"`, 325 }, 326 { 327 name: "config-and-registry", 328 in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", Registry: "testing"}, 329 expectedErr: `invalid credential spec: cannot specify both "Config" and "Registry"`, 330 }, 331 { 332 name: "file-and-registry", 333 in: composetypes.CredentialSpecConfig{File: "somefile.json", Registry: "testing"}, 334 expectedErr: `invalid credential spec: cannot specify both "File" and "Registry"`, 335 }, 336 { 337 name: "config-and-file-and-registry", 338 in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json", Registry: "testing"}, 339 expectedErr: `invalid credential spec: cannot specify both "Config", "File", and "Registry"`, 340 }, 341 { 342 name: "missing-config-reference", 343 in: composetypes.CredentialSpecConfig{Config: "missing"}, 344 expectedErr: "invalid credential spec: spec specifies config missing, but no such config can be found", 345 configs: []*swarm.ConfigReference{ 346 { 347 ConfigName: "someName", 348 ConfigID: "missing", 349 }, 350 }, 351 }, 352 { 353 name: "namespaced-config", 354 in: composetypes.CredentialSpecConfig{Config: "name"}, 355 configs: []*swarm.ConfigReference{ 356 { 357 ConfigName: "namespaced-config_name", 358 ConfigID: "someID", 359 }, 360 }, 361 out: &swarm.CredentialSpec{Config: "someID"}, 362 }, 363 { 364 name: "config", 365 in: composetypes.CredentialSpecConfig{Config: "someName"}, 366 configs: []*swarm.ConfigReference{ 367 { 368 ConfigName: "someOtherName", 369 ConfigID: "someOtherID", 370 }, { 371 ConfigName: "someName", 372 ConfigID: "someID", 373 }, 374 }, 375 out: &swarm.CredentialSpec{Config: "someID"}, 376 }, 377 { 378 name: "file", 379 in: composetypes.CredentialSpecConfig{File: "somefile.json"}, 380 out: &swarm.CredentialSpec{File: "somefile.json"}, 381 }, 382 { 383 name: "registry", 384 in: composetypes.CredentialSpecConfig{Registry: "testing"}, 385 out: &swarm.CredentialSpec{Registry: "testing"}, 386 }, 387 } 388 389 for _, tc := range tests { 390 tc := tc 391 t.Run(tc.name, func(t *testing.T) { 392 namespace := NewNamespace(tc.name) 393 swarmSpec, err := convertCredentialSpec(namespace, tc.in, tc.configs) 394 395 if tc.expectedErr != "" { 396 assert.Error(t, err, tc.expectedErr) 397 } else { 398 assert.NilError(t, err) 399 } 400 assert.DeepEqual(t, swarmSpec, tc.out) 401 }) 402 } 403 } 404 405 func TestConvertUpdateConfigOrder(t *testing.T) { 406 // test default behavior 407 updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{}) 408 assert.Check(t, is.Equal("", updateConfig.Order)) 409 410 // test start-first 411 updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ 412 Order: "start-first", 413 }) 414 assert.Check(t, is.Equal(updateConfig.Order, "start-first")) 415 416 // test stop-first 417 updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ 418 Order: "stop-first", 419 }) 420 assert.Check(t, is.Equal(updateConfig.Order, "stop-first")) 421 } 422 423 func TestConvertFileObject(t *testing.T) { 424 namespace := NewNamespace("testing") 425 config := composetypes.FileReferenceConfig{ 426 Source: "source", 427 Target: "target", 428 UID: "user", 429 GID: "group", 430 Mode: uint32Ptr(0644), 431 } 432 swarmRef, err := convertFileObject(namespace, config, lookupConfig) 433 assert.NilError(t, err) 434 435 expected := swarmReferenceObject{ 436 Name: "testing_source", 437 File: swarmReferenceTarget{ 438 Name: config.Target, 439 UID: config.UID, 440 GID: config.GID, 441 Mode: os.FileMode(0644), 442 }, 443 } 444 assert.Check(t, is.DeepEqual(expected, swarmRef)) 445 } 446 447 func lookupConfig(key string) (composetypes.FileObjectConfig, error) { 448 if key != "source" { 449 return composetypes.FileObjectConfig{}, errors.New("bad key") 450 } 451 return composetypes.FileObjectConfig{}, nil 452 } 453 454 func TestConvertFileObjectDefaults(t *testing.T) { 455 namespace := NewNamespace("testing") 456 config := composetypes.FileReferenceConfig{Source: "source"} 457 swarmRef, err := convertFileObject(namespace, config, lookupConfig) 458 assert.NilError(t, err) 459 460 expected := swarmReferenceObject{ 461 Name: "testing_source", 462 File: swarmReferenceTarget{ 463 Name: config.Source, 464 UID: "0", 465 GID: "0", 466 Mode: os.FileMode(0444), 467 }, 468 } 469 assert.Check(t, is.DeepEqual(expected, swarmRef)) 470 } 471 472 func TestServiceConvertsIsolation(t *testing.T) { 473 src := composetypes.ServiceConfig{ 474 Isolation: "hyperv", 475 } 476 result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil) 477 assert.NilError(t, err) 478 assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation)) 479 } 480 481 func TestConvertServiceSecrets(t *testing.T) { 482 namespace := Namespace{name: "foo"} 483 secrets := []composetypes.ServiceSecretConfig{ 484 {Source: "foo_secret"}, 485 {Source: "bar_secret"}, 486 } 487 secretSpecs := map[string]composetypes.SecretConfig{ 488 "foo_secret": { 489 Name: "foo_secret", 490 }, 491 "bar_secret": { 492 Name: "bar_secret", 493 }, 494 } 495 client := &fakeClient{ 496 secretListFunc: func(opts types.SecretListOptions) ([]swarm.Secret, error) { 497 assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret")) 498 assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret")) 499 return []swarm.Secret{ 500 {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}}, 501 {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}}, 502 }, nil 503 }, 504 } 505 refs, err := convertServiceSecrets(client, namespace, secrets, secretSpecs) 506 assert.NilError(t, err) 507 expected := []*swarm.SecretReference{ 508 { 509 SecretName: "bar_secret", 510 File: &swarm.SecretReferenceFileTarget{ 511 Name: "bar_secret", 512 UID: "0", 513 GID: "0", 514 Mode: 0444, 515 }, 516 }, 517 { 518 SecretName: "foo_secret", 519 File: &swarm.SecretReferenceFileTarget{ 520 Name: "foo_secret", 521 UID: "0", 522 GID: "0", 523 Mode: 0444, 524 }, 525 }, 526 } 527 assert.DeepEqual(t, expected, refs) 528 } 529 530 func TestConvertServiceConfigs(t *testing.T) { 531 namespace := Namespace{name: "foo"} 532 service := composetypes.ServiceConfig{ 533 Configs: []composetypes.ServiceConfigObjConfig{ 534 {Source: "foo_config"}, 535 {Source: "bar_config"}, 536 }, 537 CredentialSpec: composetypes.CredentialSpecConfig{ 538 Config: "baz_config", 539 }, 540 } 541 configSpecs := map[string]composetypes.ConfigObjConfig{ 542 "foo_config": { 543 Name: "foo_config", 544 }, 545 "bar_config": { 546 Name: "bar_config", 547 }, 548 "baz_config": { 549 Name: "baz_config", 550 }, 551 } 552 client := &fakeClient{ 553 configListFunc: func(opts types.ConfigListOptions) ([]swarm.Config, error) { 554 assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config")) 555 assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config")) 556 assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config")) 557 return []swarm.Config{ 558 {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}}, 559 {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}}, 560 {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}}, 561 }, nil 562 }, 563 } 564 refs, err := convertServiceConfigObjs(client, namespace, service, configSpecs) 565 assert.NilError(t, err) 566 expected := []*swarm.ConfigReference{ 567 { 568 ConfigName: "bar_config", 569 File: &swarm.ConfigReferenceFileTarget{ 570 Name: "bar_config", 571 UID: "0", 572 GID: "0", 573 Mode: 0444, 574 }, 575 }, 576 { 577 ConfigName: "baz_config", 578 Runtime: &swarm.ConfigReferenceRuntimeTarget{}, 579 }, 580 { 581 ConfigName: "foo_config", 582 File: &swarm.ConfigReferenceFileTarget{ 583 Name: "foo_config", 584 UID: "0", 585 GID: "0", 586 Mode: 0444, 587 }, 588 }, 589 } 590 assert.DeepEqual(t, expected, refs) 591 } 592 593 type fakeClient struct { 594 client.Client 595 secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) 596 configListFunc func(types.ConfigListOptions) ([]swarm.Config, error) 597 } 598 599 func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { 600 if c.secretListFunc != nil { 601 return c.secretListFunc(options) 602 } 603 return []swarm.Secret{}, nil 604 } 605 606 func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { 607 if c.configListFunc != nil { 608 return c.configListFunc(options) 609 } 610 return []swarm.Config{}, nil 611 } 612 613 func TestConvertUpdateConfigParallelism(t *testing.T) { 614 parallel := uint64(4) 615 616 // test default behavior 617 updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{}) 618 assert.Check(t, is.Equal(uint64(1), updateConfig.Parallelism)) 619 620 // Non default value 621 updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ 622 Parallelism: ¶llel, 623 }) 624 assert.Check(t, is.Equal(parallel, updateConfig.Parallelism)) 625 } 626 627 func TestConvertServiceCapAddAndCapDrop(t *testing.T) { 628 tests := []struct { 629 title string 630 in, out composetypes.ServiceConfig 631 }{ 632 { 633 title: "default behavior", 634 }, 635 { 636 title: "some values", 637 in: composetypes.ServiceConfig{ 638 CapAdd: []string{"SYS_NICE", "CAP_NET_ADMIN"}, 639 CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"}, 640 }, 641 out: composetypes.ServiceConfig{ 642 CapAdd: []string{"CAP_NET_ADMIN", "CAP_SYS_NICE"}, 643 CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID"}, 644 }, 645 }, 646 { 647 title: "adding ALL capabilities", 648 in: composetypes.ServiceConfig{ 649 CapAdd: []string{"ALL", "CAP_NET_ADMIN"}, 650 CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"}, 651 }, 652 out: composetypes.ServiceConfig{ 653 CapAdd: []string{"ALL"}, 654 CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"}, 655 }, 656 }, 657 { 658 title: "dropping ALL capabilities", 659 in: composetypes.ServiceConfig{ 660 CapAdd: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"}, 661 CapDrop: []string{"ALL", "CAP_NET_ADMIN", "CAP_FOO"}, 662 }, 663 out: composetypes.ServiceConfig{ 664 CapAdd: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"}, 665 CapDrop: []string{"ALL"}, 666 }, 667 }, 668 } 669 for _, tc := range tests { 670 tc := tc 671 t.Run(tc.title, func(t *testing.T) { 672 result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil) 673 assert.NilError(t, err) 674 assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd)) 675 assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop)) 676 }) 677 } 678 }