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