github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/loader/merge_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 "reflect" 8 "testing" 9 10 "dario.cat/mergo" 11 "github.com/khulnasoft/cli/cli/compose/types" 12 "gotest.tools/v3/assert" 13 ) 14 15 func TestLoadTwoDifferentVersion(t *testing.T) { 16 configDetails := types.ConfigDetails{ 17 ConfigFiles: []types.ConfigFile{ 18 {Filename: "base.yml", Config: map[string]any{ 19 "version": "3.1", 20 }}, 21 {Filename: "override.yml", Config: map[string]any{ 22 "version": "3.4", 23 }}, 24 }, 25 } 26 _, err := Load(configDetails) 27 assert.Error(t, err, "version mismatched between two composefiles : 3.1 and 3.4") 28 } 29 30 func TestLoadLogging(t *testing.T) { 31 loggingCases := []struct { 32 name string 33 loggingBase map[string]any 34 loggingOverride map[string]any 35 expected *types.LoggingConfig 36 }{ 37 { 38 name: "no_override_driver", 39 loggingBase: map[string]any{ 40 "logging": map[string]any{ 41 "driver": "json-file", 42 "options": map[string]any{ 43 "frequency": "2000", 44 "timeout": "23", 45 }, 46 }, 47 }, 48 loggingOverride: map[string]any{ 49 "logging": map[string]any{ 50 "options": map[string]any{ 51 "timeout": "360", 52 "pretty-print": "on", 53 }, 54 }, 55 }, 56 expected: &types.LoggingConfig{ 57 Driver: "json-file", 58 Options: map[string]string{ 59 "frequency": "2000", 60 "timeout": "360", 61 "pretty-print": "on", 62 }, 63 }, 64 }, 65 { 66 name: "override_driver", 67 loggingBase: map[string]any{ 68 "logging": map[string]any{ 69 "driver": "json-file", 70 "options": map[string]any{ 71 "frequency": "2000", 72 "timeout": "23", 73 }, 74 }, 75 }, 76 loggingOverride: map[string]any{ 77 "logging": map[string]any{ 78 "driver": "syslog", 79 "options": map[string]any{ 80 "timeout": "360", 81 "pretty-print": "on", 82 }, 83 }, 84 }, 85 expected: &types.LoggingConfig{ 86 Driver: "syslog", 87 Options: map[string]string{ 88 "timeout": "360", 89 "pretty-print": "on", 90 }, 91 }, 92 }, 93 { 94 name: "no_base_driver", 95 loggingBase: map[string]any{ 96 "logging": map[string]any{ 97 "options": map[string]any{ 98 "frequency": "2000", 99 "timeout": "23", 100 }, 101 }, 102 }, 103 loggingOverride: map[string]any{ 104 "logging": map[string]any{ 105 "driver": "json-file", 106 "options": map[string]any{ 107 "timeout": "360", 108 "pretty-print": "on", 109 }, 110 }, 111 }, 112 expected: &types.LoggingConfig{ 113 Driver: "json-file", 114 Options: map[string]string{ 115 "frequency": "2000", 116 "timeout": "360", 117 "pretty-print": "on", 118 }, 119 }, 120 }, 121 { 122 name: "no_driver", 123 loggingBase: map[string]any{ 124 "logging": map[string]any{ 125 "options": map[string]any{ 126 "frequency": "2000", 127 "timeout": "23", 128 }, 129 }, 130 }, 131 loggingOverride: map[string]any{ 132 "logging": map[string]any{ 133 "options": map[string]any{ 134 "timeout": "360", 135 "pretty-print": "on", 136 }, 137 }, 138 }, 139 expected: &types.LoggingConfig{ 140 Options: map[string]string{ 141 "frequency": "2000", 142 "timeout": "360", 143 "pretty-print": "on", 144 }, 145 }, 146 }, 147 { 148 name: "no_override_options", 149 loggingBase: map[string]any{ 150 "logging": map[string]any{ 151 "driver": "json-file", 152 "options": map[string]any{ 153 "frequency": "2000", 154 "timeout": "23", 155 }, 156 }, 157 }, 158 loggingOverride: map[string]any{ 159 "logging": map[string]any{ 160 "driver": "syslog", 161 }, 162 }, 163 expected: &types.LoggingConfig{ 164 Driver: "syslog", 165 }, 166 }, 167 { 168 name: "no_base", 169 loggingBase: map[string]any{}, 170 loggingOverride: map[string]any{ 171 "logging": map[string]any{ 172 "driver": "json-file", 173 "options": map[string]any{ 174 "frequency": "2000", 175 }, 176 }, 177 }, 178 expected: &types.LoggingConfig{ 179 Driver: "json-file", 180 Options: map[string]string{ 181 "frequency": "2000", 182 }, 183 }, 184 }, 185 } 186 187 for _, tc := range loggingCases { 188 t.Run(tc.name, func(t *testing.T) { 189 configDetails := types.ConfigDetails{ 190 ConfigFiles: []types.ConfigFile{ 191 { 192 Filename: "base.yml", 193 Config: map[string]any{ 194 "version": "3.4", 195 "services": map[string]any{ 196 "foo": tc.loggingBase, 197 }, 198 }, 199 }, 200 { 201 Filename: "override.yml", 202 Config: map[string]any{ 203 "version": "3.4", 204 "services": map[string]any{ 205 "foo": tc.loggingOverride, 206 }, 207 }, 208 }, 209 }, 210 } 211 config, err := Load(configDetails) 212 assert.NilError(t, err) 213 assert.DeepEqual(t, &types.Config{ 214 Filename: "base.yml", 215 Version: "3.4", 216 Services: []types.ServiceConfig{ 217 { 218 Name: "foo", 219 Logging: tc.expected, 220 Environment: types.MappingWithEquals{}, 221 }, 222 }, 223 Networks: map[string]types.NetworkConfig{}, 224 Volumes: map[string]types.VolumeConfig{}, 225 Secrets: map[string]types.SecretConfig{}, 226 Configs: map[string]types.ConfigObjConfig{}, 227 }, config) 228 }) 229 } 230 } 231 232 func TestLoadMultipleServicePorts(t *testing.T) { 233 portsCases := []struct { 234 name string 235 portBase map[string]any 236 portOverride map[string]any 237 expected []types.ServicePortConfig 238 }{ 239 { 240 name: "no_override", 241 portBase: map[string]any{ 242 "ports": []any{ 243 "8080:80", 244 }, 245 }, 246 portOverride: map[string]any{}, 247 expected: []types.ServicePortConfig{ 248 { 249 Mode: "ingress", 250 Published: 8080, 251 Target: 80, 252 Protocol: "tcp", 253 }, 254 }, 255 }, 256 { 257 name: "override_different_published", 258 portBase: map[string]any{ 259 "ports": []any{ 260 "8080:80", 261 }, 262 }, 263 portOverride: map[string]any{ 264 "ports": []any{ 265 "8081:80", 266 }, 267 }, 268 expected: []types.ServicePortConfig{ 269 { 270 Mode: "ingress", 271 Published: 8080, 272 Target: 80, 273 Protocol: "tcp", 274 }, 275 { 276 Mode: "ingress", 277 Published: 8081, 278 Target: 80, 279 Protocol: "tcp", 280 }, 281 }, 282 }, 283 { 284 name: "override_same_published", 285 portBase: map[string]any{ 286 "ports": []any{ 287 "8080:80", 288 }, 289 }, 290 portOverride: map[string]any{ 291 "ports": []any{ 292 "8080:81", 293 }, 294 }, 295 expected: []types.ServicePortConfig{ 296 { 297 Mode: "ingress", 298 Published: 8080, 299 Target: 81, 300 Protocol: "tcp", 301 }, 302 }, 303 }, 304 } 305 306 for _, tc := range portsCases { 307 t.Run(tc.name, func(t *testing.T) { 308 configDetails := types.ConfigDetails{ 309 ConfigFiles: []types.ConfigFile{ 310 { 311 Filename: "base.yml", 312 Config: map[string]any{ 313 "version": "3.4", 314 "services": map[string]any{ 315 "foo": tc.portBase, 316 }, 317 }, 318 }, 319 { 320 Filename: "override.yml", 321 Config: map[string]any{ 322 "version": "3.4", 323 "services": map[string]any{ 324 "foo": tc.portOverride, 325 }, 326 }, 327 }, 328 }, 329 } 330 config, err := Load(configDetails) 331 assert.NilError(t, err) 332 assert.DeepEqual(t, &types.Config{ 333 Filename: "base.yml", 334 Version: "3.4", 335 Services: []types.ServiceConfig{ 336 { 337 Name: "foo", 338 Ports: tc.expected, 339 Environment: types.MappingWithEquals{}, 340 }, 341 }, 342 Networks: map[string]types.NetworkConfig{}, 343 Volumes: map[string]types.VolumeConfig{}, 344 Secrets: map[string]types.SecretConfig{}, 345 Configs: map[string]types.ConfigObjConfig{}, 346 }, config) 347 }) 348 } 349 } 350 351 func TestLoadMultipleSecretsConfig(t *testing.T) { 352 portsCases := []struct { 353 name string 354 secretBase map[string]any 355 secretOverride map[string]any 356 expected []types.ServiceSecretConfig 357 }{ 358 { 359 name: "no_override", 360 secretBase: map[string]any{ 361 "secrets": []any{ 362 "my_secret", 363 }, 364 }, 365 secretOverride: map[string]any{}, 366 expected: []types.ServiceSecretConfig{ 367 { 368 Source: "my_secret", 369 }, 370 }, 371 }, 372 { 373 name: "override_simple", 374 secretBase: map[string]any{ 375 "secrets": []any{ 376 "foo_secret", 377 }, 378 }, 379 secretOverride: map[string]any{ 380 "secrets": []any{ 381 "bar_secret", 382 }, 383 }, 384 expected: []types.ServiceSecretConfig{ 385 { 386 Source: "bar_secret", 387 }, 388 { 389 Source: "foo_secret", 390 }, 391 }, 392 }, 393 { 394 name: "override_same_source", 395 secretBase: map[string]any{ 396 "secrets": []any{ 397 "foo_secret", 398 map[string]any{ 399 "source": "bar_secret", 400 "target": "waw_secret", 401 }, 402 }, 403 }, 404 secretOverride: map[string]any{ 405 "secrets": []any{ 406 map[string]any{ 407 "source": "bar_secret", 408 "target": "bof_secret", 409 }, 410 map[string]any{ 411 "source": "baz_secret", 412 "target": "waw_secret", 413 }, 414 }, 415 }, 416 expected: []types.ServiceSecretConfig{ 417 { 418 Source: "bar_secret", 419 Target: "bof_secret", 420 }, 421 { 422 Source: "baz_secret", 423 Target: "waw_secret", 424 }, 425 { 426 Source: "foo_secret", 427 }, 428 }, 429 }, 430 } 431 432 for _, tc := range portsCases { 433 t.Run(tc.name, func(t *testing.T) { 434 configDetails := types.ConfigDetails{ 435 ConfigFiles: []types.ConfigFile{ 436 { 437 Filename: "base.yml", 438 Config: map[string]any{ 439 "version": "3.4", 440 "services": map[string]any{ 441 "foo": tc.secretBase, 442 }, 443 }, 444 }, 445 { 446 Filename: "override.yml", 447 Config: map[string]any{ 448 "version": "3.4", 449 "services": map[string]any{ 450 "foo": tc.secretOverride, 451 }, 452 }, 453 }, 454 }, 455 } 456 config, err := Load(configDetails) 457 assert.NilError(t, err) 458 assert.DeepEqual(t, &types.Config{ 459 Filename: "base.yml", 460 Version: "3.4", 461 Services: []types.ServiceConfig{ 462 { 463 Name: "foo", 464 Secrets: tc.expected, 465 Environment: types.MappingWithEquals{}, 466 }, 467 }, 468 Networks: map[string]types.NetworkConfig{}, 469 Volumes: map[string]types.VolumeConfig{}, 470 Secrets: map[string]types.SecretConfig{}, 471 Configs: map[string]types.ConfigObjConfig{}, 472 }, config) 473 }) 474 } 475 } 476 477 func TestLoadMultipleConfigobjsConfig(t *testing.T) { 478 portsCases := []struct { 479 name string 480 configBase map[string]any 481 configOverride map[string]any 482 expected []types.ServiceConfigObjConfig 483 }{ 484 { 485 name: "no_override", 486 configBase: map[string]any{ 487 "configs": []any{ 488 "my_config", 489 }, 490 }, 491 configOverride: map[string]any{}, 492 expected: []types.ServiceConfigObjConfig{ 493 { 494 Source: "my_config", 495 }, 496 }, 497 }, 498 { 499 name: "override_simple", 500 configBase: map[string]any{ 501 "configs": []any{ 502 "foo_config", 503 }, 504 }, 505 configOverride: map[string]any{ 506 "configs": []any{ 507 "bar_config", 508 }, 509 }, 510 expected: []types.ServiceConfigObjConfig{ 511 { 512 Source: "bar_config", 513 }, 514 { 515 Source: "foo_config", 516 }, 517 }, 518 }, 519 { 520 name: "override_same_source", 521 configBase: map[string]any{ 522 "configs": []any{ 523 "foo_config", 524 map[string]any{ 525 "source": "bar_config", 526 "target": "waw_config", 527 }, 528 }, 529 }, 530 configOverride: map[string]any{ 531 "configs": []any{ 532 map[string]any{ 533 "source": "bar_config", 534 "target": "bof_config", 535 }, 536 map[string]any{ 537 "source": "baz_config", 538 "target": "waw_config", 539 }, 540 }, 541 }, 542 expected: []types.ServiceConfigObjConfig{ 543 { 544 Source: "bar_config", 545 Target: "bof_config", 546 }, 547 { 548 Source: "baz_config", 549 Target: "waw_config", 550 }, 551 { 552 Source: "foo_config", 553 }, 554 }, 555 }, 556 } 557 558 for _, tc := range portsCases { 559 t.Run(tc.name, func(t *testing.T) { 560 configDetails := types.ConfigDetails{ 561 ConfigFiles: []types.ConfigFile{ 562 { 563 Filename: "base.yml", 564 Config: map[string]any{ 565 "version": "3.4", 566 "services": map[string]any{ 567 "foo": tc.configBase, 568 }, 569 }, 570 }, 571 { 572 Filename: "override.yml", 573 Config: map[string]any{ 574 "version": "3.4", 575 "services": map[string]any{ 576 "foo": tc.configOverride, 577 }, 578 }, 579 }, 580 }, 581 } 582 config, err := Load(configDetails) 583 assert.NilError(t, err) 584 assert.DeepEqual(t, &types.Config{ 585 Filename: "base.yml", 586 Version: "3.4", 587 Services: []types.ServiceConfig{ 588 { 589 Name: "foo", 590 Configs: tc.expected, 591 Environment: types.MappingWithEquals{}, 592 }, 593 }, 594 Networks: map[string]types.NetworkConfig{}, 595 Volumes: map[string]types.VolumeConfig{}, 596 Secrets: map[string]types.SecretConfig{}, 597 Configs: map[string]types.ConfigObjConfig{}, 598 }, config) 599 }) 600 } 601 } 602 603 func TestLoadMultipleUlimits(t *testing.T) { 604 ulimitCases := []struct { 605 name string 606 ulimitBase map[string]any 607 ulimitOverride map[string]any 608 expected map[string]*types.UlimitsConfig 609 }{ 610 { 611 name: "no_override", 612 ulimitBase: map[string]any{ 613 "ulimits": map[string]any{ 614 "noproc": 65535, 615 }, 616 }, 617 ulimitOverride: map[string]any{}, 618 expected: map[string]*types.UlimitsConfig{ 619 "noproc": { 620 Single: 65535, 621 }, 622 }, 623 }, 624 { 625 name: "override_simple", 626 ulimitBase: map[string]any{ 627 "ulimits": map[string]any{ 628 "noproc": 65535, 629 }, 630 }, 631 ulimitOverride: map[string]any{ 632 "ulimits": map[string]any{ 633 "noproc": 44444, 634 }, 635 }, 636 expected: map[string]*types.UlimitsConfig{ 637 "noproc": { 638 Single: 44444, 639 }, 640 }, 641 }, 642 { 643 name: "override_different_notation", 644 ulimitBase: map[string]any{ 645 "ulimits": map[string]any{ 646 "nofile": map[string]any{ 647 "soft": 11111, 648 "hard": 99999, 649 }, 650 "noproc": 44444, 651 }, 652 }, 653 ulimitOverride: map[string]any{ 654 "ulimits": map[string]any{ 655 "nofile": 55555, 656 "noproc": map[string]any{ 657 "soft": 22222, 658 "hard": 33333, 659 }, 660 }, 661 }, 662 expected: map[string]*types.UlimitsConfig{ 663 "noproc": { 664 Soft: 22222, 665 Hard: 33333, 666 }, 667 "nofile": { 668 Single: 55555, 669 }, 670 }, 671 }, 672 } 673 674 for _, tc := range ulimitCases { 675 t.Run(tc.name, func(t *testing.T) { 676 configDetails := types.ConfigDetails{ 677 ConfigFiles: []types.ConfigFile{ 678 { 679 Filename: "base.yml", 680 Config: map[string]any{ 681 "version": "3.4", 682 "services": map[string]any{ 683 "foo": tc.ulimitBase, 684 }, 685 }, 686 }, 687 { 688 Filename: "override.yml", 689 Config: map[string]any{ 690 "version": "3.4", 691 "services": map[string]any{ 692 "foo": tc.ulimitOverride, 693 }, 694 }, 695 }, 696 }, 697 } 698 config, err := Load(configDetails) 699 assert.NilError(t, err) 700 assert.DeepEqual(t, &types.Config{ 701 Filename: "base.yml", 702 Version: "3.4", 703 Services: []types.ServiceConfig{ 704 { 705 Name: "foo", 706 Ulimits: tc.expected, 707 Environment: types.MappingWithEquals{}, 708 }, 709 }, 710 Networks: map[string]types.NetworkConfig{}, 711 Volumes: map[string]types.VolumeConfig{}, 712 Secrets: map[string]types.SecretConfig{}, 713 Configs: map[string]types.ConfigObjConfig{}, 714 }, config) 715 }) 716 } 717 } 718 719 func TestLoadMultipleServiceNetworks(t *testing.T) { 720 networkCases := []struct { 721 name string 722 networkBase map[string]any 723 networkOverride map[string]any 724 expected map[string]*types.ServiceNetworkConfig 725 }{ 726 { 727 name: "no_override", 728 networkBase: map[string]any{ 729 "networks": []any{ 730 "net1", 731 "net2", 732 }, 733 }, 734 networkOverride: map[string]any{}, 735 expected: map[string]*types.ServiceNetworkConfig{ 736 "net1": nil, 737 "net2": nil, 738 }, 739 }, 740 { 741 name: "override_simple", 742 networkBase: map[string]any{ 743 "networks": []any{ 744 "net1", 745 "net2", 746 }, 747 }, 748 networkOverride: map[string]any{ 749 "networks": []any{ 750 "net1", 751 "net3", 752 }, 753 }, 754 expected: map[string]*types.ServiceNetworkConfig{ 755 "net1": nil, 756 "net2": nil, 757 "net3": nil, 758 }, 759 }, 760 { 761 name: "override_with_aliases", 762 networkBase: map[string]any{ 763 "networks": map[string]any{ 764 "net1": map[string]any{ 765 "aliases": []any{ 766 "alias1", 767 }, 768 }, 769 "net2": nil, 770 }, 771 }, 772 networkOverride: map[string]any{ 773 "networks": map[string]any{ 774 "net1": map[string]any{ 775 "aliases": []any{ 776 "alias2", 777 "alias3", 778 }, 779 }, 780 "net3": map[string]any{}, 781 }, 782 }, 783 expected: map[string]*types.ServiceNetworkConfig{ 784 "net1": { 785 Aliases: []string{"alias2", "alias3"}, 786 }, 787 "net2": nil, 788 "net3": {}, 789 }, 790 }, 791 } 792 793 for _, tc := range networkCases { 794 t.Run(tc.name, func(t *testing.T) { 795 configDetails := types.ConfigDetails{ 796 ConfigFiles: []types.ConfigFile{ 797 { 798 Filename: "base.yml", 799 Config: map[string]any{ 800 "version": "3.4", 801 "services": map[string]any{ 802 "foo": tc.networkBase, 803 }, 804 }, 805 }, 806 { 807 Filename: "override.yml", 808 Config: map[string]any{ 809 "version": "3.4", 810 "services": map[string]any{ 811 "foo": tc.networkOverride, 812 }, 813 }, 814 }, 815 }, 816 } 817 config, err := Load(configDetails) 818 assert.NilError(t, err) 819 assert.DeepEqual(t, &types.Config{ 820 Filename: "base.yml", 821 Version: "3.4", 822 Services: []types.ServiceConfig{ 823 { 824 Name: "foo", 825 Networks: tc.expected, 826 Environment: types.MappingWithEquals{}, 827 }, 828 }, 829 Networks: map[string]types.NetworkConfig{}, 830 Volumes: map[string]types.VolumeConfig{}, 831 Secrets: map[string]types.SecretConfig{}, 832 Configs: map[string]types.ConfigObjConfig{}, 833 }, config) 834 }) 835 } 836 } 837 838 func TestLoadMultipleConfigs(t *testing.T) { 839 base := map[string]any{ 840 "version": "3.4", 841 "services": map[string]any{ 842 "foo": map[string]any{ 843 "image": "foo", 844 "build": map[string]any{ 845 "context": ".", 846 "dockerfile": "bar.Dockerfile", 847 }, 848 "ports": []any{ 849 "8080:80", 850 "9090:90", 851 }, 852 "labels": []any{ 853 "foo=bar", 854 }, 855 "cap_add": []any{ 856 "NET_ADMIN", 857 }, 858 }, 859 }, 860 "volumes": map[string]any{}, 861 "networks": map[string]any{}, 862 "secrets": map[string]any{}, 863 "configs": map[string]any{}, 864 } 865 override := map[string]any{ 866 "version": "3.4", 867 "services": map[string]any{ 868 "foo": map[string]any{ 869 "image": "baz", 870 "build": map[string]any{ 871 "dockerfile": "foo.Dockerfile", 872 "args": []any{ 873 "buildno=1", 874 "password=secret", 875 }, 876 }, 877 "ports": []any{ 878 map[string]any{ 879 "target": 81, 880 "published": 8080, 881 }, 882 }, 883 "labels": map[string]any{ 884 "foo": "baz", 885 }, 886 "cap_add": []any{ 887 "SYS_ADMIN", 888 }, 889 }, 890 "bar": map[string]any{ 891 "image": "bar", 892 }, 893 }, 894 "volumes": map[string]any{}, 895 "networks": map[string]any{}, 896 "secrets": map[string]any{}, 897 "configs": map[string]any{}, 898 } 899 configDetails := types.ConfigDetails{ 900 ConfigFiles: []types.ConfigFile{ 901 {Filename: "base.yml", Config: base}, 902 {Filename: "override.yml", Config: override}, 903 }, 904 } 905 config, err := Load(configDetails) 906 assert.NilError(t, err) 907 assert.DeepEqual(t, &types.Config{ 908 Filename: "base.yml", 909 Version: "3.4", 910 Services: []types.ServiceConfig{ 911 { 912 Name: "bar", 913 Image: "bar", 914 Environment: types.MappingWithEquals{}, 915 }, 916 { 917 Name: "foo", 918 Image: "baz", 919 Build: types.BuildConfig{ 920 Context: ".", 921 Dockerfile: "foo.Dockerfile", 922 Args: types.MappingWithEquals{ 923 "buildno": strPtr("1"), 924 "password": strPtr("secret"), 925 }, 926 }, 927 Ports: []types.ServicePortConfig{ 928 { 929 Target: 81, 930 Published: 8080, 931 }, 932 { 933 Mode: "ingress", 934 Target: 90, 935 Published: 9090, 936 Protocol: "tcp", 937 }, 938 }, 939 Labels: types.Labels{ 940 "foo": "baz", 941 }, 942 CapAdd: []string{"NET_ADMIN", "SYS_ADMIN"}, 943 Environment: types.MappingWithEquals{}, 944 }, 945 }, 946 Networks: map[string]types.NetworkConfig{}, 947 Volumes: map[string]types.VolumeConfig{}, 948 Secrets: map[string]types.SecretConfig{}, 949 Configs: map[string]types.ConfigObjConfig{}, 950 }, config) 951 } 952 953 // Issue#972 954 func TestLoadMultipleNetworks(t *testing.T) { 955 base := map[string]any{ 956 "version": "3.4", 957 "services": map[string]any{ 958 "foo": map[string]any{ 959 "image": "baz", 960 }, 961 }, 962 "volumes": map[string]any{}, 963 "networks": map[string]any{ 964 "hostnet": map[string]any{ 965 "driver": "overlay", 966 "ipam": map[string]any{ 967 "driver": "default", 968 "config": []any{ 969 map[string]any{ 970 "subnet": "10.0.0.0/20", 971 }, 972 }, 973 }, 974 }, 975 }, 976 "secrets": map[string]any{}, 977 "configs": map[string]any{}, 978 } 979 override := map[string]any{ 980 "version": "3.4", 981 "services": map[string]any{}, 982 "volumes": map[string]any{}, 983 "networks": map[string]any{ 984 "hostnet": map[string]any{ 985 "external": map[string]any{ 986 "name": "host", 987 }, 988 }, 989 }, 990 "secrets": map[string]any{}, 991 "configs": map[string]any{}, 992 } 993 configDetails := types.ConfigDetails{ 994 ConfigFiles: []types.ConfigFile{ 995 {Filename: "base.yml", Config: base}, 996 {Filename: "override.yml", Config: override}, 997 }, 998 } 999 config, err := Load(configDetails) 1000 assert.NilError(t, err) 1001 assert.DeepEqual(t, &types.Config{ 1002 Filename: "base.yml", 1003 Version: "3.4", 1004 Services: []types.ServiceConfig{ 1005 { 1006 Name: "foo", 1007 Image: "baz", 1008 Environment: types.MappingWithEquals{}, 1009 }, 1010 }, 1011 Networks: map[string]types.NetworkConfig{ 1012 "hostnet": { 1013 Name: "host", 1014 External: types.External{ 1015 External: true, 1016 }, 1017 }, 1018 }, 1019 Volumes: map[string]types.VolumeConfig{}, 1020 Secrets: map[string]types.SecretConfig{}, 1021 Configs: map[string]types.ConfigObjConfig{}, 1022 }, config) 1023 } 1024 1025 func TestLoadMultipleServiceCommands(t *testing.T) { 1026 base := map[string]any{ 1027 "version": "3.7", 1028 "services": map[string]any{ 1029 "foo": map[string]any{ 1030 "image": "baz", 1031 "command": "foo bar", 1032 }, 1033 }, 1034 "volumes": map[string]any{}, 1035 "networks": map[string]any{}, 1036 "secrets": map[string]any{}, 1037 "configs": map[string]any{}, 1038 } 1039 override := map[string]any{ 1040 "version": "3.7", 1041 "services": map[string]any{ 1042 "foo": map[string]any{ 1043 "image": "baz", 1044 "command": "foo baz", 1045 }, 1046 }, 1047 "volumes": map[string]any{}, 1048 "networks": map[string]any{}, 1049 "secrets": map[string]any{}, 1050 "configs": map[string]any{}, 1051 } 1052 configDetails := types.ConfigDetails{ 1053 ConfigFiles: []types.ConfigFile{ 1054 {Filename: "base.yml", Config: base}, 1055 {Filename: "override.yml", Config: override}, 1056 }, 1057 } 1058 config, err := Load(configDetails) 1059 assert.NilError(t, err) 1060 assert.DeepEqual(t, &types.Config{ 1061 Filename: "base.yml", 1062 Version: "3.7", 1063 Services: []types.ServiceConfig{ 1064 { 1065 Name: "foo", 1066 Image: "baz", 1067 Command: types.ShellCommand{"foo", "baz"}, 1068 Environment: types.MappingWithEquals{}, 1069 }, 1070 }, 1071 Volumes: map[string]types.VolumeConfig{}, 1072 Secrets: map[string]types.SecretConfig{}, 1073 Configs: map[string]types.ConfigObjConfig{}, 1074 Networks: map[string]types.NetworkConfig{}, 1075 }, config) 1076 } 1077 1078 func TestLoadMultipleServiceVolumes(t *testing.T) { 1079 base := map[string]any{ 1080 "version": "3.7", 1081 "services": map[string]any{ 1082 "foo": map[string]any{ 1083 "image": "baz", 1084 "volumes": []any{ 1085 map[string]any{ 1086 "type": "volume", 1087 "source": "sourceVolume", 1088 "target": "/var/app", 1089 }, 1090 }, 1091 }, 1092 }, 1093 "volumes": map[string]any{ 1094 "sourceVolume": map[string]any{}, 1095 }, 1096 "networks": map[string]any{}, 1097 "secrets": map[string]any{}, 1098 "configs": map[string]any{}, 1099 } 1100 override := map[string]any{ 1101 "version": "3.7", 1102 "services": map[string]any{ 1103 "foo": map[string]any{ 1104 "image": "baz", 1105 "volumes": []any{ 1106 map[string]any{ 1107 "type": "volume", 1108 "source": "/local", 1109 "target": "/var/app", 1110 }, 1111 }, 1112 }, 1113 }, 1114 "volumes": map[string]any{}, 1115 "networks": map[string]any{}, 1116 "secrets": map[string]any{}, 1117 "configs": map[string]any{}, 1118 } 1119 configDetails := types.ConfigDetails{ 1120 ConfigFiles: []types.ConfigFile{ 1121 {Filename: "base.yml", Config: base}, 1122 {Filename: "override.yml", Config: override}, 1123 }, 1124 } 1125 config, err := Load(configDetails) 1126 assert.NilError(t, err) 1127 assert.DeepEqual(t, &types.Config{ 1128 Filename: "base.yml", 1129 Version: "3.7", 1130 Services: []types.ServiceConfig{ 1131 { 1132 Name: "foo", 1133 Image: "baz", 1134 Environment: types.MappingWithEquals{}, 1135 Volumes: []types.ServiceVolumeConfig{ 1136 { 1137 Type: "volume", 1138 Source: "/local", 1139 Target: "/var/app", 1140 }, 1141 }, 1142 }, 1143 }, 1144 Volumes: map[string]types.VolumeConfig{ 1145 "sourceVolume": {}, 1146 }, 1147 Secrets: map[string]types.SecretConfig{}, 1148 Configs: map[string]types.ConfigObjConfig{}, 1149 Networks: map[string]types.NetworkConfig{}, 1150 }, config) 1151 } 1152 1153 func TestMergeUlimitsConfig(t *testing.T) { 1154 specials := &specials{ 1155 m: map[reflect.Type]func(dst, src reflect.Value) error{ 1156 reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig, 1157 }, 1158 } 1159 base := map[string]*types.UlimitsConfig{ 1160 "override-single": {Single: 100}, 1161 "override-single-with-soft-hard": {Single: 200}, 1162 "override-soft-hard": {Soft: 300, Hard: 301}, 1163 "override-soft-hard-with-single": {Soft: 400, Hard: 401}, 1164 "dont-override": {Single: 500}, 1165 } 1166 override := map[string]*types.UlimitsConfig{ 1167 "override-single": {Single: 110}, 1168 "override-single-with-soft-hard": {Soft: 210, Hard: 211}, 1169 "override-soft-hard": {Soft: 310, Hard: 311}, 1170 "override-soft-hard-with-single": {Single: 410}, 1171 "add": {Single: 610}, 1172 } 1173 err := mergo.Merge(&base, &override, mergo.WithOverride, mergo.WithTransformers(specials)) 1174 assert.NilError(t, err) 1175 assert.DeepEqual( 1176 t, 1177 base, 1178 map[string]*types.UlimitsConfig{ 1179 "override-single": {Single: 110}, 1180 "override-single-with-soft-hard": {Soft: 210, Hard: 211}, 1181 "override-soft-hard": {Soft: 310, Hard: 311}, 1182 "override-soft-hard-with-single": {Single: 410}, 1183 "dont-override": {Single: 500}, 1184 "add": {Single: 610}, 1185 }, 1186 ) 1187 } 1188 1189 func TestMergeServiceNetworkConfig(t *testing.T) { 1190 specials := &specials{ 1191 m: map[reflect.Type]func(dst, src reflect.Value) error{ 1192 reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig, 1193 }, 1194 } 1195 base := map[string]*types.ServiceNetworkConfig{ 1196 "override-aliases": { 1197 Aliases: []string{"100", "101"}, 1198 Ipv4Address: "127.0.0.1", 1199 Ipv6Address: "0:0:0:0:0:0:0:1", 1200 }, 1201 "dont-override": { 1202 Aliases: []string{"200", "201"}, 1203 Ipv4Address: "127.0.0.2", 1204 Ipv6Address: "0:0:0:0:0:0:0:2", 1205 }, 1206 } 1207 override := map[string]*types.ServiceNetworkConfig{ 1208 "override-aliases": { 1209 Aliases: []string{"110", "111"}, 1210 Ipv4Address: "127.0.1.1", 1211 Ipv6Address: "0:0:0:0:0:0:1:1", 1212 }, 1213 "add": { 1214 Aliases: []string{"310", "311"}, 1215 Ipv4Address: "127.0.3.1", 1216 Ipv6Address: "0:0:0:0:0:0:3:1", 1217 }, 1218 } 1219 err := mergo.Merge(&base, &override, mergo.WithOverride, mergo.WithTransformers(specials)) 1220 assert.NilError(t, err) 1221 assert.DeepEqual( 1222 t, 1223 base, 1224 map[string]*types.ServiceNetworkConfig{ 1225 "override-aliases": { 1226 Aliases: []string{"110", "111"}, 1227 Ipv4Address: "127.0.1.1", 1228 Ipv6Address: "0:0:0:0:0:0:1:1", 1229 }, 1230 "dont-override": { 1231 Aliases: []string{"200", "201"}, 1232 Ipv4Address: "127.0.0.2", 1233 Ipv6Address: "0:0:0:0:0:0:0:2", 1234 }, 1235 "add": { 1236 Aliases: []string{"310", "311"}, 1237 Ipv4Address: "127.0.3.1", 1238 Ipv6Address: "0:0:0:0:0:0:3:1", 1239 }, 1240 }, 1241 ) 1242 } 1243 1244 // issue #3293 1245 func TestMergeServiceOverrideReplicasZero(t *testing.T) { 1246 base := types.ServiceConfig{ 1247 Name: "someService", 1248 Deploy: types.DeployConfig{ 1249 Replicas: uint64Ptr(3), 1250 }, 1251 } 1252 override := types.ServiceConfig{ 1253 Name: "someService", 1254 Deploy: types.DeployConfig{ 1255 Replicas: uint64Ptr(0), 1256 }, 1257 } 1258 services, err := mergeServices([]types.ServiceConfig{base}, []types.ServiceConfig{override}) 1259 assert.NilError(t, err) 1260 assert.Equal(t, len(services), 1) 1261 actual := services[0] 1262 assert.DeepEqual( 1263 t, 1264 actual, 1265 types.ServiceConfig{ 1266 Name: "someService", 1267 Deploy: types.DeployConfig{ 1268 Replicas: uint64Ptr(0), 1269 }, 1270 }, 1271 ) 1272 } 1273 1274 func TestMergeServiceOverrideReplicasNotNil(t *testing.T) { 1275 base := types.ServiceConfig{ 1276 Name: "someService", 1277 Deploy: types.DeployConfig{ 1278 Replicas: uint64Ptr(3), 1279 }, 1280 } 1281 override := types.ServiceConfig{ 1282 Name: "someService", 1283 Deploy: types.DeployConfig{}, 1284 } 1285 services, err := mergeServices([]types.ServiceConfig{base}, []types.ServiceConfig{override}) 1286 assert.NilError(t, err) 1287 assert.Equal(t, len(services), 1) 1288 actual := services[0] 1289 assert.DeepEqual( 1290 t, 1291 actual, 1292 types.ServiceConfig{ 1293 Name: "someService", 1294 Deploy: types.DeployConfig{ 1295 Replicas: uint64Ptr(3), 1296 }, 1297 }, 1298 ) 1299 }