github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/variables.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/zclconf/go-cty/cty" 8 "github.com/zclconf/go-cty/cty/convert" 9 10 "github.com/hashicorp/terraform-plugin-sdk/internal/configs" 11 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 12 ) 13 14 // InputValue represents a value for a variable in the root module, provided 15 // as part of the definition of an operation. 16 type InputValue struct { 17 Value cty.Value 18 SourceType ValueSourceType 19 20 // SourceRange provides source location information for values whose 21 // SourceType is either ValueFromConfig or ValueFromFile. It is not 22 // populated for other source types, and so should not be used. 23 SourceRange tfdiags.SourceRange 24 } 25 26 // ValueSourceType describes what broad category of source location provided 27 // a particular value. 28 type ValueSourceType rune 29 30 const ( 31 // ValueFromUnknown is the zero value of ValueSourceType and is not valid. 32 ValueFromUnknown ValueSourceType = 0 33 34 // ValueFromConfig indicates that a value came from a .tf or .tf.json file, 35 // e.g. the default value defined for a variable. 36 ValueFromConfig ValueSourceType = 'C' 37 38 // ValueFromAutoFile indicates that a value came from a "values file", like 39 // a .tfvars file, that was implicitly loaded by naming convention. 40 ValueFromAutoFile ValueSourceType = 'F' 41 42 // ValueFromNamedFile indicates that a value came from a named "values file", 43 // like a .tfvars file, that was passed explicitly on the command line (e.g. 44 // -var-file=foo.tfvars). 45 ValueFromNamedFile ValueSourceType = 'N' 46 47 // ValueFromCLIArg indicates that the value was provided directly in 48 // a CLI argument. The name of this argument is not recorded and so it must 49 // be inferred from context. 50 ValueFromCLIArg ValueSourceType = 'A' 51 52 // ValueFromEnvVar indicates that the value was provided via an environment 53 // variable. The name of the variable is not recorded and so it must be 54 // inferred from context. 55 ValueFromEnvVar ValueSourceType = 'E' 56 57 // ValueFromInput indicates that the value was provided at an interactive 58 // input prompt. 59 ValueFromInput ValueSourceType = 'I' 60 61 // ValueFromPlan indicates that the value was retrieved from a stored plan. 62 ValueFromPlan ValueSourceType = 'P' 63 64 // ValueFromCaller indicates that the value was explicitly overridden by 65 // a caller to Context.SetVariable after the context was constructed. 66 ValueFromCaller ValueSourceType = 'S' 67 ) 68 69 func (v *InputValue) GoString() string { 70 if (v.SourceRange != tfdiags.SourceRange{}) { 71 return fmt.Sprintf("&terraform.InputValue{Value: %#v, SourceType: %#v, SourceRange: %#v}", v.Value, v.SourceType, v.SourceRange) 72 } else { 73 return fmt.Sprintf("&terraform.InputValue{Value: %#v, SourceType: %#v}", v.Value, v.SourceType) 74 } 75 } 76 77 func (v ValueSourceType) GoString() string { 78 return fmt.Sprintf("terraform.%s", v) 79 } 80 81 //go:generate go run golang.org/x/tools/cmd/stringer -type ValueSourceType 82 83 // InputValues is a map of InputValue instances. 84 type InputValues map[string]*InputValue 85 86 // InputValuesFromCaller turns the given map of naked values into an 87 // InputValues that attributes each value to "a caller", using the source 88 // type ValueFromCaller. This is primarily useful for testing purposes. 89 // 90 // This should not be used as a general way to convert map[string]cty.Value 91 // into InputValues, since in most real cases we want to set a suitable 92 // other SourceType and possibly SourceRange value. 93 func InputValuesFromCaller(vals map[string]cty.Value) InputValues { 94 ret := make(InputValues, len(vals)) 95 for k, v := range vals { 96 ret[k] = &InputValue{ 97 Value: v, 98 SourceType: ValueFromCaller, 99 } 100 } 101 return ret 102 } 103 104 // Override merges the given value maps with the receiver, overriding any 105 // conflicting keys so that the latest definition wins. 106 func (vv InputValues) Override(others ...InputValues) InputValues { 107 // FIXME: This should check to see if any of the values are maps and 108 // merge them if so, in order to preserve the behavior from prior to 109 // Terraform 0.12. 110 ret := make(InputValues) 111 for k, v := range vv { 112 ret[k] = v 113 } 114 for _, other := range others { 115 for k, v := range other { 116 ret[k] = v 117 } 118 } 119 return ret 120 } 121 122 // JustValues returns a map that just includes the values, discarding the 123 // source information. 124 func (vv InputValues) JustValues() map[string]cty.Value { 125 ret := make(map[string]cty.Value, len(vv)) 126 for k, v := range vv { 127 ret[k] = v.Value 128 } 129 return ret 130 } 131 132 // DefaultVariableValues returns an InputValues map representing the default 133 // values specified for variables in the given configuration map. 134 func DefaultVariableValues(configs map[string]*configs.Variable) InputValues { 135 ret := make(InputValues) 136 for k, c := range configs { 137 if c.Default == cty.NilVal { 138 continue 139 } 140 ret[k] = &InputValue{ 141 Value: c.Default, 142 SourceType: ValueFromConfig, 143 SourceRange: tfdiags.SourceRangeFromHCL(c.DeclRange), 144 } 145 } 146 return ret 147 } 148 149 // SameValues returns true if the given InputValues has the same values as 150 // the receiever, disregarding the source types and source ranges. 151 // 152 // Values are compared using the cty "RawEquals" method, which means that 153 // unknown values can be considered equal to one another if they are of the 154 // same type. 155 func (vv InputValues) SameValues(other InputValues) bool { 156 if len(vv) != len(other) { 157 return false 158 } 159 160 for k, v := range vv { 161 ov, exists := other[k] 162 if !exists { 163 return false 164 } 165 if !v.Value.RawEquals(ov.Value) { 166 return false 167 } 168 } 169 170 return true 171 } 172 173 // HasValues returns true if the reciever has the same values as in the given 174 // map, disregarding the source types and source ranges. 175 // 176 // Values are compared using the cty "RawEquals" method, which means that 177 // unknown values can be considered equal to one another if they are of the 178 // same type. 179 func (vv InputValues) HasValues(vals map[string]cty.Value) bool { 180 if len(vv) != len(vals) { 181 return false 182 } 183 184 for k, v := range vv { 185 oVal, exists := vals[k] 186 if !exists { 187 return false 188 } 189 if !v.Value.RawEquals(oVal) { 190 return false 191 } 192 } 193 194 return true 195 } 196 197 // Identical returns true if the given InputValues has the same values, 198 // source types, and source ranges as the receiver. 199 // 200 // Values are compared using the cty "RawEquals" method, which means that 201 // unknown values can be considered equal to one another if they are of the 202 // same type. 203 // 204 // This method is primarily for testing. For most practical purposes, it's 205 // better to use SameValues or HasValues. 206 func (vv InputValues) Identical(other InputValues) bool { 207 if len(vv) != len(other) { 208 return false 209 } 210 211 for k, v := range vv { 212 ov, exists := other[k] 213 if !exists { 214 return false 215 } 216 if !v.Value.RawEquals(ov.Value) { 217 return false 218 } 219 if v.SourceType != ov.SourceType { 220 return false 221 } 222 if v.SourceRange != ov.SourceRange { 223 return false 224 } 225 } 226 227 return true 228 } 229 230 // checkInputVariables ensures that variable values supplied at the UI conform 231 // to their corresponding declarations in configuration. 232 // 233 // The set of values is considered valid only if the returned diagnostics 234 // does not contain errors. A valid set of values may still produce warnings, 235 // which should be returned to the user. 236 func checkInputVariables(vcs map[string]*configs.Variable, vs InputValues) tfdiags.Diagnostics { 237 var diags tfdiags.Diagnostics 238 239 for name, vc := range vcs { 240 val, isSet := vs[name] 241 if !isSet { 242 // Always an error, since the caller should already have included 243 // default values from the configuration in the values map. 244 diags = diags.Append(tfdiags.Sourceless( 245 tfdiags.Error, 246 "Unassigned variable", 247 fmt.Sprintf("The input variable %q has not been assigned a value. This is a bug in Terraform; please report it in a GitHub issue.", name), 248 )) 249 continue 250 } 251 252 wantType := vc.Type 253 254 // A given value is valid if it can convert to the desired type. 255 _, err := convert.Convert(val.Value, wantType) 256 if err != nil { 257 switch val.SourceType { 258 case ValueFromConfig, ValueFromAutoFile, ValueFromNamedFile: 259 // We have source location information for these. 260 diags = diags.Append(&hcl.Diagnostic{ 261 Severity: hcl.DiagError, 262 Summary: "Invalid value for input variable", 263 Detail: fmt.Sprintf("The given value is not valid for variable %q: %s.", name, err), 264 Subject: val.SourceRange.ToHCL().Ptr(), 265 }) 266 case ValueFromEnvVar: 267 diags = diags.Append(tfdiags.Sourceless( 268 tfdiags.Error, 269 "Invalid value for input variable", 270 fmt.Sprintf("The environment variable TF_VAR_%s does not contain a valid value for variable %q: %s.", name, name, err), 271 )) 272 case ValueFromCLIArg: 273 diags = diags.Append(tfdiags.Sourceless( 274 tfdiags.Error, 275 "Invalid value for input variable", 276 fmt.Sprintf("The argument -var=\"%s=...\" does not contain a valid value for variable %q: %s.", name, name, err), 277 )) 278 case ValueFromInput: 279 diags = diags.Append(tfdiags.Sourceless( 280 tfdiags.Error, 281 "Invalid value for input variable", 282 fmt.Sprintf("The value entered for variable %q is not valid: %s.", name, err), 283 )) 284 default: 285 // The above gets us good coverage for the situations users 286 // are likely to encounter with their own inputs. The other 287 // cases are generally implementation bugs, so we'll just 288 // use a generic error for these. 289 diags = diags.Append(tfdiags.Sourceless( 290 tfdiags.Error, 291 "Invalid value for input variable", 292 fmt.Sprintf("The value provided for variable %q is not valid: %s.", name, err), 293 )) 294 } 295 } 296 } 297 298 // Check for any variables that are assigned without being configured. 299 // This is always an implementation error in the caller, because we 300 // expect undefined variables to be caught during context construction 301 // where there is better context to report it well. 302 for name := range vs { 303 if _, defined := vcs[name]; !defined { 304 diags = diags.Append(tfdiags.Sourceless( 305 tfdiags.Error, 306 "Value assigned to undeclared variable", 307 fmt.Sprintf("A value was assigned to an undeclared input variable %q.", name), 308 )) 309 } 310 } 311 312 return diags 313 }