| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package initwd | |
| import ( | |
| "bytes" | |
| "context" | |
| "flag" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "testing" | |
| "github.com/davecgh/go-spew/spew" | |
| "github.com/go-test/deep" | |
| "github.com/google/go-cmp/cmp" | |
| version "github.com/hashicorp/go-version" | |
| svchost "github.com/hashicorp/terraform-svchost" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/configs/configload" | |
| "github.com/hashicorp/terraform/internal/copy" | |
| "github.com/hashicorp/terraform/internal/registry" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| _ "github.com/hashicorp/terraform/internal/logging" | |
| ) | |
| func TestMain(m *testing.M) { | |
| flag.Parse() | |
| os.Exit(m.Run()) | |
| } | |
| func testModuleInstallerInitializer(loader *configload.Loader) Initializer { | |
| return func(rootMod *configs.Module, walker configs.ModuleWalker) (*configs.Config, tfdiags.Diagnostics) { | |
| return terraform.BuildConfigWithGraph(rootMod, walker, nil, configs.MockDataLoaderFunc(loader.LoadExternalMockData)) | |
| } | |
| } | |
| func TestModuleInstaller(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/local-modules") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| tfdiags.AssertNoDiagnostics(t, diags) | |
| wantCalls := []testInstallHookCall{ | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "child_a", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "child_a", | |
| PackageAddr: "", | |
| LocalPath: "child_a", | |
| }, | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "child_a.child_b", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "child_a.child_b", | |
| PackageAddr: "", | |
| LocalPath: "child_a/child_b", | |
| }, | |
| } | |
| if assertResultDeepEqual(t, hooks.Calls, wantCalls) { | |
| return | |
| } | |
| loader, err := configload.NewLoader(&configload.Config{ | |
| ModulesDir: modulesDir, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Make sure the configuration is loadable now. | |
| // (This ensures that correct information is recorded in the manifest.) | |
| rootMod, hclDiags := loader.LoadRootModule(".") | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(hclDiags)) | |
| config, loadDiags := terraform.BuildConfigWithGraph( | |
| rootMod, | |
| loader.ModuleWalker(), | |
| nil, | |
| configs.MockDataLoaderFunc(loader.LoadExternalMockData), | |
| ) | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) | |
| wantTraces := map[string]string{ | |
| "": "in root module", | |
| "child_a": "in child_a module", | |
| "child_a.child_b": "in child_b module", | |
| } | |
| gotTraces := map[string]string{} | |
| config.DeepEach(func(c *configs.Config) { | |
| path := strings.Join(c.Path, ".") | |
| if c.Module.Variables["v"] == nil { | |
| gotTraces[path] = "<missing>" | |
| return | |
| } | |
| varDesc := c.Module.Variables["v"].Description | |
| gotTraces[path] = varDesc | |
| }) | |
| assertResultDeepEqual(t, gotTraces, wantTraces) | |
| } | |
| func TestModuleInstaller_error(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/local-module-error") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| assertDiagnosticSummary(t, diags, "Invalid module source address") | |
| } | |
| } | |
| func TestModuleInstaller_emptyModuleName(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/empty-module-name") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| assertDiagnosticSummary(t, diags, "Invalid module instance name") | |
| } | |
| } | |
| func TestModuleInstaller_invalidModuleName(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/invalid-module-name") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| assertDiagnosticSummary(t, diags, "Invalid module instance name") | |
| } | |
| } | |
| func TestModuleInstaller_packageEscapeError(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/load-module-package-escape") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| // For this particular test we need an absolute path in the root module | |
| // that must actually resolve to our temporary directory in "dir", so | |
| // we need to do a little rewriting. We replace the arbitrary placeholder | |
| // %%BASE%% with the temporary directory path. | |
| { | |
| rootFilename := filepath.Join(dir, "package-escape.tf") | |
| template, err := ioutil.ReadFile(rootFilename) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| final := bytes.ReplaceAll(template, []byte("%%BASE%%"), []byte(filepath.ToSlash(dir))) | |
| err = ioutil.WriteFile(rootFilename, final, 0644) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| assertDiagnosticSummary(t, diags, "Local module path escapes module package") | |
| } | |
| } | |
| func TestModuleInstaller_explicitPackageBoundary(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/load-module-package-prefix") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| // For this particular test we need an absolute path in the root module | |
| // that must actually resolve to our temporary directory in "dir", so | |
| // we need to do a little rewriting. We replace the arbitrary placeholder | |
| // %%BASE%% with the temporary directory path. | |
| { | |
| rootFilename := filepath.Join(dir, "package-prefix.tf") | |
| template, err := ioutil.ReadFile(rootFilename) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| final := bytes.ReplaceAll(template, []byte("%%BASE%%"), []byte(filepath.ToSlash(dir))) | |
| err = ioutil.WriteFile(rootFilename, final, 0644) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected errors\n%s", diags.Err().Error()) | |
| } | |
| } | |
| func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) { | |
| if os.Getenv("TF_ACC") == "" { | |
| t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") | |
| } | |
| fixtureDir := filepath.Clean("testdata/prerelease-version-constraint-match") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if diags.HasErrors() { | |
| t.Fatalf("found unexpected errors: %s", diags.Err()) | |
| } | |
| if !cfg.Children["acctest_exact"].Version.Equal(version.Must(version.NewVersion("v0.0.3-alpha.1"))) { | |
| t.Fatalf("expected version %s but found version %s", "v0.0.3-alpha.1", cfg.Version.String()) | |
| } | |
| } | |
| func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) { | |
| if os.Getenv("TF_ACC") == "" { | |
| t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") | |
| } | |
| fixtureDir := filepath.Clean("testdata/prerelease-version-constraint") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, hooks) | |
| if diags.HasErrors() { | |
| t.Fatalf("found unexpected errors: %s", diags.Err()) | |
| } | |
| if !cfg.Children["acctest_partial"].Version.Equal(version.Must(version.NewVersion("v0.0.2"))) { | |
| t.Fatalf("expected version %s but found version %s", "v0.0.2", cfg.Version.String()) | |
| } | |
| } | |
| func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/invalid-version-constraint") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| // We use the presence of the "version" argument as a heuristic for | |
| // user intent to use a registry module, and so we intentionally catch | |
| // this as an invalid registry module address rather than an invalid | |
| // version constraint, so we can surface the specific address parsing | |
| // error instead of a generic version constraint error. | |
| assertDiagnosticSummary(t, diags, "Invalid registry module source address") | |
| } | |
| } | |
| func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/invalid-version-constraint") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| // We use the presence of the "version" argument as a heuristic for | |
| // user intent to use a registry module, and so we intentionally catch | |
| // this as an invalid registry module address rather than an invalid | |
| // version constraint, so we can surface the specific address parsing | |
| // error instead of a generic version constraint error. | |
| assertDiagnosticSummary(t, diags, "Invalid registry module source address") | |
| } | |
| } | |
| func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/invalid-version-constraint-local") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| // We use the presence of the "version" argument as a heuristic for | |
| // user intent to use a registry module, and so we intentionally catch | |
| // this as an invalid registry module address rather than an invalid | |
| // version constraint, so we can surface the specific address parsing | |
| // error instead of a generic version constraint error. | |
| assertDiagnosticSummary(t, diags, "Invalid registry module source address") | |
| } | |
| } | |
| func TestModuleInstaller_symlink(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/local-module-symlink") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| tfdiags.AssertNoDiagnostics(t, diags) | |
| wantCalls := []testInstallHookCall{ | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "child_a", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "child_a", | |
| PackageAddr: "", | |
| LocalPath: "child_a", | |
| }, | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "child_a.child_b", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "child_a.child_b", | |
| PackageAddr: "", | |
| LocalPath: "child_a/child_b", | |
| }, | |
| } | |
| if assertResultDeepEqual(t, hooks.Calls, wantCalls) { | |
| return | |
| } | |
| loader, err := configload.NewLoader(&configload.Config{ | |
| ModulesDir: modulesDir, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Make sure the configuration is loadable now. | |
| // (This ensures that correct information is recorded in the manifest.) | |
| rootMod, hclDiags := loader.LoadRootModule(".") | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(hclDiags)) | |
| config, loadDiags := terraform.BuildConfigWithGraph( | |
| rootMod, | |
| loader.ModuleWalker(), | |
| nil, | |
| configs.MockDataLoaderFunc(loader.LoadExternalMockData), | |
| ) | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) | |
| wantTraces := map[string]string{ | |
| "": "in root module", | |
| "child_a": "in child_a module", | |
| "child_a.child_b": "in child_b module", | |
| } | |
| gotTraces := map[string]string{} | |
| config.DeepEach(func(c *configs.Config) { | |
| path := strings.Join(c.Path, ".") | |
| if c.Module.Variables["v"] == nil { | |
| gotTraces[path] = "<missing>" | |
| return | |
| } | |
| varDesc := c.Module.Variables["v"].Description | |
| gotTraces[path] = varDesc | |
| }) | |
| assertResultDeepEqual(t, gotTraces, wantTraces) | |
| } | |
| func TestLoaderInstallModules_invalidRegistry(t *testing.T) { | |
| if os.Getenv("TF_ACC") == "" { | |
| t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") | |
| } | |
| fixtureDir := filepath.Clean("testdata/invalid-registry-modules") | |
| tmpDir, done := tempChdir(t, fixtureDir) | |
| // the module installer runs filepath.EvalSymlinks() on the destination | |
| // directory before copying files, and the resultant directory is what is | |
| // returned by the install hooks. Without this, tests could fail on machines | |
| // where the default temp dir was a symlink. | |
| dir, err := filepath.EvalSymlinks(tmpDir) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error") | |
| } else { | |
| tfdiags.AssertDiagnosticCount(t, diags, 1) | |
| assertDiagnosticSummary(t, diags, "Unreadable module subdirectory") | |
| // the diagnostic should specifically call out the submodule that failed | |
| if !strings.Contains(diags[0].Description().Detail, "target submodule modules/child_c") { | |
| t.Errorf("unmatched error detail") | |
| } | |
| } | |
| } | |
| func TestLoaderInstallModules_registry(t *testing.T) { | |
| if os.Getenv("TF_ACC") == "" { | |
| t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") | |
| } | |
| fixtureDir := filepath.Clean("testdata/registry-modules") | |
| tmpDir, done := tempChdir(t, fixtureDir) | |
| // the module installer runs filepath.EvalSymlinks() on the destination | |
| // directory before copying files, and the resultant directory is what is | |
| // returned by the install hooks. Without this, tests could fail on machines | |
| // where the default temp dir was a symlink. | |
| dir, err := filepath.EvalSymlinks(tmpDir) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) | |
| tfdiags.AssertNoDiagnostics(t, diags) | |
| v := version.Must(version.NewVersion("0.0.1")) | |
| wantCalls := []testInstallHookCall{ | |
| // the configuration builder visits each level of calls in lexicographical | |
| // order by name, so the following list is kept in the same order. | |
| // acctest_child_a accesses //modules/child_a directly | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_child_a", | |
| PackageAddr: "", | |
| Version: v, | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "acctest_child_a", | |
| PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here | |
| Version: v, | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_child_a", | |
| Version: v, | |
| // NOTE: This local path and the other paths derived from it below | |
| // can vary depending on how the registry is implemented. At the | |
| // time of writing this test, registry.terraform.io returns | |
| // git repository source addresses and so this path refers to the | |
| // root of the git clone, but historically the registry referred | |
| // to GitHub-provided tar archives which meant that there was an | |
| // extra level of subdirectory here for the typical directory | |
| // nesting in tar archives, which would've been reflected as | |
| // an extra segment on this path. If this test fails due to an | |
| // additional path segment in future, then a change to the upstream | |
| // registry might be the root cause. | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"), | |
| }, | |
| // acctest_child_a.child_b | |
| // (no download because it's a relative path inside acctest_child_a) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_child_a.child_b", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_child_a.child_b", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"), | |
| }, | |
| // acctest_child_b accesses //modules/child_b directly | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_child_b", | |
| PackageAddr: "", | |
| Version: v, | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "acctest_child_b", | |
| PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here | |
| Version: v, | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_child_b", | |
| Version: v, | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"), | |
| }, | |
| // acctest_root | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_root", | |
| PackageAddr: "", | |
| Version: v, | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "acctest_root", | |
| PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws", | |
| Version: v, | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_root", | |
| Version: v, | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root"), | |
| }, | |
| // acctest_root.child_a | |
| // (no download because it's a relative path inside acctest_root) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_root.child_a", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_root.child_a", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"), | |
| }, | |
| // acctest_root.child_a.child_b | |
| // (no download because it's a relative path inside acctest_root, via acctest_root.child_a) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_root.child_a.child_b", | |
| PackageAddr: "", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_root.child_a.child_b", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"), | |
| }, | |
| } | |
| if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { | |
| t.Fatalf("wrong installer calls\n%s", diff) | |
| } | |
| //check that the registry reponses were cached | |
| packageAddr := addrs.ModuleRegistryPackage{ | |
| Host: svchost.Hostname("registry.terraform.io"), | |
| Namespace: "hashicorp", | |
| Name: "module-installer-acctest", | |
| TargetSystem: "aws", | |
| } | |
| if _, ok := inst.registryPackageVersions[packageAddr]; !ok { | |
| t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions)) | |
| } | |
| if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok { | |
| t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) | |
| } | |
| loader, err = configload.NewLoader(&configload.Config{ | |
| ModulesDir: modulesDir, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Make sure the configuration is loadable now. | |
| // (This ensures that correct information is recorded in the manifest.) | |
| rootMod, hclDiags := loader.LoadRootModule(".") | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(hclDiags)) | |
| config, loadDiags := terraform.BuildConfigWithGraph( | |
| rootMod, | |
| loader.ModuleWalker(), | |
| nil, | |
| configs.MockDataLoaderFunc(loader.LoadExternalMockData), | |
| ) | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) | |
| wantTraces := map[string]string{ | |
| "": "in local caller for registry-modules", | |
| "acctest_root": "in root module", | |
| "acctest_root.child_a": "in child_a module", | |
| "acctest_root.child_a.child_b": "in child_b module", | |
| "acctest_child_a": "in child_a module", | |
| "acctest_child_a.child_b": "in child_b module", | |
| "acctest_child_b": "in child_b module", | |
| } | |
| gotTraces := map[string]string{} | |
| config.DeepEach(func(c *configs.Config) { | |
| path := strings.Join(c.Path, ".") | |
| if c.Module.Variables["v"] == nil { | |
| gotTraces[path] = "<missing>" | |
| return | |
| } | |
| varDesc := c.Module.Variables["v"].Description | |
| gotTraces[path] = varDesc | |
| }) | |
| assertResultDeepEqual(t, gotTraces, wantTraces) | |
| } | |
| func TestLoaderInstallModules_goGetter(t *testing.T) { | |
| if os.Getenv("TF_ACC") == "" { | |
| t.Skip("this test accesses github.com; set TF_ACC=1 to run it") | |
| } | |
| fixtureDir := filepath.Clean("testdata/go-getter-modules") | |
| tmpDir, done := tempChdir(t, fixtureDir) | |
| // the module installer runs filepath.EvalSymlinks() on the destination | |
| // directory before copying files, and the resultant directory is what is | |
| // returned by the install hooks. Without this, tests could fail on machines | |
| // where the default temp dir was a symlink. | |
| dir, err := filepath.EvalSymlinks(tmpDir) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) | |
| tfdiags.AssertNoDiagnostics(t, diags) | |
| wantCalls := []testInstallHookCall{ | |
| // the configuration builder visits each level of calls in lexicographical | |
| // order by name, so the following list is kept in the same order. | |
| // acctest_child_a accesses //modules/child_a directly | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_child_a", | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "acctest_child_a", | |
| 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 | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_child_a", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"), | |
| }, | |
| // acctest_child_a.child_b | |
| // (no download because it's a relative path inside acctest_child_a) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_child_a.child_b", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_child_a.child_b", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"), | |
| }, | |
| // acctest_child_b accesses //modules/child_b directly | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_child_b", | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "acctest_child_b", | |
| 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 | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_child_b", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"), | |
| }, | |
| // acctest_root | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_root", | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "acctest_root", | |
| PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_root", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root"), | |
| }, | |
| // acctest_root.child_a | |
| // (no download because it's a relative path inside acctest_root) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_root.child_a", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_root.child_a", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"), | |
| }, | |
| // acctest_root.child_a.child_b | |
| // (no download because it's a relative path inside acctest_root, via acctest_root.child_a) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "acctest_root.child_a.child_b", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "acctest_root.child_a.child_b", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"), | |
| }, | |
| } | |
| if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { | |
| t.Fatalf("wrong installer calls\n%s", diff) | |
| } | |
| loader, err = configload.NewLoader(&configload.Config{ | |
| ModulesDir: modulesDir, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Make sure the configuration is loadable now. | |
| // (This ensures that correct information is recorded in the manifest.) | |
| rootMod, hclDiags := loader.LoadRootModule(".") | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(hclDiags)) | |
| config, loadDiags := terraform.BuildConfigWithGraph( | |
| rootMod, | |
| loader.ModuleWalker(), | |
| nil, | |
| configs.MockDataLoaderFunc(loader.LoadExternalMockData), | |
| ) | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) | |
| wantTraces := map[string]string{ | |
| "": "in local caller for go-getter-modules", | |
| "acctest_root": "in root module", | |
| "acctest_root.child_a": "in child_a module", | |
| "acctest_root.child_a.child_b": "in child_b module", | |
| "acctest_child_a": "in child_a module", | |
| "acctest_child_a.child_b": "in child_b module", | |
| "acctest_child_b": "in child_b module", | |
| } | |
| gotTraces := map[string]string{} | |
| config.DeepEach(func(c *configs.Config) { | |
| path := strings.Join(c.Path, ".") | |
| if c.Module.Variables["v"] == nil { | |
| gotTraces[path] = "<missing>" | |
| return | |
| } | |
| varDesc := c.Module.Variables["v"].Description | |
| gotTraces[path] = varDesc | |
| }) | |
| assertResultDeepEqual(t, gotTraces, wantTraces) | |
| } | |
| func TestModuleInstaller_fromTests(t *testing.T) { | |
| fixtureDir := filepath.Clean("testdata/local-module-from-test") | |
| dir, done := tempChdir(t, fixtureDir) | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, nil, testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) | |
| tfdiags.AssertNoDiagnostics(t, diags) | |
| wantCalls := []testInstallHookCall{ | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.tests.main.setup", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "test.tests.main.setup", | |
| PackageAddr: "", | |
| LocalPath: "setup", | |
| }, | |
| } | |
| if assertResultDeepEqual(t, hooks.Calls, wantCalls) { | |
| return | |
| } | |
| loader, err := configload.NewLoader(&configload.Config{ | |
| ModulesDir: modulesDir, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Make sure the configuration is loadable now. | |
| // (This ensures that correct information is recorded in the manifest.) | |
| rootMod, hclDiags := loader.LoadRootModuleWithTests(".", "tests") | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(hclDiags)) | |
| config, loadDiags := terraform.BuildConfigWithGraph( | |
| rootMod, | |
| loader.ModuleWalker(), | |
| nil, | |
| configs.MockDataLoaderFunc(loader.LoadExternalMockData), | |
| ) | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) | |
| if config.Module.Tests["tests/main.tftest.hcl"].Runs[0].ConfigUnderTest == nil { | |
| t.Fatalf("should have loaded config into the relevant run block but did not") | |
| } | |
| } | |
| func TestLoadInstallModules_registryFromTest(t *testing.T) { | |
| if os.Getenv("TF_ACC") == "" { | |
| t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") | |
| } | |
| fixtureDir := filepath.Clean("testdata/registry-module-from-test") | |
| tmpDir, done := tempChdir(t, fixtureDir) | |
| // the module installer runs filepath.EvalSymlinks() on the destination | |
| // directory before copying files, and the resultant directory is what is | |
| // returned by the install hooks. Without this, tests could fail on machines | |
| // where the default temp dir was a symlink. | |
| dir, err := filepath.EvalSymlinks(tmpDir) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| defer done() | |
| hooks := &testInstallHooks{} | |
| modulesDir := filepath.Join(dir, ".terraform/modules") | |
| loader, close := configload.NewLoaderForTests(t) | |
| defer close() | |
| inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), testModuleInstallerInitializer(loader)) | |
| _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) | |
| tfdiags.AssertNoDiagnostics(t, diags) | |
| v := version.Must(version.NewVersion("0.0.1")) | |
| wantCalls := []testInstallHookCall{ | |
| // the configuration builder visits each level of calls in lexicographical | |
| // order by name, so the following list is kept in the same order. | |
| // setup access acctest directly. | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.main.setup", | |
| Version: v, | |
| }, | |
| { | |
| Name: "Download", | |
| ModuleAddr: "test.main.setup", | |
| PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here | |
| Version: v, | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "test.main.setup", | |
| Version: v, | |
| // NOTE: This local path and the other paths derived from it below | |
| // can vary depending on how the registry is implemented. At the | |
| // time of writing this test, registry.terraform.io returns | |
| // git repository source addresses and so this path refers to the | |
| // root of the git clone, but historically the registry referred | |
| // to GitHub-provided tar archives which meant that there was an | |
| // extra level of subdirectory here for the typical directory | |
| // nesting in tar archives, which would've been reflected as | |
| // an extra segment on this path. If this test fails due to an | |
| // additional path segment in future, then a change to the upstream | |
| // registry might be the root cause. | |
| LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup"), | |
| }, | |
| // main.tftest.hcl.setup.child_a | |
| // (no download because it's a relative path inside acctest_child_a) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.main.setup.child_a", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "test.main.setup.child_a", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_a"), | |
| }, | |
| // main.tftest.hcl.setup.child_a.child_b | |
| // (no download because it's a relative path inside main.tftest.hcl.setup.child_a) | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.main.setup.child_a.child_b", | |
| }, | |
| { | |
| Name: "Install", | |
| ModuleAddr: "test.main.setup.child_a.child_b", | |
| LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_b"), | |
| }, | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.main.setup", | |
| Version: v, | |
| }, | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.main.setup.child_a", | |
| }, | |
| { | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: "test.main.setup.child_a.child_b", | |
| }, | |
| } | |
| if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { | |
| t.Fatalf("wrong installer calls\n%s", diff) | |
| } | |
| //check that the registry reponses were cached | |
| packageAddr := addrs.ModuleRegistryPackage{ | |
| Host: svchost.Hostname("registry.terraform.io"), | |
| Namespace: "hashicorp", | |
| Name: "module-installer-acctest", | |
| TargetSystem: "aws", | |
| } | |
| if _, ok := inst.registryPackageVersions[packageAddr]; !ok { | |
| t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions)) | |
| } | |
| if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok { | |
| t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) | |
| } | |
| loader, err = configload.NewLoader(&configload.Config{ | |
| ModulesDir: modulesDir, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Make sure the configuration is loadable now. | |
| // (This ensures that correct information is recorded in the manifest.) | |
| rootMod, hclDiags := loader.LoadRootModuleWithTests(".", "tests") | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(hclDiags)) | |
| config, loadDiags := terraform.BuildConfigWithGraph( | |
| rootMod, | |
| loader.ModuleWalker(), | |
| nil, | |
| configs.MockDataLoaderFunc(loader.LoadExternalMockData), | |
| ) | |
| tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) | |
| if config.Module.Tests["main.tftest.hcl"].Runs[0].ConfigUnderTest == nil { | |
| t.Fatalf("should have loaded config into the relevant run block but did not") | |
| } | |
| } | |
| type testInstallHooks struct { | |
| Calls []testInstallHookCall | |
| } | |
| type testInstallHookCall struct { | |
| Name string | |
| ModuleAddr string | |
| PackageAddr string | |
| Version *version.Version | |
| LocalPath string | |
| } | |
| func (h *testInstallHooks) Download(moduleAddr, packageAddr string, version *version.Version) { | |
| h.Calls = append(h.Calls, testInstallHookCall{ | |
| Name: "Download", | |
| ModuleAddr: moduleAddr, | |
| PackageAddr: packageAddr, | |
| Version: version, | |
| }) | |
| } | |
| func (h *testInstallHooks) Install(moduleAddr string, version *version.Version, localPath string) { | |
| h.Calls = append(h.Calls, testInstallHookCall{ | |
| Name: "Install", | |
| ModuleAddr: moduleAddr, | |
| Version: version, | |
| LocalPath: localPath, | |
| }) | |
| } | |
| func (h *testInstallHooks) ModuleSourceResolved(ctx context.Context, req *configs.ModuleRequest, vsn string) tfdiags.Diagnostics { | |
| // we ignore errors here because local paths do not have a version | |
| v, _ := version.NewVersion(vsn) | |
| h.Calls = append(h.Calls, testInstallHookCall{ | |
| Name: "ModuleSourceResolved", | |
| ModuleAddr: strings.Join(req.Path, "."), | |
| Version: v, | |
| }) | |
| return nil | |
| } | |
| // tempChdir copies the contents of the given directory to a temporary | |
| // directory and changes the test process's current working directory to | |
| // point to that directory. Also returned is a function that should be | |
| // called at the end of the test (e.g. via "defer") to restore the previous | |
| // working directory. | |
| // | |
| // Tests using this helper cannot safely be run in parallel with other tests. | |
| func tempChdir(t *testing.T, sourceDir string) (string, func()) { | |
| t.Helper() | |
| tmpDir, err := ioutil.TempDir("", "terraform-configload") | |
| if err != nil { | |
| t.Fatalf("failed to create temporary directory: %s", err) | |
| return "", nil | |
| } | |
| if err := copy.CopyDir(tmpDir, sourceDir); err != nil { | |
| t.Fatalf("failed to copy fixture to temporary directory: %s", err) | |
| return "", nil | |
| } | |
| oldDir, err := os.Getwd() | |
| if err != nil { | |
| t.Fatalf("failed to determine current working directory: %s", err) | |
| return "", nil | |
| } | |
| err = os.Chdir(tmpDir) | |
| if err != nil { | |
| t.Fatalf("failed to switch to temp dir %s: %s", tmpDir, err) | |
| return "", nil | |
| } | |
| // Most of the tests need this, so we'll make it just in case. | |
| os.MkdirAll(filepath.Join(tmpDir, ".terraform/modules"), os.ModePerm) | |
| t.Logf("tempChdir switched to %s after copying from %s", tmpDir, sourceDir) | |
| return tmpDir, func() { | |
| err := os.Chdir(oldDir) | |
| if err != nil { | |
| panic(fmt.Errorf("failed to restore previous working directory %s: %s", oldDir, err)) | |
| } | |
| if os.Getenv("TF_CONFIGLOAD_TEST_KEEP_TMP") == "" { | |
| os.RemoveAll(tmpDir) | |
| } | |
| } | |
| } | |
| func assertDiagnosticSummary(t *testing.T, diags tfdiags.Diagnostics, want string) bool { | |
| t.Helper() | |
| for _, diag := range diags { | |
| if diag.Description().Summary == want { | |
| t.Logf("matching diagnostic detail %q", diag.Description().Detail) | |
| return false | |
| } | |
| } | |
| t.Errorf("missing diagnostic summary %q", want) | |
| for _, diag := range diags { | |
| t.Logf("- %#v", diag.Description().Summary) | |
| } | |
| return true | |
| } | |
| func assertResultDeepEqual(t *testing.T, got, want interface{}) bool { | |
| t.Helper() | |
| if diff := deep.Equal(got, want); diff != nil { | |
| for _, problem := range diff { | |
| t.Errorf("%s", problem) | |
| } | |
| return true | |
| } | |
| return false | |
| } |