github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/configs/named_values.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/ext/typeexpr" 8 "github.com/hashicorp/hcl/v2/gohcl" 9 "github.com/hashicorp/hcl/v2/hclsyntax" 10 "github.com/zclconf/go-cty/cty" 11 "github.com/zclconf/go-cty/cty/convert" 12 ) 13 14 // A consistent detail message for all "not a valid identifier" diagnostics. 15 const badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes." 16 17 // Variable represents a "variable" block in a module or file. 18 type Variable struct { 19 Name string 20 Description string 21 Default cty.Value 22 Type cty.Type 23 ParsingMode VariableParsingMode 24 25 DescriptionSet bool 26 27 DeclRange hcl.Range 28 } 29 30 func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) { 31 v := &Variable{ 32 Name: block.Labels[0], 33 DeclRange: block.DefRange, 34 } 35 36 // Unless we're building an override, we'll set some defaults 37 // which we might override with attributes below. We leave these 38 // as zero-value in the override case so we can recognize whether 39 // or not they are set when we merge. 40 if !override { 41 v.Type = cty.DynamicPseudoType 42 v.ParsingMode = VariableParseLiteral 43 } 44 45 content, diags := block.Body.Content(variableBlockSchema) 46 47 if !hclsyntax.ValidIdentifier(v.Name) { 48 diags = append(diags, &hcl.Diagnostic{ 49 Severity: hcl.DiagError, 50 Summary: "Invalid variable name", 51 Detail: badIdentifierDetail, 52 Subject: &block.LabelRanges[0], 53 }) 54 } 55 56 // Don't allow declaration of variables that would conflict with the 57 // reserved attribute and block type names in a "module" block, since 58 // these won't be usable for child modules. 59 for _, attr := range moduleBlockSchema.Attributes { 60 if attr.Name == v.Name { 61 diags = append(diags, &hcl.Diagnostic{ 62 Severity: hcl.DiagError, 63 Summary: "Invalid variable name", 64 Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name), 65 Subject: &block.LabelRanges[0], 66 }) 67 } 68 } 69 for _, blockS := range moduleBlockSchema.Blocks { 70 if blockS.Type == v.Name { 71 diags = append(diags, &hcl.Diagnostic{ 72 Severity: hcl.DiagError, 73 Summary: "Invalid variable name", 74 Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type), 75 Subject: &block.LabelRanges[0], 76 }) 77 } 78 } 79 80 if attr, exists := content.Attributes["description"]; exists { 81 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) 82 diags = append(diags, valDiags...) 83 v.DescriptionSet = true 84 } 85 86 if attr, exists := content.Attributes["type"]; exists { 87 ty, parseMode, tyDiags := decodeVariableType(attr.Expr) 88 diags = append(diags, tyDiags...) 89 v.Type = ty 90 v.ParsingMode = parseMode 91 } 92 93 if attr, exists := content.Attributes["default"]; exists { 94 val, valDiags := attr.Expr.Value(nil) 95 diags = append(diags, valDiags...) 96 97 // Convert the default to the expected type so we can catch invalid 98 // defaults early and allow later code to assume validity. 99 // Note that this depends on us having already processed any "type" 100 // attribute above. 101 // However, we can't do this if we're in an override file where 102 // the type might not be set; we'll catch that during merge. 103 if v.Type != cty.NilType { 104 var err error 105 val, err = convert.Convert(val, v.Type) 106 if err != nil { 107 diags = append(diags, &hcl.Diagnostic{ 108 Severity: hcl.DiagError, 109 Summary: "Invalid default value for variable", 110 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 111 Subject: attr.Expr.Range().Ptr(), 112 }) 113 val = cty.DynamicVal 114 } 115 } 116 117 v.Default = val 118 } 119 120 return v, diags 121 } 122 123 func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) { 124 if exprIsNativeQuotedString(expr) { 125 // Here we're accepting the pre-0.12 form of variable type argument where 126 // the string values "string", "list" and "map" are accepted has a hint 127 // about the type used primarily for deciding how to parse values 128 // given on the command line and in environment variables. 129 // Only the native syntax ends up in this codepath; we handle the 130 // JSON syntax (which is, of course, quoted even in the new format) 131 // in the normal codepath below. 132 val, diags := expr.Value(nil) 133 if diags.HasErrors() { 134 return cty.DynamicPseudoType, VariableParseHCL, diags 135 } 136 str := val.AsString() 137 switch str { 138 case "string": 139 return cty.String, VariableParseLiteral, diags 140 case "list": 141 return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags 142 case "map": 143 return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags 144 default: 145 return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{ 146 Severity: hcl.DiagError, 147 Summary: "Invalid legacy variable type hint", 148 Detail: `The legacy variable type hint form, using a quoted string, allows only the values "string", "list", and "map". To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, 149 Subject: expr.Range().Ptr(), 150 }} 151 } 152 } 153 154 // First we'll deal with some shorthand forms that the HCL-level type 155 // expression parser doesn't include. These both emulate pre-0.12 behavior 156 // of allowing a list or map of any element type as long as all of the 157 // elements are consistent. This is the same as list(any) or map(any). 158 switch hcl.ExprAsKeyword(expr) { 159 case "list": 160 return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil 161 case "map": 162 return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil 163 } 164 165 ty, diags := typeexpr.TypeConstraint(expr) 166 if diags.HasErrors() { 167 return cty.DynamicPseudoType, VariableParseHCL, diags 168 } 169 170 switch { 171 case ty.IsPrimitiveType(): 172 // Primitive types use literal parsing. 173 return ty, VariableParseLiteral, diags 174 default: 175 // Everything else uses HCL parsing 176 return ty, VariableParseHCL, diags 177 } 178 } 179 180 // VariableParsingMode defines how values of a particular variable given by 181 // text-only mechanisms (command line arguments and environment variables) 182 // should be parsed to produce the final value. 183 type VariableParsingMode rune 184 185 // VariableParseLiteral is a variable parsing mode that just takes the given 186 // string directly as a cty.String value. 187 const VariableParseLiteral VariableParsingMode = 'L' 188 189 // VariableParseHCL is a variable parsing mode that attempts to parse the given 190 // string as an HCL expression and returns the result. 191 const VariableParseHCL VariableParsingMode = 'H' 192 193 // Parse uses the receiving parsing mode to process the given variable value 194 // string, returning the result along with any diagnostics. 195 // 196 // A VariableParsingMode does not know the expected type of the corresponding 197 // variable, so it's the caller's responsibility to attempt to convert the 198 // result to the appropriate type and return to the user any diagnostics that 199 // conversion may produce. 200 // 201 // The given name is used to create a synthetic filename in case any diagnostics 202 // must be generated about the given string value. This should be the name 203 // of the root module variable whose value will be populated from the given 204 // string. 205 // 206 // If the returned diagnostics has errors, the returned value may not be 207 // valid. 208 func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { 209 switch m { 210 case VariableParseLiteral: 211 return cty.StringVal(value), nil 212 case VariableParseHCL: 213 fakeFilename := fmt.Sprintf("<value for var.%s>", name) 214 expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) 215 if diags.HasErrors() { 216 return cty.DynamicVal, diags 217 } 218 val, valDiags := expr.Value(nil) 219 diags = append(diags, valDiags...) 220 return val, diags 221 default: 222 // Should never happen 223 panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) 224 } 225 } 226 227 // Output represents an "output" block in a module or file. 228 type Output struct { 229 Name string 230 Description string 231 Expr hcl.Expression 232 DependsOn []hcl.Traversal 233 Sensitive bool 234 235 DescriptionSet bool 236 SensitiveSet bool 237 238 DeclRange hcl.Range 239 } 240 241 func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { 242 o := &Output{ 243 Name: block.Labels[0], 244 DeclRange: block.DefRange, 245 } 246 247 schema := outputBlockSchema 248 if override { 249 schema = schemaForOverrides(schema) 250 } 251 252 content, diags := block.Body.Content(schema) 253 254 if !hclsyntax.ValidIdentifier(o.Name) { 255 diags = append(diags, &hcl.Diagnostic{ 256 Severity: hcl.DiagError, 257 Summary: "Invalid output name", 258 Detail: badIdentifierDetail, 259 Subject: &block.LabelRanges[0], 260 }) 261 } 262 263 if attr, exists := content.Attributes["description"]; exists { 264 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) 265 diags = append(diags, valDiags...) 266 o.DescriptionSet = true 267 } 268 269 if attr, exists := content.Attributes["value"]; exists { 270 o.Expr = attr.Expr 271 } 272 273 if attr, exists := content.Attributes["sensitive"]; exists { 274 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) 275 diags = append(diags, valDiags...) 276 o.SensitiveSet = true 277 } 278 279 if attr, exists := content.Attributes["depends_on"]; exists { 280 deps, depsDiags := decodeDependsOn(attr) 281 diags = append(diags, depsDiags...) 282 o.DependsOn = append(o.DependsOn, deps...) 283 } 284 285 return o, diags 286 } 287 288 // Local represents a single entry from a "locals" block in a module or file. 289 // The "locals" block itself is not represented, because it serves only to 290 // provide context for us to interpret its contents. 291 type Local struct { 292 Name string 293 Expr hcl.Expression 294 295 DeclRange hcl.Range 296 } 297 298 func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { 299 attrs, diags := block.Body.JustAttributes() 300 if len(attrs) == 0 { 301 return nil, diags 302 } 303 304 locals := make([]*Local, 0, len(attrs)) 305 for name, attr := range attrs { 306 if !hclsyntax.ValidIdentifier(name) { 307 diags = append(diags, &hcl.Diagnostic{ 308 Severity: hcl.DiagError, 309 Summary: "Invalid local value name", 310 Detail: badIdentifierDetail, 311 Subject: &attr.NameRange, 312 }) 313 } 314 315 locals = append(locals, &Local{ 316 Name: name, 317 Expr: attr.Expr, 318 DeclRange: attr.Range, 319 }) 320 } 321 return locals, diags 322 } 323 324 var variableBlockSchema = &hcl.BodySchema{ 325 Attributes: []hcl.AttributeSchema{ 326 { 327 Name: "description", 328 }, 329 { 330 Name: "default", 331 }, 332 { 333 Name: "type", 334 }, 335 }, 336 } 337 338 var outputBlockSchema = &hcl.BodySchema{ 339 Attributes: []hcl.AttributeSchema{ 340 { 341 Name: "description", 342 }, 343 { 344 Name: "value", 345 Required: true, 346 }, 347 { 348 Name: "depends_on", 349 }, 350 { 351 Name: "sensitive", 352 }, 353 }, 354 }