command/format: Include unknown values in diagnostics

Previously when printing the relevant variables involved in a failed
expression evaluation we would just skip over unknown values entirely.

There are some errors, though, which are _caused by_ a value being
unknown, in which case it's helpful to show which of the inputs to that
expression were known vs. unknown so that the user can limit their further
investigation only to the unknown ones.

While here I also added a special case for sensitive values that overrides
all other display, because we don't know what about a value is sensitive
and so better to give nothing away at the expense of a slightly less
helpful error message.
This commit is contained in:
Martin Atkins 2020-10-28 18:21:51 -07:00
parent e2c64bc255
commit d24fdce5b8
2 changed files with 223 additions and 2 deletions

View File

@ -149,9 +149,17 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
continue Traversals // don't show duplicates when the same variable is referenced multiple times continue Traversals // don't show duplicates when the same variable is referenced multiple times
} }
switch { switch {
case val.IsMarked():
// We won't say anything at all about sensitive values,
// because we might give away something that was
// sensitive about them.
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] has a sensitive value"), traversalStr))
case !val.IsKnown(): case !val.IsKnown():
// Can't say anything about this yet, then. if ty := val.Type(); ty != cty.DynamicPseudoType {
continue Traversals stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is a %s, known only after apply"), traversalStr, ty.FriendlyName()))
} else {
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] will be known only after apply"), traversalStr))
}
case val.IsNull(): case val.IsNull():
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr)) stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr))
default: default:

View File

@ -1,15 +1,228 @@
package format package format
import ( import (
"strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
func TestDiagnostic(t *testing.T) {
tests := map[string]struct {
Diag interface{}
Want string
}{
"sourceless error": {
tfdiags.Sourceless(
tfdiags.Error,
"A sourceless error",
"It has no source references but it does have a pretty long detail that should wrap over multiple lines.",
),
`
[bold][red]Error: [reset][bold]A sourceless error[reset]
It has no source references but it does
have a pretty long detail that should
wrap over multiple lines.
`,
},
"sourceless warning": {
tfdiags.Sourceless(
tfdiags.Warning,
"A sourceless warning",
"It has no source references but it does have a pretty long detail that should wrap over multiple lines.",
),
`
[bold][yellow]Warning: [reset][bold]A sourceless warning[reset]
It has no source references but it does
have a pretty long detail that should
wrap over multiple lines.
`,
},
"error with source code subject": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad bad bad",
Detail: "Whatever shall we do?",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
},
`
[bold][red]Error: [reset][bold]Bad bad bad[reset]
on test.tf line 1:
1: test [underline]source[reset] code
Whatever shall we do?
`,
},
"error with source code subject and known expression": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad bad bad",
Detail: "Whatever shall we do?",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "boop"},
hcl.TraverseAttr{Name: "beep"},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"beep": cty.StringVal("blah"),
}),
},
},
},
`
[bold][red]Error: [reset][bold]Bad bad bad[reset]
on test.tf line 1:
1: test [underline]source[reset] code
[dark_gray]|----------------[reset]
[dark_gray]|[reset] [bold]boop.beep[reset] is "blah"
Whatever shall we do?
`,
},
"error with source code subject and expression referring to sensitive value": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad bad bad",
Detail: "Whatever shall we do?",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "boop"},
hcl.TraverseAttr{Name: "beep"},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"beep": cty.StringVal("blah").Mark("sensitive"),
}),
},
},
},
`
[bold][red]Error: [reset][bold]Bad bad bad[reset]
on test.tf line 1:
1: test [underline]source[reset] code
[dark_gray]|----------------[reset]
[dark_gray]|[reset] [bold]boop.beep[reset] has a sensitive value
Whatever shall we do?
`,
},
"error with source code subject and unknown string expression": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad bad bad",
Detail: "Whatever shall we do?",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "boop"},
hcl.TraverseAttr{Name: "beep"},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"beep": cty.UnknownVal(cty.String),
}),
},
},
},
`
[bold][red]Error: [reset][bold]Bad bad bad[reset]
on test.tf line 1:
1: test [underline]source[reset] code
[dark_gray]|----------------[reset]
[dark_gray]|[reset] [bold]boop.beep[reset] is a string, known only after apply
Whatever shall we do?
`,
},
"error with source code subject and unknown expression of unknown type": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad bad bad",
Detail: "Whatever shall we do?",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "boop"},
hcl.TraverseAttr{Name: "beep"},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"boop": cty.ObjectVal(map[string]cty.Value{
"beep": cty.UnknownVal(cty.DynamicPseudoType),
}),
},
},
},
`
[bold][red]Error: [reset][bold]Bad bad bad[reset]
on test.tf line 1:
1: test [underline]source[reset] code
[dark_gray]|----------------[reset]
[dark_gray]|[reset] [bold]boop.beep[reset] will be known only after apply
Whatever shall we do?
`,
},
}
sources := map[string][]byte{
"test.tf": []byte(`test source code`),
}
// This empty Colorize just passes through all of the formatting codes
// untouched, because it doesn't define any formatting keywords.
colorize := &colorstring.Colorize{}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var diags tfdiags.Diagnostics
diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic
diag := diags[0]
got := strings.TrimSpace(Diagnostic(diag, sources, colorize, 40))
want := strings.TrimSpace(test.Want)
if got != want {
t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)
}
})
}
}
func TestDiagnosticWarningsCompact(t *testing.T) { func TestDiagnosticWarningsCompact(t *testing.T) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.SimpleWarning("foo")) diags = diags.Append(tfdiags.SimpleWarning("foo"))