github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/cliconfig/cliconfig_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package cliconfig 5 6 import ( 7 "os" 8 "path/filepath" 9 "reflect" 10 "testing" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/google/go-cmp/cmp" 14 "github.com/terramate-io/tf/tfdiags" 15 ) 16 17 // This is the directory where our test fixtures are. 18 const fixtureDir = "./testdata" 19 20 func TestLoadConfig(t *testing.T) { 21 c, err := loadConfigFile(filepath.Join(fixtureDir, "config")) 22 if err != nil { 23 t.Fatalf("err: %s", err) 24 } 25 26 expected := &Config{ 27 Providers: map[string]string{ 28 "aws": "foo", 29 "do": "bar", 30 }, 31 } 32 33 if !reflect.DeepEqual(c, expected) { 34 t.Fatalf("bad: %#v", c) 35 } 36 } 37 38 func TestLoadConfig_envSubst(t *testing.T) { 39 defer os.Unsetenv("TFTEST") 40 os.Setenv("TFTEST", "hello") 41 42 c, err := loadConfigFile(filepath.Join(fixtureDir, "config-env")) 43 if err != nil { 44 t.Fatalf("err: %s", err) 45 } 46 47 expected := &Config{ 48 Providers: map[string]string{ 49 "aws": "hello", 50 "google": "bar", 51 }, 52 Provisioners: map[string]string{ 53 "local": "hello", 54 }, 55 } 56 57 if !reflect.DeepEqual(c, expected) { 58 t.Fatalf("bad: %#v", c) 59 } 60 } 61 62 func TestLoadConfig_non_existing_file(t *testing.T) { 63 tmpDir := os.TempDir() 64 cliTmpFile := filepath.Join(tmpDir, "dev.tfrc") 65 66 os.Setenv("TF_CLI_CONFIG_FILE", cliTmpFile) 67 defer os.Unsetenv("TF_CLI_CONFIG_FILE") 68 69 c, errs := LoadConfig() 70 if errs.HasErrors() || c.Validate().HasErrors() { 71 t.Fatalf("err: %s", errs) 72 } 73 74 hasOpenFileWarn := false 75 for _, err := range errs { 76 if err.Severity() == tfdiags.Warning && err.Description().Summary == "Unable to open CLI configuration file" { 77 hasOpenFileWarn = true 78 break 79 } 80 } 81 82 if !hasOpenFileWarn { 83 t.Fatal("expecting a warning message because of nonexisting CLI configuration file") 84 } 85 } 86 87 func TestEnvConfig(t *testing.T) { 88 tests := map[string]struct { 89 env map[string]string 90 want *Config 91 }{ 92 "no environment variables": { 93 nil, 94 &Config{}, 95 }, 96 "TF_PLUGIN_CACHE_DIR=boop": { 97 map[string]string{ 98 "TF_PLUGIN_CACHE_DIR": "boop", 99 }, 100 &Config{ 101 PluginCacheDir: "boop", 102 }, 103 }, 104 "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=anything_except_zero": { 105 map[string]string{ 106 "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "anything_except_zero", 107 }, 108 &Config{ 109 PluginCacheMayBreakDependencyLockFile: true, 110 }, 111 }, 112 "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=0": { 113 map[string]string{ 114 "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "0", 115 }, 116 &Config{}, 117 }, 118 "TF_PLUGIN_CACHE_DIR and TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": { 119 map[string]string{ 120 "TF_PLUGIN_CACHE_DIR": "beep", 121 "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "1", 122 }, 123 &Config{ 124 PluginCacheDir: "beep", 125 PluginCacheMayBreakDependencyLockFile: true, 126 }, 127 }, 128 } 129 130 for name, test := range tests { 131 t.Run(name, func(t *testing.T) { 132 got := envConfig(test.env) 133 want := test.want 134 135 if diff := cmp.Diff(want, got); diff != "" { 136 t.Errorf("wrong result\n%s", diff) 137 } 138 }) 139 } 140 } 141 142 func TestMakeEnvMap(t *testing.T) { 143 tests := map[string]struct { 144 environ []string 145 want map[string]string 146 }{ 147 "nil": { 148 nil, 149 nil, 150 }, 151 "one": { 152 []string{ 153 "FOO=bar", 154 }, 155 map[string]string{ 156 "FOO": "bar", 157 }, 158 }, 159 "many": { 160 []string{ 161 "FOO=1", 162 "BAR=2", 163 "BAZ=3", 164 }, 165 map[string]string{ 166 "FOO": "1", 167 "BAR": "2", 168 "BAZ": "3", 169 }, 170 }, 171 "conflict": { 172 []string{ 173 "FOO=1", 174 "BAR=1", 175 "FOO=2", 176 }, 177 map[string]string{ 178 "BAR": "1", 179 "FOO": "2", // Last entry of each name wins 180 }, 181 }, 182 "empty_val": { 183 []string{ 184 "FOO=", 185 }, 186 map[string]string{ 187 "FOO": "", 188 }, 189 }, 190 "no_equals": { 191 []string{ 192 "FOO=bar", 193 "INVALID", 194 }, 195 map[string]string{ 196 "FOO": "bar", 197 }, 198 }, 199 "multi_equals": { 200 []string{ 201 "FOO=bar=baz=boop", 202 }, 203 map[string]string{ 204 "FOO": "bar=baz=boop", 205 }, 206 }, 207 } 208 209 for name, test := range tests { 210 t.Run(name, func(t *testing.T) { 211 got := makeEnvMap(test.environ) 212 want := test.want 213 214 if diff := cmp.Diff(want, got); diff != "" { 215 t.Errorf("wrong result\n%s", diff) 216 } 217 }) 218 } 219 220 } 221 222 func TestLoadConfig_hosts(t *testing.T) { 223 got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts")) 224 if len(diags) != 0 { 225 t.Fatalf("%s", diags.Err()) 226 } 227 228 want := &Config{ 229 Hosts: map[string]*ConfigHost{ 230 "example.com": { 231 Services: map[string]interface{}{ 232 "modules.v1": "https://example.com/", 233 }, 234 }, 235 }, 236 } 237 238 if !reflect.DeepEqual(got, want) { 239 t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want)) 240 } 241 } 242 243 func TestLoadConfig_credentials(t *testing.T) { 244 got, err := loadConfigFile(filepath.Join(fixtureDir, "credentials")) 245 if err != nil { 246 t.Fatal(err) 247 } 248 249 want := &Config{ 250 Credentials: map[string]map[string]interface{}{ 251 "example.com": map[string]interface{}{ 252 "token": "foo the bar baz", 253 }, 254 "example.net": map[string]interface{}{ 255 "username": "foo", 256 "password": "baz", 257 }, 258 }, 259 CredentialsHelpers: map[string]*ConfigCredentialsHelper{ 260 "foo": &ConfigCredentialsHelper{ 261 Args: []string{"bar", "baz"}, 262 }, 263 }, 264 } 265 266 if !reflect.DeepEqual(got, want) { 267 t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want)) 268 } 269 } 270 271 func TestConfigValidate(t *testing.T) { 272 tests := map[string]struct { 273 Config *Config 274 DiagCount int 275 }{ 276 "nil": { 277 nil, 278 0, 279 }, 280 "empty": { 281 &Config{}, 282 0, 283 }, 284 "host good": { 285 &Config{ 286 Hosts: map[string]*ConfigHost{ 287 "example.com": {}, 288 }, 289 }, 290 0, 291 }, 292 "host with bad hostname": { 293 &Config{ 294 Hosts: map[string]*ConfigHost{ 295 "example..com": {}, 296 }, 297 }, 298 1, // host block has invalid hostname 299 }, 300 "credentials good": { 301 &Config{ 302 Credentials: map[string]map[string]interface{}{ 303 "example.com": map[string]interface{}{ 304 "token": "foo", 305 }, 306 }, 307 }, 308 0, 309 }, 310 "credentials with bad hostname": { 311 &Config{ 312 Credentials: map[string]map[string]interface{}{ 313 "example..com": map[string]interface{}{ 314 "token": "foo", 315 }, 316 }, 317 }, 318 1, // credentials block has invalid hostname 319 }, 320 "credentials helper good": { 321 &Config{ 322 CredentialsHelpers: map[string]*ConfigCredentialsHelper{ 323 "foo": {}, 324 }, 325 }, 326 0, 327 }, 328 "credentials helper too many": { 329 &Config{ 330 CredentialsHelpers: map[string]*ConfigCredentialsHelper{ 331 "foo": {}, 332 "bar": {}, 333 }, 334 }, 335 1, // no more than one credentials_helper block allowed 336 }, 337 "provider_installation good none": { 338 &Config{ 339 ProviderInstallation: nil, 340 }, 341 0, 342 }, 343 "provider_installation good one": { 344 &Config{ 345 ProviderInstallation: []*ProviderInstallation{ 346 {}, 347 }, 348 }, 349 0, 350 }, 351 "provider_installation too many": { 352 &Config{ 353 ProviderInstallation: []*ProviderInstallation{ 354 {}, 355 {}, 356 }, 357 }, 358 1, // no more than one provider_installation block allowed 359 }, 360 "plugin_cache_dir does not exist": { 361 &Config{ 362 PluginCacheDir: "fake", 363 }, 364 1, // The specified plugin cache dir %s cannot be opened 365 }, 366 } 367 368 for name, test := range tests { 369 t.Run(name, func(t *testing.T) { 370 diags := test.Config.Validate() 371 if len(diags) != test.DiagCount { 372 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) 373 for _, diag := range diags { 374 t.Logf("- %#v", diag.Description()) 375 } 376 } 377 }) 378 } 379 } 380 381 func TestConfig_Merge(t *testing.T) { 382 c1 := &Config{ 383 Providers: map[string]string{ 384 "foo": "bar", 385 "bar": "blah", 386 }, 387 Provisioners: map[string]string{ 388 "local": "local", 389 "remote": "bad", 390 }, 391 Hosts: map[string]*ConfigHost{ 392 "example.com": { 393 Services: map[string]interface{}{ 394 "modules.v1": "http://example.com/", 395 }, 396 }, 397 }, 398 Credentials: map[string]map[string]interface{}{ 399 "foo": { 400 "bar": "baz", 401 }, 402 }, 403 CredentialsHelpers: map[string]*ConfigCredentialsHelper{ 404 "buz": {}, 405 }, 406 ProviderInstallation: []*ProviderInstallation{ 407 { 408 Methods: []*ProviderInstallationMethod{ 409 {Location: ProviderInstallationFilesystemMirror("a")}, 410 {Location: ProviderInstallationFilesystemMirror("b")}, 411 }, 412 }, 413 { 414 Methods: []*ProviderInstallationMethod{ 415 {Location: ProviderInstallationFilesystemMirror("c")}, 416 }, 417 }, 418 }, 419 } 420 421 c2 := &Config{ 422 Providers: map[string]string{ 423 "bar": "baz", 424 "baz": "what", 425 }, 426 Provisioners: map[string]string{ 427 "remote": "remote", 428 }, 429 Hosts: map[string]*ConfigHost{ 430 "example.net": { 431 Services: map[string]interface{}{ 432 "modules.v1": "https://example.net/", 433 }, 434 }, 435 }, 436 Credentials: map[string]map[string]interface{}{ 437 "fee": { 438 "bur": "bez", 439 }, 440 }, 441 CredentialsHelpers: map[string]*ConfigCredentialsHelper{ 442 "biz": {}, 443 }, 444 ProviderInstallation: []*ProviderInstallation{ 445 { 446 Methods: []*ProviderInstallationMethod{ 447 {Location: ProviderInstallationFilesystemMirror("d")}, 448 }, 449 }, 450 }, 451 PluginCacheMayBreakDependencyLockFile: true, 452 } 453 454 expected := &Config{ 455 Providers: map[string]string{ 456 "foo": "bar", 457 "bar": "baz", 458 "baz": "what", 459 }, 460 Provisioners: map[string]string{ 461 "local": "local", 462 "remote": "remote", 463 }, 464 Hosts: map[string]*ConfigHost{ 465 "example.com": { 466 Services: map[string]interface{}{ 467 "modules.v1": "http://example.com/", 468 }, 469 }, 470 "example.net": { 471 Services: map[string]interface{}{ 472 "modules.v1": "https://example.net/", 473 }, 474 }, 475 }, 476 Credentials: map[string]map[string]interface{}{ 477 "foo": { 478 "bar": "baz", 479 }, 480 "fee": { 481 "bur": "bez", 482 }, 483 }, 484 CredentialsHelpers: map[string]*ConfigCredentialsHelper{ 485 "buz": {}, 486 "biz": {}, 487 }, 488 ProviderInstallation: []*ProviderInstallation{ 489 { 490 Methods: []*ProviderInstallationMethod{ 491 {Location: ProviderInstallationFilesystemMirror("a")}, 492 {Location: ProviderInstallationFilesystemMirror("b")}, 493 }, 494 }, 495 { 496 Methods: []*ProviderInstallationMethod{ 497 {Location: ProviderInstallationFilesystemMirror("c")}, 498 }, 499 }, 500 { 501 Methods: []*ProviderInstallationMethod{ 502 {Location: ProviderInstallationFilesystemMirror("d")}, 503 }, 504 }, 505 }, 506 PluginCacheMayBreakDependencyLockFile: true, 507 } 508 509 actual := c1.Merge(c2) 510 if diff := cmp.Diff(expected, actual); diff != "" { 511 t.Fatalf("wrong result\n%s", diff) 512 } 513 } 514 515 func TestConfig_Merge_disableCheckpoint(t *testing.T) { 516 c1 := &Config{ 517 DisableCheckpoint: true, 518 } 519 520 c2 := &Config{} 521 522 expected := &Config{ 523 Providers: map[string]string{}, 524 Provisioners: map[string]string{}, 525 DisableCheckpoint: true, 526 } 527 528 actual := c1.Merge(c2) 529 if !reflect.DeepEqual(actual, expected) { 530 t.Fatalf("bad: %#v", actual) 531 } 532 } 533 534 func TestConfig_Merge_disableCheckpointSignature(t *testing.T) { 535 c1 := &Config{ 536 DisableCheckpointSignature: true, 537 } 538 539 c2 := &Config{} 540 541 expected := &Config{ 542 Providers: map[string]string{}, 543 Provisioners: map[string]string{}, 544 DisableCheckpointSignature: true, 545 } 546 547 actual := c1.Merge(c2) 548 if !reflect.DeepEqual(actual, expected) { 549 t.Fatalf("bad: %#v", actual) 550 } 551 }