You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/command/output_test.go

549 lines
13 KiB

// 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"
"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"), 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()
mockProvider.MockStates = map[string]interface{}{
"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
}))