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