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