github.com/karlem/nomad@v0.10.2-rc1/command/agent/config_parse_test.go (about) 1 package agent 2 3 import ( 4 "fmt" 5 "path/filepath" 6 "sort" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/nomad/helper" 11 "github.com/hashicorp/nomad/nomad/structs" 12 "github.com/hashicorp/nomad/nomad/structs/config" 13 "github.com/stretchr/testify/require" 14 ) 15 16 var basicConfig = &Config{ 17 Region: "foobar", 18 Datacenter: "dc2", 19 NodeName: "my-web", 20 DataDir: "/tmp/nomad", 21 PluginDir: "/tmp/nomad-plugins", 22 LogFile: "/var/log/nomad.log", 23 LogLevel: "ERR", 24 LogJson: true, 25 BindAddr: "192.168.0.1", 26 EnableDebug: true, 27 Ports: &Ports{ 28 HTTP: 1234, 29 RPC: 2345, 30 Serf: 3456, 31 }, 32 Addresses: &Addresses{ 33 HTTP: "127.0.0.1", 34 RPC: "127.0.0.2", 35 Serf: "127.0.0.3", 36 }, 37 AdvertiseAddrs: &AdvertiseAddrs{ 38 RPC: "127.0.0.3", 39 Serf: "127.0.0.4", 40 }, 41 Client: &ClientConfig{ 42 Enabled: true, 43 StateDir: "/tmp/client-state", 44 AllocDir: "/tmp/alloc", 45 Servers: []string{"a.b.c:80", "127.0.0.1:1234"}, 46 NodeClass: "linux-medium-64bit", 47 ServerJoin: &ServerJoin{ 48 RetryJoin: []string{"1.1.1.1", "2.2.2.2"}, 49 RetryInterval: time.Duration(15) * time.Second, 50 RetryIntervalHCL: "15s", 51 RetryMaxAttempts: 3, 52 }, 53 Meta: map[string]string{ 54 "foo": "bar", 55 "baz": "zip", 56 }, 57 Options: map[string]string{ 58 "foo": "bar", 59 "baz": "zip", 60 }, 61 ChrootEnv: map[string]string{ 62 "/opt/myapp/etc": "/etc", 63 "/opt/myapp/bin": "/bin", 64 }, 65 NetworkInterface: "eth0", 66 NetworkSpeed: 100, 67 CpuCompute: 4444, 68 MemoryMB: 0, 69 MaxKillTimeout: "10s", 70 ClientMinPort: 1000, 71 ClientMaxPort: 2000, 72 Reserved: &Resources{ 73 CPU: 10, 74 MemoryMB: 10, 75 DiskMB: 10, 76 ReservedPorts: "1,100,10-12", 77 }, 78 GCInterval: 6 * time.Second, 79 GCIntervalHCL: "6s", 80 GCParallelDestroys: 6, 81 GCDiskUsageThreshold: 82, 82 GCInodeUsageThreshold: 91, 83 GCMaxAllocs: 50, 84 NoHostUUID: helper.BoolToPtr(false), 85 DisableRemoteExec: true, 86 HostVolumes: []*structs.ClientHostVolumeConfig{ 87 {Name: "tmp", Path: "/tmp"}, 88 }, 89 }, 90 Server: &ServerConfig{ 91 Enabled: true, 92 AuthoritativeRegion: "foobar", 93 BootstrapExpect: 5, 94 DataDir: "/tmp/data", 95 ProtocolVersion: 3, 96 RaftProtocol: 3, 97 NumSchedulers: helper.IntToPtr(2), 98 EnabledSchedulers: []string{"test"}, 99 NodeGCThreshold: "12h", 100 EvalGCThreshold: "12h", 101 JobGCInterval: "3m", 102 JobGCThreshold: "12h", 103 DeploymentGCThreshold: "12h", 104 HeartbeatGrace: 30 * time.Second, 105 HeartbeatGraceHCL: "30s", 106 MinHeartbeatTTL: 33 * time.Second, 107 MinHeartbeatTTLHCL: "33s", 108 MaxHeartbeatsPerSecond: 11.0, 109 RetryJoin: []string{"1.1.1.1", "2.2.2.2"}, 110 StartJoin: []string{"1.1.1.1", "2.2.2.2"}, 111 RetryInterval: 15 * time.Second, 112 RetryIntervalHCL: "15s", 113 RejoinAfterLeave: true, 114 RetryMaxAttempts: 3, 115 NonVotingServer: true, 116 RedundancyZone: "foo", 117 UpgradeVersion: "0.8.0", 118 EncryptKey: "abc", 119 ServerJoin: &ServerJoin{ 120 RetryJoin: []string{"1.1.1.1", "2.2.2.2"}, 121 RetryInterval: time.Duration(15) * time.Second, 122 RetryIntervalHCL: "15s", 123 RetryMaxAttempts: 3, 124 }, 125 }, 126 ACL: &ACLConfig{ 127 Enabled: true, 128 TokenTTL: 60 * time.Second, 129 TokenTTLHCL: "60s", 130 PolicyTTL: 60 * time.Second, 131 PolicyTTLHCL: "60s", 132 ReplicationToken: "foobar", 133 }, 134 Telemetry: &Telemetry{ 135 StatsiteAddr: "127.0.0.1:1234", 136 StatsdAddr: "127.0.0.1:2345", 137 PrometheusMetrics: true, 138 DisableHostname: true, 139 UseNodeName: false, 140 CollectionInterval: "3s", 141 collectionInterval: 3 * time.Second, 142 PublishAllocationMetrics: true, 143 PublishNodeMetrics: true, 144 DisableTaggedMetrics: true, 145 BackwardsCompatibleMetrics: true, 146 }, 147 LeaveOnInt: true, 148 LeaveOnTerm: true, 149 EnableSyslog: true, 150 SyslogFacility: "LOCAL1", 151 DisableUpdateCheck: helper.BoolToPtr(true), 152 DisableAnonymousSignature: true, 153 Consul: &config.ConsulConfig{ 154 ServerServiceName: "nomad", 155 ServerHTTPCheckName: "nomad-server-http-health-check", 156 ServerSerfCheckName: "nomad-server-serf-health-check", 157 ServerRPCCheckName: "nomad-server-rpc-health-check", 158 ClientServiceName: "nomad-client", 159 ClientHTTPCheckName: "nomad-client-http-health-check", 160 Addr: "127.0.0.1:9500", 161 Token: "token1", 162 Auth: "username:pass", 163 EnableSSL: &trueValue, 164 VerifySSL: &trueValue, 165 CAFile: "/path/to/ca/file", 166 CertFile: "/path/to/cert/file", 167 KeyFile: "/path/to/key/file", 168 ServerAutoJoin: &trueValue, 169 ClientAutoJoin: &trueValue, 170 AutoAdvertise: &trueValue, 171 ChecksUseAdvertise: &trueValue, 172 Timeout: 5 * time.Second, 173 }, 174 Vault: &config.VaultConfig{ 175 Addr: "127.0.0.1:9500", 176 AllowUnauthenticated: &trueValue, 177 ConnectionRetryIntv: config.DefaultVaultConnectRetryIntv, 178 Enabled: &falseValue, 179 Role: "test_role", 180 TLSCaFile: "/path/to/ca/file", 181 TLSCaPath: "/path/to/ca", 182 TLSCertFile: "/path/to/cert/file", 183 TLSKeyFile: "/path/to/key/file", 184 TLSServerName: "foobar", 185 TLSSkipVerify: &trueValue, 186 TaskTokenTTL: "1s", 187 Token: "12345", 188 }, 189 TLSConfig: &config.TLSConfig{ 190 EnableHTTP: true, 191 EnableRPC: true, 192 VerifyServerHostname: true, 193 CAFile: "foo", 194 CertFile: "bar", 195 KeyFile: "pipe", 196 RPCUpgradeMode: true, 197 VerifyHTTPSClient: true, 198 TLSPreferServerCipherSuites: true, 199 TLSCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 200 TLSMinVersion: "tls12", 201 }, 202 HTTPAPIResponseHeaders: map[string]string{ 203 "Access-Control-Allow-Origin": "*", 204 }, 205 Sentinel: &config.SentinelConfig{ 206 Imports: []*config.SentinelImport{ 207 { 208 Name: "foo", 209 Path: "foo", 210 Args: []string{"a", "b", "c"}, 211 }, 212 { 213 Name: "bar", 214 Path: "bar", 215 Args: []string{"x", "y", "z"}, 216 }, 217 }, 218 }, 219 Autopilot: &config.AutopilotConfig{ 220 CleanupDeadServers: &trueValue, 221 ServerStabilizationTime: 23057 * time.Second, 222 ServerStabilizationTimeHCL: "23057s", 223 LastContactThreshold: 12705 * time.Second, 224 LastContactThresholdHCL: "12705s", 225 MaxTrailingLogs: 17849, 226 EnableRedundancyZones: &trueValue, 227 DisableUpgradeMigration: &trueValue, 228 EnableCustomUpgrades: &trueValue, 229 }, 230 Plugins: []*config.PluginConfig{ 231 { 232 Name: "docker", 233 Args: []string{"foo", "bar"}, 234 Config: map[string]interface{}{ 235 "foo": "bar", 236 "nested": []map[string]interface{}{ 237 { 238 "bam": 2, 239 }, 240 }, 241 }, 242 }, 243 { 244 Name: "exec", 245 Config: map[string]interface{}{ 246 "foo": true, 247 }, 248 }, 249 }, 250 } 251 252 var pluginConfig = &Config{ 253 Region: "", 254 Datacenter: "", 255 NodeName: "", 256 DataDir: "", 257 PluginDir: "", 258 LogLevel: "", 259 BindAddr: "", 260 EnableDebug: false, 261 Ports: nil, 262 Addresses: nil, 263 AdvertiseAddrs: nil, 264 Client: &ClientConfig{ 265 Enabled: false, 266 StateDir: "", 267 AllocDir: "", 268 Servers: nil, 269 NodeClass: "", 270 Meta: nil, 271 Options: nil, 272 ChrootEnv: nil, 273 NetworkInterface: "", 274 NetworkSpeed: 0, 275 CpuCompute: 0, 276 MemoryMB: 5555, 277 MaxKillTimeout: "", 278 ClientMinPort: 0, 279 ClientMaxPort: 0, 280 Reserved: nil, 281 GCInterval: 0, 282 GCParallelDestroys: 0, 283 GCDiskUsageThreshold: 0, 284 GCInodeUsageThreshold: 0, 285 GCMaxAllocs: 0, 286 NoHostUUID: nil, 287 }, 288 Server: nil, 289 ACL: nil, 290 Telemetry: nil, 291 LeaveOnInt: false, 292 LeaveOnTerm: false, 293 EnableSyslog: false, 294 SyslogFacility: "", 295 DisableUpdateCheck: nil, 296 DisableAnonymousSignature: false, 297 Consul: nil, 298 Vault: nil, 299 TLSConfig: nil, 300 HTTPAPIResponseHeaders: map[string]string{}, 301 Sentinel: nil, 302 Plugins: []*config.PluginConfig{ 303 { 304 Name: "docker", 305 Config: map[string]interface{}{ 306 "allow_privileged": true, 307 }, 308 }, 309 { 310 Name: "raw_exec", 311 Config: map[string]interface{}{ 312 "enabled": true, 313 }, 314 }, 315 }, 316 } 317 318 var nonoptConfig = &Config{ 319 Region: "", 320 Datacenter: "", 321 NodeName: "", 322 DataDir: "", 323 PluginDir: "", 324 LogLevel: "", 325 BindAddr: "", 326 EnableDebug: false, 327 Ports: nil, 328 Addresses: nil, 329 AdvertiseAddrs: nil, 330 Client: &ClientConfig{ 331 Enabled: false, 332 StateDir: "", 333 AllocDir: "", 334 Servers: nil, 335 NodeClass: "", 336 Meta: nil, 337 Options: nil, 338 ChrootEnv: nil, 339 NetworkInterface: "", 340 NetworkSpeed: 0, 341 CpuCompute: 0, 342 MemoryMB: 5555, 343 MaxKillTimeout: "", 344 ClientMinPort: 0, 345 ClientMaxPort: 0, 346 Reserved: nil, 347 GCInterval: 0, 348 GCParallelDestroys: 0, 349 GCDiskUsageThreshold: 0, 350 GCInodeUsageThreshold: 0, 351 GCMaxAllocs: 0, 352 NoHostUUID: nil, 353 }, 354 Server: nil, 355 ACL: nil, 356 Telemetry: nil, 357 LeaveOnInt: false, 358 LeaveOnTerm: false, 359 EnableSyslog: false, 360 SyslogFacility: "", 361 DisableUpdateCheck: nil, 362 DisableAnonymousSignature: false, 363 Consul: nil, 364 Vault: nil, 365 TLSConfig: nil, 366 HTTPAPIResponseHeaders: map[string]string{}, 367 Sentinel: nil, 368 } 369 370 func TestConfig_Parse(t *testing.T) { 371 t.Parallel() 372 373 basicConfig.addDefaults() 374 pluginConfig.addDefaults() 375 nonoptConfig.addDefaults() 376 377 cases := []struct { 378 File string 379 Result *Config 380 Err bool 381 }{ 382 { 383 "basic.hcl", 384 basicConfig, 385 false, 386 }, 387 { 388 "basic.json", 389 basicConfig, 390 false, 391 }, 392 { 393 "plugin.hcl", 394 pluginConfig, 395 false, 396 }, 397 { 398 "plugin.json", 399 pluginConfig, 400 false, 401 }, 402 { 403 "non-optional.hcl", 404 nonoptConfig, 405 false, 406 }, 407 } 408 409 for _, tc := range cases { 410 t.Run(tc.File, func(t *testing.T) { 411 require := require.New(t) 412 path, err := filepath.Abs(filepath.Join("./testdata", tc.File)) 413 require.NoError(err) 414 415 actual, err := ParseConfigFile(path) 416 require.NoError(err) 417 418 // ParseConfig used to re-merge defaults for these three objects, 419 // despite them already being merged in LoadConfig. The test structs 420 // expect these defaults to be set, but not the DefaultConfig 421 // defaults, which include additional settings 422 oldDefault := &Config{ 423 Consul: config.DefaultConsulConfig(), 424 Vault: config.DefaultVaultConfig(), 425 Autopilot: config.DefaultAutopilotConfig(), 426 } 427 actual = oldDefault.Merge(actual) 428 429 //panic(fmt.Sprintf("first: %+v \n second: %+v", actual.TLSConfig, tc.Result.TLSConfig)) 430 require.EqualValues(tc.Result, removeHelperAttributes(actual)) 431 }) 432 } 433 } 434 435 // In order to compare the Config struct after parsing, and from generating what 436 // is expected in the test, we need to remove helper attributes that are 437 // instantiated in the process of parsing the configuration 438 func removeHelperAttributes(c *Config) *Config { 439 if c.TLSConfig != nil { 440 c.TLSConfig.KeyLoader = nil 441 } 442 return c 443 } 444 445 func (c *Config) addDefaults() { 446 if c.Client == nil { 447 c.Client = &ClientConfig{} 448 } 449 if c.Client.ServerJoin == nil { 450 c.Client.ServerJoin = &ServerJoin{} 451 } 452 if c.ACL == nil { 453 c.ACL = &ACLConfig{} 454 } 455 if c.Consul == nil { 456 c.Consul = config.DefaultConsulConfig() 457 } 458 if c.Autopilot == nil { 459 c.Autopilot = config.DefaultAutopilotConfig() 460 } 461 if c.Vault == nil { 462 c.Vault = config.DefaultVaultConfig() 463 } 464 if c.Telemetry == nil { 465 c.Telemetry = &Telemetry{} 466 } 467 if c.Server == nil { 468 c.Server = &ServerConfig{} 469 } 470 if c.Server.ServerJoin == nil { 471 c.Server.ServerJoin = &ServerJoin{} 472 } 473 } 474 475 // Tests for a panic parsing json with an object of exactly 476 // length 1 described in 477 // https://github.com/hashicorp/nomad/issues/1290 478 func TestConfig_ParsePanic(t *testing.T) { 479 c, err := ParseConfigFile("./testdata/obj-len-one.hcl") 480 if err != nil { 481 t.Fatalf("parse error: %s\n", err) 482 } 483 484 d, err := ParseConfigFile("./testdata/obj-len-one.json") 485 if err != nil { 486 t.Fatalf("parse error: %s\n", err) 487 } 488 489 require.EqualValues(t, c, d) 490 } 491 492 // Top level keys left by hcl when parsing slices in the config 493 // structure should not be unexpected 494 func TestConfig_ParseSliceExtra(t *testing.T) { 495 c, err := ParseConfigFile("./testdata/config-slices.json") 496 require.NoError(t, err) 497 498 opt := map[string]string{"o0": "foo", "o1": "bar"} 499 meta := map[string]string{"m0": "foo", "m1": "bar", "m2": "true", "m3": "1.2"} 500 env := map[string]string{"e0": "baz"} 501 srv := []string{"foo", "bar"} 502 503 require.EqualValues(t, opt, c.Client.Options) 504 require.EqualValues(t, meta, c.Client.Meta) 505 require.EqualValues(t, env, c.Client.ChrootEnv) 506 require.EqualValues(t, srv, c.Client.Servers) 507 require.EqualValues(t, srv, c.Server.EnabledSchedulers) 508 require.EqualValues(t, srv, c.Server.StartJoin) 509 require.EqualValues(t, srv, c.Server.RetryJoin) 510 511 // the alt format is also accepted by hcl as valid config data 512 c, err = ParseConfigFile("./testdata/config-slices-alt.json") 513 require.NoError(t, err) 514 515 require.EqualValues(t, opt, c.Client.Options) 516 require.EqualValues(t, meta, c.Client.Meta) 517 require.EqualValues(t, env, c.Client.ChrootEnv) 518 require.EqualValues(t, srv, c.Client.Servers) 519 require.EqualValues(t, srv, c.Server.EnabledSchedulers) 520 require.EqualValues(t, srv, c.Server.StartJoin) 521 require.EqualValues(t, srv, c.Server.RetryJoin) 522 523 // small files keep more extra keys than large ones 524 _, err = ParseConfigFile("./testdata/obj-len-one-server.json") 525 require.NoError(t, err) 526 } 527 528 var sample0 = &Config{ 529 Region: "global", 530 Datacenter: "dc1", 531 DataDir: "/opt/data/nomad/data", 532 LogLevel: "INFO", 533 BindAddr: "0.0.0.0", 534 AdvertiseAddrs: &AdvertiseAddrs{ 535 HTTP: "host.example.com", 536 RPC: "host.example.com", 537 Serf: "host.example.com", 538 }, 539 Client: &ClientConfig{ServerJoin: &ServerJoin{}}, 540 Server: &ServerConfig{ 541 Enabled: true, 542 BootstrapExpect: 3, 543 RetryJoin: []string{"10.0.0.101", "10.0.0.102", "10.0.0.103"}, 544 EncryptKey: "sHck3WL6cxuhuY7Mso9BHA==", 545 ServerJoin: &ServerJoin{}, 546 }, 547 ACL: &ACLConfig{ 548 Enabled: true, 549 }, 550 Telemetry: &Telemetry{ 551 PrometheusMetrics: true, 552 DisableHostname: true, 553 CollectionInterval: "60s", 554 collectionInterval: 60 * time.Second, 555 PublishAllocationMetrics: true, 556 PublishNodeMetrics: true, 557 }, 558 LeaveOnInt: true, 559 LeaveOnTerm: true, 560 EnableSyslog: true, 561 SyslogFacility: "LOCAL0", 562 Consul: &config.ConsulConfig{ 563 Token: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 564 ServerAutoJoin: helper.BoolToPtr(false), 565 ClientAutoJoin: helper.BoolToPtr(false), 566 }, 567 Vault: &config.VaultConfig{ 568 Enabled: helper.BoolToPtr(true), 569 Role: "nomad-cluster", 570 Addr: "http://host.example.com:8200", 571 }, 572 TLSConfig: &config.TLSConfig{ 573 EnableHTTP: true, 574 EnableRPC: true, 575 VerifyServerHostname: true, 576 CAFile: "/opt/data/nomad/certs/nomad-ca.pem", 577 CertFile: "/opt/data/nomad/certs/server.pem", 578 KeyFile: "/opt/data/nomad/certs/server-key.pem", 579 }, 580 Autopilot: &config.AutopilotConfig{ 581 CleanupDeadServers: helper.BoolToPtr(true), 582 }, 583 } 584 585 func TestConfig_ParseSample0(t *testing.T) { 586 c, err := ParseConfigFile("./testdata/sample0.json") 587 require.NoError(t, err) 588 require.EqualValues(t, sample0, c) 589 } 590 591 var sample1 = &Config{ 592 Region: "global", 593 Datacenter: "dc1", 594 DataDir: "/opt/data/nomad/data", 595 LogLevel: "INFO", 596 BindAddr: "0.0.0.0", 597 AdvertiseAddrs: &AdvertiseAddrs{ 598 HTTP: "host.example.com", 599 RPC: "host.example.com", 600 Serf: "host.example.com", 601 }, 602 Client: &ClientConfig{ServerJoin: &ServerJoin{}}, 603 Server: &ServerConfig{ 604 Enabled: true, 605 BootstrapExpect: 3, 606 RetryJoin: []string{"10.0.0.101", "10.0.0.102", "10.0.0.103"}, 607 EncryptKey: "sHck3WL6cxuhuY7Mso9BHA==", 608 ServerJoin: &ServerJoin{}, 609 }, 610 ACL: &ACLConfig{ 611 Enabled: true, 612 }, 613 Telemetry: &Telemetry{ 614 PrometheusMetrics: true, 615 DisableHostname: true, 616 CollectionInterval: "60s", 617 collectionInterval: 60 * time.Second, 618 PublishAllocationMetrics: true, 619 PublishNodeMetrics: true, 620 }, 621 LeaveOnInt: true, 622 LeaveOnTerm: true, 623 EnableSyslog: true, 624 SyslogFacility: "LOCAL0", 625 Consul: &config.ConsulConfig{ 626 EnableSSL: helper.BoolToPtr(true), 627 Token: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 628 ServerAutoJoin: helper.BoolToPtr(false), 629 ClientAutoJoin: helper.BoolToPtr(false), 630 }, 631 Vault: &config.VaultConfig{ 632 Enabled: helper.BoolToPtr(true), 633 Role: "nomad-cluster", 634 Addr: "http://host.example.com:8200", 635 }, 636 TLSConfig: &config.TLSConfig{ 637 EnableHTTP: true, 638 EnableRPC: true, 639 VerifyServerHostname: true, 640 CAFile: "/opt/data/nomad/certs/nomad-ca.pem", 641 CertFile: "/opt/data/nomad/certs/server.pem", 642 KeyFile: "/opt/data/nomad/certs/server-key.pem", 643 }, 644 Autopilot: &config.AutopilotConfig{ 645 CleanupDeadServers: helper.BoolToPtr(true), 646 }, 647 } 648 649 func TestConfig_ParseDir(t *testing.T) { 650 c, err := LoadConfig("./testdata/sample1") 651 require.NoError(t, err) 652 653 // LoadConfig Merges all the config files in testdata/sample1, which makes empty 654 // maps & slices rather than nil, so set those 655 require.Empty(t, c.Client.Options) 656 c.Client.Options = nil 657 require.Empty(t, c.Client.Meta) 658 c.Client.Meta = nil 659 require.Empty(t, c.Client.ChrootEnv) 660 c.Client.ChrootEnv = nil 661 require.Empty(t, c.Server.StartJoin) 662 c.Server.StartJoin = nil 663 require.Empty(t, c.HTTPAPIResponseHeaders) 664 c.HTTPAPIResponseHeaders = nil 665 666 // LoadDir lists the config files 667 expectedFiles := []string{ 668 "testdata/sample1/sample0.json", 669 "testdata/sample1/sample1.json", 670 "testdata/sample1/sample2.hcl", 671 } 672 require.Equal(t, expectedFiles, c.Files) 673 c.Files = nil 674 675 require.EqualValues(t, sample1, c) 676 } 677 678 // TestConfig_ParseDir_Matches_IndividualParsing asserts 679 // that parsing a directory config is the equivalent of 680 // parsing individual files in any order 681 func TestConfig_ParseDir_Matches_IndividualParsing(t *testing.T) { 682 dirConfig, err := LoadConfig("./testdata/sample1") 683 require.NoError(t, err) 684 685 dirConfig = DefaultConfig().Merge(dirConfig) 686 687 files := []string{ 688 "testdata/sample1/sample0.json", 689 "testdata/sample1/sample1.json", 690 "testdata/sample1/sample2.hcl", 691 } 692 693 for _, perm := range permutations(files) { 694 t.Run(fmt.Sprintf("permutation %v", perm), func(t *testing.T) { 695 config := DefaultConfig() 696 697 for _, f := range perm { 698 fc, err := LoadConfig(f) 699 require.NoError(t, err) 700 701 config = config.Merge(fc) 702 } 703 704 // sort files to get stable view 705 sort.Strings(config.Files) 706 sort.Strings(dirConfig.Files) 707 708 require.EqualValues(t, dirConfig, config) 709 }) 710 } 711 712 } 713 714 // https://stackoverflow.com/a/30226442 715 func permutations(arr []string) [][]string { 716 var helper func([]string, int) 717 res := [][]string{} 718 719 helper = func(arr []string, n int) { 720 if n == 1 { 721 tmp := make([]string, len(arr)) 722 copy(tmp, arr) 723 res = append(res, tmp) 724 } else { 725 for i := 0; i < n; i++ { 726 helper(arr, n-1) 727 if n%2 == 1 { 728 tmp := arr[i] 729 arr[i] = arr[n-1] 730 arr[n-1] = tmp 731 } else { 732 tmp := arr[0] 733 arr[0] = arr[n-1] 734 arr[n-1] = tmp 735 } 736 } 737 } 738 } 739 helper(arr, len(arr)) 740 return res 741 }