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