blob: b856bd52dfc9d0b7d210935b669410cd946930f0 [file] [edit]
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"bytes"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/cli"
testing_command "github.com/hashicorp/terraform/internal/command/testing"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/zclconf/go-cty/cty"
)
// ConsoleCommand is tested primarily with tests in the "repl" package.
// It is not tested here because the Console uses a readline-like library
// that takes over stdin/stdout. It is difficult to test directly. The
// core logic is tested in "repl"
//
// This file still contains some tests using the stdin-based input.
func TestConsole_basic(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)
p := testProvider()
ui := testUiWrapped(t)
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader("1+5\n"))()
outCloser := testStdoutCapture(t, &output)
args := []string{}
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if actual != "6\n" {
t.Fatalf("bad: %q", actual)
}
}
func TestConsole_tfvars(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-vars"), td)
t.Chdir(td)
// Write a terraform.tvars
varFilePath := filepath.Join(td, "terraform.tfvars")
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
t.Fatalf("err: %s", err)
}
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
},
},
},
},
}
ui := testUiWrapped(t)
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader("var.foo\n"))()
outCloser := testStdoutCapture(t, &output)
args := []string{}
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if actual != "\"bar\"\n" {
t.Fatalf("bad: %q", actual)
}
}
func TestConsole_unsetRequiredVars(t *testing.T) {
// This test is verifying that it's possible to run "terraform console"
// without providing values for all required variables, without
// "terraform console" producing an interactive prompt for those variables
// or producing errors. Instead, it should allow evaluation in that
// partial context but see the unset variables values as being unknown.
//
// This test fixture includes variable "foo" {}, which we are
// intentionally not setting here.
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-vars"), td)
t.Chdir(td)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
},
},
},
},
}
ui := testUiWrapped(t)
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader("var.foo\n"))()
outCloser := testStdoutCapture(t, &output)
args := []string{}
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if got, want := output.String(), "(known after apply)\n"; got != want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
}
}
func TestConsole_variables(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("variables"), td)
t.Chdir(td)
p := testProvider()
ui := testUiWrapped(t)
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
commands := map[string]string{
"var.foo\n": "\"bar\"\n",
"var.snack\n": "\"popcorn\"\n",
"var.secret_snack\n": "(sensitive value)\n",
"local.snack_bar\n": "[\n \"popcorn\",\n (sensitive value),\n]\n",
}
args := []string{}
for cmd, val := range commands {
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader(cmd))()
outCloser := testStdoutCapture(t, &output)
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if output.String() != val {
t.Fatalf("bad: %q, expected %q", actual, val)
}
}
}
func TestConsole_modules(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("modules"), td)
t.Chdir(td)
p := applyFixtureProvider()
ui := testUiWrapped(t)
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
commands := map[string]string{
"module.child.myoutput\n": "\"bar\"\n",
"module.count_child[0].myoutput\n": "\"bar\"\n",
"local.foo\n": "3\n",
}
args := []string{}
for cmd, val := range commands {
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader(cmd))()
outCloser := testStdoutCapture(t, &output)
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if output.String() != val {
t.Fatalf("bad: %q, expected %q", actual, val)
}
}
}
func TestConsole_modulesPlan(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("apply"), td)
t.Chdir(td)
p := applyFixtureProvider()
ui := testUiWrapped(t)
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
commands := map[string]string{
"test_instance.foo.ami\n": "\"bar\"\n",
}
// The -plan option means that we'll be evaluating expressions against
// a plan constructed from this configuration, instead of against its
// (non-existent) prior state.
args := []string{"-plan"}
for cmd, val := range commands {
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader(cmd))()
outCloser := testStdoutCapture(t, &output)
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if output.String() != val {
t.Fatalf("bad: %q, expected %q", actual, val)
}
}
}
func TestConsole_scope(t *testing.T) {
testCases := map[string]struct {
args []string
expectedConsoleEvals map[string]string
}{
"root-module": {
expectedConsoleEvals: map[string]string{
"var.root\n": "\"variable data\"\n",
"local.root\n": "\"variable data -> local\"\n",
// resource data is unknown as the state of this resource doesn't exist
"test_resource.root.value\n": "(known after apply)\n",
"module.child_single.child_output\n": "\"child module output\"\n",
"module.child_foreach[\"key1\"].child_output\n": "\"child module output\"\n",
"module.child_foreach[\"key2\"].child_output\n": "\"child module output\"\n",
"module.child_count[0].child_output\n": "\"child module output\"\n",
"module.child_count[1].child_output\n": "\"child module output\"\n",
},
},
"root-module-planeval": {
args: []string{"-plan"},
expectedConsoleEvals: map[string]string{
"var.root\n": "\"variable data\"\n",
"local.root\n": "\"variable data -> local\"\n",
"test_resource.root.value\n": "\"resource attr set to local -> variable data -> local\"\n",
"module.child_single.child_output\n": "\"child module output\"\n",
"module.child_foreach[\"key1\"].child_output\n": "\"child module output\"\n",
"module.child_foreach[\"key2\"].child_output\n": "\"child module output\"\n",
"module.child_count[0].child_output\n": "\"child module output\"\n",
"module.child_count[1].child_output\n": "\"child module output\"\n",
},
},
"child-single-module": {
args: []string{"-scope=module.child_single"},
expectedConsoleEvals: map[string]string{
"var.child_input\n": "\"variable data -> child\"\n",
"local.child\n": "\"variable data -> child -> local\"\n",
// resource data is unknown as the state of this resource doesn't exist
"test_resource.child.value\n": "(known after apply)\n",
"module.grandchild[\"key\"].grandchild_output\n": "\"grandchild module output\"\n",
},
},
"child-single-module-planeval": {
args: []string{"-plan", "-scope=module.child_single"},
expectedConsoleEvals: map[string]string{
"var.child_input\n": "\"variable data -> child\"\n",
"local.child\n": "\"variable data -> child -> local\"\n",
"test_resource.child.value\n": "\"resource attr set to local -> variable data -> child -> local\"\n",
"module.grandchild[\"key\"].grandchild_output\n": "\"grandchild module output\"\n",
},
},
"child-foreach-module": {
args: []string{`-scope=module.child_foreach["key1"]`},
expectedConsoleEvals: map[string]string{
"var.child_input\n": "\"variable data -> child[key1]\"\n",
"local.child\n": "\"variable data -> child[key1] -> local\"\n",
// resource data is unknown as the state of this resource doesn't exist
"test_resource.child.value\n": "(known after apply)\n",
"module.grandchild[\"key\"].grandchild_output\n": "\"grandchild module output\"\n",
},
},
"child-foreach-module-planeval": {
args: []string{"-plan", `-scope=module.child_foreach["key2"]`},
expectedConsoleEvals: map[string]string{
"var.child_input\n": "\"variable data -> child[key2]\"\n",
"local.child\n": "\"variable data -> child[key2] -> local\"\n",
"test_resource.child.value\n": "\"resource attr set to local -> variable data -> child[key2] -> local\"\n",
"module.grandchild[\"key\"].grandchild_output\n": "\"grandchild module output\"\n",
},
},
"child-count-module": {
args: []string{`-scope=module.child_count[0]`},
expectedConsoleEvals: map[string]string{
"var.child_input\n": "\"variable data -> child[0]\"\n",
"local.child\n": "\"variable data -> child[0] -> local\"\n",
// resource data is unknown as the state of this resource doesn't exist
"test_resource.child.value\n": "(known after apply)\n",
"module.grandchild[\"key\"].grandchild_output\n": "\"grandchild module output\"\n",
},
},
"child-count-module-planeval": {
args: []string{"-plan", `-scope=module.child_count[1]`},
expectedConsoleEvals: map[string]string{
"var.child_input\n": "\"variable data -> child[1]\"\n",
"local.child\n": "\"variable data -> child[1] -> local\"\n",
"test_resource.child.value\n": "\"resource attr set to local -> variable data -> child[1] -> local\"\n",
"module.grandchild[\"key\"].grandchild_output\n": "\"grandchild module output\"\n",
},
},
"grandchild-module": {
args: []string{`-scope=module.child_single.module.grandchild["key"]`},
expectedConsoleEvals: map[string]string{
"var.grandchild_input\n": "\"variable data -> child -> grandchild[key]\"\n",
"local.grandchild\n": "\"variable data -> child -> grandchild[key] -> local\"\n",
// resource data is unknown as the state of this resource doesn't exist
"test_resource.grandchild.value\n": "(known after apply)\n",
},
},
"grandchild-module-planeval": {
args: []string{"-plan", `-scope=module.child_count[0].module.grandchild["key"]`},
expectedConsoleEvals: map[string]string{
"var.grandchild_input\n": "\"variable data -> child[0] -> grandchild[key]\"\n",
"local.grandchild\n": "\"variable data -> child[0] -> grandchild[key] -> local\"\n",
"test_resource.grandchild.value\n": "\"resource attr set to local -> variable data -> child[0] -> grandchild[key] -> local\"\n",
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("console-nested-module-scopes"), td)
t.Chdir(td)
ui := cli.NewMockUi()
view, _ := testView(t)
p := testing_command.NewProvider(&testing_command.ResourceStore{}).Provider
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
tc.args = append(tc.args, []string{"-var", "root=variable data"}...)
for cmd, val := range tc.expectedConsoleEvals {
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader(cmd))()
outCloser := testStdoutCapture(t, &output)
code := c.Run(tc.args)
outCloser()
if code != 0 {
t.Errorf("bad exit code: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if output.String() != val {
t.Errorf("bad result for %q: %q, expected %q", cmd, actual, val)
}
}
})
}
}
func TestConsole_scope_errors(t *testing.T) {
testCases := map[string]struct {
args []string
expectedError string
}{
"invalid-traversal": {
args: []string{"-scope=foo."},
expectedError: `Dot must be followed by attribute name.`,
},
"invalid-module-address-syntax": {
args: []string{"-scope=test_resource.root"},
expectedError: `A module instance address must begin with "module."`,
},
"module-not-found": {
args: []string{"-scope=module.nonexistent"},
expectedError: `The module address "module.nonexistent" does not have an evaluation scope.`,
},
"module-not-found-planeval": {
args: []string{"-plan", "-scope=module.nonexistent[0]"},
expectedError: `The module address "module.nonexistent[0]" does not have an evaluation scope.`,
},
"module-instance-not-found": {
args: []string{"-scope=module.child_count[2]"},
expectedError: `The module address "module.child_count[2]" does not have an evaluation scope.`,
},
"module-instance-not-found-planeval": {
args: []string{"-plan", `-scope=module.child_foreach["key3"]`},
expectedError: "The module address \"module.child_foreach[\"key3\"]\" does not have an evaluation\nscope.",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("console-nested-module-scopes"), td)
t.Chdir(td)
ui := cli.NewMockUi()
view, _ := testView(t)
p := testing_command.NewProvider(&testing_command.ResourceStore{}).Provider
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
Ui: ui,
},
}
tc.args = append(tc.args, []string{"-var", "root=variable data"}...)
defer testStdinPipe(t, strings.NewReader(""))()
code := c.Run(tc.args)
if code == 0 {
t.Fatal("expected a non-zero exit code with error")
}
actual := ui.ErrorWriter.String()
if !strings.Contains(actual, tc.expectedError) {
t.Fatalf("expected error to include %q, but got:\n%s", tc.expectedError, actual)
}
})
}
}