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 }