github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/initwd/module_install_test.go (about) 1 package initwd 2 3 import ( 4 "flag" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "path/filepath" 10 "strings" 11 "testing" 12 13 "github.com/go-test/deep" 14 version "github.com/hashicorp/go-version" 15 "github.com/hashicorp/terraform-plugin-sdk/helper/logging" 16 "github.com/hashicorp/terraform-plugin-sdk/internal/configs" 17 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configload" 18 "github.com/hashicorp/terraform-plugin-sdk/internal/registry" 19 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 20 ) 21 22 func TestMain(m *testing.M) { 23 flag.Parse() 24 if testing.Verbose() { 25 // if we're verbose, use the logging requested by TF_LOG 26 logging.SetOutput() 27 } else { 28 // otherwise silence all logs 29 log.SetOutput(ioutil.Discard) 30 } 31 32 os.Exit(m.Run()) 33 } 34 35 func TestModuleInstaller(t *testing.T) { 36 fixtureDir := filepath.Clean("testdata/local-modules") 37 dir, done := tempChdir(t, fixtureDir) 38 defer done() 39 40 hooks := &testInstallHooks{} 41 42 modulesDir := filepath.Join(dir, ".terraform/modules") 43 inst := NewModuleInstaller(modulesDir, nil) 44 _, diags := inst.InstallModules(".", false, hooks) 45 assertNoDiagnostics(t, diags) 46 47 wantCalls := []testInstallHookCall{ 48 { 49 Name: "Install", 50 ModuleAddr: "child_a", 51 PackageAddr: "", 52 LocalPath: "child_a", 53 }, 54 { 55 Name: "Install", 56 ModuleAddr: "child_a.child_b", 57 PackageAddr: "", 58 LocalPath: "child_a/child_b", 59 }, 60 } 61 62 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 63 return 64 } 65 66 loader, err := configload.NewLoader(&configload.Config{ 67 ModulesDir: modulesDir, 68 }) 69 if err != nil { 70 t.Fatal(err) 71 } 72 73 // Make sure the configuration is loadable now. 74 // (This ensures that correct information is recorded in the manifest.) 75 config, loadDiags := loader.LoadConfig(".") 76 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 77 78 wantTraces := map[string]string{ 79 "": "in root module", 80 "child_a": "in child_a module", 81 "child_a.child_b": "in child_b module", 82 } 83 gotTraces := map[string]string{} 84 config.DeepEach(func(c *configs.Config) { 85 path := strings.Join(c.Path, ".") 86 if c.Module.Variables["v"] == nil { 87 gotTraces[path] = "<missing>" 88 return 89 } 90 varDesc := c.Module.Variables["v"].Description 91 gotTraces[path] = varDesc 92 }) 93 assertResultDeepEqual(t, gotTraces, wantTraces) 94 } 95 96 func TestModuleInstaller_error(t *testing.T) { 97 fixtureDir := filepath.Clean("testdata/local-module-error") 98 dir, done := tempChdir(t, fixtureDir) 99 defer done() 100 101 hooks := &testInstallHooks{} 102 103 modulesDir := filepath.Join(dir, ".terraform/modules") 104 inst := NewModuleInstaller(modulesDir, nil) 105 _, diags := inst.InstallModules(".", false, hooks) 106 107 if !diags.HasErrors() { 108 t.Fatal("expected error") 109 } else { 110 assertDiagnosticSummary(t, diags, "Module not found") 111 } 112 } 113 114 func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) { 115 fixtureDir := filepath.Clean("testdata/invalid-version-constraint") 116 dir, done := tempChdir(t, fixtureDir) 117 defer done() 118 119 hooks := &testInstallHooks{} 120 121 modulesDir := filepath.Join(dir, ".terraform/modules") 122 inst := NewModuleInstaller(modulesDir, nil) 123 _, diags := inst.InstallModules(".", false, hooks) 124 125 if !diags.HasErrors() { 126 t.Fatal("expected error") 127 } else { 128 assertDiagnosticSummary(t, diags, "Invalid version constraint") 129 } 130 } 131 132 func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) { 133 fixtureDir := filepath.Clean("testdata/invalid-version-constraint") 134 dir, done := tempChdir(t, fixtureDir) 135 defer done() 136 137 hooks := &testInstallHooks{} 138 139 modulesDir := filepath.Join(dir, ".terraform/modules") 140 inst := NewModuleInstaller(modulesDir, nil) 141 _, diags := inst.InstallModules(".", false, hooks) 142 143 if !diags.HasErrors() { 144 t.Fatal("expected error") 145 } else { 146 assertDiagnosticSummary(t, diags, "Invalid version constraint") 147 } 148 } 149 150 func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) { 151 fixtureDir := filepath.Clean("testdata/invalid-version-constraint-local") 152 dir, done := tempChdir(t, fixtureDir) 153 defer done() 154 155 hooks := &testInstallHooks{} 156 157 modulesDir := filepath.Join(dir, ".terraform/modules") 158 inst := NewModuleInstaller(modulesDir, nil) 159 _, diags := inst.InstallModules(".", false, hooks) 160 161 if !diags.HasErrors() { 162 t.Fatal("expected error") 163 } else { 164 assertDiagnosticSummary(t, diags, "Invalid version constraint") 165 } 166 } 167 168 func TestModuleInstaller_symlink(t *testing.T) { 169 fixtureDir := filepath.Clean("testdata/local-module-symlink") 170 dir, done := tempChdir(t, fixtureDir) 171 defer done() 172 173 hooks := &testInstallHooks{} 174 175 modulesDir := filepath.Join(dir, ".terraform/modules") 176 inst := NewModuleInstaller(modulesDir, nil) 177 _, diags := inst.InstallModules(".", false, hooks) 178 assertNoDiagnostics(t, diags) 179 180 wantCalls := []testInstallHookCall{ 181 { 182 Name: "Install", 183 ModuleAddr: "child_a", 184 PackageAddr: "", 185 LocalPath: "child_a", 186 }, 187 { 188 Name: "Install", 189 ModuleAddr: "child_a.child_b", 190 PackageAddr: "", 191 LocalPath: "child_a/child_b", 192 }, 193 } 194 195 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 196 return 197 } 198 199 loader, err := configload.NewLoader(&configload.Config{ 200 ModulesDir: modulesDir, 201 }) 202 if err != nil { 203 t.Fatal(err) 204 } 205 206 // Make sure the configuration is loadable now. 207 // (This ensures that correct information is recorded in the manifest.) 208 config, loadDiags := loader.LoadConfig(".") 209 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 210 211 wantTraces := map[string]string{ 212 "": "in root module", 213 "child_a": "in child_a module", 214 "child_a.child_b": "in child_b module", 215 } 216 gotTraces := map[string]string{} 217 config.DeepEach(func(c *configs.Config) { 218 path := strings.Join(c.Path, ".") 219 if c.Module.Variables["v"] == nil { 220 gotTraces[path] = "<missing>" 221 return 222 } 223 varDesc := c.Module.Variables["v"].Description 224 gotTraces[path] = varDesc 225 }) 226 assertResultDeepEqual(t, gotTraces, wantTraces) 227 } 228 229 func TestLoaderInstallModules_registry(t *testing.T) { 230 if os.Getenv("TF_ACC") == "" { 231 t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") 232 } 233 234 fixtureDir := filepath.Clean("testdata/registry-modules") 235 tmpDir, done := tempChdir(t, fixtureDir) 236 // the module installer runs filepath.EvalSymlinks() on the destination 237 // directory before copying files, and the resultant directory is what is 238 // returned by the install hooks. Without this, tests could fail on machines 239 // where the default temp dir was a symlink. 240 dir, err := filepath.EvalSymlinks(tmpDir) 241 if err != nil { 242 t.Error(err) 243 } 244 245 defer done() 246 247 hooks := &testInstallHooks{} 248 modulesDir := filepath.Join(dir, ".terraform/modules") 249 inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) 250 _, diags := inst.InstallModules(dir, false, hooks) 251 assertNoDiagnostics(t, diags) 252 253 v := version.Must(version.NewVersion("0.0.1")) 254 255 wantCalls := []testInstallHookCall{ 256 // the configuration builder visits each level of calls in lexicographical 257 // order by name, so the following list is kept in the same order. 258 259 // acctest_child_a accesses //modules/child_a directly 260 { 261 Name: "Download", 262 ModuleAddr: "acctest_child_a", 263 PackageAddr: "hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here 264 Version: v, 265 }, 266 { 267 Name: "Install", 268 ModuleAddr: "acctest_child_a", 269 Version: v, 270 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_a"), 271 }, 272 273 // acctest_child_a.child_b 274 // (no download because it's a relative path inside acctest_child_a) 275 { 276 Name: "Install", 277 ModuleAddr: "acctest_child_a.child_b", 278 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_b"), 279 }, 280 281 // acctest_child_b accesses //modules/child_b directly 282 { 283 Name: "Download", 284 ModuleAddr: "acctest_child_b", 285 PackageAddr: "hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here 286 Version: v, 287 }, 288 { 289 Name: "Install", 290 ModuleAddr: "acctest_child_b", 291 Version: v, 292 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_b"), 293 }, 294 295 // acctest_root 296 { 297 Name: "Download", 298 ModuleAddr: "acctest_root", 299 PackageAddr: "hashicorp/module-installer-acctest/aws", 300 Version: v, 301 }, 302 { 303 Name: "Install", 304 ModuleAddr: "acctest_root", 305 Version: v, 306 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/hashicorp-terraform-aws-module-installer-acctest-853d038"), 307 }, 308 309 // acctest_root.child_a 310 // (no download because it's a relative path inside acctest_root) 311 { 312 Name: "Install", 313 ModuleAddr: "acctest_root.child_a", 314 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_a"), 315 }, 316 317 // acctest_root.child_a.child_b 318 // (no download because it's a relative path inside acctest_root, via acctest_root.child_a) 319 { 320 Name: "Install", 321 ModuleAddr: "acctest_root.child_a.child_b", 322 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_b"), 323 }, 324 } 325 326 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 327 return 328 } 329 330 loader, err := configload.NewLoader(&configload.Config{ 331 ModulesDir: modulesDir, 332 }) 333 if err != nil { 334 t.Fatal(err) 335 } 336 337 // Make sure the configuration is loadable now. 338 // (This ensures that correct information is recorded in the manifest.) 339 config, loadDiags := loader.LoadConfig(".") 340 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 341 342 wantTraces := map[string]string{ 343 "": "in local caller for registry-modules", 344 "acctest_root": "in root module", 345 "acctest_root.child_a": "in child_a module", 346 "acctest_root.child_a.child_b": "in child_b module", 347 "acctest_child_a": "in child_a module", 348 "acctest_child_a.child_b": "in child_b module", 349 "acctest_child_b": "in child_b module", 350 } 351 gotTraces := map[string]string{} 352 config.DeepEach(func(c *configs.Config) { 353 path := strings.Join(c.Path, ".") 354 if c.Module.Variables["v"] == nil { 355 gotTraces[path] = "<missing>" 356 return 357 } 358 varDesc := c.Module.Variables["v"].Description 359 gotTraces[path] = varDesc 360 }) 361 assertResultDeepEqual(t, gotTraces, wantTraces) 362 363 } 364 365 func TestLoaderInstallModules_goGetter(t *testing.T) { 366 if os.Getenv("TF_ACC") == "" { 367 t.Skip("this test accesses github.com; set TF_ACC=1 to run it") 368 } 369 370 fixtureDir := filepath.Clean("testdata/go-getter-modules") 371 tmpDir, done := tempChdir(t, fixtureDir) 372 // the module installer runs filepath.EvalSymlinks() on the destination 373 // directory before copying files, and the resultant directory is what is 374 // returned by the install hooks. Without this, tests could fail on machines 375 // where the default temp dir was a symlink. 376 dir, err := filepath.EvalSymlinks(tmpDir) 377 if err != nil { 378 t.Error(err) 379 } 380 defer done() 381 382 hooks := &testInstallHooks{} 383 modulesDir := filepath.Join(dir, ".terraform/modules") 384 inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) 385 _, diags := inst.InstallModules(dir, false, hooks) 386 assertNoDiagnostics(t, diags) 387 388 wantCalls := []testInstallHookCall{ 389 // the configuration builder visits each level of calls in lexicographical 390 // order by name, so the following list is kept in the same order. 391 392 // acctest_child_a accesses //modules/child_a directly 393 { 394 Name: "Download", 395 ModuleAddr: "acctest_child_a", 396 PackageAddr: "github.com/hashicorp/terraform-aws-module-installer-acctest?ref=v0.0.1", // intentionally excludes the subdir because we're downloading the whole repo here 397 }, 398 { 399 Name: "Install", 400 ModuleAddr: "acctest_child_a", 401 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"), 402 }, 403 404 // acctest_child_a.child_b 405 // (no download because it's a relative path inside acctest_child_a) 406 { 407 Name: "Install", 408 ModuleAddr: "acctest_child_a.child_b", 409 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"), 410 }, 411 412 // acctest_child_b accesses //modules/child_b directly 413 { 414 Name: "Download", 415 ModuleAddr: "acctest_child_b", 416 PackageAddr: "github.com/hashicorp/terraform-aws-module-installer-acctest?ref=v0.0.1", // intentionally excludes the subdir because we're downloading the whole package here 417 }, 418 { 419 Name: "Install", 420 ModuleAddr: "acctest_child_b", 421 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"), 422 }, 423 424 // acctest_root 425 { 426 Name: "Download", 427 ModuleAddr: "acctest_root", 428 PackageAddr: "github.com/hashicorp/terraform-aws-module-installer-acctest?ref=v0.0.1", 429 }, 430 { 431 Name: "Install", 432 ModuleAddr: "acctest_root", 433 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root"), 434 }, 435 436 // acctest_root.child_a 437 // (no download because it's a relative path inside acctest_root) 438 { 439 Name: "Install", 440 ModuleAddr: "acctest_root.child_a", 441 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"), 442 }, 443 444 // acctest_root.child_a.child_b 445 // (no download because it's a relative path inside acctest_root, via acctest_root.child_a) 446 { 447 Name: "Install", 448 ModuleAddr: "acctest_root.child_a.child_b", 449 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"), 450 }, 451 } 452 453 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 454 return 455 } 456 457 loader, err := configload.NewLoader(&configload.Config{ 458 ModulesDir: modulesDir, 459 }) 460 if err != nil { 461 t.Fatal(err) 462 } 463 464 // Make sure the configuration is loadable now. 465 // (This ensures that correct information is recorded in the manifest.) 466 config, loadDiags := loader.LoadConfig(".") 467 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 468 469 wantTraces := map[string]string{ 470 "": "in local caller for go-getter-modules", 471 "acctest_root": "in root module", 472 "acctest_root.child_a": "in child_a module", 473 "acctest_root.child_a.child_b": "in child_b module", 474 "acctest_child_a": "in child_a module", 475 "acctest_child_a.child_b": "in child_b module", 476 "acctest_child_b": "in child_b module", 477 } 478 gotTraces := map[string]string{} 479 config.DeepEach(func(c *configs.Config) { 480 path := strings.Join(c.Path, ".") 481 if c.Module.Variables["v"] == nil { 482 gotTraces[path] = "<missing>" 483 return 484 } 485 varDesc := c.Module.Variables["v"].Description 486 gotTraces[path] = varDesc 487 }) 488 assertResultDeepEqual(t, gotTraces, wantTraces) 489 490 } 491 492 type testInstallHooks struct { 493 Calls []testInstallHookCall 494 } 495 496 type testInstallHookCall struct { 497 Name string 498 ModuleAddr string 499 PackageAddr string 500 Version *version.Version 501 LocalPath string 502 } 503 504 func (h *testInstallHooks) Download(moduleAddr, packageAddr string, version *version.Version) { 505 h.Calls = append(h.Calls, testInstallHookCall{ 506 Name: "Download", 507 ModuleAddr: moduleAddr, 508 PackageAddr: packageAddr, 509 Version: version, 510 }) 511 } 512 513 func (h *testInstallHooks) Install(moduleAddr string, version *version.Version, localPath string) { 514 h.Calls = append(h.Calls, testInstallHookCall{ 515 Name: "Install", 516 ModuleAddr: moduleAddr, 517 Version: version, 518 LocalPath: localPath, 519 }) 520 } 521 522 // tempChdir copies the contents of the given directory to a temporary 523 // directory and changes the test process's current working directory to 524 // point to that directory. Also returned is a function that should be 525 // called at the end of the test (e.g. via "defer") to restore the previous 526 // working directory. 527 // 528 // Tests using this helper cannot safely be run in parallel with other tests. 529 func tempChdir(t *testing.T, sourceDir string) (string, func()) { 530 t.Helper() 531 532 tmpDir, err := ioutil.TempDir("", "terraform-configload") 533 if err != nil { 534 t.Fatalf("failed to create temporary directory: %s", err) 535 return "", nil 536 } 537 538 if err := copyDir(tmpDir, sourceDir); err != nil { 539 t.Fatalf("failed to copy fixture to temporary directory: %s", err) 540 return "", nil 541 } 542 543 oldDir, err := os.Getwd() 544 if err != nil { 545 t.Fatalf("failed to determine current working directory: %s", err) 546 return "", nil 547 } 548 549 err = os.Chdir(tmpDir) 550 if err != nil { 551 t.Fatalf("failed to switch to temp dir %s: %s", tmpDir, err) 552 return "", nil 553 } 554 555 // Most of the tests need this, so we'll make it just in case. 556 os.MkdirAll(filepath.Join(tmpDir, ".terraform/modules"), os.ModePerm) 557 558 t.Logf("tempChdir switched to %s after copying from %s", tmpDir, sourceDir) 559 560 return tmpDir, func() { 561 err := os.Chdir(oldDir) 562 if err != nil { 563 panic(fmt.Errorf("failed to restore previous working directory %s: %s", oldDir, err)) 564 } 565 566 if os.Getenv("TF_CONFIGLOAD_TEST_KEEP_TMP") == "" { 567 os.RemoveAll(tmpDir) 568 } 569 } 570 } 571 572 func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) bool { 573 t.Helper() 574 return assertDiagnosticCount(t, diags, 0) 575 } 576 577 func assertDiagnosticCount(t *testing.T, diags tfdiags.Diagnostics, want int) bool { 578 t.Helper() 579 if len(diags) != 0 { 580 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), want) 581 for _, diag := range diags { 582 t.Logf("- %s", diag) 583 } 584 return true 585 } 586 return false 587 } 588 589 func assertDiagnosticSummary(t *testing.T, diags tfdiags.Diagnostics, want string) bool { 590 t.Helper() 591 592 for _, diag := range diags { 593 if diag.Description().Summary == want { 594 return false 595 } 596 } 597 598 t.Errorf("missing diagnostic summary %q", want) 599 for _, diag := range diags { 600 t.Logf("- %s", diag) 601 } 602 return true 603 } 604 605 func assertResultDeepEqual(t *testing.T, got, want interface{}) bool { 606 t.Helper() 607 if diff := deep.Equal(got, want); diff != nil { 608 for _, problem := range diff { 609 t.Errorf("%s", problem) 610 } 611 return true 612 } 613 return false 614 }