github.com/opentofu/opentofu@v1.7.1/internal/initwd/module_install_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package initwd 7 8 import ( 9 "bytes" 10 "context" 11 "flag" 12 "fmt" 13 "os" 14 "path/filepath" 15 "strings" 16 "testing" 17 18 "github.com/davecgh/go-spew/spew" 19 "github.com/go-test/deep" 20 "github.com/google/go-cmp/cmp" 21 version "github.com/hashicorp/go-version" 22 svchost "github.com/hashicorp/terraform-svchost" 23 24 "github.com/opentofu/opentofu/internal/addrs" 25 "github.com/opentofu/opentofu/internal/configs" 26 "github.com/opentofu/opentofu/internal/configs/configload" 27 "github.com/opentofu/opentofu/internal/copy" 28 "github.com/opentofu/opentofu/internal/registry" 29 "github.com/opentofu/opentofu/internal/tfdiags" 30 31 _ "github.com/opentofu/opentofu/internal/logging" 32 ) 33 34 func TestMain(m *testing.M) { 35 flag.Parse() 36 os.Exit(m.Run()) 37 } 38 39 func TestModuleInstaller(t *testing.T) { 40 fixtureDir := filepath.Clean("testdata/local-modules") 41 dir, done := tempChdir(t, fixtureDir) 42 defer done() 43 44 hooks := &testInstallHooks{} 45 46 modulesDir := filepath.Join(dir, ".terraform/modules") 47 loader, close := configload.NewLoaderForTests(t) 48 defer close() 49 inst := NewModuleInstaller(modulesDir, loader, nil) 50 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 51 assertNoDiagnostics(t, diags) 52 53 wantCalls := []testInstallHookCall{ 54 { 55 Name: "Install", 56 ModuleAddr: "child_a", 57 PackageAddr: "", 58 LocalPath: "child_a", 59 }, 60 { 61 Name: "Install", 62 ModuleAddr: "child_a.child_b", 63 PackageAddr: "", 64 LocalPath: "child_a/child_b", 65 }, 66 } 67 68 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 69 return 70 } 71 72 loader, err := configload.NewLoader(&configload.Config{ 73 ModulesDir: modulesDir, 74 }) 75 if err != nil { 76 t.Fatal(err) 77 } 78 79 // Make sure the configuration is loadable now. 80 // (This ensures that correct information is recorded in the manifest.) 81 config, loadDiags := loader.LoadConfig(".") 82 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 83 84 wantTraces := map[string]string{ 85 "": "in root module", 86 "child_a": "in child_a module", 87 "child_a.child_b": "in child_b module", 88 } 89 gotTraces := map[string]string{} 90 config.DeepEach(func(c *configs.Config) { 91 path := strings.Join(c.Path, ".") 92 if c.Module.Variables["v"] == nil { 93 gotTraces[path] = "<missing>" 94 return 95 } 96 varDesc := c.Module.Variables["v"].Description 97 gotTraces[path] = varDesc 98 }) 99 assertResultDeepEqual(t, gotTraces, wantTraces) 100 } 101 102 func TestModuleInstaller_error(t *testing.T) { 103 fixtureDir := filepath.Clean("testdata/local-module-error") 104 dir, done := tempChdir(t, fixtureDir) 105 defer done() 106 107 hooks := &testInstallHooks{} 108 109 modulesDir := filepath.Join(dir, ".terraform/modules") 110 111 loader, close := configload.NewLoaderForTests(t) 112 defer close() 113 inst := NewModuleInstaller(modulesDir, loader, nil) 114 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 115 116 if !diags.HasErrors() { 117 t.Fatal("expected error") 118 } else { 119 assertDiagnosticSummary(t, diags, "Invalid module source address") 120 } 121 } 122 123 func TestModuleInstaller_emptyModuleName(t *testing.T) { 124 fixtureDir := filepath.Clean("testdata/empty-module-name") 125 dir, done := tempChdir(t, fixtureDir) 126 defer done() 127 128 hooks := &testInstallHooks{} 129 130 modulesDir := filepath.Join(dir, ".terraform/modules") 131 132 loader, close := configload.NewLoaderForTests(t) 133 defer close() 134 inst := NewModuleInstaller(modulesDir, loader, nil) 135 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 136 137 if !diags.HasErrors() { 138 t.Fatal("expected error") 139 } else { 140 assertDiagnosticSummary(t, diags, "Invalid module instance name") 141 } 142 } 143 144 func TestModuleInstaller_invalidModuleName(t *testing.T) { 145 fixtureDir := filepath.Clean("testdata/invalid-module-name") 146 dir, done := tempChdir(t, fixtureDir) 147 defer done() 148 149 hooks := &testInstallHooks{} 150 151 modulesDir := filepath.Join(dir, ".terraform/modules") 152 153 loader, close := configload.NewLoaderForTests(t) 154 defer close() 155 inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) 156 _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) 157 if !diags.HasErrors() { 158 t.Fatal("expected error") 159 } else { 160 assertDiagnosticSummary(t, diags, "Invalid module instance name") 161 } 162 } 163 164 func TestModuleInstaller_packageEscapeError(t *testing.T) { 165 fixtureDir := filepath.Clean("testdata/load-module-package-escape") 166 dir, done := tempChdir(t, fixtureDir) 167 defer done() 168 169 // For this particular test we need an absolute path in the root module 170 // that must actually resolve to our temporary directory in "dir", so 171 // we need to do a little rewriting. We replace the arbitrary placeholder 172 // %%BASE%% with the temporary directory path. 173 { 174 rootFilename := filepath.Join(dir, "package-escape.tf") 175 template, err := os.ReadFile(rootFilename) 176 if err != nil { 177 t.Fatal(err) 178 } 179 final := bytes.ReplaceAll(template, []byte("%%BASE%%"), []byte(filepath.ToSlash(dir))) 180 err = os.WriteFile(rootFilename, final, 0644) 181 if err != nil { 182 t.Fatal(err) 183 } 184 } 185 186 hooks := &testInstallHooks{} 187 188 modulesDir := filepath.Join(dir, ".terraform/modules") 189 190 loader, close := configload.NewLoaderForTests(t) 191 defer close() 192 inst := NewModuleInstaller(modulesDir, loader, nil) 193 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 194 195 if !diags.HasErrors() { 196 t.Fatal("expected error") 197 } else { 198 assertDiagnosticSummary(t, diags, "Local module path escapes module package") 199 } 200 } 201 202 func TestModuleInstaller_explicitPackageBoundary(t *testing.T) { 203 fixtureDir := filepath.Clean("testdata/load-module-package-prefix") 204 dir, done := tempChdir(t, fixtureDir) 205 defer done() 206 207 // For this particular test we need an absolute path in the root module 208 // that must actually resolve to our temporary directory in "dir", so 209 // we need to do a little rewriting. We replace the arbitrary placeholder 210 // %%BASE%% with the temporary directory path. 211 { 212 rootFilename := filepath.Join(dir, "package-prefix.tf") 213 template, err := os.ReadFile(rootFilename) 214 if err != nil { 215 t.Fatal(err) 216 } 217 final := bytes.ReplaceAll(template, []byte("%%BASE%%"), []byte(filepath.ToSlash(dir))) 218 err = os.WriteFile(rootFilename, final, 0644) 219 if err != nil { 220 t.Fatal(err) 221 } 222 } 223 224 hooks := &testInstallHooks{} 225 226 modulesDir := filepath.Join(dir, ".terraform/modules") 227 228 loader, close := configload.NewLoaderForTests(t) 229 defer close() 230 inst := NewModuleInstaller(modulesDir, loader, nil) 231 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 232 233 if diags.HasErrors() { 234 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 235 } 236 } 237 238 func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) { 239 if os.Getenv("TF_ACC") == "" { 240 t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it") 241 } 242 243 fixtureDir := filepath.Clean("testdata/prerelease-version-constraint-match") 244 dir, done := tempChdir(t, fixtureDir) 245 defer done() 246 247 hooks := &testInstallHooks{} 248 249 modulesDir := filepath.Join(dir, ".terraform/modules") 250 251 loader, close := configload.NewLoaderForTests(t) 252 defer close() 253 inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) 254 cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 255 256 if diags.HasErrors() { 257 t.Fatalf("found unexpected errors: %s", diags.Err()) 258 } 259 260 if !cfg.Children["acctest_exact"].Version.Equal(version.Must(version.NewVersion("v0.0.3-alpha.1"))) { 261 t.Fatalf("expected version %s but found version %s", "v0.0.3-alpha.1", cfg.Version.String()) 262 } 263 } 264 265 func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) { 266 if os.Getenv("TF_ACC") == "" { 267 t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it") 268 } 269 270 fixtureDir := filepath.Clean("testdata/prerelease-version-constraint") 271 dir, done := tempChdir(t, fixtureDir) 272 defer done() 273 274 hooks := &testInstallHooks{} 275 276 modulesDir := filepath.Join(dir, ".terraform/modules") 277 278 loader, close := configload.NewLoaderForTests(t) 279 defer close() 280 inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) 281 cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 282 283 if diags.HasErrors() { 284 t.Fatalf("found unexpected errors: %s", diags.Err()) 285 } 286 287 if !cfg.Children["acctest_partial"].Version.Equal(version.Must(version.NewVersion("v0.0.2"))) { 288 t.Fatalf("expected version %s but found version %s", "v0.0.2", cfg.Version.String()) 289 } 290 } 291 292 func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) { 293 fixtureDir := filepath.Clean("testdata/invalid-version-constraint") 294 dir, done := tempChdir(t, fixtureDir) 295 defer done() 296 297 hooks := &testInstallHooks{} 298 299 modulesDir := filepath.Join(dir, ".terraform/modules") 300 301 loader, close := configload.NewLoaderForTests(t) 302 defer close() 303 inst := NewModuleInstaller(modulesDir, loader, nil) 304 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 305 306 if !diags.HasErrors() { 307 t.Fatal("expected error") 308 } else { 309 // We use the presence of the "version" argument as a heuristic for 310 // user intent to use a registry module, and so we intentionally catch 311 // this as an invalid registry module address rather than an invalid 312 // version constraint, so we can surface the specific address parsing 313 // error instead of a generic version constraint error. 314 assertDiagnosticSummary(t, diags, "Invalid registry module source address") 315 } 316 } 317 318 func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) { 319 fixtureDir := filepath.Clean("testdata/invalid-version-constraint") 320 dir, done := tempChdir(t, fixtureDir) 321 defer done() 322 323 hooks := &testInstallHooks{} 324 325 modulesDir := filepath.Join(dir, ".terraform/modules") 326 327 loader, close := configload.NewLoaderForTests(t) 328 defer close() 329 inst := NewModuleInstaller(modulesDir, loader, nil) 330 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 331 332 if !diags.HasErrors() { 333 t.Fatal("expected error") 334 } else { 335 // We use the presence of the "version" argument as a heuristic for 336 // user intent to use a registry module, and so we intentionally catch 337 // this as an invalid registry module address rather than an invalid 338 // version constraint, so we can surface the specific address parsing 339 // error instead of a generic version constraint error. 340 assertDiagnosticSummary(t, diags, "Invalid registry module source address") 341 } 342 } 343 344 func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) { 345 fixtureDir := filepath.Clean("testdata/invalid-version-constraint-local") 346 dir, done := tempChdir(t, fixtureDir) 347 defer done() 348 349 hooks := &testInstallHooks{} 350 351 modulesDir := filepath.Join(dir, ".terraform/modules") 352 353 loader, close := configload.NewLoaderForTests(t) 354 defer close() 355 inst := NewModuleInstaller(modulesDir, loader, nil) 356 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 357 358 if !diags.HasErrors() { 359 t.Fatal("expected error") 360 } else { 361 // We use the presence of the "version" argument as a heuristic for 362 // user intent to use a registry module, and so we intentionally catch 363 // this as an invalid registry module address rather than an invalid 364 // version constraint, so we can surface the specific address parsing 365 // error instead of a generic version constraint error. 366 assertDiagnosticSummary(t, diags, "Invalid registry module source address") 367 } 368 } 369 370 func TestModuleInstaller_symlink(t *testing.T) { 371 fixtureDir := filepath.Clean("testdata/local-module-symlink") 372 dir, done := tempChdir(t, fixtureDir) 373 defer done() 374 375 hooks := &testInstallHooks{} 376 377 modulesDir := filepath.Join(dir, ".terraform/modules") 378 379 loader, close := configload.NewLoaderForTests(t) 380 defer close() 381 inst := NewModuleInstaller(modulesDir, loader, nil) 382 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 383 assertNoDiagnostics(t, diags) 384 385 wantCalls := []testInstallHookCall{ 386 { 387 Name: "Install", 388 ModuleAddr: "child_a", 389 PackageAddr: "", 390 LocalPath: "child_a", 391 }, 392 { 393 Name: "Install", 394 ModuleAddr: "child_a.child_b", 395 PackageAddr: "", 396 LocalPath: "child_a/child_b", 397 }, 398 } 399 400 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 401 return 402 } 403 404 loader, err := configload.NewLoader(&configload.Config{ 405 ModulesDir: modulesDir, 406 }) 407 if err != nil { 408 t.Fatal(err) 409 } 410 411 // Make sure the configuration is loadable now. 412 // (This ensures that correct information is recorded in the manifest.) 413 config, loadDiags := loader.LoadConfig(".") 414 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 415 416 wantTraces := map[string]string{ 417 "": "in root module", 418 "child_a": "in child_a module", 419 "child_a.child_b": "in child_b module", 420 } 421 gotTraces := map[string]string{} 422 config.DeepEach(func(c *configs.Config) { 423 path := strings.Join(c.Path, ".") 424 if c.Module.Variables["v"] == nil { 425 gotTraces[path] = "<missing>" 426 return 427 } 428 varDesc := c.Module.Variables["v"].Description 429 gotTraces[path] = varDesc 430 }) 431 assertResultDeepEqual(t, gotTraces, wantTraces) 432 } 433 434 func TestLoaderInstallModules_registry(t *testing.T) { 435 if os.Getenv("TF_ACC") == "" { 436 t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it") 437 } 438 439 fixtureDir := filepath.Clean("testdata/registry-modules") 440 tmpDir, done := tempChdir(t, fixtureDir) 441 // the module installer runs filepath.EvalSymlinks() on the destination 442 // directory before copying files, and the resultant directory is what is 443 // returned by the install hooks. Without this, tests could fail on machines 444 // where the default temp dir was a symlink. 445 dir, err := filepath.EvalSymlinks(tmpDir) 446 if err != nil { 447 t.Error(err) 448 } 449 450 defer done() 451 452 hooks := &testInstallHooks{} 453 modulesDir := filepath.Join(dir, ".terraform/modules") 454 455 loader, close := configload.NewLoaderForTests(t) 456 defer close() 457 inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) 458 _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) 459 assertNoDiagnostics(t, diags) 460 461 v := version.Must(version.NewVersion("0.0.1")) 462 463 wantCalls := []testInstallHookCall{ 464 // the configuration builder visits each level of calls in lexicographical 465 // order by name, so the following list is kept in the same order. 466 467 // acctest_child_a accesses //modules/child_a directly 468 { 469 Name: "Download", 470 ModuleAddr: "acctest_child_a", 471 PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here 472 Version: v, 473 }, 474 { 475 Name: "Install", 476 ModuleAddr: "acctest_child_a", 477 Version: v, 478 // NOTE: This local path and the other paths derived from it below 479 // can vary depending on how the registry is implemented. At the 480 // time of writing this test, registry.opentofu.org returns 481 // git repository source addresses and so this path refers to the 482 // root of the git clone, but historically the registry referred 483 // to GitHub-provided tar archives which meant that there was an 484 // extra level of subdirectory here for the typical directory 485 // nesting in tar archives, which would've been reflected as 486 // an extra segment on this path. If this test fails due to an 487 // additional path segment in future, then a change to the upstream 488 // registry might be the root cause. 489 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"), 490 }, 491 492 // acctest_child_a.child_b 493 // (no download because it's a relative path inside acctest_child_a) 494 { 495 Name: "Install", 496 ModuleAddr: "acctest_child_a.child_b", 497 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"), 498 }, 499 500 // acctest_child_b accesses //modules/child_b directly 501 { 502 Name: "Download", 503 ModuleAddr: "acctest_child_b", 504 PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here 505 Version: v, 506 }, 507 { 508 Name: "Install", 509 ModuleAddr: "acctest_child_b", 510 Version: v, 511 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"), 512 }, 513 514 // acctest_root 515 { 516 Name: "Download", 517 ModuleAddr: "acctest_root", 518 PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", 519 Version: v, 520 }, 521 { 522 Name: "Install", 523 ModuleAddr: "acctest_root", 524 Version: v, 525 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root"), 526 }, 527 528 // acctest_root.child_a 529 // (no download because it's a relative path inside acctest_root) 530 { 531 Name: "Install", 532 ModuleAddr: "acctest_root.child_a", 533 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"), 534 }, 535 536 // acctest_root.child_a.child_b 537 // (no download because it's a relative path inside acctest_root, via acctest_root.child_a) 538 { 539 Name: "Install", 540 ModuleAddr: "acctest_root.child_a.child_b", 541 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"), 542 }, 543 } 544 545 if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { 546 t.Fatalf("wrong installer calls\n%s", diff) 547 } 548 549 //check that the registry reponses were cached 550 packageAddr := addrs.ModuleRegistryPackage{ 551 Host: svchost.Hostname("registry.opentofu.org"), 552 Namespace: "hashicorp", 553 Name: "module-installer-acctest", 554 TargetSystem: "aws", 555 } 556 if _, ok := inst.registryPackageVersions[packageAddr]; !ok { 557 t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions)) 558 } 559 if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok { 560 t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) 561 } 562 563 loader, err = configload.NewLoader(&configload.Config{ 564 ModulesDir: modulesDir, 565 }) 566 if err != nil { 567 t.Fatal(err) 568 } 569 570 // Make sure the configuration is loadable now. 571 // (This ensures that correct information is recorded in the manifest.) 572 config, loadDiags := loader.LoadConfig(".") 573 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 574 575 wantTraces := map[string]string{ 576 "": "in local caller for registry-modules", 577 "acctest_root": "in root module", 578 "acctest_root.child_a": "in child_a module", 579 "acctest_root.child_a.child_b": "in child_b module", 580 "acctest_child_a": "in child_a module", 581 "acctest_child_a.child_b": "in child_b module", 582 "acctest_child_b": "in child_b module", 583 } 584 gotTraces := map[string]string{} 585 config.DeepEach(func(c *configs.Config) { 586 path := strings.Join(c.Path, ".") 587 if c.Module.Variables["v"] == nil { 588 gotTraces[path] = "<missing>" 589 return 590 } 591 varDesc := c.Module.Variables["v"].Description 592 gotTraces[path] = varDesc 593 }) 594 assertResultDeepEqual(t, gotTraces, wantTraces) 595 596 } 597 598 func TestLoaderInstallModules_goGetter(t *testing.T) { 599 if os.Getenv("TF_ACC") == "" { 600 t.Skip("this test accesses github.com; set TF_ACC=1 to run it") 601 } 602 603 fixtureDir := filepath.Clean("testdata/go-getter-modules") 604 tmpDir, done := tempChdir(t, fixtureDir) 605 // the module installer runs filepath.EvalSymlinks() on the destination 606 // directory before copying files, and the resultant directory is what is 607 // returned by the install hooks. Without this, tests could fail on machines 608 // where the default temp dir was a symlink. 609 dir, err := filepath.EvalSymlinks(tmpDir) 610 if err != nil { 611 t.Error(err) 612 } 613 defer done() 614 615 hooks := &testInstallHooks{} 616 modulesDir := filepath.Join(dir, ".terraform/modules") 617 618 loader, close := configload.NewLoaderForTests(t) 619 defer close() 620 inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) 621 _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) 622 assertNoDiagnostics(t, diags) 623 624 wantCalls := []testInstallHookCall{ 625 // the configuration builder visits each level of calls in lexicographical 626 // order by name, so the following list is kept in the same order. 627 628 // acctest_child_a accesses //modules/child_a directly 629 { 630 Name: "Download", 631 ModuleAddr: "acctest_child_a", 632 PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1", // intentionally excludes the subdir because we're downloading the whole repo here 633 }, 634 { 635 Name: "Install", 636 ModuleAddr: "acctest_child_a", 637 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"), 638 }, 639 640 // acctest_child_a.child_b 641 // (no download because it's a relative path inside acctest_child_a) 642 { 643 Name: "Install", 644 ModuleAddr: "acctest_child_a.child_b", 645 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"), 646 }, 647 648 // acctest_child_b accesses //modules/child_b directly 649 { 650 Name: "Download", 651 ModuleAddr: "acctest_child_b", 652 PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1", // intentionally excludes the subdir because we're downloading the whole package here 653 }, 654 { 655 Name: "Install", 656 ModuleAddr: "acctest_child_b", 657 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"), 658 }, 659 660 // acctest_root 661 { 662 Name: "Download", 663 ModuleAddr: "acctest_root", 664 PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1", 665 }, 666 { 667 Name: "Install", 668 ModuleAddr: "acctest_root", 669 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root"), 670 }, 671 672 // acctest_root.child_a 673 // (no download because it's a relative path inside acctest_root) 674 { 675 Name: "Install", 676 ModuleAddr: "acctest_root.child_a", 677 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"), 678 }, 679 680 // acctest_root.child_a.child_b 681 // (no download because it's a relative path inside acctest_root, via acctest_root.child_a) 682 { 683 Name: "Install", 684 ModuleAddr: "acctest_root.child_a.child_b", 685 LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"), 686 }, 687 } 688 689 if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { 690 t.Fatalf("wrong installer calls\n%s", diff) 691 } 692 693 loader, err = configload.NewLoader(&configload.Config{ 694 ModulesDir: modulesDir, 695 }) 696 if err != nil { 697 t.Fatal(err) 698 } 699 700 // Make sure the configuration is loadable now. 701 // (This ensures that correct information is recorded in the manifest.) 702 config, loadDiags := loader.LoadConfig(".") 703 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 704 705 wantTraces := map[string]string{ 706 "": "in local caller for go-getter-modules", 707 "acctest_root": "in root module", 708 "acctest_root.child_a": "in child_a module", 709 "acctest_root.child_a.child_b": "in child_b module", 710 "acctest_child_a": "in child_a module", 711 "acctest_child_a.child_b": "in child_b module", 712 "acctest_child_b": "in child_b module", 713 } 714 gotTraces := map[string]string{} 715 config.DeepEach(func(c *configs.Config) { 716 path := strings.Join(c.Path, ".") 717 if c.Module.Variables["v"] == nil { 718 gotTraces[path] = "<missing>" 719 return 720 } 721 varDesc := c.Module.Variables["v"].Description 722 gotTraces[path] = varDesc 723 }) 724 assertResultDeepEqual(t, gotTraces, wantTraces) 725 726 } 727 728 func TestModuleInstaller_fromTests(t *testing.T) { 729 fixtureDir := filepath.Clean("testdata/local-module-from-test") 730 dir, done := tempChdir(t, fixtureDir) 731 defer done() 732 733 hooks := &testInstallHooks{} 734 735 modulesDir := filepath.Join(dir, ".terraform/modules") 736 loader, close := configload.NewLoaderForTests(t) 737 defer close() 738 inst := NewModuleInstaller(modulesDir, loader, nil) 739 _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) 740 assertNoDiagnostics(t, diags) 741 742 wantCalls := []testInstallHookCall{ 743 { 744 Name: "Install", 745 ModuleAddr: "test.tests.main.setup", 746 PackageAddr: "", 747 LocalPath: "setup", 748 }, 749 } 750 751 if assertResultDeepEqual(t, hooks.Calls, wantCalls) { 752 return 753 } 754 755 loader, err := configload.NewLoader(&configload.Config{ 756 ModulesDir: modulesDir, 757 }) 758 if err != nil { 759 t.Fatal(err) 760 } 761 762 // Make sure the configuration is loadable now. 763 // (This ensures that correct information is recorded in the manifest.) 764 config, loadDiags := loader.LoadConfigWithTests(".", "tests") 765 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 766 767 if config.Module.Tests["tests/main.tftest.hcl"].Runs[0].ConfigUnderTest == nil { 768 t.Fatalf("should have loaded config into the relevant run block but did not") 769 } 770 } 771 772 func TestLoadInstallModules_registryFromTest(t *testing.T) { 773 if os.Getenv("TF_ACC") == "" { 774 t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it") 775 } 776 777 fixtureDir := filepath.Clean("testdata/registry-module-from-test") 778 tmpDir, done := tempChdir(t, fixtureDir) 779 // the module installer runs filepath.EvalSymlinks() on the destination 780 // directory before copying files, and the resultant directory is what is 781 // returned by the install hooks. Without this, tests could fail on machines 782 // where the default temp dir was a symlink. 783 dir, err := filepath.EvalSymlinks(tmpDir) 784 if err != nil { 785 t.Error(err) 786 } 787 788 defer done() 789 790 hooks := &testInstallHooks{} 791 modulesDir := filepath.Join(dir, ".terraform/modules") 792 793 loader, close := configload.NewLoaderForTests(t) 794 defer close() 795 inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) 796 _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) 797 assertNoDiagnostics(t, diags) 798 799 v := version.Must(version.NewVersion("0.0.1")) 800 wantCalls := []testInstallHookCall{ 801 // the configuration builder visits each level of calls in lexicographical 802 // order by name, so the following list is kept in the same order. 803 804 // setup access acctest directly. 805 { 806 Name: "Download", 807 ModuleAddr: "test.main.setup", 808 PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here 809 Version: v, 810 }, 811 { 812 Name: "Install", 813 ModuleAddr: "test.main.setup", 814 Version: v, 815 // NOTE: This local path and the other paths derived from it below 816 // can vary depending on how the registry is implemented. At the 817 // time of writing this test, registry.opentofu.org returns 818 // git repository source addresses and so this path refers to the 819 // root of the git clone, but historically the registry referred 820 // to GitHub-provided tar archives which meant that there was an 821 // extra level of subdirectory here for the typical directory 822 // nesting in tar archives, which would've been reflected as 823 // an extra segment on this path. If this test fails due to an 824 // additional path segment in future, then a change to the upstream 825 // registry might be the root cause. 826 LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup"), 827 }, 828 829 // main.tftest.hcl.setup.child_a 830 // (no download because it's a relative path inside acctest_child_a) 831 { 832 Name: "Install", 833 ModuleAddr: "test.main.setup.child_a", 834 LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_a"), 835 }, 836 837 // main.tftest.hcl.setup.child_a.child_b 838 // (no download because it's a relative path inside main.tftest.hcl.setup.child_a) 839 { 840 Name: "Install", 841 ModuleAddr: "test.main.setup.child_a.child_b", 842 LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_b"), 843 }, 844 } 845 846 if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { 847 t.Fatalf("wrong installer calls\n%s", diff) 848 } 849 850 //check that the registry reponses were cached 851 packageAddr := addrs.ModuleRegistryPackage{ 852 Host: svchost.Hostname("registry.opentofu.org"), 853 Namespace: "hashicorp", 854 Name: "module-installer-acctest", 855 TargetSystem: "aws", 856 } 857 if _, ok := inst.registryPackageVersions[packageAddr]; !ok { 858 t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions)) 859 } 860 if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok { 861 t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) 862 } 863 864 loader, err = configload.NewLoader(&configload.Config{ 865 ModulesDir: modulesDir, 866 }) 867 if err != nil { 868 t.Fatal(err) 869 } 870 871 // Make sure the configuration is loadable now. 872 // (This ensures that correct information is recorded in the manifest.) 873 config, loadDiags := loader.LoadConfigWithTests(".", "tests") 874 assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) 875 876 if config.Module.Tests["main.tftest.hcl"].Runs[0].ConfigUnderTest == nil { 877 t.Fatalf("should have loaded config into the relevant run block but did not") 878 } 879 } 880 881 type testInstallHooks struct { 882 Calls []testInstallHookCall 883 } 884 885 type testInstallHookCall struct { 886 Name string 887 ModuleAddr string 888 PackageAddr string 889 Version *version.Version 890 LocalPath string 891 } 892 893 func (h *testInstallHooks) Download(moduleAddr, packageAddr string, version *version.Version) { 894 h.Calls = append(h.Calls, testInstallHookCall{ 895 Name: "Download", 896 ModuleAddr: moduleAddr, 897 PackageAddr: packageAddr, 898 Version: version, 899 }) 900 } 901 902 func (h *testInstallHooks) Install(moduleAddr string, version *version.Version, localPath string) { 903 h.Calls = append(h.Calls, testInstallHookCall{ 904 Name: "Install", 905 ModuleAddr: moduleAddr, 906 Version: version, 907 LocalPath: localPath, 908 }) 909 } 910 911 // tempChdir copies the contents of the given directory to a temporary 912 // directory and changes the test process's current working directory to 913 // point to that directory. Also returned is a function that should be 914 // called at the end of the test (e.g. via "defer") to restore the previous 915 // working directory. 916 // 917 // Tests using this helper cannot safely be run in parallel with other tests. 918 func tempChdir(t *testing.T, sourceDir string) (string, func()) { 919 t.Helper() 920 921 tmpDir, err := os.MkdirTemp("", "terraform-configload") 922 if err != nil { 923 t.Fatalf("failed to create temporary directory: %s", err) 924 return "", nil 925 } 926 927 if err := copy.CopyDir(tmpDir, sourceDir); err != nil { 928 t.Fatalf("failed to copy fixture to temporary directory: %s", err) 929 return "", nil 930 } 931 932 oldDir, err := os.Getwd() 933 if err != nil { 934 t.Fatalf("failed to determine current working directory: %s", err) 935 return "", nil 936 } 937 938 err = os.Chdir(tmpDir) 939 if err != nil { 940 t.Fatalf("failed to switch to temp dir %s: %s", tmpDir, err) 941 return "", nil 942 } 943 944 // Most of the tests need this, so we'll make it just in case. 945 os.MkdirAll(filepath.Join(tmpDir, ".terraform/modules"), os.ModePerm) 946 947 t.Logf("tempChdir switched to %s after copying from %s", tmpDir, sourceDir) 948 949 return tmpDir, func() { 950 err := os.Chdir(oldDir) 951 if err != nil { 952 panic(fmt.Errorf("failed to restore previous working directory %s: %w", oldDir, err)) 953 } 954 955 if os.Getenv("TF_CONFIGLOAD_TEST_KEEP_TMP") == "" { 956 os.RemoveAll(tmpDir) 957 } 958 } 959 } 960 961 func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) bool { 962 t.Helper() 963 return assertDiagnosticCount(t, diags, 0) 964 } 965 966 func assertDiagnosticCount(t *testing.T, diags tfdiags.Diagnostics, want int) bool { 967 t.Helper() 968 if len(diags) != want { 969 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), want) 970 for _, diag := range diags { 971 t.Logf("- %#v", diag) 972 } 973 return true 974 } 975 return false 976 } 977 978 func assertDiagnosticSummary(t *testing.T, diags tfdiags.Diagnostics, want string) bool { 979 t.Helper() 980 981 for _, diag := range diags { 982 if diag.Description().Summary == want { 983 return false 984 } 985 } 986 987 t.Errorf("missing diagnostic summary %q", want) 988 for _, diag := range diags { 989 t.Logf("- %#v", diag) 990 } 991 return true 992 } 993 994 func assertResultDeepEqual(t *testing.T, got, want interface{}) bool { 995 t.Helper() 996 if diff := deep.Equal(got, want); diff != nil { 997 for _, problem := range diff { 998 t.Errorf("%s", problem) 999 } 1000 return true 1001 } 1002 return false 1003 }