github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/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/assert" 16 is "gotest.tools/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.7" 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 ports: 608 - $theint 609 - "34567" 610 - target: $theint 611 published: $theint 612 ulimits: 613 nproc: $theint 614 nofile: 615 hard: $theint 616 soft: $theint 617 privileged: $thebool 618 read_only: $thebool 619 stdin_open: ${thebool} 620 tty: $thebool 621 volumes: 622 - source: data 623 type: volume 624 read_only: $thebool 625 volume: 626 nocopy: $thebool 627 628 configs: 629 appconfig: 630 external: $thebool 631 secrets: 632 super: 633 external: $thebool 634 volumes: 635 data: 636 external: $thebool 637 networks: 638 front: 639 external: $thebool 640 internal: $thebool 641 attachable: $thebool 642 643 `)) 644 assert.NilError(t, err) 645 env := map[string]string{ 646 "theint": "555", 647 "thefloat": "3.14", 648 "thebool": "true", 649 } 650 651 config, err := Load(buildConfigDetails(dict, env)) 652 assert.NilError(t, err) 653 expected := &types.Config{ 654 Filename: "filename.yml", 655 Version: "3.7", 656 Services: []types.ServiceConfig{ 657 { 658 Name: "web", 659 Configs: []types.ServiceConfigObjConfig{ 660 { 661 Source: "appconfig", 662 Mode: uint32Ptr(555), 663 }, 664 }, 665 Secrets: []types.ServiceSecretConfig{ 666 { 667 Source: "super", 668 Mode: uint32Ptr(555), 669 }, 670 }, 671 HealthCheck: &types.HealthCheckConfig{ 672 Retries: uint64Ptr(555), 673 Disable: true, 674 }, 675 Deploy: types.DeployConfig{ 676 Replicas: uint64Ptr(555), 677 UpdateConfig: &types.UpdateConfig{ 678 Parallelism: uint64Ptr(555), 679 MaxFailureRatio: 3.14, 680 }, 681 RollbackConfig: &types.UpdateConfig{ 682 Parallelism: uint64Ptr(555), 683 MaxFailureRatio: 3.14, 684 }, 685 RestartPolicy: &types.RestartPolicy{ 686 MaxAttempts: uint64Ptr(555), 687 }, 688 }, 689 Ports: []types.ServicePortConfig{ 690 {Target: 555, Mode: "ingress", Protocol: "tcp"}, 691 {Target: 34567, Mode: "ingress", Protocol: "tcp"}, 692 {Target: 555, Published: 555}, 693 }, 694 Ulimits: map[string]*types.UlimitsConfig{ 695 "nproc": {Single: 555}, 696 "nofile": {Hard: 555, Soft: 555}, 697 }, 698 Privileged: true, 699 ReadOnly: true, 700 StdinOpen: true, 701 Tty: true, 702 Volumes: []types.ServiceVolumeConfig{ 703 { 704 Source: "data", 705 Type: "volume", 706 ReadOnly: true, 707 Volume: &types.ServiceVolumeVolume{NoCopy: true}, 708 }, 709 }, 710 Environment: types.MappingWithEquals{}, 711 }, 712 }, 713 Configs: map[string]types.ConfigObjConfig{ 714 "appconfig": {External: types.External{External: true}, Name: "appconfig"}, 715 }, 716 Secrets: map[string]types.SecretConfig{ 717 "super": {External: types.External{External: true}, Name: "super"}, 718 }, 719 Volumes: map[string]types.VolumeConfig{ 720 "data": {External: types.External{External: true}, Name: "data"}, 721 }, 722 Networks: map[string]types.NetworkConfig{ 723 "front": { 724 External: types.External{External: true}, 725 Name: "front", 726 Internal: true, 727 Attachable: true, 728 }, 729 }, 730 } 731 732 assert.Check(t, is.DeepEqual(expected, config)) 733 } 734 735 func TestUnsupportedProperties(t *testing.T) { 736 dict, err := ParseYAML([]byte(` 737 version: "3" 738 services: 739 web: 740 image: web 741 build: 742 context: ./web 743 links: 744 - bar 745 pid: host 746 db: 747 image: db 748 build: 749 context: ./db 750 `)) 751 assert.NilError(t, err) 752 753 configDetails := buildConfigDetails(dict, nil) 754 755 _, err = Load(configDetails) 756 assert.NilError(t, err) 757 758 unsupported := GetUnsupportedProperties(dict) 759 assert.Check(t, is.DeepEqual([]string{"build", "links", "pid"}, unsupported)) 760 } 761 762 func TestBuildProperties(t *testing.T) { 763 dict, err := ParseYAML([]byte(` 764 version: "3" 765 services: 766 web: 767 image: web 768 build: . 769 links: 770 - bar 771 db: 772 image: db 773 build: 774 context: ./db 775 `)) 776 assert.NilError(t, err) 777 configDetails := buildConfigDetails(dict, nil) 778 _, err = Load(configDetails) 779 assert.NilError(t, err) 780 } 781 782 func TestDeprecatedProperties(t *testing.T) { 783 dict, err := ParseYAML([]byte(` 784 version: "3" 785 services: 786 web: 787 image: web 788 container_name: web 789 db: 790 image: db 791 container_name: db 792 expose: ["5434"] 793 `)) 794 assert.NilError(t, err) 795 796 configDetails := buildConfigDetails(dict, nil) 797 798 _, err = Load(configDetails) 799 assert.NilError(t, err) 800 801 deprecated := GetDeprecatedProperties(dict) 802 assert.Check(t, is.Len(deprecated, 2)) 803 assert.Check(t, is.Contains(deprecated, "container_name")) 804 assert.Check(t, is.Contains(deprecated, "expose")) 805 } 806 807 func TestForbiddenProperties(t *testing.T) { 808 _, err := loadYAML(` 809 version: "3" 810 services: 811 foo: 812 image: busybox 813 volumes: 814 - /data 815 volume_driver: some-driver 816 bar: 817 extends: 818 service: foo 819 `) 820 821 assert.ErrorType(t, err, reflect.TypeOf(&ForbiddenPropertiesError{})) 822 823 props := err.(*ForbiddenPropertiesError).Properties 824 assert.Check(t, is.Len(props, 2)) 825 assert.Check(t, is.Contains(props, "volume_driver")) 826 assert.Check(t, is.Contains(props, "extends")) 827 } 828 829 func TestInvalidResource(t *testing.T) { 830 _, err := loadYAML(` 831 version: "3" 832 services: 833 foo: 834 image: busybox 835 deploy: 836 resources: 837 impossible: 838 x: 1 839 `) 840 assert.ErrorContains(t, err, "Additional property impossible is not allowed") 841 } 842 843 func TestInvalidExternalAndDriverCombination(t *testing.T) { 844 _, err := loadYAML(` 845 version: "3" 846 volumes: 847 external_volume: 848 external: true 849 driver: foobar 850 `) 851 852 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver\" specified for volume") 853 assert.ErrorContains(t, err, "external_volume") 854 } 855 856 func TestInvalidExternalAndDirverOptsCombination(t *testing.T) { 857 _, err := loadYAML(` 858 version: "3" 859 volumes: 860 external_volume: 861 external: true 862 driver_opts: 863 beep: boop 864 `) 865 866 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver_opts\" specified for volume") 867 assert.ErrorContains(t, err, "external_volume") 868 } 869 870 func TestInvalidExternalAndLabelsCombination(t *testing.T) { 871 _, err := loadYAML(` 872 version: "3" 873 volumes: 874 external_volume: 875 external: true 876 labels: 877 - beep=boop 878 `) 879 880 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"labels\" specified for volume") 881 assert.ErrorContains(t, err, "external_volume") 882 } 883 884 func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) { 885 _, err := loadYAML(` 886 version: "3.4" 887 volumes: 888 external_volume: 889 name: user_specified_name 890 external: 891 name: external_name 892 `) 893 894 assert.ErrorContains(t, err, "volume.external.name and volume.name conflict; only use volume.name") 895 assert.ErrorContains(t, err, "external_volume") 896 } 897 898 func durationPtr(value time.Duration) *types.Duration { 899 result := types.Duration(value) 900 return &result 901 } 902 903 func uint64Ptr(value uint64) *uint64 { 904 return &value 905 } 906 907 func uint32Ptr(value uint32) *uint32 { 908 return &value 909 } 910 911 func TestFullExample(t *testing.T) { 912 bytes, err := ioutil.ReadFile("full-example.yml") 913 assert.NilError(t, err) 914 915 homeDir := "/home/foo" 916 env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"} 917 config, err := loadYAMLWithEnv(string(bytes), env) 918 assert.NilError(t, err) 919 920 workingDir, err := os.Getwd() 921 assert.NilError(t, err) 922 923 expectedConfig := fullExampleConfig(workingDir, homeDir) 924 925 assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services)) 926 assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks)) 927 assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes)) 928 assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets)) 929 assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs)) 930 assert.Check(t, is.DeepEqual(expectedConfig.Extras, config.Extras)) 931 } 932 933 func TestLoadTmpfsVolume(t *testing.T) { 934 config, err := loadYAML(` 935 version: "3.6" 936 services: 937 tmpfs: 938 image: nginx:latest 939 volumes: 940 - type: tmpfs 941 target: /app 942 tmpfs: 943 size: 10000 944 `) 945 assert.NilError(t, err) 946 947 expected := types.ServiceVolumeConfig{ 948 Target: "/app", 949 Type: "tmpfs", 950 Tmpfs: &types.ServiceVolumeTmpfs{ 951 Size: int64(10000), 952 }, 953 } 954 955 assert.Assert(t, is.Len(config.Services, 1)) 956 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 957 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 958 } 959 960 func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) { 961 _, err := loadYAML(` 962 version: "3.5" 963 services: 964 tmpfs: 965 image: nginx:latest 966 volumes: 967 - type: tmpfs 968 target: /app 969 tmpfs: 970 size: 10000 971 `) 972 assert.ErrorContains(t, err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed") 973 } 974 975 func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) { 976 _, err := loadYAML(` 977 version: "3.5" 978 services: 979 tmpfs: 980 image: nginx:latest 981 volumes: 982 - type: bind 983 target: /app 984 `) 985 assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`) 986 } 987 988 func TestLoadBindMountWithSource(t *testing.T) { 989 config, err := loadYAML(` 990 version: "3.5" 991 services: 992 bind: 993 image: nginx:latest 994 volumes: 995 - type: bind 996 target: /app 997 source: "." 998 `) 999 assert.NilError(t, err) 1000 1001 workingDir, err := os.Getwd() 1002 assert.NilError(t, err) 1003 1004 expected := types.ServiceVolumeConfig{ 1005 Type: "bind", 1006 Source: workingDir, 1007 Target: "/app", 1008 } 1009 1010 assert.Assert(t, is.Len(config.Services, 1)) 1011 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1012 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 1013 } 1014 1015 func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) { 1016 config, err := loadYAML(` 1017 version: "3.6" 1018 services: 1019 tmpfs: 1020 image: nginx:latest 1021 volumes: 1022 - type: tmpfs 1023 target: /app 1024 tmpfs: 1025 size: 0 1026 `) 1027 assert.NilError(t, err) 1028 1029 expected := types.ServiceVolumeConfig{ 1030 Target: "/app", 1031 Type: "tmpfs", 1032 Tmpfs: &types.ServiceVolumeTmpfs{}, 1033 } 1034 1035 assert.Assert(t, is.Len(config.Services, 1)) 1036 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1037 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 1038 } 1039 1040 func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) { 1041 _, err := loadYAML(` 1042 version: "3.6" 1043 services: 1044 tmpfs: 1045 image: nginx:latest 1046 volumes: 1047 - type: tmpfs 1048 target: /app 1049 tmpfs: 1050 size: -1 1051 `) 1052 assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0") 1053 } 1054 1055 func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) { 1056 _, err := loadYAML(` 1057 version: "3.6" 1058 services: 1059 tmpfs: 1060 image: nginx:latest 1061 volumes: 1062 - type: tmpfs 1063 target: /app 1064 tmpfs: 1065 size: 0.0001 1066 `) 1067 assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer") 1068 } 1069 1070 func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { 1071 sort.Slice(services, func(i, j int) bool { 1072 return services[i].Name < services[j].Name 1073 }) 1074 return services 1075 } 1076 1077 func TestLoadAttachableNetwork(t *testing.T) { 1078 config, err := loadYAML(` 1079 version: "3.2" 1080 networks: 1081 mynet1: 1082 driver: overlay 1083 attachable: true 1084 mynet2: 1085 driver: bridge 1086 `) 1087 assert.NilError(t, err) 1088 1089 expected := map[string]types.NetworkConfig{ 1090 "mynet1": { 1091 Driver: "overlay", 1092 Attachable: true, 1093 }, 1094 "mynet2": { 1095 Driver: "bridge", 1096 Attachable: false, 1097 }, 1098 } 1099 1100 assert.Check(t, is.DeepEqual(expected, config.Networks)) 1101 } 1102 1103 func TestLoadExpandedPortFormat(t *testing.T) { 1104 config, err := loadYAML(` 1105 version: "3.2" 1106 services: 1107 web: 1108 image: busybox 1109 ports: 1110 - "80-82:8080-8082" 1111 - "90-92:8090-8092/udp" 1112 - "85:8500" 1113 - 8600 1114 - protocol: udp 1115 target: 53 1116 published: 10053 1117 - mode: host 1118 target: 22 1119 published: 10022 1120 `) 1121 assert.NilError(t, err) 1122 1123 assert.Check(t, is.Len(config.Services, 1)) 1124 assert.Check(t, is.DeepEqual(samplePortsConfig, config.Services[0].Ports)) 1125 } 1126 1127 func TestLoadExpandedMountFormat(t *testing.T) { 1128 config, err := loadYAML(` 1129 version: "3.2" 1130 services: 1131 web: 1132 image: busybox 1133 volumes: 1134 - type: volume 1135 source: foo 1136 target: /target 1137 read_only: true 1138 volumes: 1139 foo: {} 1140 `) 1141 assert.NilError(t, err) 1142 1143 expected := types.ServiceVolumeConfig{ 1144 Type: "volume", 1145 Source: "foo", 1146 Target: "/target", 1147 ReadOnly: true, 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 TestLoadExtraHostsMap(t *testing.T) { 1156 config, err := loadYAML(` 1157 version: "3.2" 1158 services: 1159 web: 1160 image: busybox 1161 extra_hosts: 1162 "zulu": "162.242.195.82" 1163 "alpha": "50.31.209.229" 1164 `) 1165 assert.NilError(t, err) 1166 1167 expected := types.HostsList{ 1168 "alpha:50.31.209.229", 1169 "zulu:162.242.195.82", 1170 } 1171 1172 assert.Assert(t, is.Len(config.Services, 1)) 1173 assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) 1174 } 1175 1176 func TestLoadExtraHostsList(t *testing.T) { 1177 config, err := loadYAML(` 1178 version: "3.2" 1179 services: 1180 web: 1181 image: busybox 1182 extra_hosts: 1183 - "zulu:162.242.195.82" 1184 - "alpha:50.31.209.229" 1185 - "zulu:ff02::1" 1186 `) 1187 assert.NilError(t, err) 1188 1189 expected := types.HostsList{ 1190 "zulu:162.242.195.82", 1191 "alpha:50.31.209.229", 1192 "zulu:ff02::1", 1193 } 1194 1195 assert.Assert(t, is.Len(config.Services, 1)) 1196 assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) 1197 } 1198 1199 func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) { 1200 buf, cleanup := patchLogrus() 1201 defer cleanup() 1202 1203 source := map[string]interface{}{ 1204 "foo": map[string]interface{}{ 1205 "external": map[string]interface{}{ 1206 "name": "oops", 1207 }, 1208 }, 1209 } 1210 volumes, err := LoadVolumes(source, "3.4") 1211 assert.NilError(t, err) 1212 expected := map[string]types.VolumeConfig{ 1213 "foo": { 1214 Name: "oops", 1215 External: types.External{External: true}, 1216 }, 1217 } 1218 assert.Check(t, is.DeepEqual(expected, volumes)) 1219 assert.Check(t, is.Contains(buf.String(), "volume.external.name is deprecated")) 1220 1221 } 1222 1223 func patchLogrus() (*bytes.Buffer, func()) { 1224 buf := new(bytes.Buffer) 1225 out := logrus.StandardLogger().Out 1226 logrus.SetOutput(buf) 1227 return buf, func() { logrus.SetOutput(out) } 1228 } 1229 1230 func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) { 1231 buf, cleanup := patchLogrus() 1232 defer cleanup() 1233 1234 source := map[string]interface{}{ 1235 "foo": map[string]interface{}{ 1236 "external": map[string]interface{}{ 1237 "name": "oops", 1238 }, 1239 }, 1240 } 1241 volumes, err := LoadVolumes(source, "3.3") 1242 assert.NilError(t, err) 1243 expected := map[string]types.VolumeConfig{ 1244 "foo": { 1245 Name: "oops", 1246 External: types.External{External: true}, 1247 }, 1248 } 1249 assert.Check(t, is.DeepEqual(expected, volumes)) 1250 assert.Check(t, is.Equal("", buf.String())) 1251 } 1252 1253 func TestLoadV35(t *testing.T) { 1254 actual, err := loadYAML(` 1255 version: "3.5" 1256 services: 1257 foo: 1258 image: busybox 1259 isolation: process 1260 configs: 1261 foo: 1262 name: fooqux 1263 external: true 1264 bar: 1265 name: barqux 1266 file: ./example1.env 1267 secrets: 1268 foo: 1269 name: fooqux 1270 external: true 1271 bar: 1272 name: barqux 1273 file: ./full-example.yml 1274 `) 1275 assert.NilError(t, err) 1276 assert.Check(t, is.Len(actual.Services, 1)) 1277 assert.Check(t, is.Len(actual.Secrets, 2)) 1278 assert.Check(t, is.Len(actual.Configs, 2)) 1279 assert.Check(t, is.Equal("process", actual.Services[0].Isolation)) 1280 } 1281 1282 func TestLoadV35InvalidIsolation(t *testing.T) { 1283 // validation should be done only on the daemon side 1284 actual, err := loadYAML(` 1285 version: "3.5" 1286 services: 1287 foo: 1288 image: busybox 1289 isolation: invalid 1290 configs: 1291 super: 1292 external: true 1293 `) 1294 assert.NilError(t, err) 1295 assert.Assert(t, is.Len(actual.Services, 1)) 1296 assert.Check(t, is.Equal("invalid", actual.Services[0].Isolation)) 1297 } 1298 1299 func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) { 1300 _, err := loadYAML(` 1301 version: "3.5" 1302 secrets: 1303 external_secret: 1304 name: user_specified_name 1305 external: 1306 name: external_name 1307 `) 1308 1309 assert.ErrorContains(t, err, "secret.external.name and secret.name conflict; only use secret.name") 1310 assert.ErrorContains(t, err, "external_secret") 1311 } 1312 1313 func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) { 1314 buf, cleanup := patchLogrus() 1315 defer cleanup() 1316 1317 source := map[string]interface{}{ 1318 "foo": map[string]interface{}{ 1319 "external": map[string]interface{}{ 1320 "name": "oops", 1321 }, 1322 }, 1323 } 1324 details := types.ConfigDetails{ 1325 Version: "3.5", 1326 } 1327 secrets, err := LoadSecrets(source, details) 1328 assert.NilError(t, err) 1329 expected := map[string]types.SecretConfig{ 1330 "foo": { 1331 Name: "oops", 1332 External: types.External{External: true}, 1333 }, 1334 } 1335 assert.Check(t, is.DeepEqual(expected, secrets)) 1336 assert.Check(t, is.Contains(buf.String(), "secret.external.name is deprecated")) 1337 } 1338 1339 func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) { 1340 buf, cleanup := patchLogrus() 1341 defer cleanup() 1342 1343 source := map[string]interface{}{ 1344 "foo": map[string]interface{}{ 1345 "external": map[string]interface{}{ 1346 "name": "oops", 1347 }, 1348 }, 1349 } 1350 networks, err := LoadNetworks(source, "3.5") 1351 assert.NilError(t, err) 1352 expected := map[string]types.NetworkConfig{ 1353 "foo": { 1354 Name: "oops", 1355 External: types.External{External: true}, 1356 }, 1357 } 1358 assert.Check(t, is.DeepEqual(expected, networks)) 1359 assert.Check(t, is.Contains(buf.String(), "network.external.name is deprecated")) 1360 1361 } 1362 1363 func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) { 1364 buf, cleanup := patchLogrus() 1365 defer cleanup() 1366 1367 source := map[string]interface{}{ 1368 "foo": map[string]interface{}{ 1369 "external": map[string]interface{}{ 1370 "name": "oops", 1371 }, 1372 }, 1373 } 1374 networks, err := LoadNetworks(source, "3.4") 1375 assert.NilError(t, err) 1376 expected := map[string]types.NetworkConfig{ 1377 "foo": { 1378 Name: "oops", 1379 External: types.External{External: true}, 1380 }, 1381 } 1382 assert.Check(t, is.DeepEqual(expected, networks)) 1383 assert.Check(t, is.Equal("", buf.String())) 1384 } 1385 1386 func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) { 1387 _, err := loadYAML(` 1388 version: "3.5" 1389 networks: 1390 foo: 1391 name: user_specified_name 1392 external: 1393 name: external_name 1394 `) 1395 1396 assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name") 1397 assert.ErrorContains(t, err, "foo") 1398 } 1399 1400 func TestLoadNetworkWithName(t *testing.T) { 1401 config, err := loadYAML(` 1402 version: '3.5' 1403 services: 1404 hello-world: 1405 image: redis:alpine 1406 networks: 1407 - network1 1408 - network3 1409 1410 networks: 1411 network1: 1412 name: network2 1413 network3: 1414 `) 1415 assert.NilError(t, err) 1416 expected := &types.Config{ 1417 Filename: "filename.yml", 1418 Version: "3.5", 1419 Services: types.Services{ 1420 { 1421 Name: "hello-world", 1422 Image: "redis:alpine", 1423 Networks: map[string]*types.ServiceNetworkConfig{ 1424 "network1": nil, 1425 "network3": nil, 1426 }, 1427 }, 1428 }, 1429 Networks: map[string]types.NetworkConfig{ 1430 "network1": {Name: "network2"}, 1431 "network3": {}, 1432 }, 1433 } 1434 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1435 } 1436 1437 func TestLoadInit(t *testing.T) { 1438 booleanTrue := true 1439 booleanFalse := false 1440 1441 var testcases = []struct { 1442 doc string 1443 yaml string 1444 init *bool 1445 }{ 1446 { 1447 doc: "no init defined", 1448 yaml: ` 1449 version: '3.7' 1450 services: 1451 foo: 1452 image: alpine`, 1453 }, 1454 { 1455 doc: "has true init", 1456 yaml: ` 1457 version: '3.7' 1458 services: 1459 foo: 1460 image: alpine 1461 init: true`, 1462 init: &booleanTrue, 1463 }, 1464 { 1465 doc: "has false init", 1466 yaml: ` 1467 version: '3.7' 1468 services: 1469 foo: 1470 image: alpine 1471 init: false`, 1472 init: &booleanFalse, 1473 }, 1474 } 1475 for _, testcase := range testcases { 1476 t.Run(testcase.doc, func(t *testing.T) { 1477 config, err := loadYAML(testcase.yaml) 1478 assert.NilError(t, err) 1479 assert.Check(t, is.Len(config.Services, 1)) 1480 assert.Check(t, is.DeepEqual(config.Services[0].Init, testcase.init)) 1481 }) 1482 } 1483 } 1484 1485 func TestLoadSysctls(t *testing.T) { 1486 config, err := loadYAML(` 1487 version: "3.8" 1488 services: 1489 web: 1490 image: busybox 1491 sysctls: 1492 - net.core.somaxconn=1024 1493 - net.ipv4.tcp_syncookies=0 1494 - testing.one.one= 1495 - testing.one.two 1496 `) 1497 assert.NilError(t, err) 1498 1499 expected := types.Mapping{ 1500 "net.core.somaxconn": "1024", 1501 "net.ipv4.tcp_syncookies": "0", 1502 "testing.one.one": "", 1503 "testing.one.two": "", 1504 } 1505 1506 assert.Assert(t, is.Len(config.Services, 1)) 1507 assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls)) 1508 1509 config, err = loadYAML(` 1510 version: "3.8" 1511 services: 1512 web: 1513 image: busybox 1514 sysctls: 1515 net.core.somaxconn: 1024 1516 net.ipv4.tcp_syncookies: 0 1517 testing.one.one: "" 1518 testing.one.two: 1519 `) 1520 assert.NilError(t, err) 1521 1522 assert.Assert(t, is.Len(config.Services, 1)) 1523 assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls)) 1524 } 1525 1526 func TestTransform(t *testing.T) { 1527 var source = []interface{}{ 1528 "80-82:8080-8082", 1529 "90-92:8090-8092/udp", 1530 "85:8500", 1531 8600, 1532 map[string]interface{}{ 1533 "protocol": "udp", 1534 "target": 53, 1535 "published": 10053, 1536 }, 1537 map[string]interface{}{ 1538 "mode": "host", 1539 "target": 22, 1540 "published": 10022, 1541 }, 1542 } 1543 var ports []types.ServicePortConfig 1544 err := Transform(source, &ports) 1545 assert.NilError(t, err) 1546 1547 assert.Check(t, is.DeepEqual(samplePortsConfig, ports)) 1548 } 1549 1550 func TestLoadTemplateDriver(t *testing.T) { 1551 config, err := loadYAML(` 1552 version: '3.8' 1553 services: 1554 hello-world: 1555 image: redis:alpine 1556 secrets: 1557 - secret 1558 configs: 1559 - config 1560 1561 configs: 1562 config: 1563 name: config 1564 external: true 1565 template_driver: config-driver 1566 1567 secrets: 1568 secret: 1569 name: secret 1570 external: true 1571 template_driver: secret-driver 1572 `) 1573 assert.NilError(t, err) 1574 expected := &types.Config{ 1575 Filename: "filename.yml", 1576 Version: "3.8", 1577 Services: types.Services{ 1578 { 1579 Name: "hello-world", 1580 Image: "redis:alpine", 1581 Configs: []types.ServiceConfigObjConfig{ 1582 { 1583 Source: "config", 1584 }, 1585 }, 1586 Secrets: []types.ServiceSecretConfig{ 1587 { 1588 Source: "secret", 1589 }, 1590 }, 1591 }, 1592 }, 1593 Configs: map[string]types.ConfigObjConfig{ 1594 "config": { 1595 Name: "config", 1596 External: types.External{External: true}, 1597 TemplateDriver: "config-driver", 1598 }, 1599 }, 1600 Secrets: map[string]types.SecretConfig{ 1601 "secret": { 1602 Name: "secret", 1603 External: types.External{External: true}, 1604 TemplateDriver: "secret-driver", 1605 }, 1606 }, 1607 } 1608 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1609 } 1610 1611 func TestLoadSecretDriver(t *testing.T) { 1612 config, err := loadYAML(` 1613 version: '3.8' 1614 services: 1615 hello-world: 1616 image: redis:alpine 1617 secrets: 1618 - secret 1619 configs: 1620 - config 1621 1622 configs: 1623 config: 1624 name: config 1625 external: true 1626 1627 secrets: 1628 secret: 1629 name: secret 1630 driver: secret-bucket 1631 driver_opts: 1632 OptionA: value for driver option A 1633 OptionB: value for driver option B 1634 `) 1635 assert.NilError(t, err) 1636 expected := &types.Config{ 1637 Filename: "filename.yml", 1638 Version: "3.8", 1639 Services: types.Services{ 1640 { 1641 Name: "hello-world", 1642 Image: "redis:alpine", 1643 Configs: []types.ServiceConfigObjConfig{ 1644 { 1645 Source: "config", 1646 }, 1647 }, 1648 Secrets: []types.ServiceSecretConfig{ 1649 { 1650 Source: "secret", 1651 }, 1652 }, 1653 }, 1654 }, 1655 Configs: map[string]types.ConfigObjConfig{ 1656 "config": { 1657 Name: "config", 1658 External: types.External{External: true}, 1659 }, 1660 }, 1661 Secrets: map[string]types.SecretConfig{ 1662 "secret": { 1663 Name: "secret", 1664 Driver: "secret-bucket", 1665 DriverOpts: map[string]string{ 1666 "OptionA": "value for driver option A", 1667 "OptionB": "value for driver option B", 1668 }, 1669 }, 1670 }, 1671 } 1672 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1673 }