github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/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/gohcl" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "github.com/zclconf/go-cty/cty" 10 "github.com/zclconf/go-cty/cty/convert" 11 12 "github.com/eliastor/durgaform/internal/addrs" 13 "github.com/eliastor/durgaform/internal/typeexpr" 14 ) 15 16 // A consistent detail message for all "not a valid identifier" diagnostics. 17 const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." 18 19 // Variable represents a "variable" block in a module or file. 20 type Variable struct { 21 Name string 22 Description string 23 Default cty.Value 24 25 // Type is the concrete type of the variable value. 26 Type cty.Type 27 // ConstraintType is used for decoding and type conversions, and may 28 // contain nested ObjectWithOptionalAttr types. 29 ConstraintType cty.Type 30 TypeDefaults *typeexpr.Defaults 31 32 ParsingMode VariableParsingMode 33 Validations []*CheckRule 34 Sensitive bool 35 36 DescriptionSet bool 37 SensitiveSet bool 38 39 // Nullable indicates that null is a valid value for this variable. Setting 40 // Nullable to false means that the module can expect this variable to 41 // never be null. 42 Nullable bool 43 NullableSet bool 44 45 DeclRange hcl.Range 46 } 47 48 func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) { 49 v := &Variable{ 50 Name: block.Labels[0], 51 DeclRange: block.DefRange, 52 } 53 54 // Unless we're building an override, we'll set some defaults 55 // which we might override with attributes below. We leave these 56 // as zero-value in the override case so we can recognize whether 57 // or not they are set when we merge. 58 if !override { 59 v.Type = cty.DynamicPseudoType 60 v.ConstraintType = cty.DynamicPseudoType 61 v.ParsingMode = VariableParseLiteral 62 } 63 64 content, diags := block.Body.Content(variableBlockSchema) 65 66 if !hclsyntax.ValidIdentifier(v.Name) { 67 diags = append(diags, &hcl.Diagnostic{ 68 Severity: hcl.DiagError, 69 Summary: "Invalid variable name", 70 Detail: badIdentifierDetail, 71 Subject: &block.LabelRanges[0], 72 }) 73 } 74 75 // Don't allow declaration of variables that would conflict with the 76 // reserved attribute and block type names in a "module" block, since 77 // these won't be usable for child modules. 78 for _, attr := range moduleBlockSchema.Attributes { 79 if attr.Name == v.Name { 80 diags = append(diags, &hcl.Diagnostic{ 81 Severity: hcl.DiagError, 82 Summary: "Invalid variable name", 83 Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name), 84 Subject: &block.LabelRanges[0], 85 }) 86 } 87 } 88 for _, blockS := range moduleBlockSchema.Blocks { 89 if blockS.Type == v.Name { 90 diags = append(diags, &hcl.Diagnostic{ 91 Severity: hcl.DiagError, 92 Summary: "Invalid variable name", 93 Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type), 94 Subject: &block.LabelRanges[0], 95 }) 96 } 97 } 98 99 if attr, exists := content.Attributes["description"]; exists { 100 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) 101 diags = append(diags, valDiags...) 102 v.DescriptionSet = true 103 } 104 105 if attr, exists := content.Attributes["type"]; exists { 106 ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr) 107 diags = append(diags, tyDiags...) 108 v.ConstraintType = ty 109 v.TypeDefaults = tyDefaults 110 v.Type = ty.WithoutOptionalAttributesDeep() 111 v.ParsingMode = parseMode 112 } 113 114 if attr, exists := content.Attributes["sensitive"]; exists { 115 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive) 116 diags = append(diags, valDiags...) 117 v.SensitiveSet = true 118 } 119 120 if attr, exists := content.Attributes["nullable"]; exists { 121 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable) 122 diags = append(diags, valDiags...) 123 v.NullableSet = true 124 } else { 125 // The current default is true, which is subject to change in a future 126 // language edition. 127 v.Nullable = true 128 } 129 130 if attr, exists := content.Attributes["default"]; exists { 131 val, valDiags := attr.Expr.Value(nil) 132 diags = append(diags, valDiags...) 133 134 // Convert the default to the expected type so we can catch invalid 135 // defaults early and allow later code to assume validity. 136 // Note that this depends on us having already processed any "type" 137 // attribute above. 138 // However, we can't do this if we're in an override file where 139 // the type might not be set; we'll catch that during merge. 140 if v.ConstraintType != cty.NilType { 141 var err error 142 // If the type constraint has defaults, we must apply those 143 // defaults to the variable default value before type conversion. 144 if v.TypeDefaults != nil { 145 val = v.TypeDefaults.Apply(val) 146 } 147 val, err = convert.Convert(val, v.ConstraintType) 148 if err != nil { 149 diags = append(diags, &hcl.Diagnostic{ 150 Severity: hcl.DiagError, 151 Summary: "Invalid default value for variable", 152 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 153 Subject: attr.Expr.Range().Ptr(), 154 }) 155 val = cty.DynamicVal 156 } 157 } 158 159 if !v.Nullable && val.IsNull() { 160 diags = append(diags, &hcl.Diagnostic{ 161 Severity: hcl.DiagError, 162 Summary: "Invalid default value for variable", 163 Detail: "A null default value is not valid when nullable=false.", 164 Subject: attr.Expr.Range().Ptr(), 165 }) 166 } 167 168 v.Default = val 169 } 170 171 for _, block := range content.Blocks { 172 switch block.Type { 173 174 case "validation": 175 vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override) 176 diags = append(diags, moreDiags...) 177 v.Validations = append(v.Validations, vv) 178 179 default: 180 // The above cases should be exhaustive for all block types 181 // defined in variableBlockSchema 182 panic(fmt.Sprintf("unhandled block type %q", block.Type)) 183 } 184 } 185 186 return v, diags 187 } 188 189 func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) { 190 if exprIsNativeQuotedString(expr) { 191 // If a user provides the pre-0.12 form of variable type argument where 192 // the string values "string", "list" and "map" are accepted, we 193 // provide an error to point the user towards using the type system 194 // correctly has a hint. 195 // Only the native syntax ends up in this codepath; we handle the 196 // JSON syntax (which is, of course, quoted within the type system) 197 // in the normal codepath below. 198 val, diags := expr.Value(nil) 199 if diags.HasErrors() { 200 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 201 } 202 str := val.AsString() 203 switch str { 204 case "string": 205 diags = append(diags, &hcl.Diagnostic{ 206 Severity: hcl.DiagError, 207 Summary: "Invalid quoted type constraints", 208 Detail: "Durgaform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"string\".", 209 Subject: expr.Range().Ptr(), 210 }) 211 return cty.DynamicPseudoType, nil, VariableParseLiteral, diags 212 case "list": 213 diags = append(diags, &hcl.Diagnostic{ 214 Severity: hcl.DiagError, 215 Summary: "Invalid quoted type constraints", 216 Detail: "Durgaform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.", 217 Subject: expr.Range().Ptr(), 218 }) 219 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 220 case "map": 221 diags = append(diags, &hcl.Diagnostic{ 222 Severity: hcl.DiagError, 223 Summary: "Invalid quoted type constraints", 224 Detail: "Durgaform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.", 225 Subject: expr.Range().Ptr(), 226 }) 227 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 228 default: 229 return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{ 230 Severity: hcl.DiagError, 231 Summary: "Invalid legacy variable type hint", 232 Detail: `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, 233 Subject: expr.Range().Ptr(), 234 }} 235 } 236 } 237 238 // First we'll deal with some shorthand forms that the HCL-level type 239 // expression parser doesn't include. These both emulate pre-0.12 behavior 240 // of allowing a list or map of any element type as long as all of the 241 // elements are consistent. This is the same as list(any) or map(any). 242 switch hcl.ExprAsKeyword(expr) { 243 case "list": 244 return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil 245 case "map": 246 return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil 247 } 248 249 ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr) 250 if diags.HasErrors() { 251 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 252 } 253 254 switch { 255 case ty.IsPrimitiveType(): 256 // Primitive types use literal parsing. 257 return ty, typeDefaults, VariableParseLiteral, diags 258 default: 259 // Everything else uses HCL parsing 260 return ty, typeDefaults, VariableParseHCL, diags 261 } 262 } 263 264 // Required returns true if this variable is required to be set by the caller, 265 // or false if there is a default value that will be used when it isn't set. 266 func (v *Variable) Required() bool { 267 return v.Default == cty.NilVal 268 } 269 270 // VariableParsingMode defines how values of a particular variable given by 271 // text-only mechanisms (command line arguments and environment variables) 272 // should be parsed to produce the final value. 273 type VariableParsingMode rune 274 275 // VariableParseLiteral is a variable parsing mode that just takes the given 276 // string directly as a cty.String value. 277 const VariableParseLiteral VariableParsingMode = 'L' 278 279 // VariableParseHCL is a variable parsing mode that attempts to parse the given 280 // string as an HCL expression and returns the result. 281 const VariableParseHCL VariableParsingMode = 'H' 282 283 // Parse uses the receiving parsing mode to process the given variable value 284 // string, returning the result along with any diagnostics. 285 // 286 // A VariableParsingMode does not know the expected type of the corresponding 287 // variable, so it's the caller's responsibility to attempt to convert the 288 // result to the appropriate type and return to the user any diagnostics that 289 // conversion may produce. 290 // 291 // The given name is used to create a synthetic filename in case any diagnostics 292 // must be generated about the given string value. This should be the name 293 // of the root module variable whose value will be populated from the given 294 // string. 295 // 296 // If the returned diagnostics has errors, the returned value may not be 297 // valid. 298 func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { 299 switch m { 300 case VariableParseLiteral: 301 return cty.StringVal(value), nil 302 case VariableParseHCL: 303 fakeFilename := fmt.Sprintf("<value for var.%s>", name) 304 expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) 305 if diags.HasErrors() { 306 return cty.DynamicVal, diags 307 } 308 val, valDiags := expr.Value(nil) 309 diags = append(diags, valDiags...) 310 return val, diags 311 default: 312 // Should never happen 313 panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) 314 } 315 } 316 317 // decodeVariableValidationBlock is a wrapper around decodeCheckRuleBlock 318 // that imposes the additional rule that the condition expression can refer 319 // only to an input variable of the given name. 320 func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) { 321 vv, diags := decodeCheckRuleBlock(block, override) 322 if vv.Condition != nil { 323 // The validation condition can only refer to the variable itself, 324 // to ensure that the variable declaration can't create additional 325 // edges in the dependency graph. 326 goodRefs := 0 327 for _, traversal := range vv.Condition.Variables() { 328 ref, moreDiags := addrs.ParseRef(traversal) 329 if !moreDiags.HasErrors() { 330 if addr, ok := ref.Subject.(addrs.InputVariable); ok { 331 if addr.Name == varName { 332 goodRefs++ 333 continue // Reference is valid 334 } 335 } 336 } 337 // If we fall out here then the reference is invalid. 338 diags = diags.Append(&hcl.Diagnostic{ 339 Severity: hcl.DiagError, 340 Summary: "Invalid reference in variable validation", 341 Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName), 342 Subject: traversal.SourceRange().Ptr(), 343 }) 344 } 345 if goodRefs < 1 { 346 diags = diags.Append(&hcl.Diagnostic{ 347 Severity: hcl.DiagError, 348 Summary: "Invalid variable validation condition", 349 Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName), 350 Subject: vv.Condition.Range().Ptr(), 351 }) 352 } 353 } 354 355 if vv.ErrorMessage != nil { 356 // The same applies to the validation error message, except that 357 // references are not required. A string literal is a valid error 358 // message. 359 goodRefs := 0 360 for _, traversal := range vv.ErrorMessage.Variables() { 361 ref, moreDiags := addrs.ParseRef(traversal) 362 if !moreDiags.HasErrors() { 363 if addr, ok := ref.Subject.(addrs.InputVariable); ok { 364 if addr.Name == varName { 365 goodRefs++ 366 continue // Reference is valid 367 } 368 } 369 } 370 // If we fall out here then the reference is invalid. 371 diags = diags.Append(&hcl.Diagnostic{ 372 Severity: hcl.DiagError, 373 Summary: "Invalid reference in variable validation", 374 Detail: fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName), 375 Subject: traversal.SourceRange().Ptr(), 376 }) 377 } 378 } 379 380 return vv, diags 381 } 382 383 // Output represents an "output" block in a module or file. 384 type Output struct { 385 Name string 386 Description string 387 Expr hcl.Expression 388 DependsOn []hcl.Traversal 389 Sensitive bool 390 391 Preconditions []*CheckRule 392 393 DescriptionSet bool 394 SensitiveSet bool 395 396 DeclRange hcl.Range 397 } 398 399 func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { 400 var diags hcl.Diagnostics 401 402 o := &Output{ 403 Name: block.Labels[0], 404 DeclRange: block.DefRange, 405 } 406 407 schema := outputBlockSchema 408 if override { 409 schema = schemaForOverrides(schema) 410 } 411 412 content, moreDiags := block.Body.Content(schema) 413 diags = append(diags, moreDiags...) 414 415 if !hclsyntax.ValidIdentifier(o.Name) { 416 diags = append(diags, &hcl.Diagnostic{ 417 Severity: hcl.DiagError, 418 Summary: "Invalid output name", 419 Detail: badIdentifierDetail, 420 Subject: &block.LabelRanges[0], 421 }) 422 } 423 424 if attr, exists := content.Attributes["description"]; exists { 425 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) 426 diags = append(diags, valDiags...) 427 o.DescriptionSet = true 428 } 429 430 if attr, exists := content.Attributes["value"]; exists { 431 o.Expr = attr.Expr 432 } 433 434 if attr, exists := content.Attributes["sensitive"]; exists { 435 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) 436 diags = append(diags, valDiags...) 437 o.SensitiveSet = true 438 } 439 440 if attr, exists := content.Attributes["depends_on"]; exists { 441 deps, depsDiags := decodeDependsOn(attr) 442 diags = append(diags, depsDiags...) 443 o.DependsOn = append(o.DependsOn, deps...) 444 } 445 446 for _, block := range content.Blocks { 447 switch block.Type { 448 case "precondition": 449 cr, moreDiags := decodeCheckRuleBlock(block, override) 450 diags = append(diags, moreDiags...) 451 o.Preconditions = append(o.Preconditions, cr) 452 case "postcondition": 453 diags = append(diags, &hcl.Diagnostic{ 454 Severity: hcl.DiagError, 455 Summary: "Postconditions are not allowed", 456 Detail: "Output values can only have preconditions, not postconditions.", 457 Subject: block.TypeRange.Ptr(), 458 }) 459 default: 460 // The cases above should be exhaustive for all block types 461 // defined in the block type schema, so this shouldn't happen. 462 panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type)) 463 } 464 } 465 466 return o, diags 467 } 468 469 // Local represents a single entry from a "locals" block in a module or file. 470 // The "locals" block itself is not represented, because it serves only to 471 // provide context for us to interpret its contents. 472 type Local struct { 473 Name string 474 Expr hcl.Expression 475 476 DeclRange hcl.Range 477 } 478 479 func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { 480 attrs, diags := block.Body.JustAttributes() 481 if len(attrs) == 0 { 482 return nil, diags 483 } 484 485 locals := make([]*Local, 0, len(attrs)) 486 for name, attr := range attrs { 487 if !hclsyntax.ValidIdentifier(name) { 488 diags = append(diags, &hcl.Diagnostic{ 489 Severity: hcl.DiagError, 490 Summary: "Invalid local value name", 491 Detail: badIdentifierDetail, 492 Subject: &attr.NameRange, 493 }) 494 } 495 496 locals = append(locals, &Local{ 497 Name: name, 498 Expr: attr.Expr, 499 DeclRange: attr.Range, 500 }) 501 } 502 return locals, diags 503 } 504 505 // Addr returns the address of the local value declared by the receiver, 506 // relative to its containing module. 507 func (l *Local) Addr() addrs.LocalValue { 508 return addrs.LocalValue{ 509 Name: l.Name, 510 } 511 } 512 513 var variableBlockSchema = &hcl.BodySchema{ 514 Attributes: []hcl.AttributeSchema{ 515 { 516 Name: "description", 517 }, 518 { 519 Name: "default", 520 }, 521 { 522 Name: "type", 523 }, 524 { 525 Name: "sensitive", 526 }, 527 { 528 Name: "nullable", 529 }, 530 }, 531 Blocks: []hcl.BlockHeaderSchema{ 532 { 533 Type: "validation", 534 }, 535 }, 536 } 537 538 var outputBlockSchema = &hcl.BodySchema{ 539 Attributes: []hcl.AttributeSchema{ 540 { 541 Name: "description", 542 }, 543 { 544 Name: "value", 545 Required: true, 546 }, 547 { 548 Name: "depends_on", 549 }, 550 { 551 Name: "sensitive", 552 }, 553 }, 554 Blocks: []hcl.BlockHeaderSchema{ 555 {Type: "precondition"}, 556 {Type: "postcondition"}, 557 }, 558 }