github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/compose/loader/loader_test.go (about) 1 package loader 2 3 import ( 4 "bytes" 5 "io/ioutil" 6 "os" 7 "reflect" 8 "sort" 9 "testing" 10 "time" 11 12 "github.com/docker/cli/cli/compose/types" 13 "github.com/google/go-cmp/cmp/cmpopts" 14 "github.com/sirupsen/logrus" 15 "gotest.tools/v3/assert" 16 is "gotest.tools/v3/assert/cmp" 17 ) 18 19 func buildConfigDetails(source map[string]interface{}, env map[string]string) types.ConfigDetails { 20 workingDir, err := os.Getwd() 21 if err != nil { 22 panic(err) 23 } 24 25 return types.ConfigDetails{ 26 WorkingDir: workingDir, 27 ConfigFiles: []types.ConfigFile{ 28 {Filename: "filename.yml", Config: source}, 29 }, 30 Environment: env, 31 } 32 } 33 34 func loadYAML(yaml string) (*types.Config, error) { 35 return loadYAMLWithEnv(yaml, nil) 36 } 37 38 func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) { 39 dict, err := ParseYAML([]byte(yaml)) 40 if err != nil { 41 return nil, err 42 } 43 44 return Load(buildConfigDetails(dict, env)) 45 } 46 47 var sampleYAML = ` 48 version: "3" 49 services: 50 foo: 51 image: busybox 52 networks: 53 with_me: 54 bar: 55 image: busybox 56 environment: 57 - FOO=1 58 networks: 59 - with_ipam 60 volumes: 61 hello: 62 driver: default 63 driver_opts: 64 beep: boop 65 networks: 66 default: 67 driver: bridge 68 driver_opts: 69 beep: boop 70 with_ipam: 71 ipam: 72 driver: default 73 config: 74 - subnet: 172.28.0.0/16 75 ` 76 77 var sampleDict = map[string]interface{}{ 78 "version": "3", 79 "services": map[string]interface{}{ 80 "foo": map[string]interface{}{ 81 "image": "busybox", 82 "networks": map[string]interface{}{"with_me": nil}, 83 }, 84 "bar": map[string]interface{}{ 85 "image": "busybox", 86 "environment": []interface{}{"FOO=1"}, 87 "networks": []interface{}{"with_ipam"}, 88 }, 89 }, 90 "volumes": map[string]interface{}{ 91 "hello": map[string]interface{}{ 92 "driver": "default", 93 "driver_opts": map[string]interface{}{ 94 "beep": "boop", 95 }, 96 }, 97 }, 98 "networks": map[string]interface{}{ 99 "default": map[string]interface{}{ 100 "driver": "bridge", 101 "driver_opts": map[string]interface{}{ 102 "beep": "boop", 103 }, 104 }, 105 "with_ipam": map[string]interface{}{ 106 "ipam": map[string]interface{}{ 107 "driver": "default", 108 "config": []interface{}{ 109 map[string]interface{}{ 110 "subnet": "172.28.0.0/16", 111 }, 112 }, 113 }, 114 }, 115 }, 116 } 117 118 var samplePortsConfig = []types.ServicePortConfig{ 119 { 120 Mode: "ingress", 121 Target: 8080, 122 Published: 80, 123 Protocol: "tcp", 124 }, 125 { 126 Mode: "ingress", 127 Target: 8081, 128 Published: 81, 129 Protocol: "tcp", 130 }, 131 { 132 Mode: "ingress", 133 Target: 8082, 134 Published: 82, 135 Protocol: "tcp", 136 }, 137 { 138 Mode: "ingress", 139 Target: 8090, 140 Published: 90, 141 Protocol: "udp", 142 }, 143 { 144 Mode: "ingress", 145 Target: 8091, 146 Published: 91, 147 Protocol: "udp", 148 }, 149 { 150 Mode: "ingress", 151 Target: 8092, 152 Published: 92, 153 Protocol: "udp", 154 }, 155 { 156 Mode: "ingress", 157 Target: 8500, 158 Published: 85, 159 Protocol: "tcp", 160 }, 161 { 162 Mode: "ingress", 163 Target: 8600, 164 Published: 0, 165 Protocol: "tcp", 166 }, 167 { 168 Target: 53, 169 Published: 10053, 170 Protocol: "udp", 171 }, 172 { 173 Mode: "host", 174 Target: 22, 175 Published: 10022, 176 }, 177 } 178 179 func strPtr(val string) *string { 180 return &val 181 } 182 183 var sampleConfig = types.Config{ 184 Version: "3.0", 185 Services: []types.ServiceConfig{ 186 { 187 Name: "foo", 188 Image: "busybox", 189 Environment: map[string]*string{}, 190 Networks: map[string]*types.ServiceNetworkConfig{ 191 "with_me": nil, 192 }, 193 }, 194 { 195 Name: "bar", 196 Image: "busybox", 197 Environment: map[string]*string{"FOO": strPtr("1")}, 198 Networks: map[string]*types.ServiceNetworkConfig{ 199 "with_ipam": nil, 200 }, 201 }, 202 }, 203 Networks: map[string]types.NetworkConfig{ 204 "default": { 205 Driver: "bridge", 206 DriverOpts: map[string]string{ 207 "beep": "boop", 208 }, 209 }, 210 "with_ipam": { 211 Ipam: types.IPAMConfig{ 212 Driver: "default", 213 Config: []*types.IPAMPool{ 214 { 215 Subnet: "172.28.0.0/16", 216 }, 217 }, 218 }, 219 }, 220 }, 221 Volumes: map[string]types.VolumeConfig{ 222 "hello": { 223 Driver: "default", 224 DriverOpts: map[string]string{ 225 "beep": "boop", 226 }, 227 }, 228 }, 229 } 230 231 func TestParseYAML(t *testing.T) { 232 dict, err := ParseYAML([]byte(sampleYAML)) 233 assert.NilError(t, err) 234 assert.Check(t, is.DeepEqual(sampleDict, dict)) 235 } 236 237 func TestLoad(t *testing.T) { 238 actual, err := Load(buildConfigDetails(sampleDict, nil)) 239 assert.NilError(t, err) 240 assert.Check(t, is.Equal(sampleConfig.Version, actual.Version)) 241 assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services))) 242 assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks)) 243 assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes)) 244 } 245 246 func TestLoadExtras(t *testing.T) { 247 actual, err := loadYAML(` 248 version: "3.7" 249 services: 250 foo: 251 image: busybox 252 x-foo: bar`) 253 assert.NilError(t, err) 254 assert.Check(t, is.Len(actual.Services, 1)) 255 service := actual.Services[0] 256 assert.Check(t, is.Equal("busybox", service.Image)) 257 extras := map[string]interface{}{ 258 "x-foo": "bar", 259 } 260 assert.Check(t, is.DeepEqual(extras, service.Extras)) 261 } 262 263 func TestLoadV31(t *testing.T) { 264 actual, err := loadYAML(` 265 version: "3.1" 266 services: 267 foo: 268 image: busybox 269 secrets: [super] 270 secrets: 271 super: 272 external: true 273 `) 274 assert.NilError(t, err) 275 assert.Check(t, is.Len(actual.Services, 1)) 276 assert.Check(t, is.Len(actual.Secrets, 1)) 277 } 278 279 func TestLoadV33(t *testing.T) { 280 actual, err := loadYAML(` 281 version: "3.3" 282 services: 283 foo: 284 image: busybox 285 credential_spec: 286 file: "/foo" 287 configs: [super] 288 configs: 289 super: 290 external: true 291 `) 292 assert.NilError(t, err) 293 assert.Assert(t, is.Len(actual.Services, 1)) 294 assert.Check(t, is.Equal(actual.Services[0].CredentialSpec.File, "/foo")) 295 assert.Assert(t, is.Len(actual.Configs, 1)) 296 } 297 298 func TestLoadV38(t *testing.T) { 299 actual, err := loadYAML(` 300 version: "3.8" 301 services: 302 foo: 303 image: busybox 304 credential_spec: 305 config: "0bt9dmxjvjiqermk6xrop3ekq" 306 `) 307 assert.NilError(t, err) 308 assert.Assert(t, is.Len(actual.Services, 1)) 309 assert.Check(t, is.Equal(actual.Services[0].CredentialSpec.Config, "0bt9dmxjvjiqermk6xrop3ekq")) 310 } 311 312 func TestParseAndLoad(t *testing.T) { 313 actual, err := loadYAML(sampleYAML) 314 assert.NilError(t, err) 315 assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services))) 316 assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks)) 317 assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes)) 318 } 319 320 func TestInvalidTopLevelObjectType(t *testing.T) { 321 _, err := loadYAML("1") 322 assert.ErrorContains(t, err, "Top-level object must be a mapping") 323 324 _, err = loadYAML("\"hello\"") 325 assert.ErrorContains(t, err, "Top-level object must be a mapping") 326 327 _, err = loadYAML("[\"hello\"]") 328 assert.ErrorContains(t, err, "Top-level object must be a mapping") 329 } 330 331 func TestNonStringKeys(t *testing.T) { 332 _, err := loadYAML(` 333 version: "3" 334 123: 335 foo: 336 image: busybox 337 `) 338 assert.ErrorContains(t, err, "Non-string key at top level: 123") 339 340 _, err = loadYAML(` 341 version: "3" 342 services: 343 foo: 344 image: busybox 345 123: 346 image: busybox 347 `) 348 assert.ErrorContains(t, err, "Non-string key in services: 123") 349 350 _, err = loadYAML(` 351 version: "3" 352 services: 353 foo: 354 image: busybox 355 networks: 356 default: 357 ipam: 358 config: 359 - 123: oh dear 360 `) 361 assert.ErrorContains(t, err, "Non-string key in networks.default.ipam.config[0]: 123") 362 363 _, err = loadYAML(` 364 version: "3" 365 services: 366 dict-env: 367 image: busybox 368 environment: 369 1: FOO 370 `) 371 assert.ErrorContains(t, err, "Non-string key in services.dict-env.environment: 1") 372 } 373 374 func TestSupportedVersion(t *testing.T) { 375 _, err := loadYAML(` 376 version: "3" 377 services: 378 foo: 379 image: busybox 380 `) 381 assert.NilError(t, err) 382 383 _, err = loadYAML(` 384 version: "3.0" 385 services: 386 foo: 387 image: busybox 388 `) 389 assert.NilError(t, err) 390 } 391 392 func TestUnsupportedVersion(t *testing.T) { 393 _, err := loadYAML(` 394 version: "2" 395 services: 396 foo: 397 image: busybox 398 `) 399 assert.ErrorContains(t, err, "version") 400 401 _, err = loadYAML(` 402 version: "2.0" 403 services: 404 foo: 405 image: busybox 406 `) 407 assert.ErrorContains(t, err, "version") 408 } 409 410 func TestInvalidVersion(t *testing.T) { 411 _, err := loadYAML(` 412 version: 3 413 services: 414 foo: 415 image: busybox 416 `) 417 assert.ErrorContains(t, err, "version must be a string") 418 } 419 420 func TestV1Unsupported(t *testing.T) { 421 _, err := loadYAML(` 422 foo: 423 image: busybox 424 `) 425 assert.ErrorContains(t, err, "unsupported Compose file version: 1.0") 426 } 427 428 func TestNonMappingObject(t *testing.T) { 429 _, err := loadYAML(` 430 version: "3" 431 services: 432 - foo: 433 image: busybox 434 `) 435 assert.ErrorContains(t, err, "services must be a mapping") 436 437 _, err = loadYAML(` 438 version: "3" 439 services: 440 foo: busybox 441 `) 442 assert.ErrorContains(t, err, "services.foo must be a mapping") 443 444 _, err = loadYAML(` 445 version: "3" 446 networks: 447 - default: 448 driver: bridge 449 `) 450 assert.ErrorContains(t, err, "networks must be a mapping") 451 452 _, err = loadYAML(` 453 version: "3" 454 networks: 455 default: bridge 456 `) 457 assert.ErrorContains(t, err, "networks.default must be a mapping") 458 459 _, err = loadYAML(` 460 version: "3" 461 volumes: 462 - data: 463 driver: local 464 `) 465 assert.ErrorContains(t, err, "volumes must be a mapping") 466 467 _, err = loadYAML(` 468 version: "3" 469 volumes: 470 data: local 471 `) 472 assert.ErrorContains(t, err, "volumes.data must be a mapping") 473 } 474 475 func TestNonStringImage(t *testing.T) { 476 _, err := loadYAML(` 477 version: "3" 478 services: 479 foo: 480 image: ["busybox", "latest"] 481 `) 482 assert.ErrorContains(t, err, "services.foo.image must be a string") 483 } 484 485 func TestLoadWithEnvironment(t *testing.T) { 486 config, err := loadYAMLWithEnv(` 487 version: "3" 488 services: 489 dict-env: 490 image: busybox 491 environment: 492 FOO: "1" 493 BAR: 2 494 BAZ: 2.5 495 QUX: 496 QUUX: 497 list-env: 498 image: busybox 499 environment: 500 - FOO=1 501 - BAR=2 502 - BAZ=2.5 503 - QUX= 504 - QUUX 505 `, map[string]string{"QUX": "qux"}) 506 assert.NilError(t, err) 507 508 expected := types.MappingWithEquals{ 509 "FOO": strPtr("1"), 510 "BAR": strPtr("2"), 511 "BAZ": strPtr("2.5"), 512 "QUX": strPtr("qux"), 513 "QUUX": nil, 514 } 515 516 assert.Check(t, is.Equal(2, len(config.Services))) 517 518 for _, service := range config.Services { 519 assert.Check(t, is.DeepEqual(expected, service.Environment)) 520 } 521 } 522 523 func TestInvalidEnvironmentValue(t *testing.T) { 524 _, err := loadYAML(` 525 version: "3" 526 services: 527 dict-env: 528 image: busybox 529 environment: 530 FOO: ["1"] 531 `) 532 assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a string, number or null") 533 } 534 535 func TestInvalidEnvironmentObject(t *testing.T) { 536 _, err := loadYAML(` 537 version: "3" 538 services: 539 dict-env: 540 image: busybox 541 environment: "FOO=1" 542 `) 543 assert.ErrorContains(t, err, "services.dict-env.environment must be a mapping") 544 } 545 546 func TestLoadWithEnvironmentInterpolation(t *testing.T) { 547 home := "/home/foo" 548 config, err := loadYAMLWithEnv(` 549 version: "3" 550 services: 551 test: 552 image: busybox 553 labels: 554 - home1=$HOME 555 - home2=${HOME} 556 - nonexistent=$NONEXISTENT 557 - default=${NONEXISTENT-default} 558 networks: 559 test: 560 driver: $HOME 561 volumes: 562 test: 563 driver: $HOME 564 `, map[string]string{ 565 "HOME": home, 566 "FOO": "foo", 567 }) 568 569 assert.NilError(t, err) 570 571 expectedLabels := types.Labels{ 572 "home1": home, 573 "home2": home, 574 "nonexistent": "", 575 "default": "default", 576 } 577 578 assert.Check(t, is.DeepEqual(expectedLabels, config.Services[0].Labels)) 579 assert.Check(t, is.Equal(home, config.Networks["test"].Driver)) 580 assert.Check(t, is.Equal(home, config.Volumes["test"].Driver)) 581 } 582 583 func TestLoadWithInterpolationCastFull(t *testing.T) { 584 dict, err := ParseYAML([]byte(` 585 version: "3.8" 586 services: 587 web: 588 configs: 589 - source: appconfig 590 mode: $theint 591 secrets: 592 - source: super 593 mode: $theint 594 healthcheck: 595 retries: ${theint} 596 disable: $thebool 597 deploy: 598 replicas: $theint 599 update_config: 600 parallelism: $theint 601 max_failure_ratio: $thefloat 602 rollback_config: 603 parallelism: $theint 604 max_failure_ratio: $thefloat 605 restart_policy: 606 max_attempts: $theint 607 placement: 608 max_replicas_per_node: $theint 609 ports: 610 - $theint 611 - "34567" 612 - target: $theint 613 published: $theint 614 ulimits: 615 nproc: $theint 616 nofile: 617 hard: $theint 618 soft: $theint 619 privileged: $thebool 620 read_only: $thebool 621 stdin_open: ${thebool} 622 tty: $thebool 623 volumes: 624 - source: data 625 type: volume 626 read_only: $thebool 627 volume: 628 nocopy: $thebool 629 630 configs: 631 appconfig: 632 external: $thebool 633 secrets: 634 super: 635 external: $thebool 636 volumes: 637 data: 638 external: $thebool 639 networks: 640 front: 641 external: $thebool 642 internal: $thebool 643 attachable: $thebool 644 645 `)) 646 assert.NilError(t, err) 647 env := map[string]string{ 648 "theint": "555", 649 "thefloat": "3.14", 650 "thebool": "true", 651 } 652 653 config, err := Load(buildConfigDetails(dict, env)) 654 assert.NilError(t, err) 655 expected := &types.Config{ 656 Filename: "filename.yml", 657 Version: "3.8", 658 Services: []types.ServiceConfig{ 659 { 660 Name: "web", 661 Configs: []types.ServiceConfigObjConfig{ 662 { 663 Source: "appconfig", 664 Mode: uint32Ptr(555), 665 }, 666 }, 667 Secrets: []types.ServiceSecretConfig{ 668 { 669 Source: "super", 670 Mode: uint32Ptr(555), 671 }, 672 }, 673 HealthCheck: &types.HealthCheckConfig{ 674 Retries: uint64Ptr(555), 675 Disable: true, 676 }, 677 Deploy: types.DeployConfig{ 678 Replicas: uint64Ptr(555), 679 UpdateConfig: &types.UpdateConfig{ 680 Parallelism: uint64Ptr(555), 681 MaxFailureRatio: 3.14, 682 }, 683 RollbackConfig: &types.UpdateConfig{ 684 Parallelism: uint64Ptr(555), 685 MaxFailureRatio: 3.14, 686 }, 687 RestartPolicy: &types.RestartPolicy{ 688 MaxAttempts: uint64Ptr(555), 689 }, 690 Placement: types.Placement{ 691 MaxReplicas: 555, 692 }, 693 }, 694 Ports: []types.ServicePortConfig{ 695 {Target: 555, Mode: "ingress", Protocol: "tcp"}, 696 {Target: 34567, Mode: "ingress", Protocol: "tcp"}, 697 {Target: 555, Published: 555}, 698 }, 699 Ulimits: map[string]*types.UlimitsConfig{ 700 "nproc": {Single: 555}, 701 "nofile": {Hard: 555, Soft: 555}, 702 }, 703 Privileged: true, 704 ReadOnly: true, 705 StdinOpen: true, 706 Tty: true, 707 Volumes: []types.ServiceVolumeConfig{ 708 { 709 Source: "data", 710 Type: "volume", 711 ReadOnly: true, 712 Volume: &types.ServiceVolumeVolume{NoCopy: true}, 713 }, 714 }, 715 Environment: types.MappingWithEquals{}, 716 }, 717 }, 718 Configs: map[string]types.ConfigObjConfig{ 719 "appconfig": {External: types.External{External: true}, Name: "appconfig"}, 720 }, 721 Secrets: map[string]types.SecretConfig{ 722 "super": {External: types.External{External: true}, Name: "super"}, 723 }, 724 Volumes: map[string]types.VolumeConfig{ 725 "data": {External: types.External{External: true}, Name: "data"}, 726 }, 727 Networks: map[string]types.NetworkConfig{ 728 "front": { 729 External: types.External{External: true}, 730 Name: "front", 731 Internal: true, 732 Attachable: true, 733 }, 734 }, 735 } 736 737 assert.Check(t, is.DeepEqual(expected, config)) 738 } 739 740 func TestUnsupportedProperties(t *testing.T) { 741 dict, err := ParseYAML([]byte(` 742 version: "3" 743 services: 744 web: 745 image: web 746 build: 747 context: ./web 748 links: 749 - bar 750 pid: host 751 db: 752 image: db 753 build: 754 context: ./db 755 `)) 756 assert.NilError(t, err) 757 758 configDetails := buildConfigDetails(dict, nil) 759 760 _, err = Load(configDetails) 761 assert.NilError(t, err) 762 763 unsupported := GetUnsupportedProperties(dict) 764 assert.Check(t, is.DeepEqual([]string{"build", "links", "pid"}, unsupported)) 765 } 766 767 func TestDiscardEnvFileOption(t *testing.T) { 768 dict, err := ParseYAML([]byte(`version: "3" 769 services: 770 web: 771 image: nginx 772 env_file: 773 - example1.env 774 - example2.env 775 `)) 776 expectedEnvironmentMap := types.MappingWithEquals{ 777 "FOO": strPtr("foo_from_env_file"), 778 "BAZ": strPtr("baz_from_env_file"), 779 "BAR": strPtr("bar_from_env_file_2"), // Original value is overwritten by example2.env 780 "QUX": strPtr("quz_from_env_file_2"), 781 } 782 assert.NilError(t, err) 783 configDetails := buildConfigDetails(dict, nil) 784 785 // Default behavior keeps the `env_file` entries 786 configWithEnvFiles, err := Load(configDetails) 787 assert.NilError(t, err) 788 assert.DeepEqual(t, configWithEnvFiles.Services[0].EnvFile, types.StringList{"example1.env", 789 "example2.env"}) 790 assert.DeepEqual(t, configWithEnvFiles.Services[0].Environment, expectedEnvironmentMap) 791 792 // Custom behavior removes the `env_file` entries 793 configWithoutEnvFiles, err := Load(configDetails, WithDiscardEnvFiles) 794 assert.NilError(t, err) 795 assert.DeepEqual(t, configWithoutEnvFiles.Services[0].EnvFile, types.StringList(nil)) 796 assert.DeepEqual(t, configWithoutEnvFiles.Services[0].Environment, expectedEnvironmentMap) 797 } 798 799 func TestBuildProperties(t *testing.T) { 800 dict, err := ParseYAML([]byte(` 801 version: "3" 802 services: 803 web: 804 image: web 805 build: . 806 links: 807 - bar 808 db: 809 image: db 810 build: 811 context: ./db 812 `)) 813 assert.NilError(t, err) 814 configDetails := buildConfigDetails(dict, nil) 815 _, err = Load(configDetails) 816 assert.NilError(t, err) 817 } 818 819 func TestDeprecatedProperties(t *testing.T) { 820 dict, err := ParseYAML([]byte(` 821 version: "3" 822 services: 823 web: 824 image: web 825 container_name: web 826 db: 827 image: db 828 container_name: db 829 expose: ["5434"] 830 `)) 831 assert.NilError(t, err) 832 833 configDetails := buildConfigDetails(dict, nil) 834 835 _, err = Load(configDetails) 836 assert.NilError(t, err) 837 838 deprecated := GetDeprecatedProperties(dict) 839 assert.Check(t, is.Len(deprecated, 2)) 840 assert.Check(t, is.Contains(deprecated, "container_name")) 841 assert.Check(t, is.Contains(deprecated, "expose")) 842 } 843 844 func TestForbiddenProperties(t *testing.T) { 845 _, err := loadYAML(` 846 version: "3" 847 services: 848 foo: 849 image: busybox 850 volumes: 851 - /data 852 volume_driver: some-driver 853 bar: 854 extends: 855 service: foo 856 `) 857 858 assert.ErrorType(t, err, reflect.TypeOf(&ForbiddenPropertiesError{})) 859 860 props := err.(*ForbiddenPropertiesError).Properties 861 assert.Check(t, is.Len(props, 2)) 862 assert.Check(t, is.Contains(props, "volume_driver")) 863 assert.Check(t, is.Contains(props, "extends")) 864 } 865 866 func TestInvalidResource(t *testing.T) { 867 _, err := loadYAML(` 868 version: "3" 869 services: 870 foo: 871 image: busybox 872 deploy: 873 resources: 874 impossible: 875 x: 1 876 `) 877 assert.ErrorContains(t, err, "Additional property impossible is not allowed") 878 } 879 880 func TestInvalidExternalAndDriverCombination(t *testing.T) { 881 _, err := loadYAML(` 882 version: "3" 883 volumes: 884 external_volume: 885 external: true 886 driver: foobar 887 `) 888 889 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver\" specified for volume") 890 assert.ErrorContains(t, err, "external_volume") 891 } 892 893 func TestInvalidExternalAndDirverOptsCombination(t *testing.T) { 894 _, err := loadYAML(` 895 version: "3" 896 volumes: 897 external_volume: 898 external: true 899 driver_opts: 900 beep: boop 901 `) 902 903 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver_opts\" specified for volume") 904 assert.ErrorContains(t, err, "external_volume") 905 } 906 907 func TestInvalidExternalAndLabelsCombination(t *testing.T) { 908 _, err := loadYAML(` 909 version: "3" 910 volumes: 911 external_volume: 912 external: true 913 labels: 914 - beep=boop 915 `) 916 917 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"labels\" specified for volume") 918 assert.ErrorContains(t, err, "external_volume") 919 } 920 921 func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) { 922 _, err := loadYAML(` 923 version: "3.4" 924 volumes: 925 external_volume: 926 name: user_specified_name 927 external: 928 name: external_name 929 `) 930 931 assert.ErrorContains(t, err, "volume.external.name and volume.name conflict; only use volume.name") 932 assert.ErrorContains(t, err, "external_volume") 933 } 934 935 func durationPtr(value time.Duration) *types.Duration { 936 result := types.Duration(value) 937 return &result 938 } 939 940 func uint64Ptr(value uint64) *uint64 { 941 return &value 942 } 943 944 func uint32Ptr(value uint32) *uint32 { 945 return &value 946 } 947 948 func TestFullExample(t *testing.T) { 949 bytes, err := ioutil.ReadFile("full-example.yml") 950 assert.NilError(t, err) 951 952 homeDir := "/home/foo" 953 env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"} 954 config, err := loadYAMLWithEnv(string(bytes), env) 955 assert.NilError(t, err) 956 957 workingDir, err := os.Getwd() 958 assert.NilError(t, err) 959 960 expectedConfig := fullExampleConfig(workingDir, homeDir) 961 962 assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services)) 963 assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks)) 964 assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes)) 965 assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets)) 966 assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs)) 967 assert.Check(t, is.DeepEqual(expectedConfig.Extras, config.Extras)) 968 } 969 970 func TestLoadTmpfsVolume(t *testing.T) { 971 config, err := loadYAML(` 972 version: "3.6" 973 services: 974 tmpfs: 975 image: nginx:latest 976 volumes: 977 - type: tmpfs 978 target: /app 979 tmpfs: 980 size: 10000 981 `) 982 assert.NilError(t, err) 983 984 expected := types.ServiceVolumeConfig{ 985 Target: "/app", 986 Type: "tmpfs", 987 Tmpfs: &types.ServiceVolumeTmpfs{ 988 Size: int64(10000), 989 }, 990 } 991 992 assert.Assert(t, is.Len(config.Services, 1)) 993 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 994 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 995 } 996 997 func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) { 998 _, err := loadYAML(` 999 version: "3.5" 1000 services: 1001 tmpfs: 1002 image: nginx:latest 1003 volumes: 1004 - type: tmpfs 1005 target: /app 1006 tmpfs: 1007 size: 10000 1008 `) 1009 assert.ErrorContains(t, err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed") 1010 } 1011 1012 func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) { 1013 _, err := loadYAML(` 1014 version: "3.5" 1015 services: 1016 tmpfs: 1017 image: nginx:latest 1018 volumes: 1019 - type: bind 1020 target: /app 1021 `) 1022 assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`) 1023 } 1024 1025 func TestLoadBindMountSourceIsWindowsAbsolute(t *testing.T) { 1026 tests := []struct { 1027 doc string 1028 yaml string 1029 expected types.ServiceVolumeConfig 1030 }{ 1031 { 1032 doc: "Z-drive lowercase", 1033 yaml: ` 1034 version: '3.3' 1035 1036 services: 1037 windows: 1038 image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019 1039 volumes: 1040 - type: bind 1041 source: z:\ 1042 target: c:\data 1043 `, 1044 expected: types.ServiceVolumeConfig{Type: "bind", Source: `z:\`, Target: `c:\data`}, 1045 }, 1046 { 1047 doc: "Z-drive uppercase", 1048 yaml: ` 1049 version: '3.3' 1050 1051 services: 1052 windows: 1053 image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019 1054 volumes: 1055 - type: bind 1056 source: Z:\ 1057 target: C:\data 1058 `, 1059 expected: types.ServiceVolumeConfig{Type: "bind", Source: `Z:\`, Target: `C:\data`}, 1060 }, 1061 { 1062 doc: "Z-drive subdirectory", 1063 yaml: ` 1064 version: '3.3' 1065 1066 services: 1067 windows: 1068 image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019 1069 volumes: 1070 - type: bind 1071 source: Z:\some-dir 1072 target: C:\data 1073 `, 1074 expected: types.ServiceVolumeConfig{Type: "bind", Source: `Z:\some-dir`, Target: `C:\data`}, 1075 }, 1076 { 1077 doc: "forward-slashes", 1078 yaml: ` 1079 version: '3.3' 1080 1081 services: 1082 app: 1083 image: app:latest 1084 volumes: 1085 - type: bind 1086 source: /z/some-dir 1087 target: /c/data 1088 `, 1089 expected: types.ServiceVolumeConfig{Type: "bind", Source: `/z/some-dir`, Target: `/c/data`}, 1090 }, 1091 } 1092 1093 for _, tc := range tests { 1094 t.Run(tc.doc, func(t *testing.T) { 1095 config, err := loadYAML(tc.yaml) 1096 assert.NilError(t, err) 1097 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1098 assert.Check(t, is.DeepEqual(tc.expected, config.Services[0].Volumes[0])) 1099 }) 1100 } 1101 } 1102 1103 func TestLoadBindMountWithSource(t *testing.T) { 1104 config, err := loadYAML(` 1105 version: "3.5" 1106 services: 1107 bind: 1108 image: nginx:latest 1109 volumes: 1110 - type: bind 1111 target: /app 1112 source: "." 1113 `) 1114 assert.NilError(t, err) 1115 1116 workingDir, err := os.Getwd() 1117 assert.NilError(t, err) 1118 1119 expected := types.ServiceVolumeConfig{ 1120 Type: "bind", 1121 Source: workingDir, 1122 Target: "/app", 1123 } 1124 1125 assert.Assert(t, is.Len(config.Services, 1)) 1126 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1127 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 1128 } 1129 1130 func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) { 1131 config, err := loadYAML(` 1132 version: "3.6" 1133 services: 1134 tmpfs: 1135 image: nginx:latest 1136 volumes: 1137 - type: tmpfs 1138 target: /app 1139 tmpfs: 1140 size: 0 1141 `) 1142 assert.NilError(t, err) 1143 1144 expected := types.ServiceVolumeConfig{ 1145 Target: "/app", 1146 Type: "tmpfs", 1147 Tmpfs: &types.ServiceVolumeTmpfs{}, 1148 } 1149 1150 assert.Assert(t, is.Len(config.Services, 1)) 1151 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1152 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 1153 } 1154 1155 func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) { 1156 _, err := loadYAML(` 1157 version: "3.6" 1158 services: 1159 tmpfs: 1160 image: nginx:latest 1161 volumes: 1162 - type: tmpfs 1163 target: /app 1164 tmpfs: 1165 size: -1 1166 `) 1167 assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0") 1168 } 1169 1170 func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) { 1171 _, err := loadYAML(` 1172 version: "3.6" 1173 services: 1174 tmpfs: 1175 image: nginx:latest 1176 volumes: 1177 - type: tmpfs 1178 target: /app 1179 tmpfs: 1180 size: 0.0001 1181 `) 1182 assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer") 1183 } 1184 1185 func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { 1186 sort.Slice(services, func(i, j int) bool { 1187 return services[i].Name < services[j].Name 1188 }) 1189 return services 1190 } 1191 1192 func TestLoadAttachableNetwork(t *testing.T) { 1193 config, err := loadYAML(` 1194 version: "3.2" 1195 networks: 1196 mynet1: 1197 driver: overlay 1198 attachable: true 1199 mynet2: 1200 driver: bridge 1201 `) 1202 assert.NilError(t, err) 1203 1204 expected := map[string]types.NetworkConfig{ 1205 "mynet1": { 1206 Driver: "overlay", 1207 Attachable: true, 1208 }, 1209 "mynet2": { 1210 Driver: "bridge", 1211 Attachable: false, 1212 }, 1213 } 1214 1215 assert.Check(t, is.DeepEqual(expected, config.Networks)) 1216 } 1217 1218 func TestLoadExpandedPortFormat(t *testing.T) { 1219 config, err := loadYAML(` 1220 version: "3.2" 1221 services: 1222 web: 1223 image: busybox 1224 ports: 1225 - "80-82:8080-8082" 1226 - "90-92:8090-8092/udp" 1227 - "85:8500" 1228 - 8600 1229 - protocol: udp 1230 target: 53 1231 published: 10053 1232 - mode: host 1233 target: 22 1234 published: 10022 1235 `) 1236 assert.NilError(t, err) 1237 1238 assert.Check(t, is.Len(config.Services, 1)) 1239 assert.Check(t, is.DeepEqual(samplePortsConfig, config.Services[0].Ports)) 1240 } 1241 1242 func TestLoadExpandedMountFormat(t *testing.T) { 1243 config, err := loadYAML(` 1244 version: "3.2" 1245 services: 1246 web: 1247 image: busybox 1248 volumes: 1249 - type: volume 1250 source: foo 1251 target: /target 1252 read_only: true 1253 volumes: 1254 foo: {} 1255 `) 1256 assert.NilError(t, err) 1257 1258 expected := types.ServiceVolumeConfig{ 1259 Type: "volume", 1260 Source: "foo", 1261 Target: "/target", 1262 ReadOnly: true, 1263 } 1264 1265 assert.Assert(t, is.Len(config.Services, 1)) 1266 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1267 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 1268 } 1269 1270 func TestLoadExtraHostsMap(t *testing.T) { 1271 config, err := loadYAML(` 1272 version: "3.2" 1273 services: 1274 web: 1275 image: busybox 1276 extra_hosts: 1277 "zulu": "162.242.195.82" 1278 "alpha": "50.31.209.229" 1279 "host.docker.internal": "host-gateway" 1280 `) 1281 assert.NilError(t, err) 1282 1283 expected := types.HostsList{ 1284 "alpha:50.31.209.229", 1285 "host.docker.internal:host-gateway", 1286 "zulu:162.242.195.82", 1287 } 1288 1289 assert.Assert(t, is.Len(config.Services, 1)) 1290 assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) 1291 } 1292 1293 func TestLoadExtraHostsList(t *testing.T) { 1294 config, err := loadYAML(` 1295 version: "3.2" 1296 services: 1297 web: 1298 image: busybox 1299 extra_hosts: 1300 - "zulu:162.242.195.82" 1301 - "alpha:50.31.209.229" 1302 - "zulu:ff02::1" 1303 - "host.docker.internal:host-gateway" 1304 `) 1305 assert.NilError(t, err) 1306 1307 expected := types.HostsList{ 1308 "zulu:162.242.195.82", 1309 "alpha:50.31.209.229", 1310 "zulu:ff02::1", 1311 "host.docker.internal:host-gateway", 1312 } 1313 1314 assert.Assert(t, is.Len(config.Services, 1)) 1315 assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) 1316 } 1317 1318 func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) { 1319 buf, cleanup := patchLogrus() 1320 defer cleanup() 1321 1322 source := map[string]interface{}{ 1323 "foo": map[string]interface{}{ 1324 "external": map[string]interface{}{ 1325 "name": "oops", 1326 }, 1327 }, 1328 } 1329 volumes, err := LoadVolumes(source, "3.4") 1330 assert.NilError(t, err) 1331 expected := map[string]types.VolumeConfig{ 1332 "foo": { 1333 Name: "oops", 1334 External: types.External{External: true}, 1335 }, 1336 } 1337 assert.Check(t, is.DeepEqual(expected, volumes)) 1338 assert.Check(t, is.Contains(buf.String(), "volume.external.name is deprecated")) 1339 1340 } 1341 1342 func patchLogrus() (*bytes.Buffer, func()) { 1343 buf := new(bytes.Buffer) 1344 out := logrus.StandardLogger().Out 1345 logrus.SetOutput(buf) 1346 return buf, func() { logrus.SetOutput(out) } 1347 } 1348 1349 func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) { 1350 buf, cleanup := patchLogrus() 1351 defer cleanup() 1352 1353 source := map[string]interface{}{ 1354 "foo": map[string]interface{}{ 1355 "external": map[string]interface{}{ 1356 "name": "oops", 1357 }, 1358 }, 1359 } 1360 volumes, err := LoadVolumes(source, "3.3") 1361 assert.NilError(t, err) 1362 expected := map[string]types.VolumeConfig{ 1363 "foo": { 1364 Name: "oops", 1365 External: types.External{External: true}, 1366 }, 1367 } 1368 assert.Check(t, is.DeepEqual(expected, volumes)) 1369 assert.Check(t, is.Equal("", buf.String())) 1370 } 1371 1372 func TestLoadV35(t *testing.T) { 1373 actual, err := loadYAML(` 1374 version: "3.5" 1375 services: 1376 foo: 1377 image: busybox 1378 isolation: process 1379 configs: 1380 foo: 1381 name: fooqux 1382 external: true 1383 bar: 1384 name: barqux 1385 file: ./example1.env 1386 secrets: 1387 foo: 1388 name: fooqux 1389 external: true 1390 bar: 1391 name: barqux 1392 file: ./full-example.yml 1393 `) 1394 assert.NilError(t, err) 1395 assert.Check(t, is.Len(actual.Services, 1)) 1396 assert.Check(t, is.Len(actual.Secrets, 2)) 1397 assert.Check(t, is.Len(actual.Configs, 2)) 1398 assert.Check(t, is.Equal("process", actual.Services[0].Isolation)) 1399 } 1400 1401 func TestLoadV35InvalidIsolation(t *testing.T) { 1402 // validation should be done only on the daemon side 1403 actual, err := loadYAML(` 1404 version: "3.5" 1405 services: 1406 foo: 1407 image: busybox 1408 isolation: invalid 1409 configs: 1410 super: 1411 external: true 1412 `) 1413 assert.NilError(t, err) 1414 assert.Assert(t, is.Len(actual.Services, 1)) 1415 assert.Check(t, is.Equal("invalid", actual.Services[0].Isolation)) 1416 } 1417 1418 func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) { 1419 _, err := loadYAML(` 1420 version: "3.5" 1421 secrets: 1422 external_secret: 1423 name: user_specified_name 1424 external: 1425 name: external_name 1426 `) 1427 1428 assert.ErrorContains(t, err, "secret.external.name and secret.name conflict; only use secret.name") 1429 assert.ErrorContains(t, err, "external_secret") 1430 } 1431 1432 func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) { 1433 buf, cleanup := patchLogrus() 1434 defer cleanup() 1435 1436 source := map[string]interface{}{ 1437 "foo": map[string]interface{}{ 1438 "external": map[string]interface{}{ 1439 "name": "oops", 1440 }, 1441 }, 1442 } 1443 details := types.ConfigDetails{ 1444 Version: "3.5", 1445 } 1446 secrets, err := LoadSecrets(source, details) 1447 assert.NilError(t, err) 1448 expected := map[string]types.SecretConfig{ 1449 "foo": { 1450 Name: "oops", 1451 External: types.External{External: true}, 1452 }, 1453 } 1454 assert.Check(t, is.DeepEqual(expected, secrets)) 1455 assert.Check(t, is.Contains(buf.String(), "secret.external.name is deprecated")) 1456 } 1457 1458 func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) { 1459 buf, cleanup := patchLogrus() 1460 defer cleanup() 1461 1462 source := map[string]interface{}{ 1463 "foo": map[string]interface{}{ 1464 "external": map[string]interface{}{ 1465 "name": "oops", 1466 }, 1467 }, 1468 } 1469 networks, err := LoadNetworks(source, "3.5") 1470 assert.NilError(t, err) 1471 expected := map[string]types.NetworkConfig{ 1472 "foo": { 1473 Name: "oops", 1474 External: types.External{External: true}, 1475 }, 1476 } 1477 assert.Check(t, is.DeepEqual(expected, networks)) 1478 assert.Check(t, is.Contains(buf.String(), "network.external.name is deprecated")) 1479 1480 } 1481 1482 func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) { 1483 buf, cleanup := patchLogrus() 1484 defer cleanup() 1485 1486 source := map[string]interface{}{ 1487 "foo": map[string]interface{}{ 1488 "external": map[string]interface{}{ 1489 "name": "oops", 1490 }, 1491 }, 1492 } 1493 networks, err := LoadNetworks(source, "3.4") 1494 assert.NilError(t, err) 1495 expected := map[string]types.NetworkConfig{ 1496 "foo": { 1497 Name: "oops", 1498 External: types.External{External: true}, 1499 }, 1500 } 1501 assert.Check(t, is.DeepEqual(expected, networks)) 1502 assert.Check(t, is.Equal("", buf.String())) 1503 } 1504 1505 func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) { 1506 _, err := loadYAML(` 1507 version: "3.5" 1508 networks: 1509 foo: 1510 name: user_specified_name 1511 external: 1512 name: external_name 1513 `) 1514 1515 assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name") 1516 assert.ErrorContains(t, err, "foo") 1517 } 1518 1519 func TestLoadNetworkWithName(t *testing.T) { 1520 config, err := loadYAML(` 1521 version: '3.5' 1522 services: 1523 hello-world: 1524 image: redis:alpine 1525 networks: 1526 - network1 1527 - network3 1528 1529 networks: 1530 network1: 1531 name: network2 1532 network3: 1533 `) 1534 assert.NilError(t, err) 1535 expected := &types.Config{ 1536 Filename: "filename.yml", 1537 Version: "3.5", 1538 Services: types.Services{ 1539 { 1540 Name: "hello-world", 1541 Image: "redis:alpine", 1542 Networks: map[string]*types.ServiceNetworkConfig{ 1543 "network1": nil, 1544 "network3": nil, 1545 }, 1546 }, 1547 }, 1548 Networks: map[string]types.NetworkConfig{ 1549 "network1": {Name: "network2"}, 1550 "network3": {}, 1551 }, 1552 } 1553 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1554 } 1555 1556 func TestLoadInit(t *testing.T) { 1557 booleanTrue := true 1558 booleanFalse := false 1559 1560 var testcases = []struct { 1561 doc string 1562 yaml string 1563 init *bool 1564 }{ 1565 { 1566 doc: "no init defined", 1567 yaml: ` 1568 version: '3.7' 1569 services: 1570 foo: 1571 image: alpine`, 1572 }, 1573 { 1574 doc: "has true init", 1575 yaml: ` 1576 version: '3.7' 1577 services: 1578 foo: 1579 image: alpine 1580 init: true`, 1581 init: &booleanTrue, 1582 }, 1583 { 1584 doc: "has false init", 1585 yaml: ` 1586 version: '3.7' 1587 services: 1588 foo: 1589 image: alpine 1590 init: false`, 1591 init: &booleanFalse, 1592 }, 1593 } 1594 for _, testcase := range testcases { 1595 testcase := testcase 1596 t.Run(testcase.doc, func(t *testing.T) { 1597 config, err := loadYAML(testcase.yaml) 1598 assert.NilError(t, err) 1599 assert.Check(t, is.Len(config.Services, 1)) 1600 assert.Check(t, is.DeepEqual(config.Services[0].Init, testcase.init)) 1601 }) 1602 } 1603 } 1604 1605 func TestLoadSysctls(t *testing.T) { 1606 config, err := loadYAML(` 1607 version: "3.8" 1608 services: 1609 web: 1610 image: busybox 1611 sysctls: 1612 - net.core.somaxconn=1024 1613 - net.ipv4.tcp_syncookies=0 1614 - testing.one.one= 1615 - testing.one.two 1616 `) 1617 assert.NilError(t, err) 1618 1619 expected := types.Mapping{ 1620 "net.core.somaxconn": "1024", 1621 "net.ipv4.tcp_syncookies": "0", 1622 "testing.one.one": "", 1623 "testing.one.two": "", 1624 } 1625 1626 assert.Assert(t, is.Len(config.Services, 1)) 1627 assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls)) 1628 1629 config, err = loadYAML(` 1630 version: "3.8" 1631 services: 1632 web: 1633 image: busybox 1634 sysctls: 1635 net.core.somaxconn: 1024 1636 net.ipv4.tcp_syncookies: 0 1637 testing.one.one: "" 1638 testing.one.two: 1639 `) 1640 assert.NilError(t, err) 1641 1642 assert.Assert(t, is.Len(config.Services, 1)) 1643 assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls)) 1644 } 1645 1646 func TestTransform(t *testing.T) { 1647 var source = []interface{}{ 1648 "80-82:8080-8082", 1649 "90-92:8090-8092/udp", 1650 "85:8500", 1651 8600, 1652 map[string]interface{}{ 1653 "protocol": "udp", 1654 "target": 53, 1655 "published": 10053, 1656 }, 1657 map[string]interface{}{ 1658 "mode": "host", 1659 "target": 22, 1660 "published": 10022, 1661 }, 1662 } 1663 var ports []types.ServicePortConfig 1664 err := Transform(source, &ports) 1665 assert.NilError(t, err) 1666 1667 assert.Check(t, is.DeepEqual(samplePortsConfig, ports)) 1668 } 1669 1670 func TestLoadTemplateDriver(t *testing.T) { 1671 config, err := loadYAML(` 1672 version: '3.8' 1673 services: 1674 hello-world: 1675 image: redis:alpine 1676 secrets: 1677 - secret 1678 configs: 1679 - config 1680 1681 configs: 1682 config: 1683 name: config 1684 external: true 1685 template_driver: config-driver 1686 1687 secrets: 1688 secret: 1689 name: secret 1690 external: true 1691 template_driver: secret-driver 1692 `) 1693 assert.NilError(t, err) 1694 expected := &types.Config{ 1695 Filename: "filename.yml", 1696 Version: "3.8", 1697 Services: types.Services{ 1698 { 1699 Name: "hello-world", 1700 Image: "redis:alpine", 1701 Configs: []types.ServiceConfigObjConfig{ 1702 { 1703 Source: "config", 1704 }, 1705 }, 1706 Secrets: []types.ServiceSecretConfig{ 1707 { 1708 Source: "secret", 1709 }, 1710 }, 1711 }, 1712 }, 1713 Configs: map[string]types.ConfigObjConfig{ 1714 "config": { 1715 Name: "config", 1716 External: types.External{External: true}, 1717 TemplateDriver: "config-driver", 1718 }, 1719 }, 1720 Secrets: map[string]types.SecretConfig{ 1721 "secret": { 1722 Name: "secret", 1723 External: types.External{External: true}, 1724 TemplateDriver: "secret-driver", 1725 }, 1726 }, 1727 } 1728 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1729 } 1730 1731 func TestLoadSecretDriver(t *testing.T) { 1732 config, err := loadYAML(` 1733 version: '3.8' 1734 services: 1735 hello-world: 1736 image: redis:alpine 1737 secrets: 1738 - secret 1739 configs: 1740 - config 1741 1742 configs: 1743 config: 1744 name: config 1745 external: true 1746 1747 secrets: 1748 secret: 1749 name: secret 1750 driver: secret-bucket 1751 driver_opts: 1752 OptionA: value for driver option A 1753 OptionB: value for driver option B 1754 `) 1755 assert.NilError(t, err) 1756 expected := &types.Config{ 1757 Filename: "filename.yml", 1758 Version: "3.8", 1759 Services: types.Services{ 1760 { 1761 Name: "hello-world", 1762 Image: "redis:alpine", 1763 Configs: []types.ServiceConfigObjConfig{ 1764 { 1765 Source: "config", 1766 }, 1767 }, 1768 Secrets: []types.ServiceSecretConfig{ 1769 { 1770 Source: "secret", 1771 }, 1772 }, 1773 }, 1774 }, 1775 Configs: map[string]types.ConfigObjConfig{ 1776 "config": { 1777 Name: "config", 1778 External: types.External{External: true}, 1779 }, 1780 }, 1781 Secrets: map[string]types.SecretConfig{ 1782 "secret": { 1783 Name: "secret", 1784 Driver: "secret-bucket", 1785 DriverOpts: map[string]string{ 1786 "OptionA": "value for driver option A", 1787 "OptionB": "value for driver option B", 1788 }, 1789 }, 1790 }, 1791 } 1792 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1793 }