github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/validate.go (about)

     1  package command
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/hashicorp/terraform/terraform"
    12  	"github.com/hashicorp/terraform/tfdiags"
    13  )
    14  
    15  // ValidateCommand is a Command implementation that validates the terraform files
    16  type ValidateCommand struct {
    17  	Meta
    18  }
    19  
    20  const defaultPath = "."
    21  
    22  func (c *ValidateCommand) Run(args []string) int {
    23  	args, err := c.Meta.process(args, true)
    24  	if err != nil {
    25  		return 1
    26  	}
    27  
    28  	// TODO: The `var` and `var-file` options are not actually used, and should
    29  	// be removed in the next major release.
    30  	if c.Meta.variableArgs.items == nil {
    31  		c.Meta.variableArgs = newRawFlags("-var")
    32  	}
    33  	varValues := c.Meta.variableArgs.Alias("-var")
    34  	varFiles := c.Meta.variableArgs.Alias("-var-file")
    35  
    36  	var jsonOutput bool
    37  	cmdFlags := c.Meta.defaultFlagSet("validate")
    38  	cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")
    39  	cmdFlags.Var(varValues, "var", "variables")
    40  	cmdFlags.Var(varFiles, "var-file", "variable file")
    41  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    42  	if err := cmdFlags.Parse(args); err != nil {
    43  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    44  		return 1
    45  	}
    46  
    47  	var diags tfdiags.Diagnostics
    48  
    49  	// If set, output a warning indicating that these values are not used.
    50  	if !varValues.Empty() || !varFiles.Empty() {
    51  		diags = diags.Append(tfdiags.Sourceless(
    52  			tfdiags.Warning,
    53  			"The -var and -var-file flags are not used in validate. Setting them has no effect.",
    54  			"These flags will be removed in a future version of Terraform.",
    55  		))
    56  	}
    57  
    58  	// After this point, we must only produce JSON output if JSON mode is
    59  	// enabled, so all errors should be accumulated into diags and we'll
    60  	// print out a suitable result at the end, depending on the format
    61  	// selection. All returns from this point on must be tail-calls into
    62  	// c.showResults in order to produce the expected output.
    63  	args = cmdFlags.Args()
    64  
    65  	var dirPath string
    66  	if len(args) == 1 {
    67  		dirPath = args[0]
    68  	} else {
    69  		dirPath = "."
    70  	}
    71  	dir, err := filepath.Abs(dirPath)
    72  	if err != nil {
    73  		diags = diags.Append(fmt.Errorf("unable to locate module: %s", err))
    74  		return c.showResults(diags, jsonOutput)
    75  	}
    76  
    77  	// Check for user-supplied plugin path
    78  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    79  		diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err))
    80  		return c.showResults(diags, jsonOutput)
    81  	}
    82  
    83  	validateDiags := c.validate(dir)
    84  	diags = diags.Append(validateDiags)
    85  
    86  	return c.showResults(diags, jsonOutput)
    87  }
    88  
    89  func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
    90  	var diags tfdiags.Diagnostics
    91  
    92  	cfg, cfgDiags := c.loadConfig(dir)
    93  	diags = diags.Append(cfgDiags)
    94  
    95  	if diags.HasErrors() {
    96  		return diags
    97  	}
    98  
    99  	// "validate" is to check if the given module is valid regardless of
   100  	// input values, current state, etc. Therefore we populate all of the
   101  	// input values with unknown values of the expected type, allowing us
   102  	// to perform a type check without assuming any particular values.
   103  	varValues := make(terraform.InputValues)
   104  	for name, variable := range cfg.Module.Variables {
   105  		ty := variable.Type
   106  		if ty == cty.NilType {
   107  			// Can't predict the type at all, so we'll just mark it as
   108  			// cty.DynamicVal (unknown value of cty.DynamicPseudoType).
   109  			ty = cty.DynamicPseudoType
   110  		}
   111  		varValues[name] = &terraform.InputValue{
   112  			Value:      cty.UnknownVal(ty),
   113  			SourceType: terraform.ValueFromCLIArg,
   114  		}
   115  	}
   116  
   117  	opts := c.contextOpts()
   118  	opts.Config = cfg
   119  	opts.Variables = varValues
   120  
   121  	tfCtx, ctxDiags := terraform.NewContext(opts)
   122  	diags = diags.Append(ctxDiags)
   123  	if ctxDiags.HasErrors() {
   124  		return diags
   125  	}
   126  
   127  	validateDiags := tfCtx.Validate()
   128  	diags = diags.Append(validateDiags)
   129  	return diags
   130  }
   131  
   132  func (c *ValidateCommand) showResults(diags tfdiags.Diagnostics, jsonOutput bool) int {
   133  	switch {
   134  	case jsonOutput:
   135  		// FIXME: Eventually we'll probably want to factor this out somewhere
   136  		// to support machine-readable outputs for other commands too, but for
   137  		// now it's simplest to do this inline here.
   138  		type Pos struct {
   139  			Line   int `json:"line"`
   140  			Column int `json:"column"`
   141  			Byte   int `json:"byte"`
   142  		}
   143  		type Range struct {
   144  			Filename string `json:"filename"`
   145  			Start    Pos    `json:"start"`
   146  			End      Pos    `json:"end"`
   147  		}
   148  		type Diagnostic struct {
   149  			Severity string `json:"severity,omitempty"`
   150  			Summary  string `json:"summary,omitempty"`
   151  			Detail   string `json:"detail,omitempty"`
   152  			Range    *Range `json:"range,omitempty"`
   153  		}
   154  		type Output struct {
   155  			// We include some summary information that is actually redundant
   156  			// with the detailed diagnostics, but avoids the need for callers
   157  			// to re-implement our logic for deciding these.
   158  			Valid        bool         `json:"valid"`
   159  			ErrorCount   int          `json:"error_count"`
   160  			WarningCount int          `json:"warning_count"`
   161  			Diagnostics  []Diagnostic `json:"diagnostics"`
   162  		}
   163  
   164  		var output Output
   165  		output.Valid = true // until proven otherwise
   166  		for _, diag := range diags {
   167  			var jsonDiag Diagnostic
   168  			switch diag.Severity() {
   169  			case tfdiags.Error:
   170  				jsonDiag.Severity = "error"
   171  				output.ErrorCount++
   172  				output.Valid = false
   173  			case tfdiags.Warning:
   174  				jsonDiag.Severity = "warning"
   175  				output.WarningCount++
   176  			}
   177  
   178  			desc := diag.Description()
   179  			jsonDiag.Summary = desc.Summary
   180  			jsonDiag.Detail = desc.Detail
   181  
   182  			ranges := diag.Source()
   183  			if ranges.Subject != nil {
   184  				subj := ranges.Subject
   185  				jsonDiag.Range = &Range{
   186  					Filename: subj.Filename,
   187  					Start: Pos{
   188  						Line:   subj.Start.Line,
   189  						Column: subj.Start.Column,
   190  						Byte:   subj.Start.Byte,
   191  					},
   192  					End: Pos{
   193  						Line:   subj.End.Line,
   194  						Column: subj.End.Column,
   195  						Byte:   subj.End.Byte,
   196  					},
   197  				}
   198  			}
   199  
   200  			output.Diagnostics = append(output.Diagnostics, jsonDiag)
   201  		}
   202  		if output.Diagnostics == nil {
   203  			// Make sure this always appears as an array in our output, since
   204  			// this is easier to consume for dynamically-typed languages.
   205  			output.Diagnostics = []Diagnostic{}
   206  		}
   207  
   208  		j, err := json.MarshalIndent(&output, "", "  ")
   209  		if err != nil {
   210  			// Should never happen because we fully-control the input here
   211  			panic(err)
   212  		}
   213  		c.Ui.Output(string(j))
   214  
   215  	default:
   216  		if len(diags) == 0 {
   217  			c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid.\n"))
   218  		} else {
   219  			c.showDiagnostics(diags)
   220  
   221  			if !diags.HasErrors() {
   222  				c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n"))
   223  			}
   224  		}
   225  	}
   226  
   227  	if diags.HasErrors() {
   228  		return 1
   229  	}
   230  	return 0
   231  }
   232  
   233  func (c *ValidateCommand) Synopsis() string {
   234  	return "Validates the Terraform files"
   235  }
   236  
   237  func (c *ValidateCommand) Help() string {
   238  	helpText := `
   239  Usage: terraform validate [options] [dir]
   240  
   241    Validate the configuration files in a directory, referring only to the
   242    configuration and not accessing any remote services such as remote state,
   243    provider APIs, etc.
   244  
   245    Validate runs checks that verify whether a configuration is syntactically
   246    valid and internally consistent, regardless of any provided variables or
   247    existing state. It is thus primarily useful for general verification of
   248    reusable modules, including correctness of attribute names and value types.
   249  
   250    It is safe to run this command automatically, for example as a post-save
   251    check in a text editor or as a test step for a re-usable module in a CI
   252    system.
   253  
   254    Validation requires an initialized working directory with any referenced
   255    plugins and modules installed. To initialize a working directory for
   256    validation without accessing any configured remote backend, use:
   257        terraform init -backend=false
   258  
   259    If dir is not specified, then the current directory will be used.
   260  
   261    To verify configuration in the context of a particular run (a particular
   262    target workspace, input variable values, etc), use the 'terraform plan'
   263    command instead, which includes an implied validation check.
   264  
   265  Options:
   266  
   267    -json        Produce output in a machine-readable JSON format, suitable for
   268                 use in text editor integrations and other automated systems.
   269                 Always disables color.
   270  
   271    -no-color    If specified, output won't contain any color.
   272  `
   273  	return strings.TrimSpace(helpText)
   274  }