cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
package views
|
|
|
|
|
|
|
|
import (
|
2021-02-16 14:48:31 +01:00
|
|
|
"strings"
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
"testing"
|
|
|
|
|
2021-05-17 21:07:38 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
2021-05-17 21:43:35 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/states"
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
"github.com/hashicorp/terraform/internal/terminal"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
)
|
|
|
|
|
2021-02-16 14:48:31 +01:00
|
|
|
// Test various single output values for human-readable UI. Note that since
|
|
|
|
// OutputHuman defers to repl.FormatValue to render a single value, most of the
|
|
|
|
// test coverage should be in that package.
|
|
|
|
func TestOutputHuman_single(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
|
|
|
value cty.Value
|
|
|
|
want string
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
"string": {
|
|
|
|
value: cty.StringVal("hello"),
|
|
|
|
want: "\"hello\"\n",
|
|
|
|
},
|
|
|
|
"list of maps": {
|
|
|
|
value: cty.ListVal([]cty.Value{
|
|
|
|
cty.MapVal(map[string]cty.Value{
|
|
|
|
"key": cty.StringVal("value"),
|
|
|
|
"key2": cty.StringVal("value2"),
|
|
|
|
}),
|
|
|
|
cty.MapVal(map[string]cty.Value{
|
|
|
|
"key": cty.StringVal("value"),
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
want: `tolist([
|
|
|
|
tomap({
|
|
|
|
"key" = "value"
|
|
|
|
"key2" = "value2"
|
|
|
|
}),
|
|
|
|
tomap({
|
|
|
|
"key" = "value"
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(arguments.ViewHuman, NewView(streams))
|
|
|
|
|
|
|
|
outputs := map[string]*states.OutputValue{
|
|
|
|
"foo": {Value: tc.value},
|
|
|
|
}
|
|
|
|
diags := v.Output("foo", outputs)
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
if !tc.wantErr {
|
|
|
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
} else if tc.wantErr {
|
|
|
|
t.Fatalf("succeeded, but want error")
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := done(t).Stdout(), tc.want; got != want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sensitive output values are rendered to the console intentionally when
|
|
|
|
// requesting a single output.
|
|
|
|
func TestOutput_sensitive(t *testing.T) {
|
|
|
|
testCases := map[string]arguments.ViewType{
|
|
|
|
"human": arguments.ViewHuman,
|
|
|
|
"json": arguments.ViewJSON,
|
|
|
|
"raw": arguments.ViewRaw,
|
|
|
|
}
|
|
|
|
for name, vt := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(vt, NewView(streams))
|
|
|
|
|
|
|
|
outputs := map[string]*states.OutputValue{
|
|
|
|
"foo": {
|
|
|
|
Value: cty.StringVal("secret"),
|
|
|
|
Sensitive: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
diags := v.Output("foo", outputs)
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test for substring match here because we don't care about exact
|
|
|
|
// output format in this test, just the presence of the sensitive
|
|
|
|
// value.
|
|
|
|
if got, want := done(t).Stdout(), "secret"; !strings.Contains(got, want) {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Showing all outputs is supported by human and JSON output format.
|
|
|
|
func TestOutput_all(t *testing.T) {
|
|
|
|
outputs := map[string]*states.OutputValue{
|
|
|
|
"foo": {
|
|
|
|
Value: cty.StringVal("secret"),
|
|
|
|
Sensitive: true,
|
|
|
|
},
|
|
|
|
"bar": {
|
|
|
|
Value: cty.ListVal([]cty.Value{cty.True, cty.False, cty.True}),
|
|
|
|
},
|
|
|
|
"baz": {
|
|
|
|
Value: cty.ObjectVal(map[string]cty.Value{
|
|
|
|
"boop": cty.NumberIntVal(5),
|
|
|
|
"beep": cty.StringVal("true"),
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
testCases := map[string]struct {
|
|
|
|
vt arguments.ViewType
|
|
|
|
want string
|
|
|
|
}{
|
|
|
|
"human": {
|
|
|
|
arguments.ViewHuman,
|
|
|
|
`bar = tolist([
|
|
|
|
true,
|
|
|
|
false,
|
|
|
|
true,
|
|
|
|
])
|
|
|
|
baz = {
|
|
|
|
"beep" = "true"
|
|
|
|
"boop" = 5
|
|
|
|
}
|
|
|
|
foo = <sensitive>
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
"json": {
|
|
|
|
arguments.ViewJSON,
|
|
|
|
`{
|
|
|
|
"bar": {
|
|
|
|
"sensitive": false,
|
|
|
|
"type": [
|
|
|
|
"list",
|
|
|
|
"bool"
|
|
|
|
],
|
|
|
|
"value": [
|
|
|
|
true,
|
|
|
|
false,
|
|
|
|
true
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"baz": {
|
|
|
|
"sensitive": false,
|
|
|
|
"type": [
|
|
|
|
"object",
|
|
|
|
{
|
|
|
|
"beep": "string",
|
|
|
|
"boop": "number"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"value": {
|
|
|
|
"beep": "true",
|
|
|
|
"boop": 5
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"foo": {
|
|
|
|
"sensitive": true,
|
|
|
|
"type": "string",
|
|
|
|
"value": "secret"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(tc.vt, NewView(streams))
|
|
|
|
diags := v.Output("", outputs)
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if got := done(t).Stdout(); got != tc.want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// JSON output format supports empty outputs by rendering an empty object
|
|
|
|
// without diagnostics.
|
|
|
|
func TestOutputJSON_empty(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(arguments.ViewJSON, NewView(streams))
|
|
|
|
|
|
|
|
diags := v.Output("", map[string]*states.OutputValue{})
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := done(t).Stdout(), "{}\n"; got != want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Human and raw formats render a warning if there are no outputs.
|
|
|
|
func TestOutput_emptyWarning(t *testing.T) {
|
|
|
|
testCases := map[string]arguments.ViewType{
|
|
|
|
"human": arguments.ViewHuman,
|
|
|
|
"raw": arguments.ViewRaw,
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, vt := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(vt, NewView(streams))
|
|
|
|
|
|
|
|
diags := v.Output("", map[string]*states.OutputValue{})
|
|
|
|
|
|
|
|
if got, want := done(t).Stdout(), ""; got != want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(diags) != 1 {
|
|
|
|
t.Fatalf("expected 1 diagnostic, got %d", len(diags))
|
|
|
|
}
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
t.Fatalf("unexpected error diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := diags[0].Description().Summary, "No outputs found"; got != want {
|
|
|
|
t.Errorf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Raw output is a simple unquoted output format designed for shell scripts,
|
|
|
|
// which relies on the cty.AsString() implementation. This test covers
|
|
|
|
// formatting for supported value types.
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
func TestOutputRaw(t *testing.T) {
|
|
|
|
values := map[string]cty.Value{
|
|
|
|
"str": cty.StringVal("bar"),
|
|
|
|
"multistr": cty.StringVal("bar\nbaz"),
|
|
|
|
"num": cty.NumberIntVal(2),
|
|
|
|
"bool": cty.True,
|
|
|
|
"obj": cty.EmptyObjectVal,
|
|
|
|
"null": cty.NullVal(cty.String),
|
2021-02-16 14:48:31 +01:00
|
|
|
"unknown": cty.UnknownVal(cty.String),
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
tests := map[string]struct {
|
|
|
|
WantOutput string
|
|
|
|
WantErr bool
|
|
|
|
}{
|
|
|
|
"str": {WantOutput: "bar"},
|
|
|
|
"multistr": {WantOutput: "bar\nbaz"},
|
|
|
|
"num": {WantOutput: "2"},
|
|
|
|
"bool": {WantOutput: "true"},
|
|
|
|
"obj": {WantErr: true},
|
|
|
|
"null": {WantErr: true},
|
2021-02-16 14:48:31 +01:00
|
|
|
"unknown": {WantErr: true},
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for name, test := range tests {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
2021-02-16 14:48:31 +01:00
|
|
|
v := NewOutput(arguments.ViewRaw, NewView(streams))
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
|
|
|
|
value := values[name]
|
|
|
|
outputs := map[string]*states.OutputValue{
|
|
|
|
name: {Value: value},
|
|
|
|
}
|
|
|
|
diags := v.Output(name, outputs)
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
if !test.WantErr {
|
|
|
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
} else if test.WantErr {
|
|
|
|
t.Fatalf("succeeded, but want error")
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := done(t).Stdout(), test.WantOutput; got != want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-02-16 14:48:31 +01:00
|
|
|
|
|
|
|
// Raw cannot render all outputs.
|
|
|
|
func TestOutputRaw_all(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(arguments.ViewRaw, NewView(streams))
|
|
|
|
|
|
|
|
outputs := map[string]*states.OutputValue{
|
|
|
|
"foo": {Value: cty.StringVal("secret")},
|
|
|
|
"bar": {Value: cty.True},
|
|
|
|
}
|
|
|
|
diags := v.Output("", outputs)
|
|
|
|
|
|
|
|
if got, want := done(t).Stdout(), ""; got != want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !diags.HasErrors() {
|
|
|
|
t.Fatalf("expected diagnostics, got %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := diags.Err().Error(), "Raw output format is only supported for single outputs"; got != want {
|
|
|
|
t.Errorf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// All outputs render an error if a specific output is requested which is
|
|
|
|
// missing from the map of outputs.
|
|
|
|
func TestOutput_missing(t *testing.T) {
|
|
|
|
testCases := map[string]arguments.ViewType{
|
|
|
|
"human": arguments.ViewHuman,
|
|
|
|
"json": arguments.ViewJSON,
|
|
|
|
"raw": arguments.ViewRaw,
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, vt := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
v := NewOutput(vt, NewView(streams))
|
|
|
|
|
|
|
|
diags := v.Output("foo", map[string]*states.OutputValue{
|
|
|
|
"bar": {Value: cty.StringVal("boop")},
|
|
|
|
})
|
|
|
|
|
|
|
|
if len(diags) != 1 {
|
|
|
|
t.Fatalf("expected 1 diagnostic, got %d", len(diags))
|
|
|
|
}
|
|
|
|
|
|
|
|
if !diags.HasErrors() {
|
|
|
|
t.Fatalf("expected error diagnostics, got %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := diags[0].Description().Summary, `Output "foo" not found`; got != want {
|
|
|
|
t.Errorf("unexpected diagnostics: %s", diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if got, want := done(t).Stdout(), ""; got != want {
|
|
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|