github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/decode.go (about) 1 package parse 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/turbot/go-kit/helpers" 10 "github.com/turbot/pipe-fittings/hclhelpers" 11 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 12 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig/var_config" 13 ) 14 15 // A consistent detail message for all "not a valid identifier" diagnostics. 16 const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." 17 18 var missingVariableErrors = []string{ 19 // returned when the context variables does not have top level 'type' node (locals/control/etc) 20 "Unknown variable", 21 // returned when the variables have the type object but a field has not yet been populated 22 "Unsupported attribute", 23 "Missing map element", 24 } 25 26 func decode(parseCtx *ModParseContext) hcl.Diagnostics { 27 var diags hcl.Diagnostics 28 29 blocks, err := parseCtx.BlocksToDecode() 30 // build list of blocks to decode 31 if err != nil { 32 diags = append(diags, &hcl.Diagnostic{ 33 Severity: hcl.DiagError, 34 Summary: "failed to determine required dependency order", 35 Detail: err.Error()}) 36 return diags 37 } 38 39 // now clear dependencies from run context - they will be rebuilt 40 parseCtx.ClearDependencies() 41 42 for _, block := range blocks { 43 if block.Type == modconfig.BlockTypeLocals { 44 resources, res := decodeLocalsBlock(block, parseCtx) 45 if !res.Success() { 46 diags = append(diags, res.Diags...) 47 continue 48 } 49 for _, resource := range resources { 50 resourceDiags := addResourceToMod(resource, block, parseCtx) 51 diags = append(diags, resourceDiags...) 52 } 53 } else { 54 resource, res := decodeBlock(block, parseCtx) 55 diags = append(diags, res.Diags...) 56 if !res.Success() || resource == nil { 57 continue 58 } 59 60 resourceDiags := addResourceToMod(resource, block, parseCtx) 61 diags = append(diags, resourceDiags...) 62 } 63 } 64 65 return diags 66 } 67 68 func addResourceToMod(resource modconfig.HclResource, block *hcl.Block, parseCtx *ModParseContext) hcl.Diagnostics { 69 if !shouldAddToMod(resource, block, parseCtx) { 70 return nil 71 } 72 return parseCtx.CurrentMod.AddResource(resource) 73 74 } 75 76 func shouldAddToMod(resource modconfig.HclResource, block *hcl.Block, parseCtx *ModParseContext) bool { 77 switch resource.(type) { 78 // do not add mods, withs 79 case *modconfig.Mod, *modconfig.DashboardWith: 80 return false 81 82 case *modconfig.DashboardCategory, *modconfig.DashboardInput: 83 // if this is a dashboard category or dashboard input, only add top level blocks 84 // this is to allow nested categories/inputs to have the same name as top level categories 85 // (nested inputs are added by Dashboard.InitInputs) 86 return parseCtx.IsTopLevelBlock(block) 87 default: 88 return true 89 } 90 } 91 92 // special case decode logic for locals 93 func decodeLocalsBlock(block *hcl.Block, parseCtx *ModParseContext) ([]modconfig.HclResource, *DecodeResult) { 94 var resources []modconfig.HclResource 95 var res = newDecodeResult() 96 97 // TODO remove and call ShouldIncludeBlock from BlocksToDecode 98 // https://github.com/turbot/steampipe/issues/2640 99 // if opts specifies block types, then check whether this type is included 100 if !parseCtx.ShouldIncludeBlock(block) { 101 return nil, res 102 } 103 104 // check name is valid 105 diags := validateName(block) 106 if diags.HasErrors() { 107 res.addDiags(diags) 108 return nil, res 109 } 110 111 var locals []*modconfig.Local 112 locals, res = decodeLocals(block, parseCtx) 113 for _, local := range locals { 114 resources = append(resources, local) 115 handleModDecodeResult(local, res, block, parseCtx) 116 } 117 118 return resources, res 119 } 120 121 func decodeBlock(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, *DecodeResult) { 122 var resource modconfig.HclResource 123 var res = newDecodeResult() 124 125 // TODO remove and call ShouldIncludeBlock from BlocksToDecode 126 // https://github.com/turbot/steampipe/issues/2640 127 // if opts specifies block types, then check whether this type is included 128 if !parseCtx.ShouldIncludeBlock(block) { 129 return nil, res 130 } 131 132 // has this block already been decoded? 133 // (this could happen if it is a child block and has been decoded before its parent as part of second decode phase) 134 if resource, ok := parseCtx.GetDecodedResourceForBlock(block); ok { 135 return resource, res 136 } 137 138 // check name is valid 139 diags := validateName(block) 140 if diags.HasErrors() { 141 res.addDiags(diags) 142 return nil, res 143 } 144 145 // now do the actual decode 146 switch { 147 case helpers.StringSliceContains(modconfig.NodeAndEdgeProviderBlocks, block.Type): 148 resource, res = decodeNodeAndEdgeProvider(block, parseCtx) 149 case helpers.StringSliceContains(modconfig.QueryProviderBlocks, block.Type): 150 resource, res = decodeQueryProvider(block, parseCtx) 151 default: 152 switch block.Type { 153 case modconfig.BlockTypeMod: 154 // decodeMode has slightly different args as this code is shared with ParseModDefinition 155 resource, res = decodeMod(block, parseCtx.EvalCtx, parseCtx.CurrentMod) 156 case modconfig.BlockTypeDashboard: 157 resource, res = decodeDashboard(block, parseCtx) 158 case modconfig.BlockTypeContainer: 159 resource, res = decodeDashboardContainer(block, parseCtx) 160 case modconfig.BlockTypeVariable: 161 resource, res = decodeVariable(block, parseCtx) 162 case modconfig.BlockTypeBenchmark: 163 resource, res = decodeBenchmark(block, parseCtx) 164 default: 165 // all other blocks are treated the same: 166 resource, res = decodeResource(block, parseCtx) 167 } 168 } 169 170 // handle the result 171 // - if there are dependencies, add to run context 172 handleModDecodeResult(resource, res, block, parseCtx) 173 174 return resource, res 175 } 176 177 func decodeMod(block *hcl.Block, evalCtx *hcl.EvalContext, mod *modconfig.Mod) (*modconfig.Mod, *DecodeResult) { 178 res := newDecodeResult() 179 // decode the body 180 diags := decodeHclBody(block.Body, evalCtx, mod, mod) 181 res.handleDecodeDiags(diags) 182 return mod, res 183 } 184 185 // generic decode function for any resource we do not have custom decode logic for 186 func decodeResource(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, *DecodeResult) { 187 res := newDecodeResult() 188 // get shell resource 189 resource, diags := resourceForBlock(block, parseCtx) 190 res.handleDecodeDiags(diags) 191 if diags.HasErrors() { 192 return nil, res 193 } 194 195 diags = decodeHclBody(block.Body, parseCtx.EvalCtx, parseCtx, resource) 196 if len(diags) > 0 { 197 res.handleDecodeDiags(diags) 198 } 199 return resource, res 200 } 201 202 // return a shell resource for the given block 203 func resourceForBlock(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, hcl.Diagnostics) { 204 var resource modconfig.HclResource 205 // parseCtx already contains the current mod 206 mod := parseCtx.CurrentMod 207 blockName := parseCtx.DetermineBlockName(block) 208 209 factoryFuncs := map[string]func(*hcl.Block, *modconfig.Mod, string) modconfig.HclResource{ 210 // for block type mod, just use the current mod 211 modconfig.BlockTypeMod: func(*hcl.Block, *modconfig.Mod, string) modconfig.HclResource { return mod }, 212 modconfig.BlockTypeQuery: modconfig.NewQuery, 213 modconfig.BlockTypeControl: modconfig.NewControl, 214 modconfig.BlockTypeBenchmark: modconfig.NewBenchmark, 215 modconfig.BlockTypeDashboard: modconfig.NewDashboard, 216 modconfig.BlockTypeContainer: modconfig.NewDashboardContainer, 217 modconfig.BlockTypeChart: modconfig.NewDashboardChart, 218 modconfig.BlockTypeCard: modconfig.NewDashboardCard, 219 modconfig.BlockTypeFlow: modconfig.NewDashboardFlow, 220 modconfig.BlockTypeGraph: modconfig.NewDashboardGraph, 221 modconfig.BlockTypeHierarchy: modconfig.NewDashboardHierarchy, 222 modconfig.BlockTypeImage: modconfig.NewDashboardImage, 223 modconfig.BlockTypeInput: modconfig.NewDashboardInput, 224 modconfig.BlockTypeTable: modconfig.NewDashboardTable, 225 modconfig.BlockTypeText: modconfig.NewDashboardText, 226 modconfig.BlockTypeNode: modconfig.NewDashboardNode, 227 modconfig.BlockTypeEdge: modconfig.NewDashboardEdge, 228 modconfig.BlockTypeCategory: modconfig.NewDashboardCategory, 229 modconfig.BlockTypeWith: modconfig.NewDashboardWith, 230 } 231 232 factoryFunc, ok := factoryFuncs[block.Type] 233 if !ok { 234 return nil, hcl.Diagnostics{&hcl.Diagnostic{ 235 Severity: hcl.DiagError, 236 Summary: fmt.Sprintf("resourceForBlock called for unsupported block type %s", block.Type), 237 Subject: hclhelpers.BlockRangePointer(block), 238 }, 239 } 240 } 241 resource = factoryFunc(block, mod, blockName) 242 return resource, nil 243 } 244 245 func decodeLocals(block *hcl.Block, parseCtx *ModParseContext) ([]*modconfig.Local, *DecodeResult) { 246 res := newDecodeResult() 247 attrs, diags := block.Body.JustAttributes() 248 if len(attrs) == 0 { 249 res.Diags = diags 250 return nil, res 251 } 252 253 // build list of locals 254 locals := make([]*modconfig.Local, 0, len(attrs)) 255 for name, attr := range attrs { 256 if !hclsyntax.ValidIdentifier(name) { 257 res.Diags = append(res.Diags, &hcl.Diagnostic{ 258 Severity: hcl.DiagError, 259 Summary: "Invalid local value name", 260 Detail: badIdentifierDetail, 261 Subject: &attr.NameRange, 262 }) 263 continue 264 } 265 // try to evaluate expression 266 val, diags := attr.Expr.Value(parseCtx.EvalCtx) 267 // handle any resulting diags, which may specify dependencies 268 res.handleDecodeDiags(diags) 269 270 // add to our list 271 locals = append(locals, modconfig.NewLocal(name, val, attr.Range, parseCtx.CurrentMod)) 272 } 273 return locals, res 274 } 275 276 func decodeVariable(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.Variable, *DecodeResult) { 277 res := newDecodeResult() 278 279 var variable *modconfig.Variable 280 content, diags := block.Body.Content(VariableBlockSchema) 281 res.handleDecodeDiags(diags) 282 283 v, diags := var_config.DecodeVariableBlock(block, content, false) 284 res.handleDecodeDiags(diags) 285 286 if res.Success() { 287 variable = modconfig.NewVariable(v, parseCtx.CurrentMod) 288 } 289 290 return variable, res 291 292 } 293 294 func decodeQueryProvider(block *hcl.Block, parseCtx *ModParseContext) (modconfig.QueryProvider, *DecodeResult) { 295 res := newDecodeResult() 296 // get shell resource 297 resource, diags := resourceForBlock(block, parseCtx) 298 res.handleDecodeDiags(diags) 299 if diags.HasErrors() { 300 return nil, res 301 } 302 // do a partial decode using an empty schema - use to pull out all body content in the remain block 303 _, remain, diags := block.Body.PartialContent(&hcl.BodySchema{}) 304 res.handleDecodeDiags(diags) 305 if !res.Success() { 306 return nil, res 307 } 308 309 // decode the body into 'resource' to populate all properties that can be automatically decoded 310 diags = decodeHclBody(remain, parseCtx.EvalCtx, parseCtx, resource) 311 res.handleDecodeDiags(diags) 312 313 // decode 'with',args and params blocks 314 res.Merge(decodeQueryProviderBlocks(block, remain.(*hclsyntax.Body), resource, parseCtx)) 315 316 return resource.(modconfig.QueryProvider), res 317 } 318 319 func decodeQueryProviderBlocks(block *hcl.Block, content *hclsyntax.Body, resource modconfig.HclResource, parseCtx *ModParseContext) *DecodeResult { 320 var diags hcl.Diagnostics 321 res := newDecodeResult() 322 queryProvider, ok := resource.(modconfig.QueryProvider) 323 if !ok { 324 // coding error 325 panic(fmt.Sprintf("block type %s not convertible to a QueryProvider", block.Type)) 326 } 327 328 if attr, exists := content.Attributes[modconfig.AttributeArgs]; exists { 329 args, runtimeDependencies, diags := decodeArgs(attr.AsHCLAttribute(), parseCtx.EvalCtx, queryProvider) 330 if diags.HasErrors() { 331 // handle dependencies 332 res.handleDecodeDiags(diags) 333 } else { 334 queryProvider.SetArgs(args) 335 queryProvider.AddRuntimeDependencies(runtimeDependencies) 336 } 337 } 338 339 var params []*modconfig.ParamDef 340 for _, b := range content.Blocks { 341 block = b.AsHCLBlock() 342 switch block.Type { 343 case modconfig.BlockTypeParam: 344 paramDef, runtimeDependencies, moreDiags := decodeParam(block, parseCtx) 345 if !moreDiags.HasErrors() { 346 params = append(params, paramDef) 347 queryProvider.AddRuntimeDependencies(runtimeDependencies) 348 // add and references contained in the param block to the control refs 349 moreDiags = AddReferences(resource, block, parseCtx) 350 } 351 diags = append(diags, moreDiags...) 352 } 353 } 354 355 queryProvider.SetParams(params) 356 res.handleDecodeDiags(diags) 357 return res 358 } 359 360 func decodeNodeAndEdgeProvider(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, *DecodeResult) { 361 res := newDecodeResult() 362 363 // get shell resource 364 resource, diags := resourceForBlock(block, parseCtx) 365 res.handleDecodeDiags(diags) 366 if diags.HasErrors() { 367 return nil, res 368 } 369 370 nodeAndEdgeProvider, ok := resource.(modconfig.NodeAndEdgeProvider) 371 if !ok { 372 // coding error 373 panic(fmt.Sprintf("block type %s not convertible to a NodeAndEdgeProvider", block.Type)) 374 } 375 376 // do a partial decode using an empty schema - use to pull out all body content in the remain block 377 _, r, diags := block.Body.PartialContent(&hcl.BodySchema{}) 378 body := r.(*hclsyntax.Body) 379 res.handleDecodeDiags(diags) 380 if !res.Success() { 381 return nil, res 382 } 383 384 // decode the body into 'resource' to populate all properties that can be automatically decoded 385 diags = decodeHclBody(body, parseCtx.EvalCtx, parseCtx, resource) 386 // handle any resulting diags, which may specify dependencies 387 res.handleDecodeDiags(diags) 388 389 // decode sql args and params 390 res.Merge(decodeQueryProviderBlocks(block, body, resource, parseCtx)) 391 392 // now decode child blocks 393 if len(body.Blocks) > 0 { 394 blocksRes := decodeNodeAndEdgeProviderBlocks(body, nodeAndEdgeProvider, parseCtx) 395 res.Merge(blocksRes) 396 } 397 398 return resource, res 399 } 400 401 func decodeNodeAndEdgeProviderBlocks(content *hclsyntax.Body, nodeAndEdgeProvider modconfig.NodeAndEdgeProvider, parseCtx *ModParseContext) *DecodeResult { 402 var res = newDecodeResult() 403 404 for _, b := range content.Blocks { 405 block := b.AsHCLBlock() 406 switch block.Type { 407 case modconfig.BlockTypeCategory: 408 // decode block 409 category, blockRes := decodeBlock(block, parseCtx) 410 res.Merge(blockRes) 411 if !blockRes.Success() { 412 continue 413 } 414 415 // add the category to the nodeAndEdgeProvider 416 res.addDiags(nodeAndEdgeProvider.AddCategory(category.(*modconfig.DashboardCategory))) 417 418 // DO NOT add the category to the mod 419 420 case modconfig.BlockTypeNode, modconfig.BlockTypeEdge: 421 child, childRes := decodeQueryProvider(block, parseCtx) 422 423 // TACTICAL if child has any runtime dependencies, claim them 424 // this is to ensure if this resource is used as base, we can be correctly identified 425 // as the publisher of the runtime dependencies 426 for _, r := range child.GetRuntimeDependencies() { 427 r.Provider = nodeAndEdgeProvider 428 } 429 430 // populate metadata, set references and call OnDecoded 431 handleModDecodeResult(child, childRes, block, parseCtx) 432 res.Merge(childRes) 433 if res.Success() { 434 moreDiags := nodeAndEdgeProvider.AddChild(child) 435 res.addDiags(moreDiags) 436 } 437 case modconfig.BlockTypeWith: 438 with, withRes := decodeBlock(block, parseCtx) 439 res.Merge(withRes) 440 if res.Success() { 441 moreDiags := nodeAndEdgeProvider.AddWith(with.(*modconfig.DashboardWith)) 442 res.addDiags(moreDiags) 443 } 444 } 445 446 } 447 448 return res 449 } 450 451 func decodeDashboard(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.Dashboard, *DecodeResult) { 452 res := newDecodeResult() 453 dashboard := modconfig.NewDashboard(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*modconfig.Dashboard) 454 455 // do a partial decode using an empty schema - use to pull out all body content in the remain block 456 _, r, diags := block.Body.PartialContent(&hcl.BodySchema{}) 457 body := r.(*hclsyntax.Body) 458 res.handleDecodeDiags(diags) 459 460 // decode the body into 'dashboardContainer' to populate all properties that can be automatically decoded 461 diags = decodeHclBody(body, parseCtx.EvalCtx, parseCtx, dashboard) 462 // handle any resulting diags, which may specify dependencies 463 res.handleDecodeDiags(diags) 464 465 if dashboard.Base != nil && len(dashboard.Base.ChildNames) > 0 { 466 supportedChildren := []string{modconfig.BlockTypeContainer, modconfig.BlockTypeChart, modconfig.BlockTypeControl, modconfig.BlockTypeCard, modconfig.BlockTypeFlow, modconfig.BlockTypeGraph, modconfig.BlockTypeHierarchy, modconfig.BlockTypeImage, modconfig.BlockTypeInput, modconfig.BlockTypeTable, modconfig.BlockTypeText} 467 // TACTICAL: we should be passing in the block for the Base resource - but this is only used for diags 468 // and we do not expect to get any (as this function has already succeeded when the base was originally parsed) 469 children, _ := resolveChildrenFromNames(dashboard.Base.ChildNames, block, supportedChildren, parseCtx) 470 dashboard.Base.SetChildren(children) 471 } 472 if !res.Success() { 473 return dashboard, res 474 } 475 476 // now decode child blocks 477 if len(body.Blocks) > 0 { 478 blocksRes := decodeDashboardBlocks(body, dashboard, parseCtx) 479 res.Merge(blocksRes) 480 } 481 482 return dashboard, res 483 } 484 485 func decodeDashboardBlocks(content *hclsyntax.Body, dashboard *modconfig.Dashboard, parseCtx *ModParseContext) *DecodeResult { 486 var res = newDecodeResult() 487 // set dashboard as parent on the run context - this is used when generating names for anonymous blocks 488 parseCtx.PushParent(dashboard) 489 defer func() { 490 parseCtx.PopParent() 491 }() 492 493 for _, b := range content.Blocks { 494 block := b.AsHCLBlock() 495 496 // decode block 497 resource, blockRes := decodeBlock(block, parseCtx) 498 res.Merge(blockRes) 499 if !blockRes.Success() { 500 continue 501 } 502 503 // we expect either inputs or child report nodes 504 // add the resource to the mod 505 res.addDiags(addResourceToMod(resource, block, parseCtx)) 506 // add to the dashboard children 507 // (we expect this cast to always succeed) 508 if child, ok := resource.(modconfig.ModTreeItem); ok { 509 dashboard.AddChild(child) 510 } 511 512 } 513 514 moreDiags := dashboard.InitInputs() 515 res.addDiags(moreDiags) 516 517 return res 518 } 519 520 func decodeDashboardContainer(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.DashboardContainer, *DecodeResult) { 521 res := newDecodeResult() 522 container := modconfig.NewDashboardContainer(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*modconfig.DashboardContainer) 523 524 // do a partial decode using an empty schema - use to pull out all body content in the remain block 525 _, r, diags := block.Body.PartialContent(&hcl.BodySchema{}) 526 body := r.(*hclsyntax.Body) 527 res.handleDecodeDiags(diags) 528 if !res.Success() { 529 return nil, res 530 } 531 532 // decode the body into 'dashboardContainer' to populate all properties that can be automatically decoded 533 diags = decodeHclBody(body, parseCtx.EvalCtx, parseCtx, container) 534 // handle any resulting diags, which may specify dependencies 535 res.handleDecodeDiags(diags) 536 537 // now decode child blocks 538 if len(body.Blocks) > 0 { 539 blocksRes := decodeDashboardContainerBlocks(body, container, parseCtx) 540 res.Merge(blocksRes) 541 } 542 543 return container, res 544 } 545 546 func decodeDashboardContainerBlocks(content *hclsyntax.Body, dashboardContainer *modconfig.DashboardContainer, parseCtx *ModParseContext) *DecodeResult { 547 var res = newDecodeResult() 548 549 // set container as parent on the run context - this is used when generating names for anonymous blocks 550 parseCtx.PushParent(dashboardContainer) 551 defer func() { 552 parseCtx.PopParent() 553 }() 554 555 for _, b := range content.Blocks { 556 block := b.AsHCLBlock() 557 resource, blockRes := decodeBlock(block, parseCtx) 558 res.Merge(blockRes) 559 if !blockRes.Success() { 560 continue 561 } 562 563 // special handling for inputs 564 if b.Type == modconfig.BlockTypeInput { 565 input := resource.(*modconfig.DashboardInput) 566 dashboardContainer.Inputs = append(dashboardContainer.Inputs, input) 567 dashboardContainer.AddChild(input) 568 // the input will be added to the mod by the parent dashboard 569 570 } else { 571 // for all other children, add to mod and children 572 res.addDiags(addResourceToMod(resource, block, parseCtx)) 573 if child, ok := resource.(modconfig.ModTreeItem); ok { 574 dashboardContainer.AddChild(child) 575 } 576 } 577 } 578 579 return res 580 } 581 582 func decodeBenchmark(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.Benchmark, *DecodeResult) { 583 res := newDecodeResult() 584 benchmark := modconfig.NewBenchmark(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*modconfig.Benchmark) 585 content, diags := block.Body.Content(BenchmarkBlockSchema) 586 res.handleDecodeDiags(diags) 587 588 diags = decodeProperty(content, "children", &benchmark.ChildNames, parseCtx.EvalCtx) 589 res.handleDecodeDiags(diags) 590 591 diags = decodeProperty(content, "description", &benchmark.Description, parseCtx.EvalCtx) 592 res.handleDecodeDiags(diags) 593 594 diags = decodeProperty(content, "documentation", &benchmark.Documentation, parseCtx.EvalCtx) 595 res.handleDecodeDiags(diags) 596 597 diags = decodeProperty(content, "tags", &benchmark.Tags, parseCtx.EvalCtx) 598 res.handleDecodeDiags(diags) 599 600 diags = decodeProperty(content, "title", &benchmark.Title, parseCtx.EvalCtx) 601 res.handleDecodeDiags(diags) 602 603 diags = decodeProperty(content, "type", &benchmark.Type, parseCtx.EvalCtx) 604 res.handleDecodeDiags(diags) 605 606 diags = decodeProperty(content, "display", &benchmark.Display, parseCtx.EvalCtx) 607 res.handleDecodeDiags(diags) 608 609 // now add children 610 if res.Success() { 611 supportedChildren := []string{modconfig.BlockTypeBenchmark, modconfig.BlockTypeControl} 612 children, diags := resolveChildrenFromNames(benchmark.ChildNames.StringList(), block, supportedChildren, parseCtx) 613 res.handleDecodeDiags(diags) 614 615 // now set children and child name strings 616 benchmark.SetChildren(children) 617 benchmark.ChildNameStrings = getChildNameStringsFromModTreeItem(children) 618 } 619 620 diags = decodeProperty(content, "base", &benchmark.Base, parseCtx.EvalCtx) 621 res.handleDecodeDiags(diags) 622 if benchmark.Base != nil && len(benchmark.Base.ChildNames) > 0 { 623 supportedChildren := []string{modconfig.BlockTypeBenchmark, modconfig.BlockTypeControl} 624 // TACTICAL: we should be passing in the block for the Base resource - but this is only used for diags 625 // and we do not expect to get any (as this function has already succeeded when the base was originally parsed) 626 children, _ := resolveChildrenFromNames(benchmark.Base.ChildNameStrings, block, supportedChildren, parseCtx) 627 benchmark.Base.SetChildren(children) 628 } 629 diags = decodeProperty(content, "width", &benchmark.Width, parseCtx.EvalCtx) 630 res.handleDecodeDiags(diags) 631 return benchmark, res 632 } 633 634 func decodeProperty(content *hcl.BodyContent, property string, dest interface{}, evalCtx *hcl.EvalContext) hcl.Diagnostics { 635 var diags hcl.Diagnostics 636 if attr, ok := content.Attributes[property]; ok { 637 diags = gohcl.DecodeExpression(attr.Expr, evalCtx, dest) 638 } 639 return diags 640 } 641 642 // handleModDecodeResult 643 // if decode was successful: 644 // - generate and set resource metadata 645 // - add resource to ModParseContext (which adds it to the mod)handleModDecodeResult 646 func handleModDecodeResult(resource modconfig.HclResource, res *DecodeResult, block *hcl.Block, parseCtx *ModParseContext) { 647 if !res.Success() { 648 if len(res.Depends) > 0 { 649 moreDiags := parseCtx.AddDependencies(block, resource.GetUnqualifiedName(), res.Depends) 650 res.addDiags(moreDiags) 651 } 652 return 653 } 654 // set whether this is a top level resource 655 resource.SetTopLevel(parseCtx.IsTopLevelBlock(block)) 656 657 // call post decode hook 658 // NOTE: must do this BEFORE adding resource to run context to ensure we respect the base property 659 moreDiags := resource.OnDecoded(block, parseCtx) 660 res.addDiags(moreDiags) 661 662 // add references 663 moreDiags = AddReferences(resource, block, parseCtx) 664 res.addDiags(moreDiags) 665 666 // validate the resource 667 moreDiags = validateResource(resource) 668 res.addDiags(moreDiags) 669 // if we failed validation, return 670 if !res.Success() { 671 return 672 } 673 674 // if resource is NOT anonymous, and this is a TOP LEVEL BLOCK, add into the run context 675 // NOTE: we can only reference resources defined in a top level block 676 if !resourceIsAnonymous(resource) && resource.IsTopLevel() { 677 moreDiags = parseCtx.AddResource(resource) 678 res.addDiags(moreDiags) 679 } 680 681 // if resource supports metadata, save it 682 if resourceWithMetadata, ok := resource.(modconfig.ResourceWithMetadata); ok { 683 moreDiags = addResourceMetadata(resourceWithMetadata, resource.GetHclResourceImpl().DeclRange, parseCtx) 684 res.addDiags(moreDiags) 685 } 686 } 687 688 func resourceIsAnonymous(resource modconfig.HclResource) bool { 689 // (if a resource anonymous it must support ResourceWithMetadata) 690 resourceWithMetadata, ok := resource.(modconfig.ResourceWithMetadata) 691 anonymousResource := ok && resourceWithMetadata.IsAnonymous() 692 return anonymousResource 693 } 694 695 func addResourceMetadata(resourceWithMetadata modconfig.ResourceWithMetadata, srcRange hcl.Range, parseCtx *ModParseContext) hcl.Diagnostics { 696 metadata, err := GetMetadataForParsedResource(resourceWithMetadata.Name(), srcRange, parseCtx.FileData, parseCtx.CurrentMod) 697 if err != nil { 698 return hcl.Diagnostics{&hcl.Diagnostic{ 699 Severity: hcl.DiagError, 700 Summary: err.Error(), 701 Subject: &srcRange, 702 }} 703 } 704 // set on resource 705 resourceWithMetadata.SetMetadata(metadata) 706 return nil 707 } 708 709 func validateName(block *hcl.Block) hcl.Diagnostics { 710 if len(block.Labels) == 0 { 711 return nil 712 } 713 714 if !hclsyntax.ValidIdentifier(block.Labels[0]) { 715 return hcl.Diagnostics{&hcl.Diagnostic{ 716 Severity: hcl.DiagError, 717 Summary: "Invalid name", 718 Detail: badIdentifierDetail, 719 Subject: &block.LabelRanges[0], 720 }} 721 } 722 return nil 723 } 724 725 // Validate all blocks and attributes are supported 726 // We use partial decoding so that we can automatically decode as many properties as possible 727 // and only manually decode properties requiring special logic. 728 // The problem is the partial decode does not return errors for invalid attributes/blocks, so we must implement our own 729 func validateHcl(blockType string, body *hclsyntax.Body, schema *hcl.BodySchema) hcl.Diagnostics { 730 var diags hcl.Diagnostics 731 732 // identify any blocks specified by hcl tags 733 var supportedBlocks = make(map[string]struct{}) 734 var supportedAttributes = make(map[string]struct{}) 735 for _, b := range schema.Blocks { 736 supportedBlocks[b.Type] = struct{}{} 737 } 738 for _, b := range schema.Attributes { 739 supportedAttributes[b.Name] = struct{}{} 740 } 741 742 // now check for invalid blocks 743 for _, block := range body.Blocks { 744 if _, ok := supportedBlocks[block.Type]; !ok { 745 diags = append(diags, &hcl.Diagnostic{ 746 Severity: hcl.DiagError, 747 Summary: fmt.Sprintf(`Unsupported block type: Blocks of type '%s' are not expected here.`, block.Type), 748 Subject: &block.TypeRange, 749 }) 750 } 751 } 752 for _, attribute := range body.Attributes { 753 if _, ok := supportedAttributes[attribute.Name]; !ok { 754 // special case code for deprecated properties 755 subject := attribute.Range() 756 if isDeprecated(attribute, blockType) { 757 diags = append(diags, &hcl.Diagnostic{ 758 Severity: hcl.DiagWarning, 759 Summary: fmt.Sprintf(`Deprecated attribute: '%s' is deprecated for '%s' blocks and will be ignored.`, attribute.Name, blockType), 760 Subject: &subject, 761 }) 762 } else { 763 diags = append(diags, &hcl.Diagnostic{ 764 Severity: hcl.DiagError, 765 Summary: fmt.Sprintf(`Unsupported attribute: '%s' not expected here.`, attribute.Name), 766 Subject: &subject, 767 }) 768 } 769 } 770 } 771 772 return diags 773 } 774 775 func isDeprecated(attribute *hclsyntax.Attribute, blockType string) bool { 776 switch attribute.Name { 777 case "search_path", "search_path_prefix": 778 return blockType == modconfig.BlockTypeQuery || blockType == modconfig.BlockTypeControl 779 default: 780 return false 781 } 782 }