| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "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/backend/remote-state/inmem" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| testing_provider "github.com/hashicorp/terraform/internal/providers/testing" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/states/statefile" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| func TestOutput(t *testing.T) { | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| statePath := testStateFile(t, originalState) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "foo", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| actual := strings.TrimSpace(output.Stdout()) | |
| if actual != `"bar"` { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestOutput_stateStore(t *testing.T) { | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("state-store-unchanged/provider-managed-by-terraform"), td) | |
| t.Chdir(td) | |
| // Get bytes describing the state | |
| var stateBuf bytes.Buffer | |
| if err := statefile.Write(statefile.New(originalState, "", 1), &stateBuf); err != nil { | |
| t.Fatalf("error during test setup: %s", err) | |
| } | |
| // Create a mock that contains a persisted "default" state that uses the bytes from above. | |
| mockProvider := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) | |
| mockProvider.MockStates = testing_provider.NewMockStateBytesWithSingleState( | |
| "test_store", | |
| "default", | |
| stateBuf.Bytes(), | |
| ) | |
| mockProviderAddress := addrs.NewDefaultProvider("test") | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| AllowExperimentalFeatures: true, | |
| testingOverrides: &testingOverrides{ | |
| Providers: map[addrs.Provider]providers.Factory{ | |
| mockProviderAddress: providers.FactoryFixed(mockProvider), | |
| }, | |
| }, | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "foo", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| actual := strings.TrimSpace(output.Stdout()) | |
| if actual != `"bar"` { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestOutput_json(t *testing.T) { | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| statePath := testStateFile(t, originalState) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "-json", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| actual := strings.TrimSpace(output.Stdout()) | |
| expected := "{\n \"foo\": {\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}" | |
| if actual != expected { | |
| t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected) | |
| } | |
| } | |
| func TestOutput_emptyOutputs(t *testing.T) { | |
| originalState := states.NewState() | |
| statePath := testStateFile(t, originalState) | |
| p := testProvider() | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-no-color", | |
| "-state", statePath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| // Warning diagnostics should go to stdout | |
| if got, want := output.Stdout(), "Warning: No outputs found"; !strings.Contains(got, want) { | |
| t.Fatalf("bad output: expected to contain %q, got:\n%s", want, got) | |
| } | |
| } | |
| func TestOutput_badVar(t *testing.T) { | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| statePath := testStateFile(t, originalState) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "bar", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| } | |
| func TestOutput_blank(t *testing.T) { | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "name"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("john-doe"), | |
| false, | |
| ) | |
| }) | |
| statePath := testStateFile(t, originalState) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| expectedOutput := "foo = \"bar\"\nname = \"john-doe\"\n" | |
| if got := output.Stdout(); got != expectedOutput { | |
| t.Fatalf("wrong output\ngot: %#v\nwant: %#v", got, expectedOutput) | |
| } | |
| } | |
| func TestOutput_manyArgs(t *testing.T) { | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "bad", | |
| "bad", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("bad: \n%s", output.Stdout()) | |
| } | |
| } | |
| func TestOutput_noArgs(t *testing.T) { | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stdout()) | |
| } | |
| } | |
| func TestOutput_noState(t *testing.T) { | |
| originalState := states.NewState() | |
| statePath := testStateFile(t, originalState) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "foo", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| } | |
| func TestOutput_noVars(t *testing.T) { | |
| originalState := states.NewState() | |
| statePath := testStateFile(t, originalState) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "bar", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| } | |
| func TestOutput_stateDefault(t *testing.T) { | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| // Write the state file in a temporary directory with the | |
| // default filename. | |
| td := testTempDir(t) | |
| statePath := filepath.Join(td, DefaultStateFilename) | |
| f, err := os.Create(statePath) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| err = writeStateForTesting(originalState, f) | |
| f.Close() | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| // Change to that directory | |
| cwd, err := os.Getwd() | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if err := os.Chdir(filepath.Dir(statePath)); err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| defer os.Chdir(cwd) | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "foo", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: \n%s", output.Stderr()) | |
| } | |
| actual := strings.TrimSpace(output.Stdout()) | |
| if actual != `"bar"` { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| // deprecatedInmemBackend wraps the inmem backend and injects a deprecation | |
| // warning from PrepareConfig, simulating a backend with deprecated attributes | |
| // (like the S3 backend's dynamodb_table). | |
| type deprecatedInmemBackend struct { | |
| backend.Backend | |
| } | |
| func (b *deprecatedInmemBackend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { | |
| newObj, diags := b.Backend.PrepareConfig(obj) | |
| diags = diags.Append(tfdiags.SimpleWarning(`The attribute "deprecated_attr" is deprecated.`)) | |
| return newObj, diags | |
| } | |
| func TestOutputRaw_warningsSuppressed(t *testing.T) { | |
| // Pre-populate the inmem backend with a state containing an output value | |
| inmem.Reset() | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| // Register a backend that wraps inmem with a deprecation warning, | |
| // simulating a backend like S3 whose PrepareConfig warns about | |
| // deprecated attributes (e.g. dynamodb_table). | |
| backendInit.Set("inmem", func() backend.Backend { | |
| return &deprecatedInmemBackend{Backend: inmem.New()} | |
| }) | |
| defer backendInit.Set("inmem", inmem.New) | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("output-backend-with-deprecation"), td) | |
| t.Chdir(td) | |
| // Write the state into the inmem backend's default workspace | |
| b := inmem.New() | |
| b.Configure(cty.ObjectVal(map[string]cty.Value{ | |
| "lock_id": cty.NullVal(cty.String), | |
| })) | |
| sMgr, sDiags := b.StateMgr(backend.DefaultStateName) | |
| if sDiags.HasErrors() { | |
| t.Fatalf("unexpected error: %s", sDiags.Err()) | |
| } | |
| sMgr.WriteState(originalState) | |
| if err := sMgr.PersistState(nil); err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-raw", "foo"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("unexpected exit code %d\nstderr:\n%s", code, output.Stderr()) | |
| } | |
| // The key assertion: warnings must not appear in raw output | |
| // as they would be indistinguishable from the value. | |
| stderr := output.Stderr() | |
| if strings.Contains(stderr, "deprecated") { | |
| t.Fatalf("warnings should be suppressed, got:\n%s", stderr) | |
| } | |
| actual := strings.TrimSpace(output.Stdout()) | |
| if actual != `bar` { | |
| t.Fatalf("expected output \"bar\", got: %#v", actual) | |
| } | |
| } | |
| func TestOutputJson_warningsSuppressed(t *testing.T) { | |
| // Pre-populate the inmem backend with a state containing an output value | |
| inmem.Reset() | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetOutputValue( | |
| addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), | |
| cty.StringVal("bar"), | |
| false, | |
| ) | |
| }) | |
| // Register a backend that wraps inmem with a deprecation warning, | |
| // simulating a backend like S3 whose PrepareConfig warns about | |
| // deprecated attributes (e.g. dynamodb_table). | |
| backendInit.Set("inmem", func() backend.Backend { | |
| return &deprecatedInmemBackend{Backend: inmem.New()} | |
| }) | |
| defer backendInit.Set("inmem", inmem.New) | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("output-backend-with-deprecation"), td) | |
| t.Chdir(td) | |
| // Write the state into the inmem backend's default workspace | |
| b := inmem.New() | |
| b.Configure(cty.ObjectVal(map[string]cty.Value{ | |
| "lock_id": cty.NullVal(cty.String), | |
| })) | |
| sMgr, sDiags := b.StateMgr(backend.DefaultStateName) | |
| if sDiags.HasErrors() { | |
| t.Fatalf("unexpected error: %s", sDiags.Err()) | |
| } | |
| sMgr.WriteState(originalState) | |
| if err := sMgr.PersistState(nil); err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| view, done := testView(t) | |
| c := &OutputCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-json"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("unexpected exit code %d\nstderr:\n%s", code, output.Stderr()) | |
| } | |
| stderr := output.Stderr() | |
| if strings.Contains(stderr, "deprecated") { | |
| t.Fatalf("warnings should be suppressed, got:\n%s", stderr) | |
| } | |
| expectedJson := `{ | |
| "foo":{ | |
| "sensitive":false, | |
| "type":"string", | |
| "value":"bar" | |
| } | |
| }` | |
| if diff := cmp.Diff(expectedJson, output.Stdout(), transformJSON); diff != "" { | |
| t.Fatalf("unexpected output: %s", diff) | |
| } | |
| } | |
| var transformJSON = cmp.FilterValues(func(x, y string) bool { | |
| xBytes := []byte(x) | |
| yBytes := []byte(y) | |
| return json.Valid(xBytes) && json.Valid(yBytes) | |
| }, cmp.Transformer("ParseJSON", func(in string) (out any) { | |
| inBytes := []byte(in) | |
| if err := json.Unmarshal(inBytes, &out); err != nil { | |
| panic(err) | |
| } | |
| return out | |
| })) |