github.com/prestonp/nomad@v0.10.4/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 AllowUnauthenticated: &trueValue, 162 Token: "token1", 163 Auth: "username:pass", 164 EnableSSL: &trueValue, 165 VerifySSL: &trueValue, 166 CAFile: "/path/to/ca/file", 167 CertFile: "/path/to/cert/file", 168 KeyFile: "/path/to/key/file", 169 ServerAutoJoin: &trueValue, 170 ClientAutoJoin: &trueValue, 171 AutoAdvertise: &trueValue, 172 ChecksUseAdvertise: &trueValue, 173 Timeout: 5 * time.Second, 174 }, 175 Vault: &config.VaultConfig{ 176 Addr: "127.0.0.1:9500", 177 AllowUnauthenticated: &trueValue, 178 ConnectionRetryIntv: config.DefaultVaultConnectRetryIntv, 179 Enabled: &falseValue, 180 Role: "test_role", 181 TLSCaFile: "/path/to/ca/file", 182 TLSCaPath: "/path/to/ca", 183 TLSCertFile: "/path/to/cert/file", 184 TLSKeyFile: "/path/to/key/file", 185 TLSServerName: "foobar", 186 TLSSkipVerify: &trueValue, 187 TaskTokenTTL: "1s", 188 Token: "12345", 189 }, 190 TLSConfig: &config.TLSConfig{ 191 EnableHTTP: true, 192 EnableRPC: true, 193 VerifyServerHostname: true, 194 CAFile: "foo", 195 CertFile: "bar", 196 KeyFile: "pipe", 197 RPCUpgradeMode: true, 198 VerifyHTTPSClient: true, 199 TLSPreferServerCipherSuites: true, 200 TLSCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 201 TLSMinVersion: "tls12", 202 }, 203 HTTPAPIResponseHeaders: map[string]string{ 204 "Access-Control-Allow-Origin": "*", 205 }, 206 Sentinel: &config.SentinelConfig{ 207 Imports: []*config.SentinelImport{ 208 { 209 Name: "foo", 210 Path: "foo", 211 Args: []string{"a", "b", "c"}, 212 }, 213 { 214 Name: "bar", 215 Path: "bar", 216 Args: []string{"x", "y", "z"}, 217 }, 218 }, 219 }, 220 Autopilot: &config.AutopilotConfig{ 221 CleanupDeadServers: &trueValue, 222 ServerStabilizationTime: 23057 * time.Second, 223 ServerStabilizationTimeHCL: "23057s", 224 LastContactThreshold: 12705 * time.Second, 225 LastContactThresholdHCL: "12705s", 226 MaxTrailingLogs: 17849, 227 EnableRedundancyZones: &trueValue, 228 DisableUpgradeMigration: &trueValue, 229 EnableCustomUpgrades: &trueValue, 230 }, 231 Plugins: []*config.PluginConfig{ 232 { 233 Name: "docker", 234 Args: []string{"foo", "bar"}, 235 Config: map[string]interface{}{ 236 "foo": "bar", 237 "nested": []map[string]interface{}{ 238 { 239 "bam": 2, 240 }, 241 }, 242 }, 243 }, 244 { 245 Name: "exec", 246 Config: map[string]interface{}{ 247 "foo": true, 248 }, 249 }, 250 }, 251 } 252 253 var pluginConfig = &Config{ 254 Region: "", 255 Datacenter: "", 256 NodeName: "", 257 DataDir: "", 258 PluginDir: "", 259 LogLevel: "", 260 BindAddr: "", 261 EnableDebug: false, 262 Ports: nil, 263 Addresses: nil, 264 AdvertiseAddrs: nil, 265 Client: &ClientConfig{ 266 Enabled: false, 267 StateDir: "", 268 AllocDir: "", 269 Servers: nil, 270 NodeClass: "", 271 Meta: nil, 272 Options: nil, 273 ChrootEnv: nil, 274 NetworkInterface: "", 275 NetworkSpeed: 0, 276 CpuCompute: 0, 277 MemoryMB: 5555, 278 MaxKillTimeout: "", 279 ClientMinPort: 0, 280 ClientMaxPort: 0, 281 Reserved: nil, 282 GCInterval: 0, 283 GCParallelDestroys: 0, 284 GCDiskUsageThreshold: 0, 285 GCInodeUsageThreshold: 0, 286 GCMaxAllocs: 0, 287 NoHostUUID: nil, 288 }, 289 Server: nil, 290 ACL: nil, 291 Telemetry: nil, 292 LeaveOnInt: false, 293 LeaveOnTerm: false, 294 EnableSyslog: false, 295 SyslogFacility: "", 296 DisableUpdateCheck: nil, 297 DisableAnonymousSignature: false, 298 Consul: nil, 299 Vault: nil, 300 TLSConfig: nil, 301 HTTPAPIResponseHeaders: map[string]string{}, 302 Sentinel: nil, 303 Plugins: []*config.PluginConfig{ 304 { 305 Name: "docker", 306 Config: map[string]interface{}{ 307 "allow_privileged": true, 308 }, 309 }, 310 { 311 Name: "raw_exec", 312 Config: map[string]interface{}{ 313 "enabled": true, 314 }, 315 }, 316 }, 317 } 318 319 var nonoptConfig = &Config{ 320 Region: "", 321 Datacenter: "", 322 NodeName: "", 323 DataDir: "", 324 PluginDir: "", 325 LogLevel: "", 326 BindAddr: "", 327 EnableDebug: false, 328 Ports: nil, 329 Addresses: nil, 330 AdvertiseAddrs: nil, 331 Client: &ClientConfig{ 332 Enabled: false, 333 StateDir: "", 334 AllocDir: "", 335 Servers: nil, 336 NodeClass: "", 337 Meta: nil, 338 Options: nil, 339 ChrootEnv: nil, 340 NetworkInterface: "", 341 NetworkSpeed: 0, 342 CpuCompute: 0, 343 MemoryMB: 5555, 344 MaxKillTimeout: "", 345 ClientMinPort: 0, 346 ClientMaxPort: 0, 347 Reserved: nil, 348 GCInterval: 0, 349 GCParallelDestroys: 0, 350 GCDiskUsageThreshold: 0, 351 GCInodeUsageThreshold: 0, 352 GCMaxAllocs: 0, 353 NoHostUUID: nil, 354 }, 355 Server: nil, 356 ACL: nil, 357 Telemetry: nil, 358 LeaveOnInt: false, 359 LeaveOnTerm: false, 360 EnableSyslog: false, 361 SyslogFacility: "", 362 DisableUpdateCheck: nil, 363 DisableAnonymousSignature: false, 364 Consul: nil, 365 Vault: nil, 366 TLSConfig: nil, 367 HTTPAPIResponseHeaders: map[string]string{}, 368 Sentinel: nil, 369 } 370 371 func TestConfig_Parse(t *testing.T) { 372 t.Parallel() 373 374 basicConfig.addDefaults() 375 pluginConfig.addDefaults() 376 nonoptConfig.addDefaults() 377 378 cases := []struct { 379 File string 380 Result *Config 381 Err bool 382 }{ 383 { 384 "basic.hcl", 385 basicConfig, 386 false, 387 }, 388 { 389 "basic.json", 390 basicConfig, 391 false, 392 }, 393 { 394 "plugin.hcl", 395 pluginConfig, 396 false, 397 }, 398 { 399 "plugin.json", 400 pluginConfig, 401 false, 402 }, 403 { 404 "non-optional.hcl", 405 nonoptConfig, 406 false, 407 }, 408 } 409 410 for _, tc := range cases { 411 t.Run(tc.File, func(t *testing.T) { 412 require := require.New(t) 413 path, err := filepath.Abs(filepath.Join("./testdata", tc.File)) 414 require.NoError(err) 415 416 actual, err := ParseConfigFile(path) 417 require.NoError(err) 418 419 // ParseConfig used to re-merge defaults for these three objects, 420 // despite them already being merged in LoadConfig. The test structs 421 // expect these defaults to be set, but not the DefaultConfig 422 // defaults, which include additional settings 423 oldDefault := &Config{ 424 Consul: config.DefaultConsulConfig(), 425 Vault: config.DefaultVaultConfig(), 426 Autopilot: config.DefaultAutopilotConfig(), 427 } 428 actual = oldDefault.Merge(actual) 429 430 //panic(fmt.Sprintf("first: %+v \n second: %+v", actual.TLSConfig, tc.Result.TLSConfig)) 431 require.EqualValues(tc.Result, removeHelperAttributes(actual)) 432 }) 433 } 434 } 435 436 // In order to compare the Config struct after parsing, and from generating what 437 // is expected in the test, we need to remove helper attributes that are 438 // instantiated in the process of parsing the configuration 439 func removeHelperAttributes(c *Config) *Config { 440 if c.TLSConfig != nil { 441 c.TLSConfig.KeyLoader = nil 442 } 443 return c 444 } 445 446 func (c *Config) addDefaults() { 447 if c.Client == nil { 448 c.Client = &ClientConfig{} 449 } 450 if c.Client.ServerJoin == nil { 451 c.Client.ServerJoin = &ServerJoin{} 452 } 453 if c.ACL == nil { 454 c.ACL = &ACLConfig{} 455 } 456 if c.Consul == nil { 457 c.Consul = config.DefaultConsulConfig() 458 } 459 if c.Autopilot == nil { 460 c.Autopilot = config.DefaultAutopilotConfig() 461 } 462 if c.Vault == nil { 463 c.Vault = config.DefaultVaultConfig() 464 } 465 if c.Telemetry == nil { 466 c.Telemetry = &Telemetry{} 467 } 468 if c.Server == nil { 469 c.Server = &ServerConfig{} 470 } 471 if c.Server.ServerJoin == nil { 472 c.Server.ServerJoin = &ServerJoin{} 473 } 474 } 475 476 // Tests for a panic parsing json with an object of exactly 477 // length 1 described in 478 // https://github.com/hashicorp/nomad/issues/1290 479 func TestConfig_ParsePanic(t *testing.T) { 480 c, err := ParseConfigFile("./testdata/obj-len-one.hcl") 481 if err != nil { 482 t.Fatalf("parse error: %s\n", err) 483 } 484 485 d, err := ParseConfigFile("./testdata/obj-len-one.json") 486 if err != nil { 487 t.Fatalf("parse error: %s\n", err) 488 } 489 490 require.EqualValues(t, c, d) 491 } 492 493 // Top level keys left by hcl when parsing slices in the config 494 // structure should not be unexpected 495 func TestConfig_ParseSliceExtra(t *testing.T) { 496 c, err := ParseConfigFile("./testdata/config-slices.json") 497 require.NoError(t, err) 498 499 opt := map[string]string{"o0": "foo", "o1": "bar"} 500 meta := map[string]string{"m0": "foo", "m1": "bar", "m2": "true", "m3": "1.2"} 501 env := map[string]string{"e0": "baz"} 502 srv := []string{"foo", "bar"} 503 504 require.EqualValues(t, opt, c.Client.Options) 505 require.EqualValues(t, meta, c.Client.Meta) 506 require.EqualValues(t, env, c.Client.ChrootEnv) 507 require.EqualValues(t, srv, c.Client.Servers) 508 require.EqualValues(t, srv, c.Server.EnabledSchedulers) 509 require.EqualValues(t, srv, c.Server.StartJoin) 510 require.EqualValues(t, srv, c.Server.RetryJoin) 511 512 // the alt format is also accepted by hcl as valid config data 513 c, err = ParseConfigFile("./testdata/config-slices-alt.json") 514 require.NoError(t, err) 515 516 require.EqualValues(t, opt, c.Client.Options) 517 require.EqualValues(t, meta, c.Client.Meta) 518 require.EqualValues(t, env, c.Client.ChrootEnv) 519 require.EqualValues(t, srv, c.Client.Servers) 520 require.EqualValues(t, srv, c.Server.EnabledSchedulers) 521 require.EqualValues(t, srv, c.Server.StartJoin) 522 require.EqualValues(t, srv, c.Server.RetryJoin) 523 524 // small files keep more extra keys than large ones 525 _, err = ParseConfigFile("./testdata/obj-len-one-server.json") 526 require.NoError(t, err) 527 } 528 529 var sample0 = &Config{ 530 Region: "global", 531 Datacenter: "dc1", 532 DataDir: "/opt/data/nomad/data", 533 LogLevel: "INFO", 534 BindAddr: "0.0.0.0", 535 AdvertiseAddrs: &AdvertiseAddrs{ 536 HTTP: "host.example.com", 537 RPC: "host.example.com", 538 Serf: "host.example.com", 539 }, 540 Client: &ClientConfig{ServerJoin: &ServerJoin{}}, 541 Server: &ServerConfig{ 542 Enabled: true, 543 BootstrapExpect: 3, 544 RetryJoin: []string{"10.0.0.101", "10.0.0.102", "10.0.0.103"}, 545 EncryptKey: "sHck3WL6cxuhuY7Mso9BHA==", 546 ServerJoin: &ServerJoin{}, 547 }, 548 ACL: &ACLConfig{ 549 Enabled: true, 550 }, 551 Telemetry: &Telemetry{ 552 PrometheusMetrics: true, 553 DisableHostname: true, 554 CollectionInterval: "60s", 555 collectionInterval: 60 * time.Second, 556 PublishAllocationMetrics: true, 557 PublishNodeMetrics: true, 558 }, 559 LeaveOnInt: true, 560 LeaveOnTerm: true, 561 EnableSyslog: true, 562 SyslogFacility: "LOCAL0", 563 Consul: &config.ConsulConfig{ 564 Token: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 565 ServerAutoJoin: helper.BoolToPtr(false), 566 ClientAutoJoin: helper.BoolToPtr(false), 567 }, 568 Vault: &config.VaultConfig{ 569 Enabled: helper.BoolToPtr(true), 570 Role: "nomad-cluster", 571 Addr: "http://host.example.com:8200", 572 }, 573 TLSConfig: &config.TLSConfig{ 574 EnableHTTP: true, 575 EnableRPC: true, 576 VerifyServerHostname: true, 577 CAFile: "/opt/data/nomad/certs/nomad-ca.pem", 578 CertFile: "/opt/data/nomad/certs/server.pem", 579 KeyFile: "/opt/data/nomad/certs/server-key.pem", 580 }, 581 Autopilot: &config.AutopilotConfig{ 582 CleanupDeadServers: helper.BoolToPtr(true), 583 }, 584 } 585 586 func TestConfig_ParseSample0(t *testing.T) { 587 c, err := ParseConfigFile("./testdata/sample0.json") 588 require.NoError(t, err) 589 require.EqualValues(t, sample0, c) 590 } 591 592 var sample1 = &Config{ 593 Region: "global", 594 Datacenter: "dc1", 595 DataDir: "/opt/data/nomad/data", 596 LogLevel: "INFO", 597 BindAddr: "0.0.0.0", 598 AdvertiseAddrs: &AdvertiseAddrs{ 599 HTTP: "host.example.com", 600 RPC: "host.example.com", 601 Serf: "host.example.com", 602 }, 603 Client: &ClientConfig{ServerJoin: &ServerJoin{}}, 604 Server: &ServerConfig{ 605 Enabled: true, 606 BootstrapExpect: 3, 607 RetryJoin: []string{"10.0.0.101", "10.0.0.102", "10.0.0.103"}, 608 EncryptKey: "sHck3WL6cxuhuY7Mso9BHA==", 609 ServerJoin: &ServerJoin{}, 610 }, 611 ACL: &ACLConfig{ 612 Enabled: true, 613 }, 614 Telemetry: &Telemetry{ 615 PrometheusMetrics: true, 616 DisableHostname: true, 617 CollectionInterval: "60s", 618 collectionInterval: 60 * time.Second, 619 PublishAllocationMetrics: true, 620 PublishNodeMetrics: true, 621 }, 622 LeaveOnInt: true, 623 LeaveOnTerm: true, 624 EnableSyslog: true, 625 SyslogFacility: "LOCAL0", 626 Consul: &config.ConsulConfig{ 627 EnableSSL: helper.BoolToPtr(true), 628 Token: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 629 ServerAutoJoin: helper.BoolToPtr(false), 630 ClientAutoJoin: helper.BoolToPtr(false), 631 }, 632 Vault: &config.VaultConfig{ 633 Enabled: helper.BoolToPtr(true), 634 Role: "nomad-cluster", 635 Addr: "http://host.example.com:8200", 636 }, 637 TLSConfig: &config.TLSConfig{ 638 EnableHTTP: true, 639 EnableRPC: true, 640 VerifyServerHostname: true, 641 CAFile: "/opt/data/nomad/certs/nomad-ca.pem", 642 CertFile: "/opt/data/nomad/certs/server.pem", 643 KeyFile: "/opt/data/nomad/certs/server-key.pem", 644 }, 645 Autopilot: &config.AutopilotConfig{ 646 CleanupDeadServers: helper.BoolToPtr(true), 647 }, 648 } 649 650 func TestConfig_ParseDir(t *testing.T) { 651 c, err := LoadConfig("./testdata/sample1") 652 require.NoError(t, err) 653 654 // LoadConfig Merges all the config files in testdata/sample1, which makes empty 655 // maps & slices rather than nil, so set those 656 require.Empty(t, c.Client.Options) 657 c.Client.Options = nil 658 require.Empty(t, c.Client.Meta) 659 c.Client.Meta = nil 660 require.Empty(t, c.Client.ChrootEnv) 661 c.Client.ChrootEnv = nil 662 require.Empty(t, c.Server.StartJoin) 663 c.Server.StartJoin = nil 664 require.Empty(t, c.HTTPAPIResponseHeaders) 665 c.HTTPAPIResponseHeaders = nil 666 667 // LoadDir lists the config files 668 expectedFiles := []string{ 669 "testdata/sample1/sample0.json", 670 "testdata/sample1/sample1.json", 671 "testdata/sample1/sample2.hcl", 672 } 673 require.Equal(t, expectedFiles, c.Files) 674 c.Files = nil 675 676 require.EqualValues(t, sample1, c) 677 } 678 679 // TestConfig_ParseDir_Matches_IndividualParsing asserts 680 // that parsing a directory config is the equivalent of 681 // parsing individual files in any order 682 func TestConfig_ParseDir_Matches_IndividualParsing(t *testing.T) { 683 dirConfig, err := LoadConfig("./testdata/sample1") 684 require.NoError(t, err) 685 686 dirConfig = DefaultConfig().Merge(dirConfig) 687 688 files := []string{ 689 "testdata/sample1/sample0.json", 690 "testdata/sample1/sample1.json", 691 "testdata/sample1/sample2.hcl", 692 } 693 694 for _, perm := range permutations(files) { 695 t.Run(fmt.Sprintf("permutation %v", perm), func(t *testing.T) { 696 config := DefaultConfig() 697 698 for _, f := range perm { 699 fc, err := LoadConfig(f) 700 require.NoError(t, err) 701 702 config = config.Merge(fc) 703 } 704 705 // sort files to get stable view 706 sort.Strings(config.Files) 707 sort.Strings(dirConfig.Files) 708 709 require.EqualValues(t, dirConfig, config) 710 }) 711 } 712 713 } 714 715 // https://stackoverflow.com/a/30226442 716 func permutations(arr []string) [][]string { 717 var helper func([]string, int) 718 res := [][]string{} 719 720 helper = func(arr []string, n int) { 721 if n == 1 { 722 tmp := make([]string, len(arr)) 723 copy(tmp, arr) 724 res = append(res, tmp) 725 } else { 726 for i := 0; i < n; i++ { 727 helper(arr, n-1) 728 if n%2 == 1 { 729 tmp := arr[i] 730 arr[i] = arr[n-1] 731 arr[n-1] = tmp 732 } else { 733 tmp := arr[0] 734 arr[0] = arr[n-1] 735 arr[n-1] = tmp 736 } 737 } 738 } 739 } 740 helper(arr, len(arr)) 741 return res 742 }