| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "fmt" | |
| "path/filepath" | |
| "strings" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| backendInit "github.com/hashicorp/terraform/internal/backend/init" | |
| "github.com/hashicorp/terraform/internal/command/arguments" | |
| "github.com/hashicorp/terraform/internal/command/views" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // ValidateCommand is a Command implementation that validates the terraform files | |
| type ValidateCommand struct { | |
| Meta | |
| ParsedArgs *arguments.Validate | |
| } | |
| func (c *ValidateCommand) Run(rawArgs []string) int { | |
| // Parse and apply global view arguments | |
| common, rawArgs := arguments.ParseView(rawArgs) | |
| c.View.Configure(common) | |
| // Parse and validate flags | |
| args, diags := arguments.ParseValidate(rawArgs) | |
| c.ParsedArgs = args | |
| view := views.NewValidate(args.ViewType, c.View) | |
| // Now the view is ready, process any diagnostics from argument parsing | |
| if diags.HasErrors() { | |
| if args.ViewType == arguments.ViewHuman { | |
| defer c.View.HelpPrompt("validate") | |
| } | |
| return view.Results(diags) | |
| } | |
| // If the query flag is set, include query files in the validation. | |
| c.includeQueryFiles = c.ParsedArgs.Query | |
| // 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 | |
| // view.Results in order to produce the expected output. | |
| loader, err := c.initConfigLoader() | |
| if err != nil { | |
| diags = diags.Append(err) | |
| return view.Results(diags) | |
| } | |
| var varDiags tfdiags.Diagnostics | |
| c.VariableValues, varDiags = args.Vars.CollectValues(func(filename string, src []byte) { | |
| loader.Parser().ForceFileSource(filename, src) | |
| }) | |
| if varDiags.HasErrors() { | |
| diags = diags.Append(varDiags) | |
| return view.Results(diags) | |
| } | |
| dir, err := filepath.Abs(args.Path) | |
| if err != nil { | |
| diags = diags.Append(fmt.Errorf("unable to locate module: %s", err)) | |
| 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 view.Results(diags) | |
| } | |
| validateDiags := c.validate(dir) | |
| diags = diags.Append(validateDiags) | |
| // Validating with dev overrides in effect means that the result might | |
| // not be valid for a stable release, so we'll warn about that in case | |
| // the user is trying to use "terraform validate" as a sort of pre-flight | |
| // check before submitting a change. | |
| diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) | |
| return view.Results(diags) | |
| } | |
| func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics { | |
| var diags tfdiags.Diagnostics | |
| var cfg *configs.Config | |
| diags = diags.Append(c.resolveConstVariables(dir, c.ParsedArgs.ViewType)) | |
| if diags.HasErrors() { | |
| return diags | |
| } | |
| if c.ParsedArgs.NoTests { | |
| cfg, diags = c.loadConfig(dir) | |
| } else { | |
| cfg, diags = c.loadConfigWithTests(dir, c.ParsedArgs.TestDirectory) | |
| } | |
| if diags.HasErrors() { | |
| return diags | |
| } | |
| diags = diags.Append(c.validateConfig(cfg)) | |
| // Validation of backend block, if present | |
| // Backend blocks live outside the Terraform graph so we have to do this separately. | |
| if cfg.Module.Backend != nil { | |
| diags = diags.Append(c.validateBackendTypeSupported(cfg.Module.Backend)) | |
| } | |
| // Unless excluded, we'll also do a quick validation of the Terraform test files. These live | |
| // outside the Terraform graph so we have to do this separately. | |
| if !c.ParsedArgs.NoTests { | |
| diags = diags.Append(c.validateTestFiles(cfg)) | |
| } | |
| return diags | |
| } | |
| func (c *ValidateCommand) validateConfig(cfg *configs.Config) tfdiags.Diagnostics { | |
| var diags tfdiags.Diagnostics | |
| opts, err := c.contextOpts() | |
| if err != nil { | |
| diags = diags.Append(err) | |
| return diags | |
| } | |
| tfCtx, ctxDiags := terraform.NewContext(opts) | |
| diags = diags.Append(ctxDiags) | |
| if ctxDiags.HasErrors() { | |
| return diags | |
| } | |
| return diags.Append(tfCtx.Validate(cfg, nil)) | |
| } | |
| func (c *ValidateCommand) validateTestFiles(cfg *configs.Config) tfdiags.Diagnostics { | |
| diags := tfdiags.Diagnostics{} | |
| validatedModules := make(map[string]bool) | |
| for _, file := range cfg.Module.Tests { | |
| // The file validation only returns warnings so we'll just add them | |
| // without checking anything about them. | |
| diags = diags.Append(file.Validate(cfg)) | |
| for _, run := range file.Runs { | |
| if run.Module != nil { | |
| // Then we can also validate the referenced modules, but we are | |
| // only going to do this is if they are local modules. | |
| // | |
| // Basically, local testing modules are something the user can | |
| // reasonably go and fix. If it's a module being downloaded from | |
| // the registry, the expectation is that the author of the | |
| // module should have ran `terraform validate` themselves. | |
| if _, ok := run.Module.Source.(addrs.ModuleSourceLocal); ok { | |
| if validated := validatedModules[run.Module.Source.String()]; !validated { | |
| // Since we can reference the same module twice, let's | |
| // not validate the same thing multiple times. | |
| validatedModules[run.Module.Source.String()] = true | |
| diags = diags.Append(c.validateConfig(run.ConfigUnderTest)) | |
| } | |
| } | |
| diags = diags.Append(run.Validate(run.ConfigUnderTest)) | |
| } else { | |
| diags = diags.Append(run.Validate(cfg)) | |
| } | |
| } | |
| } | |
| return diags | |
| } | |
| // Validate that the config includes a backend type that exists in the current binary. | |
| // A name could be mistyped or the config could be using an old backend type that's been | |
| // removed from the version of Terraform in use. | |
| func (c *ValidateCommand) validateBackendTypeSupported(cfg *configs.Backend) tfdiags.Diagnostics { | |
| var diags tfdiags.Diagnostics | |
| bf := backendInit.Backend(cfg.Type) | |
| if bf == nil { | |
| detail := fmt.Sprintf("There is no backend type named %q.", cfg.Type) | |
| if msg, removed := backendInit.RemovedBackends[cfg.Type]; removed { | |
| detail = msg | |
| } | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Unsupported backend type", | |
| Detail: detail, | |
| Subject: &cfg.TypeRange, | |
| }) | |
| return diags | |
| } | |
| return diags | |
| } | |
| func (c *ValidateCommand) Synopsis() string { | |
| return "Check whether the configuration is valid" | |
| } | |
| func (c *ValidateCommand) Help() string { | |
| helpText := ` | |
| 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, | |
| provider APIs, etc. | |
| Validate runs checks that verify whether a configuration is syntactically | |
| valid and internally consistent, regardless of any provided variables or | |
| existing state. It is thus primarily useful for general verification of | |
| reusable modules, including correctness of attribute names and value types. | |
| It is safe to run this command automatically, for example as a post-save | |
| check in a text editor or as a test step for a re-usable module in a CI | |
| system. | |
| Validation requires an initialized working directory with any referenced | |
| plugins and modules installed. To initialize a working directory for | |
| validation without accessing any configured remote backend, use: | |
| terraform init -backend=false | |
| 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. | |
| Options: | |
| -json Produce output in a machine-readable JSON format, | |
| suitable for use in text editor integrations and other | |
| automated systems. Always disables color. | |
| -no-color If specified, output won't contain any color. | |
| -no-tests If specified, Terraform will not validate test files. | |
| -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) | |
| } |