From 579d74c40923084c50d9edeead4454bb9f68336a Mon Sep 17 00:00:00 2001 From: Siddhartha Sonker <158144589+siddharthasonker95@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:28:57 +0530 Subject: [PATCH] Add `-show-sensitive` flag to tofu plan, apply, state-show and output commands (#1554) Signed-off-by: siddharthasonker95 <158144589+siddharthasonker95@users.noreply.github.com> --- CHANGELOG.md | 1 + internal/command/apply.go | 4 + internal/command/apply_test.go | 37 ++++++ internal/command/arguments/apply.go | 4 + internal/command/arguments/output.go | 4 + internal/command/arguments/plan.go | 4 + internal/command/arguments/show.go | 4 + internal/command/arguments/view.go | 3 + internal/command/jsonformat/computed/diff.go | 9 +- .../computed/renderers/sensitive.go | 5 + internal/command/jsonformat/plan.go | 4 +- internal/command/jsonformat/renderer.go | 5 +- internal/command/output.go | 4 + internal/command/output_test.go | 70 +++++++++++ internal/command/plan.go | 4 + internal/command/plan_test.go | 54 ++++++++ internal/command/show.go | 4 + internal/command/show_test.go | 68 ++++++++++ internal/command/state_show.go | 7 ++ internal/command/state_show_test.go | 118 ++++++++++++++++++ .../testdata/plan-sensitive-output/main.tf | 12 ++ internal/command/views/operation.go | 1 + internal/command/views/output.go | 6 +- internal/command/views/show.go | 1 + internal/command/views/view.go | 7 ++ 25 files changed, 431 insertions(+), 9 deletions(-) create mode 100644 internal/command/testdata/plan-sensitive-output/main.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 50439e0f54..04442a2989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ UPGRADE NOTES: NEW FEATURES: ENHANCEMENTS: +* Added `-show-sensitive` flag to tofu plan, apply, state-show and output commands to display sensitive data in output. ([#1554](https://github.com/opentofu/opentofu/pull/1554)) BUG FIXES: diff --git a/internal/command/apply.go b/internal/command/apply.go index 9c8a18b9ee..eb7461b428 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -49,6 +49,8 @@ func (c *ApplyCommand) Run(rawArgs []string) int { args, diags = arguments.ParseApply(rawArgs) } + c.View.SetShowSensitive(args.ShowSensitive) + // Instantiate the view, even if there are flag errors, so that we render // diagnostics according to the desired view view := views.NewApply(args.ViewType, c.Destroy, c.View) @@ -378,6 +380,8 @@ Options: "-state". This can be used to preserve the old state. + -show-sensitive If specified, sensitive values will be displayed. + If you don't provide a saved plan file then this command will also accept all of the plan-customization options accepted by the tofu plan command. For more information on those options, run: diff --git a/internal/command/apply_test.go b/internal/command/apply_test.go index 8e0221f4bf..d67bfe86da 100644 --- a/internal/command/apply_test.go +++ b/internal/command/apply_test.go @@ -2167,6 +2167,43 @@ func TestApply_warnings(t *testing.T) { }) } +func TestApply_showSensitiveArg(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("apply-sensitive-output"), td) + defer testChdir(t, td)() + + p := testProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + statePath := testTempFile(t) + + args := []string{ + "-state", statePath, + "-auto-approve", + "-show-sensitive", + } + + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + stdout := output.Stdout() + if !strings.Contains(stdout, "notsensitive = \"Hello world\"") { + t.Fatalf("bad: output should contain 'notsensitive' output\n%s", stdout) + } + if !strings.Contains(stdout, "sensitive = \"Hello world\"") { + t.Fatalf("bad: output should contain 'sensitive' output\n%s", stdout) + } +} + // applyFixtureSchema returns a schema suitable for processing the // configuration in testdata/apply . This schema should be // assigned to a mock provider named "test". diff --git a/internal/command/arguments/apply.go b/internal/command/arguments/apply.go index db040b9a71..3baa1498cf 100644 --- a/internal/command/arguments/apply.go +++ b/internal/command/arguments/apply.go @@ -31,6 +31,9 @@ type Apply struct { // ViewType specifies which output format to use ViewType ViewType + + // ShowSensitive is used to display the value of variables marked as sensitive. + ShowSensitive bool } // ParseApply processes CLI arguments, returning an Apply value and errors. @@ -47,6 +50,7 @@ func ParseApply(args []string) (*Apply, tfdiags.Diagnostics) { cmdFlags := extendedFlagSet("apply", apply.State, apply.Operation, apply.Vars) cmdFlags.BoolVar(&apply.AutoApprove, "auto-approve", false, "auto-approve") cmdFlags.BoolVar(&apply.InputEnabled, "input", true, "input") + cmdFlags.BoolVar(&apply.ShowSensitive, "show-sensitive", false, "displays sensitive values") var json bool cmdFlags.BoolVar(&json, "json", false, "json") diff --git a/internal/command/arguments/output.go b/internal/command/arguments/output.go index 23f80c642a..6219b6566b 100644 --- a/internal/command/arguments/output.go +++ b/internal/command/arguments/output.go @@ -23,6 +23,9 @@ type Output struct { ViewType ViewType Vars *Vars + + // ShowSensitive is used to display the value of variables marked as sensitive. + ShowSensitive bool } // ParseOutput processes CLI arguments, returning an Output value and errors. @@ -40,6 +43,7 @@ func ParseOutput(args []string) (*Output, tfdiags.Diagnostics) { cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.BoolVar(&rawOutput, "raw", false, "raw") cmdFlags.StringVar(&statePath, "state", "", "path") + cmdFlags.BoolVar(&output.ShowSensitive, "show-sensitive", false, "displays sensitive values") if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( diff --git a/internal/command/arguments/plan.go b/internal/command/arguments/plan.go index 23f23e2df4..3f51401c60 100644 --- a/internal/command/arguments/plan.go +++ b/internal/command/arguments/plan.go @@ -34,6 +34,9 @@ type Plan struct { // ViewType specifies which output format to use ViewType ViewType + + // ShowSensitive is used to display the value of variables marked as sensitive. + ShowSensitive bool } // ParsePlan processes CLI arguments, returning a Plan value and errors. @@ -52,6 +55,7 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) { cmdFlags.BoolVar(&plan.InputEnabled, "input", true, "input") cmdFlags.StringVar(&plan.OutPath, "out", "", "out") cmdFlags.StringVar(&plan.GenerateConfigPath, "generate-config-out", "", "generate-config-out") + cmdFlags.BoolVar(&plan.ShowSensitive, "show-sensitive", false, "displays sensitive values") var json bool cmdFlags.BoolVar(&json, "json", false, "json") diff --git a/internal/command/arguments/show.go b/internal/command/arguments/show.go index d297922c1b..c1e6e3bf44 100644 --- a/internal/command/arguments/show.go +++ b/internal/command/arguments/show.go @@ -19,6 +19,9 @@ type Show struct { ViewType ViewType Vars *Vars + + // ShowSensitive is used to display the value of variables marked as sensitive. + ShowSensitive bool } // ParseShow processes CLI arguments, returning a Show value and errors. @@ -34,6 +37,7 @@ func ParseShow(args []string) (*Show, tfdiags.Diagnostics) { var jsonOutput bool cmdFlags := extendedFlagSet("show", nil, nil, show.Vars) cmdFlags.BoolVar(&jsonOutput, "json", false, "json") + cmdFlags.BoolVar(&show.ShowSensitive, "show-sensitive", false, "displays sensitive values") if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( diff --git a/internal/command/arguments/view.go b/internal/command/arguments/view.go index 2d64e8bf60..3b6e11be26 100644 --- a/internal/command/arguments/view.go +++ b/internal/command/arguments/view.go @@ -19,6 +19,9 @@ type View struct { // Concise is used to reduce the level of noise in the output and display // only the important details. Concise bool + + // ShowSensitive is used to display the value of variables marked as sensitive. + ShowSensitive bool } // ParseView processes CLI arguments, returning a View value and a diff --git a/internal/command/jsonformat/computed/diff.go b/internal/command/jsonformat/computed/diff.go index 2616c618fc..41efebb11c 100644 --- a/internal/command/jsonformat/computed/diff.go +++ b/internal/command/jsonformat/computed/diff.go @@ -96,13 +96,17 @@ type RenderHumanOpts struct { // HideDiffActionSymbols tells the renderer not to show the '+'/'-' symbols // and to skip the places where the symbols would result in an offset. HideDiffActionSymbols bool + + // ShowSensitive is used to display the value of variables marked as sensitive. + ShowSensitive bool } // NewRenderHumanOpts creates a new RenderHumanOpts struct with the required // fields set. -func NewRenderHumanOpts(colorize *colorstring.Colorize) RenderHumanOpts { +func NewRenderHumanOpts(colorize *colorstring.Colorize, showSensitive bool) RenderHumanOpts { return RenderHumanOpts{ - Colorize: colorize, + Colorize: colorize, + ShowSensitive: showSensitive, } } @@ -121,5 +125,6 @@ func (opts RenderHumanOpts) Clone() RenderHumanOpts { // children should override their internal Replace logic, instead of // an ancestor making the switch and affecting the entire tree. OverrideForcesReplacement: false, + ShowSensitive: opts.ShowSensitive, } } diff --git a/internal/command/jsonformat/computed/renderers/sensitive.go b/internal/command/jsonformat/computed/renderers/sensitive.go index 442ea3df9c..1b7c960b6e 100644 --- a/internal/command/jsonformat/computed/renderers/sensitive.go +++ b/internal/command/jsonformat/computed/renderers/sensitive.go @@ -30,6 +30,11 @@ type sensitiveRenderer struct { } func (renderer sensitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + // If the -show-sensitive argument is set, then invoke RenderHuman with the inner computed.Diff to display sensitive values. + if opts.ShowSensitive { + return renderer.inner.RenderHuman(indent, opts) + } + return fmt.Sprintf("(sensitive value)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) } diff --git a/internal/command/jsonformat/plan.go b/internal/command/jsonformat/plan.go index a21d5e3ab4..e9236e7fca 100644 --- a/internal/command/jsonformat/plan.go +++ b/internal/command/jsonformat/plan.go @@ -274,7 +274,7 @@ func renderHumanDiffOutputs(renderer Renderer, outputs map[string]computed.Diff) for _, key := range keys { output := outputs[key] if output.Action != plans.NoOp { - rendered = append(rendered, fmt.Sprintf("%s %-*s = %s", renderer.Colorize.Color(format.DiffActionSymbol(output.Action)), escapedKeyMaxLen, escapedKeys[key], output.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize)))) + rendered = append(rendered, fmt.Sprintf("%s %-*s = %s", renderer.Colorize.Color(format.DiffActionSymbol(output.Action)), escapedKeyMaxLen, escapedKeys[key], output.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize, renderer.ShowSensitive)))) } } return strings.Join(rendered, "\n") @@ -356,7 +356,7 @@ func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool) var buf bytes.Buffer buf.WriteString(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause))) - opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts := computed.NewRenderHumanOpts(renderer.Colorize, renderer.ShowSensitive) if action == plans.Forget { opts.HideDiffActionSymbols = true diff --git a/internal/command/jsonformat/renderer.go b/internal/command/jsonformat/renderer.go index ea0bf0c494..9851133a4b 100644 --- a/internal/command/jsonformat/renderer.go +++ b/internal/command/jsonformat/renderer.go @@ -82,6 +82,7 @@ type Renderer struct { Colorize *colorstring.Colorize RunningInAutomation bool + ShowSensitive bool } func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...plans.Quality) { @@ -106,7 +107,7 @@ func (renderer Renderer) RenderHumanState(state State) { return } - opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts := computed.NewRenderHumanOpts(renderer.Colorize, renderer.ShowSensitive) opts.ShowUnchangedChildren = true opts.HideDiffActionSymbols = true @@ -143,7 +144,7 @@ func (renderer Renderer) RenderLog(log *JSONLog) error { return err } - opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts := computed.NewRenderHumanOpts(renderer.Colorize, renderer.ShowSensitive) opts.ShowUnchangedChildren = true outputDiff := differ.ComputeDiffForType(change, ctype) diff --git a/internal/command/output.go b/internal/command/output.go index 0e0001671c..772826e21c 100644 --- a/internal/command/output.go +++ b/internal/command/output.go @@ -35,6 +35,8 @@ func (c *OutputCommand) Run(rawArgs []string) int { return 1 } + c.View.SetShowSensitive(args.ShowSensitive) + view := views.NewOutput(args.ViewType, c.View) // Inject variables from args into meta for static evaluation @@ -149,6 +151,8 @@ Options: string directly, rather than a human-oriented representation of the value. + -show-sensitive If specified, sensitive values will be displayed. + -var 'foo=bar' Set a value for one of the input variables in the root module of the configuration. Use this option more than once to set more than one variable. diff --git a/internal/command/output_test.go b/internal/command/output_test.go index ca98aef636..541caffd0b 100644 --- a/internal/command/output_test.go +++ b/internal/command/output_test.go @@ -325,3 +325,73 @@ func TestOutput_stateDefault(t *testing.T) { t.Fatalf("bad: %#v", actual) } } + +func TestOutput_showSensitiveArg(t *testing.T) { + originalState := stateWithSensitiveValueForOutput() + + statePath := testStateFile(t, originalState) + + view, done := testView(t) + c := &OutputCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + View: view, + }, + } + + args := []string{ + "-state", statePath, + "-show-sensitive", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + actual := strings.TrimSpace(output.Stdout()) + if actual != "foo = \"bar\"" { + t.Fatalf("bad: %#v", actual) + } +} + +func TestOutput_withoutShowSensitiveArg(t *testing.T) { + originalState := stateWithSensitiveValueForOutput() + + 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()) + } + + actual := strings.TrimSpace(output.Stdout()) + if actual != "foo = " { + t.Fatalf("bad: %#v", actual) + } +} + +// stateWithSensitiveValueForOutput return a state with an output value +// marked as sensitive. +func stateWithSensitiveValueForOutput() *states.State { + state := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + true, + ) + }) + return state +} diff --git a/internal/command/plan.go b/internal/command/plan.go index 97e8fd01c4..f80f69a541 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -36,6 +36,8 @@ func (c *PlanCommand) Run(rawArgs []string) int { // Parse and validate flags args, diags := arguments.ParsePlan(rawArgs) + c.View.SetShowSensitive(args.ShowSensitive) + // Instantiate the view, even if there are flag errors, so that we render // diagnostics according to the desired view view := views.NewPlan(args.ViewType, c.View) @@ -288,6 +290,8 @@ Other Options: -state=statefile A legacy option used for the local backend only. See the local backend's documentation for more information. + + -show-sensitive If specified, sensitive values will be displayed. ` return strings.TrimSpace(helpText) } diff --git a/internal/command/plan_test.go b/internal/command/plan_test.go index 492f466dac..5768f4dd63 100644 --- a/internal/command/plan_test.go +++ b/internal/command/plan_test.go @@ -1655,6 +1655,60 @@ func planFixtureSchema() *providers.GetProviderSchemaResponse { } } +func TestPlan_showSensitiveArg(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("plan-sensitive-output"), td) + defer testChdir(t, td)() + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-show-sensitive", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad status code: \n%s", output.Stderr()) + } + + if got, want := output.Stdout(), "sensitive = \"Hello world\""; !strings.Contains(got, want) { + t.Fatalf("got incorrect output, want %q, got:\n%s", want, got) + } +} + +func TestPlan_withoutShowSensitiveArg(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("plan-sensitive-output"), td) + defer testChdir(t, td)() + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{} + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad status code: \n%s", output.Stderr()) + } + + if got, want := output.Stdout(), "sensitive = (sensitive value)"; !strings.Contains(got, want) { + t.Fatalf("got incorrect output, want %q, got:\n%s", want, got) + } +} + // planFixtureProvider returns a mock provider that is configured for basic // operation with the configuration in testdata/plan. This mock has // GetSchemaResponse and PlanResourceChangeFn populated, with the plan diff --git a/internal/command/show.go b/internal/command/show.go index 2a783d9e17..4d49765c2e 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -69,6 +69,7 @@ func (c *ShowCommand) Run(rawArgs []string) int { return 1 } c.viewType = args.ViewType + c.View.SetShowSensitive(args.ShowSensitive) // Set up view view := views.NewShow(args.ViewType, c.View) @@ -114,9 +115,12 @@ Usage: tofu [global options] show [options] [path] Options: -no-color If specified, output won't contain any color. + -json If specified, output the OpenTofu plan or state in a machine-readable form. + -show-sensitive If specified, sensitive values will be displayed. + -var 'foo=bar' Set a value for one of the input variables in the root module of the configuration. Use this option more than once to set more than one variable. diff --git a/internal/command/show_test.go b/internal/command/show_test.go index da4a8e8f2f..401ab02e97 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -1000,6 +1000,74 @@ func TestShow_corruptStatefile(t *testing.T) { } } +func TestShow_showSensitiveArg(t *testing.T) { + originalState := stateWithSensitiveValueForShow() + + testStateFileDefault(t, originalState) + + view, done := testView(t) + c := &ShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + View: view, + }, + } + + args := []string{ + "-show-sensitive", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + actual := strings.TrimSpace(output.Stdout()) + expected := "Outputs:\n\nfoo = \"bar\"" + if actual != expected { + t.Fatalf("got incorrect output: %#v", actual) + } +} + +func TestShow_withoutShowSensitiveArg(t *testing.T) { + originalState := stateWithSensitiveValueForShow() + + testStateFileDefault(t, originalState) + + view, done := testView(t) + c := &ShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + View: view, + }, + } + + code := c.Run([]string{}) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + actual := strings.TrimSpace(output.Stdout()) + expected := "Outputs:\n\nfoo = (sensitive value)" + if actual != expected { + t.Fatalf("got incorrect output: %#v", actual) + } +} + +// stateWithSensitiveValueForShow return a state with an output value +// marked as sensitive. +func stateWithSensitiveValueForShow() *states.State { + state := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + true, + ) + }) + return state +} + // showFixtureSchema returns a schema suitable for processing the configuration // in testdata/show. This schema should be assigned to a mock provider // named "test". diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 99a61b5027..42b43f80f7 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -35,6 +35,10 @@ func (c *StateShowCommand) Run(args []string) int { cmdFlags := c.Meta.defaultFlagSet("state show") c.Meta.varFlagSet(cmdFlags) cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") + + showSensitive := false + cmdFlags.BoolVar(&showSensitive, "show-sensitive", false, "displays sensitive values") + if err := cmdFlags.Parse(args); err != nil { c.Streams.Eprintf("Error parsing command-line flags: %s\n", err.Error()) return 1 @@ -187,6 +191,7 @@ func (c *StateShowCommand) Run(args []string) int { Streams: c.Streams, Colorize: c.Colorize(), RunningInAutomation: c.RunningInAutomation, + ShowSensitive: showSensitive, } renderer.RenderHumanState(jstate) @@ -209,6 +214,8 @@ Options: up OpenTofu-managed resources. By default it will use the state "terraform.tfstate" if it exists. + -show-sensitive If specified, sensitive values will be displayed. + -var 'foo=bar' Set a value for one of the input variables in the root module of the configuration. Use this option more than once to set more than one variable. diff --git a/internal/command/state_show_test.go b/internal/command/state_show_test.go index 06ff9b06e1..39b00b5538 100644 --- a/internal/command/state_show_test.go +++ b/internal/command/state_show_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/providers" @@ -270,6 +271,123 @@ func TestStateShow_configured_provider(t *testing.T) { } } +func TestStateShow_withoutShowSensitiveArg(t *testing.T) { + state := stateWithSensitiveValueForStateShow() + statePath := testStateFile(t, state) + + p := testProvider() + p.GetProviderSchemaResponse = providerWithSensitiveValueForStateShow() + + streams, done := terminal.StreamsForTesting(t) + c := &StateShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Streams: streams, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.foo", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + expected := `# test_instance.foo: +resource "test_instance" "foo" { + bar = "value" + foo = "value" + id = (sensitive value) +}` + actual := strings.TrimSpace(output.Stdout()) + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Fatalf("got incorrect output\n %v", diff) + } +} + +func TestStateShow_showSensitiveArg(t *testing.T) { + state := stateWithSensitiveValueForStateShow() + statePath := testStateFile(t, state) + + p := testProvider() + p.GetProviderSchemaResponse = providerWithSensitiveValueForStateShow() + + streams, done := terminal.StreamsForTesting(t) + c := &StateShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Streams: streams, + }, + } + + args := []string{ + "-show-sensitive", + "-state", statePath, + "test_instance.foo", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + expected := `# test_instance.foo: +resource "test_instance" "foo" { + bar = "value" + foo = "value" + id = "bar" +}` + actual := strings.TrimSpace(output.Stdout()) + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Fatalf("got incorrect output\n %v", diff) + } +} + +// stateWithSensitiveValueForStateShow returns a state with a resource +// instance. +func stateWithSensitiveValueForStateShow() *states.State { + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + + return state +} + +// providerWithSensitiveValueForStateShow returns a provider schema response +// with the "id" attribute flagged as sensitive. +func providerWithSensitiveValueForStateShow() *providers.GetProviderSchemaResponse { + return &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true, Sensitive: true}, + "foo": {Type: cty.String, Optional: true}, + "bar": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + } +} + const testStateShowOutput = ` # test_instance.foo: resource "test_instance" "foo" { diff --git a/internal/command/testdata/plan-sensitive-output/main.tf b/internal/command/testdata/plan-sensitive-output/main.tf new file mode 100644 index 0000000000..87994ae9fc --- /dev/null +++ b/internal/command/testdata/plan-sensitive-output/main.tf @@ -0,0 +1,12 @@ +variable "input" { + default = "Hello world" +} + +output "notsensitive" { + value = "${var.input}" +} + +output "sensitive" { + sensitive = true + value = "${var.input}" +} diff --git a/internal/command/views/operation.go b/internal/command/views/operation.go index 392e5334a2..d5ef27659b 100644 --- a/internal/command/views/operation.go +++ b/internal/command/views/operation.go @@ -105,6 +105,7 @@ func (v *OperationHuman) Plan(plan *plans.Plan, schemas *tofu.Schemas) { Colorize: v.view.colorize, Streams: v.view.streams, RunningInAutomation: v.inAutomation, + ShowSensitive: v.view.showSensitive, } jplan := jsonformat.Plan{ diff --git a/internal/command/views/output.go b/internal/command/views/output.go index e6eef58b22..fae9a97b03 100644 --- a/internal/command/views/output.go +++ b/internal/command/views/output.go @@ -84,13 +84,13 @@ func (v *OutputHuman) Output(name string, outputs map[string]*states.OutputValue sort.Strings(ks) for _, k := range ks { - v := outputs[k] - if v.Sensitive { + vs := outputs[k] + if vs.Sensitive && !v.view.showSensitive { outputBuf.WriteString(fmt.Sprintf("%s = \n", k)) continue } - result := repl.FormatValue(v.Value, 0) + result := repl.FormatValue(vs.Value, 0) outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result)) } } diff --git a/internal/command/views/show.go b/internal/command/views/show.go index a351b4afed..3538cffd16 100644 --- a/internal/command/views/show.go +++ b/internal/command/views/show.go @@ -53,6 +53,7 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON * Colorize: v.view.colorize, Streams: v.view.streams, RunningInAutomation: v.view.runningInAutomation, + ShowSensitive: v.view.showSensitive, } // Prefer to display a pre-built JSON plan, if we got one; then, fall back diff --git a/internal/command/views/view.go b/internal/command/views/view.go index 178d949b16..07a48c2426 100644 --- a/internal/command/views/view.go +++ b/internal/command/views/view.go @@ -33,6 +33,9 @@ type View struct { // only the important details. concise bool + // showSensitive is used to display the value of variables marked as sensitive. + showSensitive bool + // This unfortunate wart is required to enable rendering of diagnostics which // have associated source code in the configuration. This function pointer // will be dereferenced as late as possible when rendering diagnostics in @@ -170,3 +173,7 @@ func (v *View) errorColumns() int { func (v *View) outputHorizRule() { v.streams.Println(format.HorizontalRule(v.colorize, v.outputColumns())) } + +func (v *View) SetShowSensitive(showSensitive bool) { + v.showSensitive = showSensitive +}