github.com/kunnos/engine@v1.13.1/cli/compose/loader/loader_test.go (about) 1 package loader 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "sort" 8 "testing" 9 "time" 10 11 "github.com/docker/docker/cli/compose/types" 12 "github.com/stretchr/testify/assert" 13 ) 14 15 func buildConfigDetails(source types.Dict) types.ConfigDetails { 16 workingDir, err := os.Getwd() 17 if err != nil { 18 panic(err) 19 } 20 21 return types.ConfigDetails{ 22 WorkingDir: workingDir, 23 ConfigFiles: []types.ConfigFile{ 24 {Filename: "filename.yml", Config: source}, 25 }, 26 Environment: nil, 27 } 28 } 29 30 var sampleYAML = ` 31 version: "3" 32 services: 33 foo: 34 image: busybox 35 networks: 36 with_me: 37 bar: 38 image: busybox 39 environment: 40 - FOO=1 41 networks: 42 - with_ipam 43 volumes: 44 hello: 45 driver: default 46 driver_opts: 47 beep: boop 48 networks: 49 default: 50 driver: bridge 51 driver_opts: 52 beep: boop 53 with_ipam: 54 ipam: 55 driver: default 56 config: 57 - subnet: 172.28.0.0/16 58 ` 59 60 var sampleDict = types.Dict{ 61 "version": "3", 62 "services": types.Dict{ 63 "foo": types.Dict{ 64 "image": "busybox", 65 "networks": types.Dict{"with_me": nil}, 66 }, 67 "bar": types.Dict{ 68 "image": "busybox", 69 "environment": []interface{}{"FOO=1"}, 70 "networks": []interface{}{"with_ipam"}, 71 }, 72 }, 73 "volumes": types.Dict{ 74 "hello": types.Dict{ 75 "driver": "default", 76 "driver_opts": types.Dict{ 77 "beep": "boop", 78 }, 79 }, 80 }, 81 "networks": types.Dict{ 82 "default": types.Dict{ 83 "driver": "bridge", 84 "driver_opts": types.Dict{ 85 "beep": "boop", 86 }, 87 }, 88 "with_ipam": types.Dict{ 89 "ipam": types.Dict{ 90 "driver": "default", 91 "config": []interface{}{ 92 types.Dict{ 93 "subnet": "172.28.0.0/16", 94 }, 95 }, 96 }, 97 }, 98 }, 99 } 100 101 var sampleConfig = types.Config{ 102 Services: []types.ServiceConfig{ 103 { 104 Name: "foo", 105 Image: "busybox", 106 Environment: map[string]string{}, 107 Networks: map[string]*types.ServiceNetworkConfig{ 108 "with_me": nil, 109 }, 110 }, 111 { 112 Name: "bar", 113 Image: "busybox", 114 Environment: map[string]string{"FOO": "1"}, 115 Networks: map[string]*types.ServiceNetworkConfig{ 116 "with_ipam": nil, 117 }, 118 }, 119 }, 120 Networks: map[string]types.NetworkConfig{ 121 "default": { 122 Driver: "bridge", 123 DriverOpts: map[string]string{ 124 "beep": "boop", 125 }, 126 }, 127 "with_ipam": { 128 Ipam: types.IPAMConfig{ 129 Driver: "default", 130 Config: []*types.IPAMPool{ 131 { 132 Subnet: "172.28.0.0/16", 133 }, 134 }, 135 }, 136 }, 137 }, 138 Volumes: map[string]types.VolumeConfig{ 139 "hello": { 140 Driver: "default", 141 DriverOpts: map[string]string{ 142 "beep": "boop", 143 }, 144 }, 145 }, 146 } 147 148 func TestParseYAML(t *testing.T) { 149 dict, err := ParseYAML([]byte(sampleYAML)) 150 if !assert.NoError(t, err) { 151 return 152 } 153 assert.Equal(t, sampleDict, dict) 154 } 155 156 func TestLoad(t *testing.T) { 157 actual, err := Load(buildConfigDetails(sampleDict)) 158 if !assert.NoError(t, err) { 159 return 160 } 161 assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services)) 162 assert.Equal(t, sampleConfig.Networks, actual.Networks) 163 assert.Equal(t, sampleConfig.Volumes, actual.Volumes) 164 } 165 166 func TestLoadV31(t *testing.T) { 167 actual, err := loadYAML(` 168 version: "3.1" 169 services: 170 foo: 171 image: busybox 172 secrets: [super] 173 secrets: 174 super: 175 external: true 176 `) 177 if !assert.NoError(t, err) { 178 return 179 } 180 assert.Equal(t, len(actual.Services), 1) 181 assert.Equal(t, len(actual.Secrets), 1) 182 } 183 184 func TestParseAndLoad(t *testing.T) { 185 actual, err := loadYAML(sampleYAML) 186 if !assert.NoError(t, err) { 187 return 188 } 189 assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services)) 190 assert.Equal(t, sampleConfig.Networks, actual.Networks) 191 assert.Equal(t, sampleConfig.Volumes, actual.Volumes) 192 } 193 194 func TestInvalidTopLevelObjectType(t *testing.T) { 195 _, err := loadYAML("1") 196 assert.Error(t, err) 197 assert.Contains(t, err.Error(), "Top-level object must be a mapping") 198 199 _, err = loadYAML("\"hello\"") 200 assert.Error(t, err) 201 assert.Contains(t, err.Error(), "Top-level object must be a mapping") 202 203 _, err = loadYAML("[\"hello\"]") 204 assert.Error(t, err) 205 assert.Contains(t, err.Error(), "Top-level object must be a mapping") 206 } 207 208 func TestNonStringKeys(t *testing.T) { 209 _, err := loadYAML(` 210 version: "3" 211 123: 212 foo: 213 image: busybox 214 `) 215 assert.Error(t, err) 216 assert.Contains(t, err.Error(), "Non-string key at top level: 123") 217 218 _, err = loadYAML(` 219 version: "3" 220 services: 221 foo: 222 image: busybox 223 123: 224 image: busybox 225 `) 226 assert.Error(t, err) 227 assert.Contains(t, err.Error(), "Non-string key in services: 123") 228 229 _, err = loadYAML(` 230 version: "3" 231 services: 232 foo: 233 image: busybox 234 networks: 235 default: 236 ipam: 237 config: 238 - 123: oh dear 239 `) 240 assert.Error(t, err) 241 assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123") 242 243 _, err = loadYAML(` 244 version: "3" 245 services: 246 dict-env: 247 image: busybox 248 environment: 249 1: FOO 250 `) 251 assert.Error(t, err) 252 assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1") 253 } 254 255 func TestSupportedVersion(t *testing.T) { 256 _, err := loadYAML(` 257 version: "3" 258 services: 259 foo: 260 image: busybox 261 `) 262 assert.NoError(t, err) 263 264 _, err = loadYAML(` 265 version: "3.0" 266 services: 267 foo: 268 image: busybox 269 `) 270 assert.NoError(t, err) 271 } 272 273 func TestUnsupportedVersion(t *testing.T) { 274 _, err := loadYAML(` 275 version: "2" 276 services: 277 foo: 278 image: busybox 279 `) 280 assert.Error(t, err) 281 assert.Contains(t, err.Error(), "version") 282 283 _, err = loadYAML(` 284 version: "2.0" 285 services: 286 foo: 287 image: busybox 288 `) 289 assert.Error(t, err) 290 assert.Contains(t, err.Error(), "version") 291 } 292 293 func TestInvalidVersion(t *testing.T) { 294 _, err := loadYAML(` 295 version: 3 296 services: 297 foo: 298 image: busybox 299 `) 300 assert.Error(t, err) 301 assert.Contains(t, err.Error(), "version must be a string") 302 } 303 304 func TestV1Unsupported(t *testing.T) { 305 _, err := loadYAML(` 306 foo: 307 image: busybox 308 `) 309 assert.Error(t, err) 310 } 311 312 func TestNonMappingObject(t *testing.T) { 313 _, err := loadYAML(` 314 version: "3" 315 services: 316 - foo: 317 image: busybox 318 `) 319 assert.Error(t, err) 320 assert.Contains(t, err.Error(), "services must be a mapping") 321 322 _, err = loadYAML(` 323 version: "3" 324 services: 325 foo: busybox 326 `) 327 assert.Error(t, err) 328 assert.Contains(t, err.Error(), "services.foo must be a mapping") 329 330 _, err = loadYAML(` 331 version: "3" 332 networks: 333 - default: 334 driver: bridge 335 `) 336 assert.Error(t, err) 337 assert.Contains(t, err.Error(), "networks must be a mapping") 338 339 _, err = loadYAML(` 340 version: "3" 341 networks: 342 default: bridge 343 `) 344 assert.Error(t, err) 345 assert.Contains(t, err.Error(), "networks.default must be a mapping") 346 347 _, err = loadYAML(` 348 version: "3" 349 volumes: 350 - data: 351 driver: local 352 `) 353 assert.Error(t, err) 354 assert.Contains(t, err.Error(), "volumes must be a mapping") 355 356 _, err = loadYAML(` 357 version: "3" 358 volumes: 359 data: local 360 `) 361 assert.Error(t, err) 362 assert.Contains(t, err.Error(), "volumes.data must be a mapping") 363 } 364 365 func TestNonStringImage(t *testing.T) { 366 _, err := loadYAML(` 367 version: "3" 368 services: 369 foo: 370 image: ["busybox", "latest"] 371 `) 372 assert.Error(t, err) 373 assert.Contains(t, err.Error(), "services.foo.image must be a string") 374 } 375 376 func TestValidEnvironment(t *testing.T) { 377 config, err := loadYAML(` 378 version: "3" 379 services: 380 dict-env: 381 image: busybox 382 environment: 383 FOO: "1" 384 BAR: 2 385 BAZ: 2.5 386 QUUX: 387 list-env: 388 image: busybox 389 environment: 390 - FOO=1 391 - BAR=2 392 - BAZ=2.5 393 - QUUX= 394 `) 395 assert.NoError(t, err) 396 397 expected := map[string]string{ 398 "FOO": "1", 399 "BAR": "2", 400 "BAZ": "2.5", 401 "QUUX": "", 402 } 403 404 assert.Equal(t, 2, len(config.Services)) 405 406 for _, service := range config.Services { 407 assert.Equal(t, expected, service.Environment) 408 } 409 } 410 411 func TestInvalidEnvironmentValue(t *testing.T) { 412 _, err := loadYAML(` 413 version: "3" 414 services: 415 dict-env: 416 image: busybox 417 environment: 418 FOO: ["1"] 419 `) 420 assert.Error(t, err) 421 assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null") 422 } 423 424 func TestInvalidEnvironmentObject(t *testing.T) { 425 _, err := loadYAML(` 426 version: "3" 427 services: 428 dict-env: 429 image: busybox 430 environment: "FOO=1" 431 `) 432 assert.Error(t, err) 433 assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping") 434 } 435 436 func TestEnvironmentInterpolation(t *testing.T) { 437 config, err := loadYAML(` 438 version: "3" 439 services: 440 test: 441 image: busybox 442 labels: 443 - home1=$HOME 444 - home2=${HOME} 445 - nonexistent=$NONEXISTENT 446 - default=${NONEXISTENT-default} 447 networks: 448 test: 449 driver: $HOME 450 volumes: 451 test: 452 driver: $HOME 453 `) 454 455 assert.NoError(t, err) 456 457 home := os.Getenv("HOME") 458 459 expectedLabels := map[string]string{ 460 "home1": home, 461 "home2": home, 462 "nonexistent": "", 463 "default": "default", 464 } 465 466 assert.Equal(t, expectedLabels, config.Services[0].Labels) 467 assert.Equal(t, home, config.Networks["test"].Driver) 468 assert.Equal(t, home, config.Volumes["test"].Driver) 469 } 470 471 func TestUnsupportedProperties(t *testing.T) { 472 dict, err := ParseYAML([]byte(` 473 version: "3" 474 services: 475 web: 476 image: web 477 build: ./web 478 links: 479 - bar 480 db: 481 image: db 482 build: ./db 483 `)) 484 assert.NoError(t, err) 485 486 configDetails := buildConfigDetails(dict) 487 488 _, err = Load(configDetails) 489 assert.NoError(t, err) 490 491 unsupported := GetUnsupportedProperties(configDetails) 492 assert.Equal(t, []string{"build", "links"}, unsupported) 493 } 494 495 func TestDeprecatedProperties(t *testing.T) { 496 dict, err := ParseYAML([]byte(` 497 version: "3" 498 services: 499 web: 500 image: web 501 container_name: web 502 db: 503 image: db 504 container_name: db 505 expose: ["5434"] 506 `)) 507 assert.NoError(t, err) 508 509 configDetails := buildConfigDetails(dict) 510 511 _, err = Load(configDetails) 512 assert.NoError(t, err) 513 514 deprecated := GetDeprecatedProperties(configDetails) 515 assert.Equal(t, 2, len(deprecated)) 516 assert.Contains(t, deprecated, "container_name") 517 assert.Contains(t, deprecated, "expose") 518 } 519 520 func TestForbiddenProperties(t *testing.T) { 521 _, err := loadYAML(` 522 version: "3" 523 services: 524 foo: 525 image: busybox 526 volumes: 527 - /data 528 volume_driver: some-driver 529 bar: 530 extends: 531 service: foo 532 `) 533 534 assert.Error(t, err) 535 assert.IsType(t, &ForbiddenPropertiesError{}, err) 536 fmt.Println(err) 537 forbidden := err.(*ForbiddenPropertiesError).Properties 538 539 assert.Equal(t, 2, len(forbidden)) 540 assert.Contains(t, forbidden, "volume_driver") 541 assert.Contains(t, forbidden, "extends") 542 } 543 544 func durationPtr(value time.Duration) *time.Duration { 545 return &value 546 } 547 548 func int64Ptr(value int64) *int64 { 549 return &value 550 } 551 552 func uint64Ptr(value uint64) *uint64 { 553 return &value 554 } 555 556 func TestFullExample(t *testing.T) { 557 bytes, err := ioutil.ReadFile("full-example.yml") 558 assert.NoError(t, err) 559 560 config, err := loadYAML(string(bytes)) 561 if !assert.NoError(t, err) { 562 return 563 } 564 565 workingDir, err := os.Getwd() 566 assert.NoError(t, err) 567 568 homeDir := os.Getenv("HOME") 569 stopGracePeriod := time.Duration(20 * time.Second) 570 571 expectedServiceConfig := types.ServiceConfig{ 572 Name: "foo", 573 574 CapAdd: []string{"ALL"}, 575 CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"}, 576 CgroupParent: "m-executor-abcd", 577 Command: []string{"bundle", "exec", "thin", "-p", "3000"}, 578 ContainerName: "my-web-container", 579 DependsOn: []string{"db", "redis"}, 580 Deploy: types.DeployConfig{ 581 Mode: "replicated", 582 Replicas: uint64Ptr(6), 583 Labels: map[string]string{"FOO": "BAR"}, 584 UpdateConfig: &types.UpdateConfig{ 585 Parallelism: uint64Ptr(3), 586 Delay: time.Duration(10 * time.Second), 587 FailureAction: "continue", 588 Monitor: time.Duration(60 * time.Second), 589 MaxFailureRatio: 0.3, 590 }, 591 Resources: types.Resources{ 592 Limits: &types.Resource{ 593 NanoCPUs: "0.001", 594 MemoryBytes: 50 * 1024 * 1024, 595 }, 596 Reservations: &types.Resource{ 597 NanoCPUs: "0.0001", 598 MemoryBytes: 20 * 1024 * 1024, 599 }, 600 }, 601 RestartPolicy: &types.RestartPolicy{ 602 Condition: "on_failure", 603 Delay: durationPtr(5 * time.Second), 604 MaxAttempts: uint64Ptr(3), 605 Window: durationPtr(2 * time.Minute), 606 }, 607 Placement: types.Placement{ 608 Constraints: []string{"node=foo"}, 609 }, 610 }, 611 Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"}, 612 DNS: []string{"8.8.8.8", "9.9.9.9"}, 613 DNSSearch: []string{"dc1.example.com", "dc2.example.com"}, 614 DomainName: "foo.com", 615 Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"}, 616 Environment: map[string]string{ 617 "RACK_ENV": "development", 618 "SHOW": "true", 619 "SESSION_SECRET": "", 620 "FOO": "1", 621 "BAR": "2", 622 "BAZ": "3", 623 }, 624 Expose: []string{"3000", "8000"}, 625 ExternalLinks: []string{ 626 "redis_1", 627 "project_db_1:mysql", 628 "project_db_1:postgresql", 629 }, 630 ExtraHosts: map[string]string{ 631 "otherhost": "50.31.209.229", 632 "somehost": "162.242.195.82", 633 }, 634 HealthCheck: &types.HealthCheckConfig{ 635 Test: []string{ 636 "CMD-SHELL", 637 "echo \"hello world\"", 638 }, 639 Interval: "10s", 640 Timeout: "1s", 641 Retries: uint64Ptr(5), 642 }, 643 Hostname: "foo", 644 Image: "redis", 645 Ipc: "host", 646 Labels: map[string]string{ 647 "com.example.description": "Accounting webapp", 648 "com.example.number": "42", 649 "com.example.empty-label": "", 650 }, 651 Links: []string{ 652 "db", 653 "db:database", 654 "redis", 655 }, 656 Logging: &types.LoggingConfig{ 657 Driver: "syslog", 658 Options: map[string]string{ 659 "syslog-address": "tcp://192.168.0.42:123", 660 }, 661 }, 662 MacAddress: "02:42:ac:11:65:43", 663 NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b", 664 Networks: map[string]*types.ServiceNetworkConfig{ 665 "some-network": { 666 Aliases: []string{"alias1", "alias3"}, 667 Ipv4Address: "", 668 Ipv6Address: "", 669 }, 670 "other-network": { 671 Ipv4Address: "172.16.238.10", 672 Ipv6Address: "2001:3984:3989::10", 673 }, 674 "other-other-network": nil, 675 }, 676 Pid: "host", 677 Ports: []string{ 678 "3000", 679 "3000-3005", 680 "8000:8000", 681 "9090-9091:8080-8081", 682 "49100:22", 683 "127.0.0.1:8001:8001", 684 "127.0.0.1:5000-5010:5000-5010", 685 }, 686 Privileged: true, 687 ReadOnly: true, 688 Restart: "always", 689 SecurityOpt: []string{ 690 "label=level:s0:c100,c200", 691 "label=type:svirt_apache_t", 692 }, 693 StdinOpen: true, 694 StopSignal: "SIGUSR1", 695 StopGracePeriod: &stopGracePeriod, 696 Tmpfs: []string{"/run", "/tmp"}, 697 Tty: true, 698 Ulimits: map[string]*types.UlimitsConfig{ 699 "nproc": { 700 Single: 65535, 701 }, 702 "nofile": { 703 Soft: 20000, 704 Hard: 40000, 705 }, 706 }, 707 User: "someone", 708 Volumes: []string{ 709 "/var/lib/mysql", 710 "/opt/data:/var/lib/mysql", 711 fmt.Sprintf("%s:/code", workingDir), 712 fmt.Sprintf("%s/static:/var/www/html", workingDir), 713 fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir), 714 "datavolume:/var/lib/mysql", 715 }, 716 WorkingDir: "/code", 717 } 718 719 assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services) 720 721 expectedNetworkConfig := map[string]types.NetworkConfig{ 722 "some-network": {}, 723 724 "other-network": { 725 Driver: "overlay", 726 DriverOpts: map[string]string{ 727 "foo": "bar", 728 "baz": "1", 729 }, 730 Ipam: types.IPAMConfig{ 731 Driver: "overlay", 732 Config: []*types.IPAMPool{ 733 {Subnet: "172.16.238.0/24"}, 734 {Subnet: "2001:3984:3989::/64"}, 735 }, 736 }, 737 }, 738 739 "external-network": { 740 External: types.External{ 741 Name: "external-network", 742 External: true, 743 }, 744 }, 745 746 "other-external-network": { 747 External: types.External{ 748 Name: "my-cool-network", 749 External: true, 750 }, 751 }, 752 } 753 754 assert.Equal(t, expectedNetworkConfig, config.Networks) 755 756 expectedVolumeConfig := map[string]types.VolumeConfig{ 757 "some-volume": {}, 758 "other-volume": { 759 Driver: "flocker", 760 DriverOpts: map[string]string{ 761 "foo": "bar", 762 "baz": "1", 763 }, 764 }, 765 "external-volume": { 766 External: types.External{ 767 Name: "external-volume", 768 External: true, 769 }, 770 }, 771 "other-external-volume": { 772 External: types.External{ 773 Name: "my-cool-volume", 774 External: true, 775 }, 776 }, 777 } 778 779 assert.Equal(t, expectedVolumeConfig, config.Volumes) 780 } 781 782 func loadYAML(yaml string) (*types.Config, error) { 783 dict, err := ParseYAML([]byte(yaml)) 784 if err != nil { 785 return nil, err 786 } 787 788 return Load(buildConfigDetails(dict)) 789 } 790 791 func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { 792 sort.Sort(servicesByName(services)) 793 return services 794 } 795 796 type servicesByName []types.ServiceConfig 797 798 func (sbn servicesByName) Len() int { return len(sbn) } 799 func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] } 800 func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }