terraform/internal/command/init2_test.go
2026-03-04 11:45:59 +01:00

673 lines
19 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/cli"
)
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", "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)
view, done := testView(t)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
},
}
// 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())
}
}
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)
}
}