github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/unparsed_value.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package backend 5 6 import ( 7 "fmt" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/terramate-io/tf/configs" 11 "github.com/terramate-io/tf/terraform" 12 "github.com/terramate-io/tf/tfdiags" 13 "github.com/zclconf/go-cty/cty" 14 ) 15 16 // UnparsedVariableValue represents a variable value provided by the caller 17 // whose parsing must be deferred until configuration is available. 18 // 19 // This exists to allow processing of variable-setting arguments (e.g. in the 20 // command package) to be separated from parsing (in the backend package). 21 type UnparsedVariableValue interface { 22 // ParseVariableValue information in the provided variable configuration 23 // to parse (if necessary) and return the variable value encapsulated in 24 // the receiver. 25 // 26 // If error diagnostics are returned, the resulting value may be invalid 27 // or incomplete. 28 ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) 29 } 30 31 // ParseUndeclaredVariableValues processes a map of unparsed variable values 32 // and returns an input values map of the ones not declared in the specified 33 // declaration map along with detailed diagnostics about values of undeclared 34 // variables being present, depending on the source of these values. If more 35 // than two undeclared values are present in file form (config, auto, -var-file) 36 // the remaining errors are summarized to avoid a massive list of errors. 37 func ParseUndeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { 38 var diags tfdiags.Diagnostics 39 ret := make(terraform.InputValues, len(vv)) 40 seenUndeclaredInFile := 0 41 42 for name, rv := range vv { 43 if _, declared := decls[name]; declared { 44 // Only interested in parsing undeclared variables 45 continue 46 } 47 48 val, valDiags := rv.ParseVariableValue(configs.VariableParseLiteral) 49 if valDiags.HasErrors() { 50 continue 51 } 52 53 ret[name] = val 54 55 switch val.SourceType { 56 case terraform.ValueFromConfig, terraform.ValueFromAutoFile, terraform.ValueFromNamedFile: 57 // We allow undeclared names for variable values from files and warn in case 58 // users have forgotten a variable {} declaration or have a typo in their var name. 59 // Some users will actively ignore this warning because they use a .tfvars file 60 // across multiple configurations. 61 if seenUndeclaredInFile < 2 { 62 diags = diags.Append(tfdiags.Sourceless( 63 tfdiags.Warning, 64 "Value for undeclared variable", 65 fmt.Sprintf("The root module does not declare a variable named %q but a value was found in file %q. If you meant to use this value, add a \"variable\" block to the configuration.\n\nTo silence these warnings, use TF_VAR_... environment variables to provide certain \"global\" settings to all configurations in your organization. To reduce the verbosity of these warnings, use the -compact-warnings option.", name, val.SourceRange.Filename), 66 )) 67 } 68 seenUndeclaredInFile++ 69 70 case terraform.ValueFromEnvVar: 71 // We allow and ignore undeclared names for environment 72 // variables, because users will often set these globally 73 // when they are used across many (but not necessarily all) 74 // configurations. 75 case terraform.ValueFromCLIArg: 76 diags = diags.Append(tfdiags.Sourceless( 77 tfdiags.Error, 78 "Value for undeclared variable", 79 fmt.Sprintf("A variable named %q was assigned on the command line, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name), 80 )) 81 default: 82 // For all other source types we are more vague, but other situations 83 // don't generally crop up at this layer in practice. 84 diags = diags.Append(tfdiags.Sourceless( 85 tfdiags.Error, 86 "Value for undeclared variable", 87 fmt.Sprintf("A variable named %q was assigned a value, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name), 88 )) 89 } 90 } 91 92 if seenUndeclaredInFile > 2 { 93 extras := seenUndeclaredInFile - 2 94 diags = diags.Append(&hcl.Diagnostic{ 95 Severity: hcl.DiagWarning, 96 Summary: "Values for undeclared variables", 97 Detail: fmt.Sprintf("In addition to the other similar warnings shown, %d other variable(s) defined without being declared.", extras), 98 }) 99 } 100 101 return ret, diags 102 } 103 104 // ParseDeclaredVariableValues processes a map of unparsed variable values 105 // and returns an input values map of the ones declared in the specified 106 // variable declaration mapping. Diagnostics will be populating with 107 // any variable parsing errors encountered within this collection. 108 func ParseDeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { 109 var diags tfdiags.Diagnostics 110 ret := make(terraform.InputValues, len(vv)) 111 112 for name, rv := range vv { 113 var mode configs.VariableParsingMode 114 config, declared := decls[name] 115 116 if declared { 117 mode = config.ParsingMode 118 } else { 119 // Only interested in parsing declared variables 120 continue 121 } 122 123 val, valDiags := rv.ParseVariableValue(mode) 124 diags = diags.Append(valDiags) 125 if valDiags.HasErrors() { 126 continue 127 } 128 129 ret[name] = val 130 } 131 132 return ret, diags 133 } 134 135 // Checks all given terraform.InputValues variable maps for the existance of 136 // a named variable 137 func isDefinedAny(name string, maps ...terraform.InputValues) bool { 138 for _, m := range maps { 139 if _, defined := m[name]; defined { 140 return true 141 } 142 } 143 return false 144 } 145 146 // ParseVariableValues processes a map of unparsed variable values by 147 // correlating each one with the given variable declarations which should 148 // be from a root module. 149 // 150 // The map of unparsed variable values should include variables from all 151 // possible root module declarations sources such that it is as complete as 152 // it can possibly be for the current operation. If any declared variables 153 // are not included in the map, ParseVariableValues will either substitute 154 // a configured default value or produce an error. 155 // 156 // If this function returns without any errors in the diagnostics, the 157 // resulting input values map is guaranteed to be valid and ready to pass 158 // to terraform.NewContext. If the diagnostics contains errors, the returned 159 // InputValues may be incomplete but will include the subset of variables 160 // that were successfully processed, allowing for careful analysis of the 161 // partial result. 162 func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { 163 ret, diags := ParseDeclaredVariableValues(vv, decls) 164 undeclared, diagsUndeclared := ParseUndeclaredVariableValues(vv, decls) 165 166 diags = diags.Append(diagsUndeclared) 167 168 // By this point we should've gathered all of the required root module 169 // variables from one of the many possible sources. We'll now populate 170 // any we haven't gathered as unset placeholders which Terraform Core 171 // can then react to. 172 for name, vc := range decls { 173 if isDefinedAny(name, ret, undeclared) { 174 continue 175 } 176 177 // This check is redundant with a check made in Terraform Core when 178 // processing undeclared variables, but allows us to generate a more 179 // specific error message which mentions -var and -var-file command 180 // line options, whereas the one in Terraform Core is more general 181 // due to supporting both root and child module variables. 182 if vc.Required() { 183 diags = diags.Append(&hcl.Diagnostic{ 184 Severity: hcl.DiagError, 185 Summary: "No value for required variable", 186 Detail: fmt.Sprintf("The root module input variable %q is not set, and has no default value. Use a -var or -var-file command line argument to provide a value for this variable.", name), 187 Subject: vc.DeclRange.Ptr(), 188 }) 189 190 // We'll include a placeholder value anyway, just so that our 191 // result is complete for any calling code that wants to cautiously 192 // analyze it for diagnostic purposes. Since our diagnostics now 193 // includes an error, normal processing will ignore this result. 194 ret[name] = &terraform.InputValue{ 195 Value: cty.DynamicVal, 196 SourceType: terraform.ValueFromConfig, 197 SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), 198 } 199 } else { 200 // We're still required to put an entry for this variable 201 // in the mapping to be explicit to Terraform Core that we 202 // visited it, but its value will be cty.NilVal to represent 203 // that it wasn't set at all at this layer, and so Terraform Core 204 // should substitute a default if available, or generate an error 205 // if not. 206 ret[name] = &terraform.InputValue{ 207 Value: cty.NilVal, 208 SourceType: terraform.ValueFromConfig, 209 SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), 210 } 211 } 212 } 213 214 return ret, diags 215 }