diff --git a/internal/command/cloud_mock.go b/internal/command/cloud_mock.go index cf324624e9..93f59199bf 100644 --- a/internal/command/cloud_mock.go +++ b/internal/command/cloud_mock.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -143,6 +144,7 @@ func TestNewVariableBackend(result map[string]string) backend.Backend { } var _ backendrun.ConstVariableSupplier = (*TestVariableBackend)(nil) +var _ backendrun.Local = (*TestVariableBackend)(nil) func (b *TestVariableBackend) FetchVariables(ctx context.Context, workspace string) (map[string]arguments.UnparsedVariableValue, tfdiags.Diagnostics) { result := make(map[string]arguments.UnparsedVariableValue) @@ -157,6 +159,27 @@ func (b *TestVariableBackend) FetchVariables(ctx context.Context, workspace stri return result, nil } +func (b *TestVariableBackend) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) { + // Sometimes a command (like graph) requires a local backend. The cloud + // backend implements LocalRun and will fetch variables from the backend. + // But our mock TestVariableBackend will fail in these tests, because it + // embends the backendrun.Local backend and never calls FetchVariables. + // + // We now set the variables manually here and defer to the regular backend + // run implementation, this helps us test the desired behavior. + if op.Variables == nil { + op.Variables = make(map[string]arguments.UnparsedVariableValue) + } + fetchedVars, _ := b.FetchVariables(context.Background(), op.Workspace) + for k, v := range fetchedVars { + if _, ok := op.Variables[k]; !ok { + op.Variables[k] = v + } + } + + return b.Local.LocalRun(op) +} + type testUnparsedVariableValueString struct { str string name string diff --git a/internal/command/graph_test.go b/internal/command/graph_test.go index 9490ed130c..71f6611dd1 100644 --- a/internal/command/graph_test.go +++ b/internal/command/graph_test.go @@ -14,6 +14,8 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend" + backendInit "github.com/hashicorp/terraform/internal/backend/init" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/initwd" @@ -359,3 +361,103 @@ func TestGraph_applyPhaseSavedPlan(t *testing.T) { t.Fatalf("doesn't look like digraph:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr()) } } + +func TestGraph_constVariable(t *testing.T) { + t.Run("missing value", func(t *testing.T) { + wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var") + t.Chdir(wd.RootModuleDir()) + + ui := cli.NewMockUi() + streams, closeStreams := terminal.StreamsForTesting(t) + c := &GraphCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + Streams: streams, + WorkingDir: wd, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + output := closeStreams(t) + t.Fatalf("expected exit status 1\nstdout:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr()) + } + + if !strings.Contains(ui.ErrorWriter.String(), "No value for required variable") { + t.Fatalf("expected missing variable error, got:\n%s", ui.ErrorWriter.String()) + } + closeStreams(t) + }) + + t.Run("value via cli", func(t *testing.T) { + wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var") + t.Chdir(wd.RootModuleDir()) + + ui := cli.NewMockUi() + streams, closeStreams := terminal.StreamsForTesting(t) + c := &GraphCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + Streams: streams, + WorkingDir: wd, + }, + } + + args := []string{"-var", "module_name=child"} + if code := c.Run(args); code != 0 { + output := closeStreams(t) + t.Fatalf("bad:\nstdout:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr()) + } + + output := closeStreams(t) + wantOutput := []string{ + `"module.child.test_instance.test" [label="test_instance.test"]`, + } + for _, want := range wantOutput { + if !strings.Contains(output.Stdout(), want) { + t.Fatalf("output missing %s:\n%s", want, output.Stdout()) + } + } + }) + + t.Run("value via backend", func(t *testing.T) { + mockBackend := TestNewVariableBackend(map[string]string{ + "module_name": "child", + }) + backendInit.Set("local-vars", func() backend.Backend { return mockBackend }) + defer backendInit.Set("local-vars", nil) + + wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var-backend") + t.Chdir(wd.RootModuleDir()) + + ui := cli.NewMockUi() + streams, closeStreams := terminal.StreamsForTesting(t) + c := &GraphCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + Streams: streams, + WorkingDir: wd, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + output := closeStreams(t) + stderr := ui.ErrorWriter.String() + t.Fatalf("bad:\nstdout:\n%s\n\nstderr:\n%s", output.Stdout(), stderr) + } + + output := closeStreams(t) + wantOutput := []string{ + `"module.child.test_instance.test" [label="test_instance.test"]`, + } + for _, want := range wantOutput { + if !strings.Contains(output.Stdout(), want) { + t.Fatalf("output missing %s:\n%s", want, output.Stdout()) + } + } + }) +}