1package loader 2 3import ( 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 19func 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 34func loadYAML(yaml string) (*types.Config, error) { 35 return loadYAMLWithEnv(yaml, nil) 36} 37 38func 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 47var sampleYAML = ` 48version: "3" 49services: 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 60volumes: 61 hello: 62 driver: default 63 driver_opts: 64 beep: boop 65networks: 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 77var 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 118func strPtr(val string) *string { 119 return &val 120} 121 122var sampleConfig = types.Config{ 123 Version: "3.0", 124 Services: []types.ServiceConfig{ 125 { 126 Name: "foo", 127 Image: "busybox", 128 Environment: map[string]*string{}, 129 Networks: map[string]*types.ServiceNetworkConfig{ 130 "with_me": nil, 131 }, 132 }, 133 { 134 Name: "bar", 135 Image: "busybox", 136 Environment: map[string]*string{"FOO": strPtr("1")}, 137 Networks: map[string]*types.ServiceNetworkConfig{ 138 "with_ipam": nil, 139 }, 140 }, 141 }, 142 Networks: map[string]types.NetworkConfig{ 143 "default": { 144 Driver: "bridge", 145 DriverOpts: map[string]string{ 146 "beep": "boop", 147 }, 148 }, 149 "with_ipam": { 150 Ipam: types.IPAMConfig{ 151 Driver: "default", 152 Config: []*types.IPAMPool{ 153 { 154 Subnet: "172.28.0.0/16", 155 }, 156 }, 157 }, 158 }, 159 }, 160 Volumes: map[string]types.VolumeConfig{ 161 "hello": { 162 Driver: "default", 163 DriverOpts: map[string]string{ 164 "beep": "boop", 165 }, 166 }, 167 }, 168} 169 170func TestParseYAML(t *testing.T) { 171 dict, err := ParseYAML([]byte(sampleYAML)) 172 assert.NilError(t, err) 173 assert.Check(t, is.DeepEqual(sampleDict, dict)) 174} 175 176func TestLoad(t *testing.T) { 177 actual, err := Load(buildConfigDetails(sampleDict, nil)) 178 assert.NilError(t, err) 179 assert.Check(t, is.Equal(sampleConfig.Version, actual.Version)) 180 assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services))) 181 assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks)) 182 assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes)) 183} 184 185func TestLoadExtras(t *testing.T) { 186 actual, err := loadYAML(` 187version: "3.7" 188services: 189 foo: 190 image: busybox 191 x-foo: bar`) 192 assert.NilError(t, err) 193 assert.Check(t, is.Len(actual.Services, 1)) 194 service := actual.Services[0] 195 assert.Check(t, is.Equal("busybox", service.Image)) 196 extras := map[string]interface{}{ 197 "x-foo": "bar", 198 } 199 assert.Check(t, is.DeepEqual(extras, service.Extras)) 200} 201 202func TestLoadV31(t *testing.T) { 203 actual, err := loadYAML(` 204version: "3.1" 205services: 206 foo: 207 image: busybox 208 secrets: [super] 209secrets: 210 super: 211 external: true 212`) 213 assert.NilError(t, err) 214 assert.Check(t, is.Len(actual.Services, 1)) 215 assert.Check(t, is.Len(actual.Secrets, 1)) 216} 217 218func TestLoadV33(t *testing.T) { 219 actual, err := loadYAML(` 220version: "3.3" 221services: 222 foo: 223 image: busybox 224 credential_spec: 225 File: "/foo" 226 configs: [super] 227configs: 228 super: 229 external: true 230`) 231 assert.NilError(t, err) 232 assert.Assert(t, is.Len(actual.Services, 1)) 233 assert.Check(t, is.Equal(actual.Services[0].CredentialSpec.File, "/foo")) 234 assert.Assert(t, is.Len(actual.Configs, 1)) 235} 236 237func TestParseAndLoad(t *testing.T) { 238 actual, err := loadYAML(sampleYAML) 239 assert.NilError(t, err) 240 assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services))) 241 assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks)) 242 assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes)) 243} 244 245func TestInvalidTopLevelObjectType(t *testing.T) { 246 _, err := loadYAML("1") 247 assert.ErrorContains(t, err, "Top-level object must be a mapping") 248 249 _, err = loadYAML("\"hello\"") 250 assert.ErrorContains(t, err, "Top-level object must be a mapping") 251 252 _, err = loadYAML("[\"hello\"]") 253 assert.ErrorContains(t, err, "Top-level object must be a mapping") 254} 255 256func TestNonStringKeys(t *testing.T) { 257 _, err := loadYAML(` 258version: "3" 259123: 260 foo: 261 image: busybox 262`) 263 assert.ErrorContains(t, err, "Non-string key at top level: 123") 264 265 _, err = loadYAML(` 266version: "3" 267services: 268 foo: 269 image: busybox 270 123: 271 image: busybox 272`) 273 assert.ErrorContains(t, err, "Non-string key in services: 123") 274 275 _, err = loadYAML(` 276version: "3" 277services: 278 foo: 279 image: busybox 280networks: 281 default: 282 ipam: 283 config: 284 - 123: oh dear 285`) 286 assert.ErrorContains(t, err, "Non-string key in networks.default.ipam.config[0]: 123") 287 288 _, err = loadYAML(` 289version: "3" 290services: 291 dict-env: 292 image: busybox 293 environment: 294 1: FOO 295`) 296 assert.ErrorContains(t, err, "Non-string key in services.dict-env.environment: 1") 297} 298 299func TestSupportedVersion(t *testing.T) { 300 _, err := loadYAML(` 301version: "3" 302services: 303 foo: 304 image: busybox 305`) 306 assert.NilError(t, err) 307 308 _, err = loadYAML(` 309version: "3.0" 310services: 311 foo: 312 image: busybox 313`) 314 assert.NilError(t, err) 315} 316 317func TestUnsupportedVersion(t *testing.T) { 318 _, err := loadYAML(` 319version: "2" 320services: 321 foo: 322 image: busybox 323`) 324 assert.ErrorContains(t, err, "version") 325 326 _, err = loadYAML(` 327version: "2.0" 328services: 329 foo: 330 image: busybox 331`) 332 assert.ErrorContains(t, err, "version") 333} 334 335func TestInvalidVersion(t *testing.T) { 336 _, err := loadYAML(` 337version: 3 338services: 339 foo: 340 image: busybox 341`) 342 assert.ErrorContains(t, err, "version must be a string") 343} 344 345func TestV1Unsupported(t *testing.T) { 346 _, err := loadYAML(` 347foo: 348 image: busybox 349`) 350 assert.ErrorContains(t, err, "unsupported Compose file version: 1.0") 351} 352 353func TestNonMappingObject(t *testing.T) { 354 _, err := loadYAML(` 355version: "3" 356services: 357 - foo: 358 image: busybox 359`) 360 assert.ErrorContains(t, err, "services must be a mapping") 361 362 _, err = loadYAML(` 363version: "3" 364services: 365 foo: busybox 366`) 367 assert.ErrorContains(t, err, "services.foo must be a mapping") 368 369 _, err = loadYAML(` 370version: "3" 371networks: 372 - default: 373 driver: bridge 374`) 375 assert.ErrorContains(t, err, "networks must be a mapping") 376 377 _, err = loadYAML(` 378version: "3" 379networks: 380 default: bridge 381`) 382 assert.ErrorContains(t, err, "networks.default must be a mapping") 383 384 _, err = loadYAML(` 385version: "3" 386volumes: 387 - data: 388 driver: local 389`) 390 assert.ErrorContains(t, err, "volumes must be a mapping") 391 392 _, err = loadYAML(` 393version: "3" 394volumes: 395 data: local 396`) 397 assert.ErrorContains(t, err, "volumes.data must be a mapping") 398} 399 400func TestNonStringImage(t *testing.T) { 401 _, err := loadYAML(` 402version: "3" 403services: 404 foo: 405 image: ["busybox", "latest"] 406`) 407 assert.ErrorContains(t, err, "services.foo.image must be a string") 408} 409 410func TestLoadWithEnvironment(t *testing.T) { 411 config, err := loadYAMLWithEnv(` 412version: "3" 413services: 414 dict-env: 415 image: busybox 416 environment: 417 FOO: "1" 418 BAR: 2 419 BAZ: 2.5 420 QUX: 421 QUUX: 422 list-env: 423 image: busybox 424 environment: 425 - FOO=1 426 - BAR=2 427 - BAZ=2.5 428 - QUX= 429 - QUUX 430`, map[string]string{"QUX": "qux"}) 431 assert.NilError(t, err) 432 433 expected := types.MappingWithEquals{ 434 "FOO": strPtr("1"), 435 "BAR": strPtr("2"), 436 "BAZ": strPtr("2.5"), 437 "QUX": strPtr("qux"), 438 "QUUX": nil, 439 } 440 441 assert.Check(t, is.Equal(2, len(config.Services))) 442 443 for _, service := range config.Services { 444 assert.Check(t, is.DeepEqual(expected, service.Environment)) 445 } 446} 447 448func TestInvalidEnvironmentValue(t *testing.T) { 449 _, err := loadYAML(` 450version: "3" 451services: 452 dict-env: 453 image: busybox 454 environment: 455 FOO: ["1"] 456`) 457 assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a string, number or null") 458} 459 460func TestInvalidEnvironmentObject(t *testing.T) { 461 _, err := loadYAML(` 462version: "3" 463services: 464 dict-env: 465 image: busybox 466 environment: "FOO=1" 467`) 468 assert.ErrorContains(t, err, "services.dict-env.environment must be a mapping") 469} 470 471func TestLoadWithEnvironmentInterpolation(t *testing.T) { 472 home := "/home/foo" 473 config, err := loadYAMLWithEnv(` 474version: "3" 475services: 476 test: 477 image: busybox 478 labels: 479 - home1=$HOME 480 - home2=${HOME} 481 - nonexistent=$NONEXISTENT 482 - default=${NONEXISTENT-default} 483networks: 484 test: 485 driver: $HOME 486volumes: 487 test: 488 driver: $HOME 489`, map[string]string{ 490 "HOME": home, 491 "FOO": "foo", 492 }) 493 494 assert.NilError(t, err) 495 496 expectedLabels := types.Labels{ 497 "home1": home, 498 "home2": home, 499 "nonexistent": "", 500 "default": "default", 501 } 502 503 assert.Check(t, is.DeepEqual(expectedLabels, config.Services[0].Labels)) 504 assert.Check(t, is.Equal(home, config.Networks["test"].Driver)) 505 assert.Check(t, is.Equal(home, config.Volumes["test"].Driver)) 506} 507 508func TestLoadWithInterpolationCastFull(t *testing.T) { 509 dict, err := ParseYAML([]byte(` 510version: "3.4" 511services: 512 web: 513 configs: 514 - source: appconfig 515 mode: $theint 516 secrets: 517 - source: super 518 mode: $theint 519 healthcheck: 520 retries: ${theint} 521 disable: $thebool 522 deploy: 523 replicas: $theint 524 update_config: 525 parallelism: $theint 526 max_failure_ratio: $thefloat 527 restart_policy: 528 max_attempts: $theint 529 ports: 530 - $theint 531 - "34567" 532 - target: $theint 533 published: $theint 534 ulimits: 535 nproc: $theint 536 nofile: 537 hard: $theint 538 soft: $theint 539 privileged: $thebool 540 read_only: $thebool 541 stdin_open: ${thebool} 542 tty: $thebool 543 volumes: 544 - source: data 545 type: volume 546 read_only: $thebool 547 volume: 548 nocopy: $thebool 549 550configs: 551 appconfig: 552 external: $thebool 553secrets: 554 super: 555 external: $thebool 556volumes: 557 data: 558 external: $thebool 559networks: 560 front: 561 external: $thebool 562 internal: $thebool 563 attachable: $thebool 564 565`)) 566 assert.NilError(t, err) 567 env := map[string]string{ 568 "theint": "555", 569 "thefloat": "3.14", 570 "thebool": "true", 571 } 572 573 config, err := Load(buildConfigDetails(dict, env)) 574 assert.NilError(t, err) 575 expected := &types.Config{ 576 Filename: "filename.yml", 577 Version: "3.4", 578 Services: []types.ServiceConfig{ 579 { 580 Name: "web", 581 Configs: []types.ServiceConfigObjConfig{ 582 { 583 Source: "appconfig", 584 Mode: uint32Ptr(555), 585 }, 586 }, 587 Secrets: []types.ServiceSecretConfig{ 588 { 589 Source: "super", 590 Mode: uint32Ptr(555), 591 }, 592 }, 593 HealthCheck: &types.HealthCheckConfig{ 594 Retries: uint64Ptr(555), 595 Disable: true, 596 }, 597 Deploy: types.DeployConfig{ 598 Replicas: uint64Ptr(555), 599 UpdateConfig: &types.UpdateConfig{ 600 Parallelism: uint64Ptr(555), 601 MaxFailureRatio: 3.14, 602 }, 603 RestartPolicy: &types.RestartPolicy{ 604 MaxAttempts: uint64Ptr(555), 605 }, 606 }, 607 Ports: []types.ServicePortConfig{ 608 {Target: 555, Mode: "ingress", Protocol: "tcp"}, 609 {Target: 34567, Mode: "ingress", Protocol: "tcp"}, 610 {Target: 555, Published: 555}, 611 }, 612 Ulimits: map[string]*types.UlimitsConfig{ 613 "nproc": {Single: 555}, 614 "nofile": {Hard: 555, Soft: 555}, 615 }, 616 Privileged: true, 617 ReadOnly: true, 618 StdinOpen: true, 619 Tty: true, 620 Volumes: []types.ServiceVolumeConfig{ 621 { 622 Source: "data", 623 Type: "volume", 624 ReadOnly: true, 625 Volume: &types.ServiceVolumeVolume{NoCopy: true}, 626 }, 627 }, 628 Environment: types.MappingWithEquals{}, 629 }, 630 }, 631 Configs: map[string]types.ConfigObjConfig{ 632 "appconfig": {External: types.External{External: true}, Name: "appconfig"}, 633 }, 634 Secrets: map[string]types.SecretConfig{ 635 "super": {External: types.External{External: true}, Name: "super"}, 636 }, 637 Volumes: map[string]types.VolumeConfig{ 638 "data": {External: types.External{External: true}, Name: "data"}, 639 }, 640 Networks: map[string]types.NetworkConfig{ 641 "front": { 642 External: types.External{External: true}, 643 Name: "front", 644 Internal: true, 645 Attachable: true, 646 }, 647 }, 648 } 649 650 assert.Check(t, is.DeepEqual(expected, config)) 651} 652 653func TestUnsupportedProperties(t *testing.T) { 654 dict, err := ParseYAML([]byte(` 655version: "3" 656services: 657 web: 658 image: web 659 build: 660 context: ./web 661 links: 662 - bar 663 pid: host 664 db: 665 image: db 666 build: 667 context: ./db 668`)) 669 assert.NilError(t, err) 670 671 configDetails := buildConfigDetails(dict, nil) 672 673 _, err = Load(configDetails) 674 assert.NilError(t, err) 675 676 unsupported := GetUnsupportedProperties(dict) 677 assert.Check(t, is.DeepEqual([]string{"build", "links", "pid"}, unsupported)) 678} 679 680func TestBuildProperties(t *testing.T) { 681 dict, err := ParseYAML([]byte(` 682version: "3" 683services: 684 web: 685 image: web 686 build: . 687 links: 688 - bar 689 db: 690 image: db 691 build: 692 context: ./db 693`)) 694 assert.NilError(t, err) 695 configDetails := buildConfigDetails(dict, nil) 696 _, err = Load(configDetails) 697 assert.NilError(t, err) 698} 699 700func TestDeprecatedProperties(t *testing.T) { 701 dict, err := ParseYAML([]byte(` 702version: "3" 703services: 704 web: 705 image: web 706 container_name: web 707 db: 708 image: db 709 container_name: db 710 expose: ["5434"] 711`)) 712 assert.NilError(t, err) 713 714 configDetails := buildConfigDetails(dict, nil) 715 716 _, err = Load(configDetails) 717 assert.NilError(t, err) 718 719 deprecated := GetDeprecatedProperties(dict) 720 assert.Check(t, is.Len(deprecated, 2)) 721 assert.Check(t, is.Contains(deprecated, "container_name")) 722 assert.Check(t, is.Contains(deprecated, "expose")) 723} 724 725func TestForbiddenProperties(t *testing.T) { 726 _, err := loadYAML(` 727version: "3" 728services: 729 foo: 730 image: busybox 731 volumes: 732 - /data 733 volume_driver: some-driver 734 bar: 735 extends: 736 service: foo 737`) 738 739 assert.ErrorType(t, err, reflect.TypeOf(&ForbiddenPropertiesError{})) 740 741 props := err.(*ForbiddenPropertiesError).Properties 742 assert.Check(t, is.Len(props, 2)) 743 assert.Check(t, is.Contains(props, "volume_driver")) 744 assert.Check(t, is.Contains(props, "extends")) 745} 746 747func TestInvalidResource(t *testing.T) { 748 _, err := loadYAML(` 749 version: "3" 750 services: 751 foo: 752 image: busybox 753 deploy: 754 resources: 755 impossible: 756 x: 1 757`) 758 assert.ErrorContains(t, err, "Additional property impossible is not allowed") 759} 760 761func TestInvalidExternalAndDriverCombination(t *testing.T) { 762 _, err := loadYAML(` 763version: "3" 764volumes: 765 external_volume: 766 external: true 767 driver: foobar 768`) 769 770 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver\" specified for volume") 771 assert.ErrorContains(t, err, "external_volume") 772} 773 774func TestInvalidExternalAndDirverOptsCombination(t *testing.T) { 775 _, err := loadYAML(` 776version: "3" 777volumes: 778 external_volume: 779 external: true 780 driver_opts: 781 beep: boop 782`) 783 784 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"driver_opts\" specified for volume") 785 assert.ErrorContains(t, err, "external_volume") 786} 787 788func TestInvalidExternalAndLabelsCombination(t *testing.T) { 789 _, err := loadYAML(` 790version: "3" 791volumes: 792 external_volume: 793 external: true 794 labels: 795 - beep=boop 796`) 797 798 assert.ErrorContains(t, err, "conflicting parameters \"external\" and \"labels\" specified for volume") 799 assert.ErrorContains(t, err, "external_volume") 800} 801 802func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) { 803 _, err := loadYAML(` 804version: "3.4" 805volumes: 806 external_volume: 807 name: user_specified_name 808 external: 809 name: external_name 810`) 811 812 assert.ErrorContains(t, err, "volume.external.name and volume.name conflict; only use volume.name") 813 assert.ErrorContains(t, err, "external_volume") 814} 815 816func durationPtr(value time.Duration) *time.Duration { 817 return &value 818} 819 820func uint64Ptr(value uint64) *uint64 { 821 return &value 822} 823 824func uint32Ptr(value uint32) *uint32 { 825 return &value 826} 827 828func TestFullExample(t *testing.T) { 829 bytes, err := ioutil.ReadFile("full-example.yml") 830 assert.NilError(t, err) 831 832 homeDir := "/home/foo" 833 env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"} 834 config, err := loadYAMLWithEnv(string(bytes), env) 835 assert.NilError(t, err) 836 837 workingDir, err := os.Getwd() 838 assert.NilError(t, err) 839 840 expectedConfig := fullExampleConfig(workingDir, homeDir) 841 842 assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services)) 843 assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks)) 844 assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes)) 845 assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets)) 846 assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs)) 847 assert.Check(t, is.DeepEqual(expectedConfig.Extras, config.Extras)) 848} 849 850func TestLoadTmpfsVolume(t *testing.T) { 851 config, err := loadYAML(` 852version: "3.6" 853services: 854 tmpfs: 855 image: nginx:latest 856 volumes: 857 - type: tmpfs 858 target: /app 859 tmpfs: 860 size: 10000 861`) 862 assert.NilError(t, err) 863 864 expected := types.ServiceVolumeConfig{ 865 Target: "/app", 866 Type: "tmpfs", 867 Tmpfs: &types.ServiceVolumeTmpfs{ 868 Size: int64(10000), 869 }, 870 } 871 872 assert.Assert(t, is.Len(config.Services, 1)) 873 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 874 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 875} 876 877func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) { 878 _, err := loadYAML(` 879version: "3.5" 880services: 881 tmpfs: 882 image: nginx:latest 883 volumes: 884 - type: tmpfs 885 target: /app 886 tmpfs: 887 size: 10000 888`) 889 assert.ErrorContains(t, err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed") 890} 891 892func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) { 893 _, err := loadYAML(` 894version: "3.5" 895services: 896 tmpfs: 897 image: nginx:latest 898 volumes: 899 - type: bind 900 target: /app 901`) 902 assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`) 903} 904 905func TestLoadBindMountWithSource(t *testing.T) { 906 config, err := loadYAML(` 907version: "3.5" 908services: 909 bind: 910 image: nginx:latest 911 volumes: 912 - type: bind 913 target: /app 914 source: "." 915`) 916 assert.NilError(t, err) 917 918 workingDir, err := os.Getwd() 919 assert.NilError(t, err) 920 921 expected := types.ServiceVolumeConfig{ 922 Type: "bind", 923 Source: workingDir, 924 Target: "/app", 925 } 926 927 assert.Assert(t, is.Len(config.Services, 1)) 928 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 929 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 930} 931 932func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) { 933 config, err := loadYAML(` 934version: "3.6" 935services: 936 tmpfs: 937 image: nginx:latest 938 volumes: 939 - type: tmpfs 940 target: /app 941 tmpfs: 942 size: 0 943`) 944 assert.NilError(t, err) 945 946 expected := types.ServiceVolumeConfig{ 947 Target: "/app", 948 Type: "tmpfs", 949 Tmpfs: &types.ServiceVolumeTmpfs{}, 950 } 951 952 assert.Assert(t, is.Len(config.Services, 1)) 953 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 954 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 955} 956 957func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) { 958 _, err := loadYAML(` 959version: "3.6" 960services: 961 tmpfs: 962 image: nginx:latest 963 volumes: 964 - type: tmpfs 965 target: /app 966 tmpfs: 967 size: -1 968`) 969 assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0") 970} 971 972func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) { 973 _, err := loadYAML(` 974version: "3.6" 975services: 976 tmpfs: 977 image: nginx:latest 978 volumes: 979 - type: tmpfs 980 target: /app 981 tmpfs: 982 size: 0.0001 983`) 984 assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer") 985} 986 987func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { 988 sort.Slice(services, func(i, j int) bool { 989 return services[i].Name < services[j].Name 990 }) 991 return services 992} 993 994func TestLoadAttachableNetwork(t *testing.T) { 995 config, err := loadYAML(` 996version: "3.2" 997networks: 998 mynet1: 999 driver: overlay 1000 attachable: true 1001 mynet2: 1002 driver: bridge 1003`) 1004 assert.NilError(t, err) 1005 1006 expected := map[string]types.NetworkConfig{ 1007 "mynet1": { 1008 Driver: "overlay", 1009 Attachable: true, 1010 }, 1011 "mynet2": { 1012 Driver: "bridge", 1013 Attachable: false, 1014 }, 1015 } 1016 1017 assert.Check(t, is.DeepEqual(expected, config.Networks)) 1018} 1019 1020func TestLoadExpandedPortFormat(t *testing.T) { 1021 config, err := loadYAML(` 1022version: "3.2" 1023services: 1024 web: 1025 image: busybox 1026 ports: 1027 - "80-82:8080-8082" 1028 - "90-92:8090-8092/udp" 1029 - "85:8500" 1030 - 8600 1031 - protocol: udp 1032 target: 53 1033 published: 10053 1034 - mode: host 1035 target: 22 1036 published: 10022 1037`) 1038 assert.NilError(t, err) 1039 1040 expected := []types.ServicePortConfig{ 1041 { 1042 Mode: "ingress", 1043 Target: 8080, 1044 Published: 80, 1045 Protocol: "tcp", 1046 }, 1047 { 1048 Mode: "ingress", 1049 Target: 8081, 1050 Published: 81, 1051 Protocol: "tcp", 1052 }, 1053 { 1054 Mode: "ingress", 1055 Target: 8082, 1056 Published: 82, 1057 Protocol: "tcp", 1058 }, 1059 { 1060 Mode: "ingress", 1061 Target: 8090, 1062 Published: 90, 1063 Protocol: "udp", 1064 }, 1065 { 1066 Mode: "ingress", 1067 Target: 8091, 1068 Published: 91, 1069 Protocol: "udp", 1070 }, 1071 { 1072 Mode: "ingress", 1073 Target: 8092, 1074 Published: 92, 1075 Protocol: "udp", 1076 }, 1077 { 1078 Mode: "ingress", 1079 Target: 8500, 1080 Published: 85, 1081 Protocol: "tcp", 1082 }, 1083 { 1084 Mode: "ingress", 1085 Target: 8600, 1086 Published: 0, 1087 Protocol: "tcp", 1088 }, 1089 { 1090 Target: 53, 1091 Published: 10053, 1092 Protocol: "udp", 1093 }, 1094 { 1095 Mode: "host", 1096 Target: 22, 1097 Published: 10022, 1098 }, 1099 } 1100 1101 assert.Check(t, is.Len(config.Services, 1)) 1102 assert.Check(t, is.DeepEqual(expected, config.Services[0].Ports)) 1103} 1104 1105func TestLoadExpandedMountFormat(t *testing.T) { 1106 config, err := loadYAML(` 1107version: "3.2" 1108services: 1109 web: 1110 image: busybox 1111 volumes: 1112 - type: volume 1113 source: foo 1114 target: /target 1115 read_only: true 1116volumes: 1117 foo: {} 1118`) 1119 assert.NilError(t, err) 1120 1121 expected := types.ServiceVolumeConfig{ 1122 Type: "volume", 1123 Source: "foo", 1124 Target: "/target", 1125 ReadOnly: true, 1126 } 1127 1128 assert.Assert(t, is.Len(config.Services, 1)) 1129 assert.Check(t, is.Len(config.Services[0].Volumes, 1)) 1130 assert.Check(t, is.DeepEqual(expected, config.Services[0].Volumes[0])) 1131} 1132 1133func TestLoadExtraHostsMap(t *testing.T) { 1134 config, err := loadYAML(` 1135version: "3.2" 1136services: 1137 web: 1138 image: busybox 1139 extra_hosts: 1140 "zulu": "162.242.195.82" 1141 "alpha": "50.31.209.229" 1142`) 1143 assert.NilError(t, err) 1144 1145 expected := types.HostsList{ 1146 "alpha:50.31.209.229", 1147 "zulu:162.242.195.82", 1148 } 1149 1150 assert.Assert(t, is.Len(config.Services, 1)) 1151 assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) 1152} 1153 1154func TestLoadExtraHostsList(t *testing.T) { 1155 config, err := loadYAML(` 1156version: "3.2" 1157services: 1158 web: 1159 image: busybox 1160 extra_hosts: 1161 - "zulu:162.242.195.82" 1162 - "alpha:50.31.209.229" 1163 - "zulu:ff02::1" 1164`) 1165 assert.NilError(t, err) 1166 1167 expected := types.HostsList{ 1168 "zulu:162.242.195.82", 1169 "alpha:50.31.209.229", 1170 "zulu:ff02::1", 1171 } 1172 1173 assert.Assert(t, is.Len(config.Services, 1)) 1174 assert.Check(t, is.DeepEqual(expected, config.Services[0].ExtraHosts)) 1175} 1176 1177func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) { 1178 buf, cleanup := patchLogrus() 1179 defer cleanup() 1180 1181 source := map[string]interface{}{ 1182 "foo": map[string]interface{}{ 1183 "external": map[string]interface{}{ 1184 "name": "oops", 1185 }, 1186 }, 1187 } 1188 volumes, err := LoadVolumes(source, "3.4") 1189 assert.NilError(t, err) 1190 expected := map[string]types.VolumeConfig{ 1191 "foo": { 1192 Name: "oops", 1193 External: types.External{External: true}, 1194 }, 1195 } 1196 assert.Check(t, is.DeepEqual(expected, volumes)) 1197 assert.Check(t, is.Contains(buf.String(), "volume.external.name is deprecated")) 1198 1199} 1200 1201func patchLogrus() (*bytes.Buffer, func()) { 1202 buf := new(bytes.Buffer) 1203 out := logrus.StandardLogger().Out 1204 logrus.SetOutput(buf) 1205 return buf, func() { logrus.SetOutput(out) } 1206} 1207 1208func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) { 1209 buf, cleanup := patchLogrus() 1210 defer cleanup() 1211 1212 source := map[string]interface{}{ 1213 "foo": map[string]interface{}{ 1214 "external": map[string]interface{}{ 1215 "name": "oops", 1216 }, 1217 }, 1218 } 1219 volumes, err := LoadVolumes(source, "3.3") 1220 assert.NilError(t, err) 1221 expected := map[string]types.VolumeConfig{ 1222 "foo": { 1223 Name: "oops", 1224 External: types.External{External: true}, 1225 }, 1226 } 1227 assert.Check(t, is.DeepEqual(expected, volumes)) 1228 assert.Check(t, is.Equal("", buf.String())) 1229} 1230 1231func TestLoadV35(t *testing.T) { 1232 actual, err := loadYAML(` 1233version: "3.5" 1234services: 1235 foo: 1236 image: busybox 1237 isolation: process 1238configs: 1239 foo: 1240 name: fooqux 1241 external: true 1242 bar: 1243 name: barqux 1244 file: ./example1.env 1245secrets: 1246 foo: 1247 name: fooqux 1248 external: true 1249 bar: 1250 name: barqux 1251 file: ./full-example.yml 1252`) 1253 assert.NilError(t, err) 1254 assert.Check(t, is.Len(actual.Services, 1)) 1255 assert.Check(t, is.Len(actual.Secrets, 2)) 1256 assert.Check(t, is.Len(actual.Configs, 2)) 1257 assert.Check(t, is.Equal("process", actual.Services[0].Isolation)) 1258} 1259 1260func TestLoadV35InvalidIsolation(t *testing.T) { 1261 // validation should be done only on the daemon side 1262 actual, err := loadYAML(` 1263version: "3.5" 1264services: 1265 foo: 1266 image: busybox 1267 isolation: invalid 1268configs: 1269 super: 1270 external: true 1271`) 1272 assert.NilError(t, err) 1273 assert.Assert(t, is.Len(actual.Services, 1)) 1274 assert.Check(t, is.Equal("invalid", actual.Services[0].Isolation)) 1275} 1276 1277func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) { 1278 _, err := loadYAML(` 1279version: "3.5" 1280secrets: 1281 external_secret: 1282 name: user_specified_name 1283 external: 1284 name: external_name 1285`) 1286 1287 assert.ErrorContains(t, err, "secret.external.name and secret.name conflict; only use secret.name") 1288 assert.ErrorContains(t, err, "external_secret") 1289} 1290 1291func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) { 1292 buf, cleanup := patchLogrus() 1293 defer cleanup() 1294 1295 source := map[string]interface{}{ 1296 "foo": map[string]interface{}{ 1297 "external": map[string]interface{}{ 1298 "name": "oops", 1299 }, 1300 }, 1301 } 1302 details := types.ConfigDetails{ 1303 Version: "3.5", 1304 } 1305 secrets, err := LoadSecrets(source, details) 1306 assert.NilError(t, err) 1307 expected := map[string]types.SecretConfig{ 1308 "foo": { 1309 Name: "oops", 1310 External: types.External{External: true}, 1311 }, 1312 } 1313 assert.Check(t, is.DeepEqual(expected, secrets)) 1314 assert.Check(t, is.Contains(buf.String(), "secret.external.name is deprecated")) 1315} 1316 1317func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) { 1318 buf, cleanup := patchLogrus() 1319 defer cleanup() 1320 1321 source := map[string]interface{}{ 1322 "foo": map[string]interface{}{ 1323 "external": map[string]interface{}{ 1324 "name": "oops", 1325 }, 1326 }, 1327 } 1328 networks, err := LoadNetworks(source, "3.5") 1329 assert.NilError(t, err) 1330 expected := map[string]types.NetworkConfig{ 1331 "foo": { 1332 Name: "oops", 1333 External: types.External{External: true}, 1334 }, 1335 } 1336 assert.Check(t, is.DeepEqual(expected, networks)) 1337 assert.Check(t, is.Contains(buf.String(), "network.external.name is deprecated")) 1338 1339} 1340 1341func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) { 1342 buf, cleanup := patchLogrus() 1343 defer cleanup() 1344 1345 source := map[string]interface{}{ 1346 "foo": map[string]interface{}{ 1347 "external": map[string]interface{}{ 1348 "name": "oops", 1349 }, 1350 }, 1351 } 1352 networks, err := LoadNetworks(source, "3.4") 1353 assert.NilError(t, err) 1354 expected := map[string]types.NetworkConfig{ 1355 "foo": { 1356 Name: "oops", 1357 External: types.External{External: true}, 1358 }, 1359 } 1360 assert.Check(t, is.DeepEqual(expected, networks)) 1361 assert.Check(t, is.Equal("", buf.String())) 1362} 1363 1364func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) { 1365 _, err := loadYAML(` 1366version: "3.5" 1367networks: 1368 foo: 1369 name: user_specified_name 1370 external: 1371 name: external_name 1372`) 1373 1374 assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name") 1375 assert.ErrorContains(t, err, "foo") 1376} 1377 1378func TestLoadNetworkWithName(t *testing.T) { 1379 config, err := loadYAML(` 1380version: '3.5' 1381services: 1382 hello-world: 1383 image: redis:alpine 1384 networks: 1385 - network1 1386 - network3 1387 1388networks: 1389 network1: 1390 name: network2 1391 network3: 1392`) 1393 assert.NilError(t, err) 1394 expected := &types.Config{ 1395 Filename: "filename.yml", 1396 Version: "3.5", 1397 Services: types.Services{ 1398 { 1399 Name: "hello-world", 1400 Image: "redis:alpine", 1401 Networks: map[string]*types.ServiceNetworkConfig{ 1402 "network1": nil, 1403 "network3": nil, 1404 }, 1405 }, 1406 }, 1407 Networks: map[string]types.NetworkConfig{ 1408 "network1": {Name: "network2"}, 1409 "network3": {}, 1410 }, 1411 } 1412 assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) 1413} 1414 1415func TestLoadInit(t *testing.T) { 1416 booleanTrue := true 1417 booleanFalse := false 1418 1419 var testcases = []struct { 1420 doc string 1421 yaml string 1422 init *bool 1423 }{ 1424 { 1425 doc: "no init defined", 1426 yaml: ` 1427version: '3.7' 1428services: 1429 foo: 1430 image: alpine`, 1431 }, 1432 { 1433 doc: "has true init", 1434 yaml: ` 1435version: '3.7' 1436services: 1437 foo: 1438 image: alpine 1439 init: true`, 1440 init: &booleanTrue, 1441 }, 1442 { 1443 doc: "has false init", 1444 yaml: ` 1445version: '3.7' 1446services: 1447 foo: 1448 image: alpine 1449 init: false`, 1450 init: &booleanFalse, 1451 }, 1452 } 1453 for _, testcase := range testcases { 1454 t.Run(testcase.doc, func(t *testing.T) { 1455 config, err := loadYAML(testcase.yaml) 1456 assert.NilError(t, err) 1457 assert.Check(t, is.Len(config.Services, 1)) 1458 assert.Check(t, is.DeepEqual(config.Services[0].Init, testcase.init)) 1459 }) 1460 } 1461} 1462