diff --git a/internal/command/arguments/validate.go b/internal/command/arguments/validate.go index 8c337b37e9..2dcccb776a 100644 --- a/internal/command/arguments/validate.go +++ b/internal/command/arguments/validate.go @@ -27,6 +27,8 @@ type Validate struct { // Query indicates that Terraform should also validate .tfquery files. Query bool + + Vars *Vars } // ParseValidate processes CLI arguments, returning a Validate value and errors. @@ -36,10 +38,11 @@ func ParseValidate(args []string) (*Validate, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics validate := &Validate{ Path: ".", + Vars: &Vars{}, } var jsonOutput bool - cmdFlags := defaultFlagSet("validate") + cmdFlags := extendedFlagSet("validate", nil, nil, validate.Vars) cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&validate.TestDirectory, "test-directory", "tests", "test-directory") cmdFlags.BoolVar(&validate.NoTests, "no-tests", false, "no-tests") diff --git a/internal/command/arguments/validate_test.go b/internal/command/arguments/validate_test.go index 1e9f0939dc..8619822e0c 100644 --- a/internal/command/arguments/validate_test.go +++ b/internal/command/arguments/validate_test.go @@ -6,6 +6,8 @@ package arguments import ( "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -19,6 +21,7 @@ func TestParseValidate_valid(t *testing.T) { &Validate{ Path: ".", TestDirectory: "tests", + Vars: &Vars{}, ViewType: ViewHuman, }, }, @@ -27,6 +30,7 @@ func TestParseValidate_valid(t *testing.T) { &Validate{ Path: ".", TestDirectory: "tests", + Vars: &Vars{}, ViewType: ViewJSON, }, }, @@ -35,6 +39,7 @@ func TestParseValidate_valid(t *testing.T) { &Validate{ Path: "foo", TestDirectory: "tests", + Vars: &Vars{}, ViewType: ViewJSON, }, }, @@ -43,6 +48,7 @@ func TestParseValidate_valid(t *testing.T) { &Validate{ Path: ".", TestDirectory: "other", + Vars: &Vars{}, ViewType: ViewHuman, }, }, @@ -51,25 +57,72 @@ func TestParseValidate_valid(t *testing.T) { &Validate{ Path: ".", TestDirectory: "tests", + Vars: &Vars{}, ViewType: ViewHuman, NoTests: true, }, }, } + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + 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 { + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } }) } } +func TestParseValidate_vars(t *testing.T) { + testCases := map[string]struct { + args []string + want []FlagNameValue + }{ + "var": { + args: []string{"-var", "foo=bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + }, + }, + "var-file": { + args: []string{"-var-file", "cool.tfvars"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, + }, + }, + "both": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, + }, + }, + } + + 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 vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected vars: %#v", vars) + } + }) + } +} + func TestParseValidate_invalid(t *testing.T) { testCases := map[string]struct { args []string @@ -81,6 +134,7 @@ func TestParseValidate_invalid(t *testing.T) { &Validate{ Path: ".", TestDirectory: "tests", + Vars: &Vars{}, ViewType: ViewHuman, }, tfdiags.Diagnostics{ @@ -96,6 +150,7 @@ func TestParseValidate_invalid(t *testing.T) { &Validate{ Path: "bar", TestDirectory: "tests", + Vars: &Vars{}, ViewType: ViewJSON, }, tfdiags.Diagnostics{ @@ -108,10 +163,12 @@ func TestParseValidate_invalid(t *testing.T) { }, } + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, gotDiags := ParseValidate(tc.args) - if *got != *tc.want { + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) diff --git a/internal/command/validate.go b/internal/command/validate.go index 952abb3fb2..5ac75db902 100644 --- a/internal/command/validate.go +++ b/internal/command/validate.go @@ -47,6 +47,26 @@ func (c *ValidateCommand) Run(rawArgs []string) int { c.ParsedArgs = args view := views.NewValidate(args.ViewType, c.View) + // If the query flag is set, include query files in the validation. + c.includeQueryFiles = c.ParsedArgs.Query + + loader, err := c.initConfigLoader() + if err != nil { + diags = diags.Append(err) + view.Diagnostics(diags) + return 1 + } + + var varDiags tfdiags.Diagnostics + c.VariableValues, varDiags = args.Vars.CollectValues(func(filename string, src []byte) { + loader.Parser().ForceFileSource(filename, src) + }) + diags = diags.Append(varDiags) + if diags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + // 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 @@ -81,9 +101,6 @@ func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics { var diags tfdiags.Diagnostics var cfg *configs.Config - // If the query flag is set, include query files in the validation. - c.includeQueryFiles = c.ParsedArgs.Query - if c.ParsedArgs.NoTests { cfg, diags = c.loadConfig(dir) } else { @@ -360,9 +377,19 @@ Options: -no-tests If specified, Terraform will not validate test files. - -test-directory=path Set the Terraform test directory, defaults to "tests". - + -test-directory=path Set the Terraform test directory, defaults to "tests". + -query If specified, the command will also validate .tfquery.hcl files. + + -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. + + -var-file=filename Load variable values from the given file, in addition + to the default files terraform.tfvars and *.auto.tfvars. + Use this option more than once to include more than one + variables file. + ` return strings.TrimSpace(helpText) }