cli: Migrate validate command to views
This commit is contained in:
parent
c6278bbe37
commit
dd380d0b58
|
@ -0,0 +1,59 @@
|
|||
package arguments
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
// Validate represents the command-line arguments for the validate command.
|
||||
type Validate struct {
|
||||
// Path is the directory containing the configuration to be validated. If
|
||||
// unspecified, validate will use the current directory.
|
||||
Path string
|
||||
|
||||
// ViewType specifies which output format to use: human, JSON, or "raw".
|
||||
ViewType ViewType
|
||||
}
|
||||
|
||||
// ParseValidate processes CLI arguments, returning a Validate value and errors.
|
||||
// If errors are encountered, a Validate value is still returned representing
|
||||
// the best effort interpretation of the arguments.
|
||||
func ParseValidate(args []string) (*Validate, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
validate := &Validate{
|
||||
Path: ".",
|
||||
}
|
||||
|
||||
var jsonOutput bool
|
||||
cmdFlags := defaultFlagSet("validate")
|
||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
args = cmdFlags.Args()
|
||||
if len(args) > 1 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Too many command line arguments",
|
||||
"Expected at most one positional argument.",
|
||||
))
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
validate.Path = args[0]
|
||||
}
|
||||
|
||||
switch {
|
||||
case jsonOutput:
|
||||
validate.ViewType = ViewJSON
|
||||
default:
|
||||
validate.ViewType = ViewHuman
|
||||
}
|
||||
|
||||
return validate, diags
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package arguments
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
func TestParseValidate_valid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *Validate
|
||||
}{
|
||||
"defaults": {
|
||||
nil,
|
||||
&Validate{
|
||||
Path: ".",
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
},
|
||||
"json": {
|
||||
[]string{"-json"},
|
||||
&Validate{
|
||||
Path: ".",
|
||||
ViewType: ViewJSON,
|
||||
},
|
||||
},
|
||||
"path": {
|
||||
[]string{"-json", "foo"},
|
||||
&Validate{
|
||||
Path: "foo",
|
||||
ViewType: ViewJSON,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := ParseValidate(tc.args)
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValidate_invalid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *Validate
|
||||
wantDiags tfdiags.Diagnostics
|
||||
}{
|
||||
"unknown flag": {
|
||||
[]string{"-boop"},
|
||||
&Validate{
|
||||
Path: ".",
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
"flag provided but not defined: -boop",
|
||||
),
|
||||
},
|
||||
},
|
||||
"too many arguments": {
|
||||
[]string{"-json", "bar", "baz"},
|
||||
&Validate{
|
||||
Path: "bar",
|
||||
ViewType: ViewJSON,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Too many command line arguments",
|
||||
"Expected at most one positional argument.",
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, gotDiags := ParseValidate(tc.args)
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
if !reflect.DeepEqual(gotDiags, tc.wantDiags) {
|
||||
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
viewsjson "github.com/hashicorp/terraform/command/views/json"
|
||||
"github.com/hashicorp/terraform/command/arguments"
|
||||
"github.com/hashicorp/terraform/command/views"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
@ -18,43 +18,37 @@ type ValidateCommand struct {
|
|||
Meta
|
||||
}
|
||||
|
||||
func (c *ValidateCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args)
|
||||
func (c *ValidateCommand) Run(rawArgs []string) int {
|
||||
// Parse and apply global view arguments
|
||||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
|
||||
var jsonOutput bool
|
||||
cmdFlags := c.Meta.defaultFlagSet("validate")
|
||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
||||
// Parse and validate flags
|
||||
args, diags := arguments.ParseValidate(rawArgs)
|
||||
if diags.HasErrors() {
|
||||
c.View.Diagnostics(diags)
|
||||
c.View.HelpPrompt("validate")
|
||||
return 1
|
||||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
view := views.NewValidate(args.ViewType, c.View)
|
||||
|
||||
// After this point, we must only produce JSON output if JSON mode is
|
||||
// enabled, so all errors should be accumulated into diags and we'll
|
||||
// print out a suitable result at the end, depending on the format
|
||||
// selection. All returns from this point on must be tail-calls into
|
||||
// c.showResults in order to produce the expected output.
|
||||
args = cmdFlags.Args()
|
||||
// view.Results in order to produce the expected output.
|
||||
|
||||
var dirPath string
|
||||
if len(args) == 1 {
|
||||
dirPath = args[0]
|
||||
} else {
|
||||
dirPath = "."
|
||||
}
|
||||
dir, err := filepath.Abs(dirPath)
|
||||
dir, err := filepath.Abs(args.Path)
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("unable to locate module: %s", err))
|
||||
return c.showResults(diags, jsonOutput)
|
||||
return view.Results(diags)
|
||||
}
|
||||
|
||||
// Check for user-supplied plugin path
|
||||
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
||||
diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err))
|
||||
return c.showResults(diags, jsonOutput)
|
||||
return view.Results(diags)
|
||||
}
|
||||
|
||||
validateDiags := c.validate(dir)
|
||||
|
@ -66,7 +60,7 @@ func (c *ValidateCommand) Run(args []string) int {
|
|||
// check before submitting a change.
|
||||
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
|
||||
|
||||
return c.showResults(diags, jsonOutput)
|
||||
return view.Results(diags)
|
||||
}
|
||||
|
||||
func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
|
||||
|
@ -116,80 +110,13 @@ func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
func (c *ValidateCommand) showResults(diags tfdiags.Diagnostics, jsonOutput bool) int {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
// FormatVersion represents the version of the json format and will be
|
||||
// incremented for any change to this format that requires changes to a
|
||||
// consuming parser.
|
||||
const FormatVersion = "0.1"
|
||||
|
||||
type Output struct {
|
||||
FormatVersion string `json:"format_version"`
|
||||
|
||||
// We include some summary information that is actually redundant
|
||||
// with the detailed diagnostics, but avoids the need for callers
|
||||
// to re-implement our logic for deciding these.
|
||||
Valid bool `json:"valid"`
|
||||
ErrorCount int `json:"error_count"`
|
||||
WarningCount int `json:"warning_count"`
|
||||
Diagnostics []*viewsjson.Diagnostic `json:"diagnostics"`
|
||||
}
|
||||
|
||||
output := Output{
|
||||
FormatVersion: FormatVersion,
|
||||
Valid: true, // until proven otherwise
|
||||
}
|
||||
configSources := c.configSources()
|
||||
for _, diag := range diags {
|
||||
output.Diagnostics = append(output.Diagnostics, viewsjson.NewDiagnostic(diag, configSources))
|
||||
|
||||
switch diag.Severity() {
|
||||
case tfdiags.Error:
|
||||
output.ErrorCount++
|
||||
output.Valid = false
|
||||
case tfdiags.Warning:
|
||||
output.WarningCount++
|
||||
}
|
||||
}
|
||||
if output.Diagnostics == nil {
|
||||
// Make sure this always appears as an array in our output, since
|
||||
// this is easier to consume for dynamically-typed languages.
|
||||
output.Diagnostics = []*viewsjson.Diagnostic{}
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(&output, "", " ")
|
||||
if err != nil {
|
||||
// Should never happen because we fully-control the input here
|
||||
panic(err)
|
||||
}
|
||||
c.Ui.Output(string(j))
|
||||
|
||||
default:
|
||||
if len(diags) == 0 {
|
||||
c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid.\n"))
|
||||
} else {
|
||||
c.showDiagnostics(diags)
|
||||
|
||||
if !diags.HasErrors() {
|
||||
c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *ValidateCommand) Synopsis() string {
|
||||
return "Check whether the configuration is valid"
|
||||
}
|
||||
|
||||
func (c *ValidateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform [global options] validate [options] [dir]
|
||||
Usage: terraform [global options] validate [options]
|
||||
|
||||
Validate the configuration files in a directory, referring only to the
|
||||
configuration and not accessing any remote services such as remote state,
|
||||
|
@ -209,8 +136,6 @@ Usage: terraform [global options] validate [options] [dir]
|
|||
validation without accessing any configured remote backend, use:
|
||||
terraform init -backend=false
|
||||
|
||||
If dir is not specified, then the current directory will be used.
|
||||
|
||||
To verify configuration in the context of a particular run (a particular
|
||||
target workspace, input variable values, etc), use the 'terraform plan'
|
||||
command instead, which includes an implied validation check.
|
||||
|
|
|
@ -9,15 +9,15 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
)
|
||||
|
||||
func setupTest(fixturepath string, args ...string) (*cli.MockUi, int) {
|
||||
ui := new(cli.MockUi)
|
||||
func setupTest(t *testing.T, fixturepath string, args ...string) (*terminal.TestOutput, int) {
|
||||
view, done := testView(t)
|
||||
p := testProvider()
|
||||
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
|
@ -45,19 +45,20 @@ func setupTest(fixturepath string, args ...string) (*cli.MockUi, int) {
|
|||
c := &ValidateCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args = append(args, "-no-color")
|
||||
args = append(args, testFixturePath(fixturepath))
|
||||
|
||||
code := c.Run(args)
|
||||
return ui, code
|
||||
return done(t), code
|
||||
}
|
||||
|
||||
func TestValidateCommand(t *testing.T) {
|
||||
if ui, code := setupTest("validate-valid"); code != 0 {
|
||||
t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
if output, code := setupTest(t, "validate-valid"); code != 0 {
|
||||
t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,135 +70,137 @@ func TestValidateCommandWithTfvarsFile(t *testing.T) {
|
|||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &ValidateCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFailingCommand(t *testing.T) {
|
||||
if ui, code := setupTest("validate-invalid"); code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
if output, code := setupTest(t, "validate-invalid"); code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFailingCommandMissingQuote(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/missing_quote")
|
||||
output, code := setupTest(t, "validate-invalid/missing_quote")
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
wantError := "Error: Invalid reference"
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFailingCommandMissingVariable(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/missing_var")
|
||||
output, code := setupTest(t, "validate-invalid/missing_var")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
wantError := "Error: Reference to undeclared input variable"
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameProviderMutipleTimesShouldFail(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/multiple_providers")
|
||||
output, code := setupTest(t, "validate-invalid/multiple_providers")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
wantError := "Error: Duplicate provider configuration"
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameModuleMultipleTimesShouldFail(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/multiple_modules")
|
||||
output, code := setupTest(t, "validate-invalid/multiple_modules")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
wantError := "Error: Duplicate module call"
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameResourceMultipleTimesShouldFail(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/multiple_resources")
|
||||
output, code := setupTest(t, "validate-invalid/multiple_resources")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
wantError := `Error: Duplicate resource "aws_instance" configuration`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputWithoutValueShouldFail(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/outputs")
|
||||
output, code := setupTest(t, "validate-invalid/outputs")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
wantError := `The argument "value" is required, but no definition was found.`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
wantError = `An argument named "values" is not expected here. Did you mean "value"?`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleWithIncorrectNameShouldFail(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/incorrectmodulename")
|
||||
output, code := setupTest(t, "validate-invalid/incorrectmodulename")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
wantError := `Error: Invalid module instance name`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
wantError = `Error: Variables not allowed`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWronglyUsedInterpolationShouldFail(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/interpolation")
|
||||
output, code := setupTest(t, "validate-invalid/interpolation")
|
||||
if code != 1 {
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
wantError := `Error: Variables not allowed`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
wantError = `A single static variable reference is required`
|
||||
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
|
||||
if !strings.Contains(output.Stderr(), wantError) {
|
||||
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingDefinedVar(t *testing.T) {
|
||||
ui, code := setupTest("validate-invalid/missing_defined_var")
|
||||
output, code := setupTest(t, "validate-invalid/missing_defined_var")
|
||||
// This is allowed because validate tests only that variables are referenced
|
||||
// correctly, not that they all have defined values.
|
||||
if code != 0 {
|
||||
t.Fatalf("Should have passed: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
t.Fatalf("Should have passed: %d\n\n%s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,9 +240,9 @@ func TestValidate_json(t *testing.T) {
|
|||
t.Fatalf("failed to unmarshal expected JSON: %s", err)
|
||||
}
|
||||
|
||||
ui, code := setupTest(tc.path, "-json")
|
||||
output, code := setupTest(t, tc.path, "-json")
|
||||
|
||||
gotString := ui.OutputWriter.String()
|
||||
gotString := output.Stdout()
|
||||
err = json.Unmarshal([]byte(gotString), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal actual JSON: %s", err)
|
||||
|
@ -256,7 +259,7 @@ func TestValidate_json(t *testing.T) {
|
|||
t.Errorf("wrong exit code: want 1, got %d", code)
|
||||
}
|
||||
|
||||
if errorOutput := ui.ErrorWriter.String(); errorOutput != "" {
|
||||
if errorOutput := output.Stderr(); errorOutput != "" {
|
||||
t.Errorf("unexpected error output:\n%s", errorOutput)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/command/arguments"
|
||||
"github.com/hashicorp/terraform/command/format"
|
||||
viewsjson "github.com/hashicorp/terraform/command/views/json"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
// The Validate is used for the validate command.
|
||||
type Validate interface {
|
||||
// Results renders the diagnostics returned from a validation walk, and
|
||||
// returns a CLI exit code: 0 if there are no errors, 1 otherwise
|
||||
Results(diags tfdiags.Diagnostics) int
|
||||
|
||||
// Diagnostics renders early diagnostics, resulting from argument parsing.
|
||||
Diagnostics(diags tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// NewValidate returns an initialized Validate implementation for the given ViewType.
|
||||
func NewValidate(vt arguments.ViewType, view *View) Validate {
|
||||
switch vt {
|
||||
case arguments.ViewJSON:
|
||||
return &ValidateJSON{view: view}
|
||||
case arguments.ViewHuman:
|
||||
return &ValidateHuman{view: view}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", vt))
|
||||
}
|
||||
}
|
||||
|
||||
// The ValidateHuman implementation renders diagnostics in a human-readable form,
|
||||
// along with a success/failure message if Terraform is able to execute the
|
||||
// validation walk.
|
||||
type ValidateHuman struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Validate = (*ValidateHuman)(nil)
|
||||
|
||||
func (v *ValidateHuman) Results(diags tfdiags.Diagnostics) int {
|
||||
columns := v.view.outputColumns()
|
||||
|
||||
if len(diags) == 0 {
|
||||
v.view.streams.Println(format.WordWrap(v.view.colorize.Color(validateSuccess), columns))
|
||||
} else {
|
||||
v.Diagnostics(diags)
|
||||
|
||||
if !diags.HasErrors() {
|
||||
v.view.streams.Println(format.WordWrap(v.view.colorize.Color(validateWarnings), columns))
|
||||
}
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const validateSuccess = "[green][bold]Success![reset] The configuration is valid.\n"
|
||||
|
||||
const validateWarnings = "[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n"
|
||||
|
||||
func (v *ValidateHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
// The ValidateJSON implementation renders validation results as a JSON object.
|
||||
// This object includes top-level fields summarizing the result, and an array
|
||||
// of JSON diagnostic objects.
|
||||
type ValidateJSON struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Validate = (*ValidateJSON)(nil)
|
||||
|
||||
func (v *ValidateJSON) Results(diags tfdiags.Diagnostics) int {
|
||||
// FormatVersion represents the version of the json format and will be
|
||||
// incremented for any change to this format that requires changes to a
|
||||
// consuming parser.
|
||||
const FormatVersion = "0.1"
|
||||
|
||||
type Output struct {
|
||||
FormatVersion string `json:"format_version"`
|
||||
|
||||
// We include some summary information that is actually redundant
|
||||
// with the detailed diagnostics, but avoids the need for callers
|
||||
// to re-implement our logic for deciding these.
|
||||
Valid bool `json:"valid"`
|
||||
ErrorCount int `json:"error_count"`
|
||||
WarningCount int `json:"warning_count"`
|
||||
Diagnostics []*viewsjson.Diagnostic `json:"diagnostics"`
|
||||
}
|
||||
|
||||
output := Output{
|
||||
FormatVersion: FormatVersion,
|
||||
Valid: true, // until proven otherwise
|
||||
}
|
||||
configSources := v.view.configSources()
|
||||
for _, diag := range diags {
|
||||
output.Diagnostics = append(output.Diagnostics, viewsjson.NewDiagnostic(diag, configSources))
|
||||
|
||||
switch diag.Severity() {
|
||||
case tfdiags.Error:
|
||||
output.ErrorCount++
|
||||
output.Valid = false
|
||||
case tfdiags.Warning:
|
||||
output.WarningCount++
|
||||
}
|
||||
}
|
||||
if output.Diagnostics == nil {
|
||||
// Make sure this always appears as an array in our output, since
|
||||
// this is easier to consume for dynamically-typed languages.
|
||||
output.Diagnostics = []*viewsjson.Diagnostic{}
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(&output, "", " ")
|
||||
if err != nil {
|
||||
// Should never happen because we fully-control the input here
|
||||
panic(err)
|
||||
}
|
||||
v.view.streams.Println(string(j))
|
||||
|
||||
if diags.HasErrors() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Diagnostics should only be called if the validation walk cannot be executed.
|
||||
// In this case, we choose to render human-readable diagnostic output,
|
||||
// primarily for backwards compatibility.
|
||||
func (v *ValidateJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
func TestValidateHuman(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
diag tfdiags.Diagnostic
|
||||
wantSuccess bool
|
||||
wantSubstring string
|
||||
}{
|
||||
"success": {
|
||||
nil,
|
||||
true,
|
||||
"The configuration is valid.",
|
||||
},
|
||||
"warning": {
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Your shoelaces are untied",
|
||||
"Watch out, or you'll trip!",
|
||||
),
|
||||
true,
|
||||
"The configuration is valid, but there were some validation warnings",
|
||||
},
|
||||
"error": {
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Configuration is missing random_pet",
|
||||
"Every configuration should have a random_pet.",
|
||||
),
|
||||
false,
|
||||
"Error: Configuration is missing random_pet",
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
view.Configure(&arguments.View{NoColor: true})
|
||||
v := NewValidate(arguments.ViewHuman, view)
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if tc.diag != nil {
|
||||
diags = diags.Append(tc.diag)
|
||||
}
|
||||
|
||||
ret := v.Results(diags)
|
||||
|
||||
if tc.wantSuccess && ret != 0 {
|
||||
t.Errorf("expected 0 return code, got %d", ret)
|
||||
} else if !tc.wantSuccess && ret != 1 {
|
||||
t.Errorf("expected 1 return code, got %d", ret)
|
||||
}
|
||||
|
||||
got := done(t).All()
|
||||
if strings.Contains(got, "Success!") != tc.wantSuccess {
|
||||
t.Errorf("unexpected output:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, tc.wantSubstring) {
|
||||
t.Errorf("expected output to include %q, but was:\n%s", tc.wantSubstring, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
diag tfdiags.Diagnostic
|
||||
wantSuccess bool
|
||||
}{
|
||||
"success": {
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
"warning": {
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Your shoelaces are untied",
|
||||
"Watch out, or you'll trip!",
|
||||
),
|
||||
true,
|
||||
},
|
||||
"error": {
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Configuration is missing random_pet",
|
||||
"Every configuration should have a random_pet.",
|
||||
),
|
||||
false,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
view.Configure(&arguments.View{NoColor: true})
|
||||
v := NewValidate(arguments.ViewJSON, view)
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if tc.diag != nil {
|
||||
diags = diags.Append(tc.diag)
|
||||
}
|
||||
|
||||
ret := v.Results(diags)
|
||||
|
||||
if tc.wantSuccess && ret != 0 {
|
||||
t.Errorf("expected 0 return code, got %d", ret)
|
||||
} else if !tc.wantSuccess && ret != 1 {
|
||||
t.Errorf("expected 1 return code, got %d", ret)
|
||||
}
|
||||
|
||||
got := done(t).All()
|
||||
|
||||
// Make sure the result looks like JSON; we comprehensively test
|
||||
// the structure of this output in the command package tests.
|
||||
var result map[string]interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue