Add -show-sensitive flag to tofu plan, apply, state-show and output commands ()

Signed-off-by: siddharthasonker95 <158144589+siddharthasonker95@users.noreply.github.com>
This commit is contained in:
Siddhartha Sonker 2024-07-22 15:28:57 +05:30 committed by GitHub
parent 5079292cb2
commit 579d74c409
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 431 additions and 9 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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".

View File

@ -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")

View File

@ -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(

View File

@ -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")

View File

@ -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(

View File

@ -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

View File

@ -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,
}
}

View File

@ -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))
}

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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 = <sensitive>" {
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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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.

View File

@ -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".

View File

@ -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.

View File

@ -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" {

View File

@ -0,0 +1,12 @@
variable "input" {
default = "Hello world"
}
output "notsensitive" {
value = "${var.input}"
}
output "sensitive" {
sensitive = true
value = "${var.input}"
}

View File

@ -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{

View File

@ -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 = <sensitive>\n", k))
continue
}
result := repl.FormatValue(v.Value, 0)
result := repl.FormatValue(vs.Value, 0)
outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result))
}
}

View File

@ -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

View File

@ -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
}