From 45ba6796ba96278d590459715768cd566fa2c9db Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Fri, 27 Feb 2026 16:43:11 +0100 Subject: [PATCH] Add more dynamic module sources tests --- internal/command/init2_test.go | 700 ++++++++++++++++-- .../apply-plan-with-dynamic-source/main.tf | 9 + .../modules/example/main.tf | 3 + .../apply-with-dynamic-source/main.tf | 8 + .../modules/example/main.tf | 3 + .../count-in-module-source/main.tf | 4 + .../each-in-module-source/main.tf | 4 + .../source-module/main.tf | 8 + .../get-false-with-dynamic-source/main.tf | 8 + .../modules/example/empty.tf | 1 + .../local-source-with-local-value/main.tf | 12 + .../modules/example/empty.tf | 1 + .../main.tf | 7 + .../main.tf | 9 + .../modules/alternate/empty.tf | 0 .../modules/example/empty.tf | 1 + .../local-source-with-variable/main.tf | 8 + .../modules/example/empty.tf | 1 + .../local-source-with-varsfile/main.tf | 8 + .../modules/example/empty.tf | 1 + .../local-source-with-varsfile/test.tfvars | 1 + .../module-with-count/main.tf | 10 + .../module-with-count/modules/example/main.tf | 7 + .../module-with-for-each/main.tf | 10 + .../modules/example/main.tf | 7 + .../main.tf | 9 + .../modules/child/main.tf | 1 + .../modules/parent/main.tf | 8 + .../path-attr-in-module-source/main.tf | 3 + .../modules/example/empty.tf | 1 + .../plan-with-dynamic-source/main.tf | 8 + .../modules/example/main.tf | 3 + .../.terraform/modules/child/empty.tf | 1 + .../.terraform/modules/modules.json | 15 + .../plan-with-version-mismatch/main.tf | 15 + .../main.tf | 7 + .../modules/example/main.tf | 3 + .../source-with-resource-reference/main.tf | 5 + .../terraform-attr-in-module-source/main.tf | 3 + internal/terraform/node_module_install.go | 2 +- 40 files changed, 850 insertions(+), 65 deletions(-) create mode 100644 internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/modules/example/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/modules/example/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/count-in-module-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/each-in-module-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/from-module-with-dynamic-source/source-module/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/modules/example/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-local-value/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-local-value/modules/example/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-non-const-variable/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/alternate/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/example/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-variable/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-variable/modules/example/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/modules/example/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/test.tfvars create mode 100644 internal/command/testdata/dynamic-module-sources/module-with-count/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/module-with-count/modules/example/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/module-with-for-each/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/module-with-for-each/modules/example/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/child/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/parent/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/modules/example/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/modules/example/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/child/empty.tf create mode 100644 internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/modules.json create mode 100644 internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/modules/example/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/source-with-resource-reference/main.tf create mode 100644 internal/command/testdata/dynamic-module-sources/terraform-attr-in-module-source/main.tf diff --git a/internal/command/init2_test.go b/internal/command/init2_test.go index 9dcfcf50c4..5efc70ce3b 100644 --- a/internal/command/init2_test.go +++ b/internal/command/init2_test.go @@ -4,6 +4,7 @@ package command import ( + "os" "path/filepath" "strings" "testing" @@ -11,11 +12,197 @@ import ( "github.com/hashicorp/cli" ) -func TestInit2_versionConstraintAdded(t *testing.T) { - // This test is for what happens when there is a version constraint added - // to a module that previously didn't have one. +func TestInit2_dynamicSourceErrors(t *testing.T) { + tests := map[string]struct { + fixture string + args []string + wantError string + }{ + "version constraint added to previously unversioned module": { + fixture: "add-version-constraint", + args: []string{"-get=false"}, + wantError: "Module version requirements have changed", + }, + "invalid registry source with version argument": { + fixture: "invalid-registry-source-with-module", + wantError: "Invalid registry module source address", + }, + "local source with version argument": { + fixture: "local-source-with-version", + wantError: "Invalid registry module source address", + }, + "non-const variable in module source": { + fixture: "local-source-with-non-const-variable", + args: []string{"-var", "module_name=example"}, + wantError: "Invalid module source", + }, + "resource reference in module source": { + fixture: "source-with-resource-reference", + wantError: "Invalid module source", + }, + "module output reference in module source": { + fixture: "source-with-module-output-reference", + wantError: "Invalid module source", + }, + "each.key in module source": { + fixture: "each-in-module-source", + wantError: "Invalid module source", + }, + "count.index in module source": { + fixture: "count-in-module-source", + wantError: "Invalid module source", + }, + "terraform.workspace in module source": { + fixture: "terraform-attr-in-module-source", + wantError: "Invalid module source", + }, + "required const variable not set": { + fixture: "local-source-with-variable", + wantError: "No value for required variable", + }, + "override default with nonexistent module": { + fixture: "local-source-with-variable-default", + args: []string{"-var", "module_name=nonexistent"}, + wantError: "", // any error; the module directory doesn't exist + }, + "version mismatch with dynamic constraint": { + fixture: "plan-with-version-mismatch", + args: []string{"-get=false", "-var", "module_version=0.0.2"}, + wantError: "Module version requirements have changed", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", tc.fixture)), td) + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + code := c.Run(tc.args) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) + } + + if tc.wantError != "" { + got := testOutput.All() + if !strings.Contains(got, tc.wantError) { + t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, tc.wantError) + } + } + }) + } +} + +func TestInit2_dynamicSourceSuccess(t *testing.T) { + tests := map[string]struct { + fixture string + args []string + }{ + "const variable via -var": { + fixture: "local-source-with-variable", + args: []string{"-var", "module_name=example"}, + }, + "const variable with default value": { + fixture: "local-source-with-variable-default", + }, + "local value referencing const variable": { + fixture: "local-source-with-local-value", + args: []string{"-var", "module_name=example"}, + }, + "nested module with variable passed through parent": { + fixture: "nested-module-with-variable-source", + args: []string{"-var", "child_name=child"}, + }, + "const variable from tfvars file": { + fixture: "local-source-with-varsfile", + args: []string{"-var-file", "test.tfvars"}, + }, + "path.module in module source": { + fixture: "path-attr-in-module-source", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", tc.fixture)), td) + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + code := c.Run(tc.args) + testOutput := done(t) + if code != 0 { + t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) + } + }) + } +} + +func TestInit2_getFalseWithDynamicSource(t *testing.T) { td := t.TempDir() - testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "add-version-constraint")), td) + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "get-false-with-dynamic-source")), td) + t.Chdir(td) + + // First, run init normally to install the module + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-var", "module_name=example"} + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("first init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) + } + + // Now run init with -get=false; should succeed since modules are already installed + ui2 := new(cli.MockUi) + view2, done2 := testView(t) + c2 := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui2, + View: view2, + }, + } + + args2 := []string{"-get=false", "-var", "module_name=example"} + code = c2.Run(args2) + testOutput2 := done2(t) + if code != 0 { + t.Fatalf("init -get=false failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, testOutput2.Stderr(), testOutput2.Stdout()) + } +} + +func TestInit2_getFalseWithDynamicSourceNotInstalled(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "get-false-with-dynamic-source")), td) t.Chdir(td) ui := new(cli.MockUi) @@ -28,74 +215,459 @@ func TestInit2_versionConstraintAdded(t *testing.T) { }, } - args := []string{"-get=false"} + // Run init with -get=false without having installed modules first + args := []string{"-get=false", "-var", "module_name=example"} code := c.Run(args) testOutput := done(t) if code != 1 { t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } - got := testOutput.All() +} + +func TestInit2_reinitWithDifferentVariable(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "local-source-with-variable-default")), td) + t.Chdir(td) + + // First init with default variable (example) + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + code := c.Run([]string{}) + testOutput := done(t) + if code != 0 { + t.Fatalf("first init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) + } + + // Re-init with different variable + ui2 := new(cli.MockUi) + view2, done2 := testView(t) + c2 := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui2, + View: view2, + }, + } + + code = c2.Run([]string{"-var", "module_name=alternate"}) + testOutput2 := done2(t) + if code != 0 { + t.Fatalf("second init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, testOutput2.Stderr(), testOutput2.Stdout()) + } +} + +func TestInit2_fromModuleWithDynamicSource(t *testing.T) { + // TODO: -from-module currently panics when the copied configuration + // contains a dynamic module source (e.g. "./modules/${var.module_name}"). + t.Skip("skipping: -from-module panics on dynamic module sources (see TODO in from_module.go)") + + // Create an empty target directory for -from-module to copy into + td := t.TempDir() + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + // Use -from-module to copy the source module (which has a dynamic source) + // into the empty working directory. This should copy the files but the + // nested dynamic module won't be resolved by -from-module itself. + srcDir := testFixturePath(filepath.Join("dynamic-module-sources", "from-module-with-dynamic-source", "source-module")) + args := []string{"-from-module=" + srcDir} + code := c.Run(args) + testOutput := done(t) + + // -from-module should succeed in copying. The dynamic module source + // within the copied configuration won't be resolved yet — that requires + // a separate init with the variable value. + if code != 0 { + t.Fatalf("init -from-module failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) + } + + // Verify the main.tf was copied + if _, err := os.Stat(filepath.Join(td, "main.tf")); os.IsNotExist(err) { + t.Fatal("main.tf was not copied from the source module") + } +} + +func TestPlan_dynamicModuleSource(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "plan-with-dynamic-source")), td) + t.Chdir(td) + + p := planFixtureProvider() + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + args := []string{"-var", "module_name=example"} + + initUi := new(cli.MockUi) + initView, initDone := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: initUi, + View: initView, + ProviderSource: providerSource, + }, + } + + initCode := initCmd.Run(args) + initOutput := initDone(t) + if initCode != 0 { + t.Fatalf("init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", initCode, initOutput.Stderr(), initOutput.Stdout()) + } + + // Now run plan + planUi := new(cli.MockUi) + planView, planDone := testView(t) + planCmd := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: planUi, + View: planView, + }, + } + + planCode := planCmd.Run(args) + planOutput := planDone(t) + if planCode != 0 { + t.Fatalf("plan failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", planCode, planOutput.Stderr(), planOutput.Stdout()) + } + + output := planOutput.Stdout() + if !strings.Contains(output, "1 to add") { + t.Fatalf("expected plan to show 1 resource to add, got:\n%s", output) + } +} + +func TestPlan_dynamicModuleSourceMismatch(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "plan-with-dynamic-source")), td) + t.Chdir(td) + + p := planFixtureProvider() + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + args := []string{"-var", "module_name=example"} + + initUi := new(cli.MockUi) + initView, initDone := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: initUi, + View: initView, + ProviderSource: providerSource, + }, + } + + initCode := initCmd.Run(args) + initOutput := initDone(t) + if initCode != 0 { + t.Fatalf("init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", initCode, initOutput.Stderr(), initOutput.Stdout()) + } + + // Now run plan with a different variable value + planUi := new(cli.MockUi) + planView, planDone := testView(t) + planCmd := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: planUi, + View: planView, + }, + } + + planArgs := []string{"-var", "module_name=nonexistent"} + code := planCmd.Run(planArgs) + planOutput := planDone(t) + if code == 0 { + t.Fatalf("expected plan to fail, but got exit status 0\nstdout:\n%s", planOutput.Stdout()) + } +} + +func TestApply_dynamicModuleSource(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "apply-with-dynamic-source")), td) + t.Chdir(td) + + p := planFixtureProvider() + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + args := []string{"-var", "module_name=example"} + + initUi := new(cli.MockUi) + initView, initDone := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: initUi, + View: initView, + ProviderSource: providerSource, + }, + } + + initCode := initCmd.Run(args) + initOutput := initDone(t) + if initCode != 0 { + t.Fatalf("init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", initCode, initOutput.Stderr(), initOutput.Stdout()) + } + + applyUi := new(cli.MockUi) + applyView, applyDone := testView(t) + applyCmd := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: applyUi, + View: applyView, + }, + } + + applyArgs := []string{"-auto-approve", "-var", "module_name=example"} + code := applyCmd.Run(applyArgs) + applyOutput := applyDone(t) + if code != 0 { + t.Fatalf("apply failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, applyOutput.Stderr(), applyOutput.Stdout()) + } + + output := applyOutput.Stdout() + if !strings.Contains(output, "Apply complete!") { + t.Fatalf("expected apply to succeed, got:\n%s", output) + } +} + +func TestApply_dynamicModuleSourceWithDefaultPlanFile(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "apply-plan-with-dynamic-source")), td) + t.Chdir(td) + + p := planFixtureProvider() + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + initUi := new(cli.MockUi) + initView, initDone := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: initUi, + View: initView, + ProviderSource: providerSource, + }, + } + + initCode := initCmd.Run([]string{}) + initOutput := initDone(t) + if initCode != 0 { + t.Fatalf("init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", initCode, initOutput.Stderr(), initOutput.Stdout()) + } + + // Run plan with -out + planPath := filepath.Join(td, "saved.plan") + planUi := new(cli.MockUi) + planView, planDone := testView(t) + planCmd := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: planUi, + View: planView, + }, + } + + planArgs := []string{"-out", planPath} + code := planCmd.Run(planArgs) + planOutput := planDone(t) + if code != 0 { + t.Fatalf("plan failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, planOutput.Stderr(), planOutput.Stdout()) + } + + // Verify the plan file was created + if _, err := os.Stat(planPath); os.IsNotExist(err) { + t.Fatalf("plan file was not created at %s", planPath) + } + + // Apply the saved plan + applyUi := new(cli.MockUi) + applyView, applyDone := testView(t) + applyCmd := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: applyUi, + View: applyView, + }, + } + + applyArgs := []string{planPath} + code = applyCmd.Run(applyArgs) + applyOutput := applyDone(t) + if code != 0 { + t.Fatalf("apply failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", code, applyOutput.Stderr(), applyOutput.Stdout()) + } + + output := applyOutput.Stdout() + if !strings.Contains(output, "Apply complete!") { + t.Fatalf("expected apply to succeed, got:\n%s", output) + } +} + +func TestPlan_dynamicModuleSourceWithCount(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "module-with-count")), td) + t.Chdir(td) + + p := planFixtureProvider() + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + args := []string{"-var", "module_name=example"} + + initUi := new(cli.MockUi) + initView, initDone := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: initUi, + View: initView, + ProviderSource: providerSource, + }, + } + + initCode := initCmd.Run(args) + initOutput := initDone(t) + if initCode != 0 { + t.Fatalf("init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", initCode, initOutput.Stderr(), initOutput.Stdout()) + } + + // Now run plan + planUi := new(cli.MockUi) + planView, planDone := testView(t) + planCmd := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: planUi, + View: planView, + }, + } + + planCode := planCmd.Run(args) + planOutput := planDone(t) + if planCode != 0 { + t.Fatalf("plan failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", planCode, planOutput.Stderr(), planOutput.Stdout()) + } + + output := planOutput.Stdout() + if !strings.Contains(output, "2 to add") { + t.Fatalf("expected plan to show 2 resources to add, got:\n%s", output) + } +} + +func TestPlan_dynamicModuleSourceWithForEach(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "module-with-for-each")), td) + t.Chdir(td) + + p := planFixtureProvider() + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + args := []string{"-var", "module_name=example"} + + initUi := new(cli.MockUi) + initView, initDone := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: initUi, + View: initView, + ProviderSource: providerSource, + }, + } + + initCode := initCmd.Run(args) + initOutput := initDone(t) + if initCode != 0 { + t.Fatalf("init failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", initCode, initOutput.Stderr(), initOutput.Stdout()) + } + + // Now run plan + planUi := new(cli.MockUi) + planView, planDone := testView(t) + planCmd := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: planUi, + View: planView, + }, + } + + planCode := planCmd.Run(args) + planOutput := planDone(t) + if planCode != 0 { + t.Fatalf("plan failed with exit status %d\nstderr:\n%s\n\nstdout:\n%s", planCode, planOutput.Stderr(), planOutput.Stdout()) + } + + output := planOutput.Stdout() + if !strings.Contains(output, "2 to add") { + t.Fatalf("expected plan to show 2 resources to add, got:\n%s", output) + } +} + +func TestPlan_dynamicModuleVersionMismatch(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "plan-with-version-mismatch")), td) + t.Chdir(td) + + p := planFixtureProvider() + + // Plan should fail because the installed module version (0.0.1 in + // modules.json) doesn't satisfy the constraint we provide. + planUi := new(cli.MockUi) + planView, planDone := testView(t) + planCmd := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: planUi, + View: planView, + }, + } + + planArgs := []string{"-var", "module_version=0.0.2"} + code := planCmd.Run(planArgs) + planOutput := planDone(t) + if code == 0 { + t.Fatalf("expected plan to fail, but got exit status 0\nstdout:\n%s", planOutput.Stdout()) + } + got := planOutput.All() want := "Module version requirements have changed" if !strings.Contains(got, want) { t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want) } } - -func TestInit2_invalidRegistrySourceWithModule(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "invalid-registry-source-with-module")), td) - t.Chdir(td) - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - View: view, - }, - } - - args := []string{} - code := c.Run(args) - testOutput := done(t) - if code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) - } - got := testOutput.All() - - want := "Invalid registry module source address" - if !strings.Contains(got, want) { - t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want) - } -} - -func TestInit2_localSourceWithVersion(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath(filepath.Join("dynamic-module-sources", "local-source-with-version")), td) - t.Chdir(td) - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - View: view, - }, - } - - args := []string{} - code := c.Run(args) - testOutput := done(t) - if code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) - } - got := testOutput.All() - - want := "Invalid registry module source address" - if !strings.Contains(got, want) { - t.Fatalf("wrong error\ngot:\n%s\n\nwant: containing %q", got, want) - } -} diff --git a/internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/main.tf b/internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/main.tf new file mode 100644 index 0000000000..98de3a2672 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/main.tf @@ -0,0 +1,9 @@ +variable "module_name" { + type = string + const = true + default = "example" +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/modules/example/main.tf b/internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/modules/example/main.tf new file mode 100644 index 0000000000..11bc1e75a5 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/apply-plan-with-dynamic-source/modules/example/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "example" { + ami = "bar" +} diff --git a/internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/main.tf b/internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/main.tf new file mode 100644 index 0000000000..9b60651f81 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/main.tf @@ -0,0 +1,8 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/modules/example/main.tf b/internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/modules/example/main.tf new file mode 100644 index 0000000000..11bc1e75a5 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/apply-with-dynamic-source/modules/example/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "example" { + ami = "bar" +} diff --git a/internal/command/testdata/dynamic-module-sources/count-in-module-source/main.tf b/internal/command/testdata/dynamic-module-sources/count-in-module-source/main.tf new file mode 100644 index 0000000000..50f77309dc --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/count-in-module-source/main.tf @@ -0,0 +1,4 @@ +module "example" { + count = 2 + source = "./modules/${count.index}" +} diff --git a/internal/command/testdata/dynamic-module-sources/each-in-module-source/main.tf b/internal/command/testdata/dynamic-module-sources/each-in-module-source/main.tf new file mode 100644 index 0000000000..53616a849c --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/each-in-module-source/main.tf @@ -0,0 +1,4 @@ +module "example" { + for_each = toset(["one", "two"]) + source = "./modules/${each.key}" +} diff --git a/internal/command/testdata/dynamic-module-sources/from-module-with-dynamic-source/source-module/main.tf b/internal/command/testdata/dynamic-module-sources/from-module-with-dynamic-source/source-module/main.tf new file mode 100644 index 0000000000..9b60651f81 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/from-module-with-dynamic-source/source-module/main.tf @@ -0,0 +1,8 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/main.tf b/internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/main.tf new file mode 100644 index 0000000000..9b60651f81 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/main.tf @@ -0,0 +1,8 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/modules/example/empty.tf b/internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/modules/example/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/get-false-with-dynamic-source/modules/example/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-local-value/main.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-local-value/main.tf new file mode 100644 index 0000000000..a4278dae8c --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-local-value/main.tf @@ -0,0 +1,12 @@ +variable "module_name" { + type = string + const = true +} + +locals { + module_path = "./modules/${var.module_name}" +} + +module "example" { + source = local.module_path +} diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-local-value/modules/example/empty.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-local-value/modules/example/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-local-value/modules/example/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-non-const-variable/main.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-non-const-variable/main.tf new file mode 100644 index 0000000000..836e88c627 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-non-const-variable/main.tf @@ -0,0 +1,7 @@ +variable "module_name" { + type = string +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/main.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/main.tf new file mode 100644 index 0000000000..98de3a2672 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/main.tf @@ -0,0 +1,9 @@ +variable "module_name" { + type = string + const = true + default = "example" +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/alternate/empty.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/alternate/empty.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/example/empty.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/example/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-variable-default/modules/example/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-variable/main.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-variable/main.tf new file mode 100644 index 0000000000..9b60651f81 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-variable/main.tf @@ -0,0 +1,8 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-variable/modules/example/empty.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-variable/modules/example/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-variable/modules/example/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/main.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/main.tf new file mode 100644 index 0000000000..9b60651f81 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/main.tf @@ -0,0 +1,8 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/modules/example/empty.tf b/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/modules/example/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/modules/example/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/test.tfvars b/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/test.tfvars new file mode 100644 index 0000000000..ae980f90f1 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/local-source-with-varsfile/test.tfvars @@ -0,0 +1 @@ +module_name = "example" diff --git a/internal/command/testdata/dynamic-module-sources/module-with-count/main.tf b/internal/command/testdata/dynamic-module-sources/module-with-count/main.tf new file mode 100644 index 0000000000..ccec44c408 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/module-with-count/main.tf @@ -0,0 +1,10 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" + count = 2 + number = count.index +} diff --git a/internal/command/testdata/dynamic-module-sources/module-with-count/modules/example/main.tf b/internal/command/testdata/dynamic-module-sources/module-with-count/modules/example/main.tf new file mode 100644 index 0000000000..1855478473 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/module-with-count/modules/example/main.tf @@ -0,0 +1,7 @@ +resource "test_instance" "example" { + ami = "bar" +} + +variable "number" { + type = number +} diff --git a/internal/command/testdata/dynamic-module-sources/module-with-for-each/main.tf b/internal/command/testdata/dynamic-module-sources/module-with-for-each/main.tf new file mode 100644 index 0000000000..d50cd6e85b --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/module-with-for-each/main.tf @@ -0,0 +1,10 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" + for_each = toset(["a", "b"]) + letter = each.value +} diff --git a/internal/command/testdata/dynamic-module-sources/module-with-for-each/modules/example/main.tf b/internal/command/testdata/dynamic-module-sources/module-with-for-each/modules/example/main.tf new file mode 100644 index 0000000000..9dbbae2569 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/module-with-for-each/modules/example/main.tf @@ -0,0 +1,7 @@ +resource "test_instance" "example" { + ami = "bar" +} + +variable "letter" { + type = string +} diff --git a/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/main.tf b/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/main.tf new file mode 100644 index 0000000000..b0db6a9f5e --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/main.tf @@ -0,0 +1,9 @@ +variable "child_name" { + type = string + const = true +} + +module "parent" { + source = "./modules/parent" + child_name = var.child_name +} diff --git a/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/child/main.tf b/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/child/main.tf new file mode 100644 index 0000000000..048ba194cc --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/child/main.tf @@ -0,0 +1 @@ +# Empty child module used by dynamic-module-sources tests diff --git a/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/parent/main.tf b/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/parent/main.tf new file mode 100644 index 0000000000..6a81115838 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/nested-module-with-variable-source/modules/parent/main.tf @@ -0,0 +1,8 @@ +variable "child_name" { + type = string + const = true +} + +module "child" { + source = "../${var.child_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/main.tf b/internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/main.tf new file mode 100644 index 0000000000..855ccd0aef --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/main.tf @@ -0,0 +1,3 @@ +module "example" { + source = "${path.module}/modules/example" +} diff --git a/internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/modules/example/empty.tf b/internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/modules/example/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/path-attr-in-module-source/modules/example/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/main.tf b/internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/main.tf new file mode 100644 index 0000000000..9b60651f81 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/main.tf @@ -0,0 +1,8 @@ +variable "module_name" { + type = string + const = true +} + +module "example" { + source = "./modules/${var.module_name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/modules/example/main.tf b/internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/modules/example/main.tf new file mode 100644 index 0000000000..11bc1e75a5 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/plan-with-dynamic-source/modules/example/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "example" { + ami = "bar" +} diff --git a/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/child/empty.tf b/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/child/empty.tf new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/child/empty.tf @@ -0,0 +1 @@ + diff --git a/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/modules.json b/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/modules.json new file mode 100644 index 0000000000..6b52e103bc --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/.terraform/modules/modules.json @@ -0,0 +1,15 @@ +{ + "Modules": [ + { + "Key": "", + "Source": "", + "Dir": "" + }, + { + "Key": "child", + "Source": "hashicorp/module-installer-acctest/aws", + "Version": "0.0.1", + "Dir": ".terraform/modules/child" + } + ] +} diff --git a/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/main.tf b/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/main.tf new file mode 100644 index 0000000000..766cf9987f --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/plan-with-version-mismatch/main.tf @@ -0,0 +1,15 @@ +# This fixture tests that plan detects a version mismatch when the dynamic +# version constraint changes between init and plan. +# +# The pre-populated .terraform/modules/modules.json records version 0.0.1 +# but the configuration requires a version determined by the const variable. + +variable "module_version" { + type = string + const = true +} + +module "child" { + source = "hashicorp/module-installer-acctest/aws" + version = var.module_version +} diff --git a/internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/main.tf b/internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/main.tf new file mode 100644 index 0000000000..355f9c1001 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/main.tf @@ -0,0 +1,7 @@ +module "example" { + source = "./modules/example" +} + +module "example2" { + source = "./modules/${module.example.name}" +} diff --git a/internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/modules/example/main.tf b/internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/modules/example/main.tf new file mode 100644 index 0000000000..a3e6a00391 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/source-with-module-output-reference/modules/example/main.tf @@ -0,0 +1,3 @@ +output "name" { + value = "example" +} diff --git a/internal/command/testdata/dynamic-module-sources/source-with-resource-reference/main.tf b/internal/command/testdata/dynamic-module-sources/source-with-resource-reference/main.tf new file mode 100644 index 0000000000..113df8bd34 --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/source-with-resource-reference/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "example" {} + +module "example" { + source = "./modules/${test_instance.example.id}" +} diff --git a/internal/command/testdata/dynamic-module-sources/terraform-attr-in-module-source/main.tf b/internal/command/testdata/dynamic-module-sources/terraform-attr-in-module-source/main.tf new file mode 100644 index 0000000000..8757a3094e --- /dev/null +++ b/internal/command/testdata/dynamic-module-sources/terraform-attr-in-module-source/main.tf @@ -0,0 +1,3 @@ +module "example" { + source = "./modules/${terraform.workspace}" +} diff --git a/internal/terraform/node_module_install.go b/internal/terraform/node_module_install.go index a14b417165..59982538c7 100644 --- a/internal/terraform/node_module_install.go +++ b/internal/terraform/node_module_install.go @@ -167,7 +167,7 @@ func evalSource(sourceExpr hcl.Expression, hasVersion bool, ctx EvalContext) (ad for _, ref := range refs { switch ref.Subject.(type) { - case addrs.InputVariable, addrs.LocalValue: + case addrs.InputVariable, addrs.LocalValue, addrs.PathAttr: // These are allowed default: diags = diags.Append(&hcl.Diagnostic{