mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
787 lines
27 KiB
Go
787 lines
27 KiB
Go
package parse
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/turbot/go-kit/helpers"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig/var_config"
|
|
)
|
|
|
|
// A consistent detail message for all "not a valid identifier" diagnostics.
|
|
const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes."
|
|
|
|
var missingVariableErrors = []string{
|
|
// returned when the context variables does not have top level 'type' node (locals/control/etc)
|
|
"Unknown variable",
|
|
// returned when the variables have the type object but a field has not yet been populated
|
|
"Unsupported attribute",
|
|
"Missing map element",
|
|
}
|
|
|
|
func decode(parseCtx *ModParseContext) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
blocks, err := parseCtx.BlocksToDecode()
|
|
// build list of blocks to decode
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "failed to determine required dependency order",
|
|
Detail: err.Error()})
|
|
return diags
|
|
}
|
|
|
|
// now clear dependencies from run context - they will be rebuilt
|
|
parseCtx.ClearDependencies()
|
|
|
|
for _, block := range blocks {
|
|
if block.Type == modconfig.BlockTypeLocals {
|
|
resources, res := decodeLocalsBlock(block, parseCtx)
|
|
if !res.Success() {
|
|
diags = append(diags, res.Diags...)
|
|
continue
|
|
}
|
|
for _, resource := range resources {
|
|
resourceDiags := addResourceToMod(resource, block, parseCtx)
|
|
diags = append(diags, resourceDiags...)
|
|
}
|
|
} else {
|
|
resource, res := decodeBlock(block, parseCtx)
|
|
diags = append(diags, res.Diags...)
|
|
if !res.Success() || resource == nil {
|
|
continue
|
|
}
|
|
|
|
resourceDiags := addResourceToMod(resource, block, parseCtx)
|
|
diags = append(diags, resourceDiags...)
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func addResourceToMod(resource modconfig.HclResource, block *hcl.Block, parseCtx *ModParseContext) hcl.Diagnostics {
|
|
if !shouldAddToMod(resource, block, parseCtx) {
|
|
return nil
|
|
}
|
|
return parseCtx.CurrentMod.AddResource(resource)
|
|
|
|
}
|
|
|
|
func shouldAddToMod(resource modconfig.HclResource, block *hcl.Block, parseCtx *ModParseContext) bool {
|
|
switch resource.(type) {
|
|
// do not add mods, withs
|
|
case *modconfig.Mod, *modconfig.DashboardWith:
|
|
return false
|
|
|
|
case *modconfig.DashboardCategory, *modconfig.DashboardInput:
|
|
// if this is a dashboard category or dashboard input, only add top level blocks
|
|
// this is to allow nested categories/inputs to have the same name as top level categories
|
|
// (nested inputs are added by Dashboard.InitInputs)
|
|
return parseCtx.IsTopLevelBlock(block)
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// special case decode logic for locals
|
|
func decodeLocalsBlock(block *hcl.Block, parseCtx *ModParseContext) ([]modconfig.HclResource, *DecodeResult) {
|
|
var resources []modconfig.HclResource
|
|
var res = newDecodeResult()
|
|
|
|
// TODO remove and call ShouldIncludeBlock from BlocksToDecode
|
|
// https://github.com/turbot/steampipe/issues/2640
|
|
// if opts specifies block types, then check whether this type is included
|
|
if !parseCtx.ShouldIncludeBlock(block) {
|
|
return nil, res
|
|
}
|
|
|
|
// check name is valid
|
|
diags := validateName(block)
|
|
if diags.HasErrors() {
|
|
res.addDiags(diags)
|
|
return nil, res
|
|
}
|
|
|
|
var locals []*modconfig.Local
|
|
locals, res = decodeLocals(block, parseCtx)
|
|
for _, local := range locals {
|
|
resources = append(resources, local)
|
|
handleModDecodeResult(local, res, block, parseCtx)
|
|
}
|
|
|
|
return resources, res
|
|
}
|
|
|
|
func decodeBlock(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, *DecodeResult) {
|
|
var resource modconfig.HclResource
|
|
var res = newDecodeResult()
|
|
|
|
// TODO remove and call ShouldIncludeBlock from BlocksToDecode
|
|
// https://github.com/turbot/steampipe/issues/2640
|
|
// if opts specifies block types, then check whether this type is included
|
|
if !parseCtx.ShouldIncludeBlock(block) {
|
|
return nil, res
|
|
}
|
|
|
|
// has this block already been decoded?
|
|
// (this could happen if it is a child block and has been decoded before its parent as part of second decode phase)
|
|
if resource, ok := parseCtx.GetDecodedResourceForBlock(block); ok {
|
|
return resource, res
|
|
}
|
|
|
|
// check name is valid
|
|
diags := validateName(block)
|
|
if diags.HasErrors() {
|
|
res.addDiags(diags)
|
|
return nil, res
|
|
}
|
|
|
|
// now do the actual decode
|
|
switch {
|
|
case helpers.StringSliceContains(modconfig.NodeAndEdgeProviderBlocks, block.Type):
|
|
resource, res = decodeNodeAndEdgeProvider(block, parseCtx)
|
|
case helpers.StringSliceContains(modconfig.QueryProviderBlocks, block.Type):
|
|
resource, res = decodeQueryProvider(block, parseCtx)
|
|
default:
|
|
switch block.Type {
|
|
case modconfig.BlockTypeMod:
|
|
// decodeMode has slightly different args as this code is shared with ParseModDefinition
|
|
resource, res = decodeMod(block, parseCtx.EvalCtx, parseCtx.CurrentMod)
|
|
case modconfig.BlockTypeDashboard:
|
|
resource, res = decodeDashboard(block, parseCtx)
|
|
case modconfig.BlockTypeContainer:
|
|
resource, res = decodeDashboardContainer(block, parseCtx)
|
|
case modconfig.BlockTypeVariable:
|
|
resource, res = decodeVariable(block, parseCtx)
|
|
case modconfig.BlockTypeBenchmark:
|
|
resource, res = decodeBenchmark(block, parseCtx)
|
|
default:
|
|
// all other blocks are treated the same:
|
|
resource, res = decodeResource(block, parseCtx)
|
|
}
|
|
}
|
|
|
|
// handle the result
|
|
// - if there are dependencies, add to run context
|
|
handleModDecodeResult(resource, res, block, parseCtx)
|
|
|
|
return resource, res
|
|
}
|
|
|
|
func decodeMod(block *hcl.Block, evalCtx *hcl.EvalContext, mod *modconfig.Mod) (*modconfig.Mod, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
// decode the body
|
|
diags := decodeHclBody(block.Body, evalCtx, mod, mod)
|
|
res.handleDecodeDiags(diags)
|
|
return mod, res
|
|
}
|
|
|
|
// generic decode function for any resource we do not have custom decode logic for
|
|
func decodeResource(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
// get shell resource
|
|
resource, diags := resourceForBlock(block, parseCtx)
|
|
res.handleDecodeDiags(diags)
|
|
if diags.HasErrors() {
|
|
return nil, res
|
|
}
|
|
|
|
diags = decodeHclBody(block.Body, parseCtx.EvalCtx, parseCtx, resource)
|
|
if len(diags) > 0 {
|
|
res.handleDecodeDiags(diags)
|
|
}
|
|
return resource, res
|
|
}
|
|
|
|
// return a shell resource for the given block
|
|
func resourceForBlock(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, hcl.Diagnostics) {
|
|
var resource modconfig.HclResource
|
|
// parseCtx already contains the current mod
|
|
mod := parseCtx.CurrentMod
|
|
blockName := parseCtx.DetermineBlockName(block)
|
|
|
|
factoryFuncs := map[string]func(*hcl.Block, *modconfig.Mod, string) modconfig.HclResource{
|
|
// for block type mod, just use the current mod
|
|
modconfig.BlockTypeMod: func(*hcl.Block, *modconfig.Mod, string) modconfig.HclResource { return mod },
|
|
modconfig.BlockTypeQuery: modconfig.NewQuery,
|
|
modconfig.BlockTypeControl: modconfig.NewControl,
|
|
modconfig.BlockTypeBenchmark: modconfig.NewBenchmark,
|
|
modconfig.BlockTypeDashboard: modconfig.NewDashboard,
|
|
modconfig.BlockTypeContainer: modconfig.NewDashboardContainer,
|
|
modconfig.BlockTypeChart: modconfig.NewDashboardChart,
|
|
modconfig.BlockTypeCard: modconfig.NewDashboardCard,
|
|
modconfig.BlockTypeFlow: modconfig.NewDashboardFlow,
|
|
modconfig.BlockTypeGraph: modconfig.NewDashboardGraph,
|
|
modconfig.BlockTypeHierarchy: modconfig.NewDashboardHierarchy,
|
|
modconfig.BlockTypeImage: modconfig.NewDashboardImage,
|
|
modconfig.BlockTypeInput: modconfig.NewDashboardInput,
|
|
modconfig.BlockTypeTable: modconfig.NewDashboardTable,
|
|
modconfig.BlockTypeText: modconfig.NewDashboardText,
|
|
modconfig.BlockTypeNode: modconfig.NewDashboardNode,
|
|
modconfig.BlockTypeEdge: modconfig.NewDashboardEdge,
|
|
modconfig.BlockTypeCategory: modconfig.NewDashboardCategory,
|
|
modconfig.BlockTypeWith: modconfig.NewDashboardWith,
|
|
}
|
|
|
|
factoryFunc, ok := factoryFuncs[block.Type]
|
|
if !ok {
|
|
return nil, hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("resourceForBlock called for unsupported block type %s", block.Type),
|
|
Subject: &block.DefRange,
|
|
},
|
|
}
|
|
}
|
|
resource = factoryFunc(block, mod, blockName)
|
|
return resource, nil
|
|
}
|
|
|
|
func decodeLocals(block *hcl.Block, parseCtx *ModParseContext) ([]*modconfig.Local, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
attrs, diags := block.Body.JustAttributes()
|
|
if len(attrs) == 0 {
|
|
res.Diags = diags
|
|
return nil, res
|
|
}
|
|
|
|
// build list of locals
|
|
locals := make([]*modconfig.Local, 0, len(attrs))
|
|
for name, attr := range attrs {
|
|
if !hclsyntax.ValidIdentifier(name) {
|
|
res.Diags = append(res.Diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid local value name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &attr.NameRange,
|
|
})
|
|
continue
|
|
}
|
|
// try to evaluate expression
|
|
val, diags := attr.Expr.Value(parseCtx.EvalCtx)
|
|
// handle any resulting diags, which may specify dependencies
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// add to our list
|
|
locals = append(locals, modconfig.NewLocal(name, val, attr.Range, parseCtx.CurrentMod))
|
|
}
|
|
return locals, res
|
|
}
|
|
|
|
func decodeVariable(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.Variable, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
|
|
var variable *modconfig.Variable
|
|
content, diags := block.Body.Content(VariableBlockSchema)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
v, diags := var_config.DecodeVariableBlock(block, content, false)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
if res.Success() {
|
|
variable = modconfig.NewVariable(v, parseCtx.CurrentMod)
|
|
}
|
|
|
|
return variable, res
|
|
|
|
}
|
|
|
|
func decodeQueryProvider(block *hcl.Block, parseCtx *ModParseContext) (modconfig.QueryProvider, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
|
|
// TODO [node_reuse] need raise errors for invalid properties https://github.com/turbot/steampipe/issues/2923
|
|
|
|
// get shell resource
|
|
resource, diags := resourceForBlock(block, parseCtx)
|
|
res.handleDecodeDiags(diags)
|
|
if diags.HasErrors() {
|
|
return nil, res
|
|
}
|
|
// do a partial decode using an empty schema - use to pull out all body content in the remain block
|
|
_, remain, diags := block.Body.PartialContent(&hcl.BodySchema{})
|
|
res.handleDecodeDiags(diags)
|
|
if !res.Success() {
|
|
return nil, res
|
|
}
|
|
|
|
// decode the body into 'resource' to populate all properties that can be automatically decoded
|
|
diags = decodeHclBody(remain, parseCtx.EvalCtx, parseCtx, resource)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// decode 'with',args and params blocks
|
|
res.Merge(decodeQueryProviderBlocks(block, remain.(*hclsyntax.Body), resource, parseCtx))
|
|
|
|
return resource.(modconfig.QueryProvider), res
|
|
}
|
|
|
|
func decodeQueryProviderBlocks(block *hcl.Block, content *hclsyntax.Body, resource modconfig.HclResource, parseCtx *ModParseContext) *DecodeResult {
|
|
var diags hcl.Diagnostics
|
|
res := newDecodeResult()
|
|
queryProvider, ok := resource.(modconfig.QueryProvider)
|
|
if !ok {
|
|
// coding error
|
|
panic(fmt.Sprintf("block type %s not convertible to a QueryProvider", block.Type))
|
|
}
|
|
|
|
if attr, exists := content.Attributes[modconfig.AttributeArgs]; exists {
|
|
args, runtimeDependencies, diags := decodeArgs(attr.AsHCLAttribute(), parseCtx.EvalCtx, queryProvider)
|
|
if diags.HasErrors() {
|
|
// handle dependencies
|
|
res.handleDecodeDiags(diags)
|
|
} else {
|
|
queryProvider.SetArgs(args)
|
|
queryProvider.AddRuntimeDependencies(runtimeDependencies)
|
|
}
|
|
}
|
|
|
|
var params []*modconfig.ParamDef
|
|
for _, b := range content.Blocks {
|
|
block = b.AsHCLBlock()
|
|
switch block.Type {
|
|
case modconfig.BlockTypeParam:
|
|
paramDef, runtimeDependencies, moreDiags := decodeParam(block, parseCtx)
|
|
if !moreDiags.HasErrors() {
|
|
params = append(params, paramDef)
|
|
queryProvider.AddRuntimeDependencies(runtimeDependencies)
|
|
// add and references contained in the param block to the control refs
|
|
moreDiags = AddReferences(resource, block, parseCtx)
|
|
}
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
}
|
|
|
|
queryProvider.SetParams(params)
|
|
res.handleDecodeDiags(diags)
|
|
return res
|
|
}
|
|
|
|
func decodeNodeAndEdgeProvider(block *hcl.Block, parseCtx *ModParseContext) (modconfig.HclResource, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
|
|
// TODO [node_reuse] need raise errors for invalid properties https://github.com/turbot/steampipe/issues/2923
|
|
|
|
// get shell resource
|
|
resource, diags := resourceForBlock(block, parseCtx)
|
|
res.handleDecodeDiags(diags)
|
|
if diags.HasErrors() {
|
|
return nil, res
|
|
}
|
|
|
|
nodeAndEdgeProvider, ok := resource.(modconfig.NodeAndEdgeProvider)
|
|
if !ok {
|
|
// coding error
|
|
panic(fmt.Sprintf("block type %s not convertible to a NodeAndEdgeProvider", block.Type))
|
|
}
|
|
|
|
// do a partial decode using an empty schema - use to pull out all body content in the remain block
|
|
_, r, diags := block.Body.PartialContent(&hcl.BodySchema{})
|
|
body := r.(*hclsyntax.Body)
|
|
res.handleDecodeDiags(diags)
|
|
if !res.Success() {
|
|
return nil, res
|
|
}
|
|
|
|
// decode the body into 'resource' to populate all properties that can be automatically decoded
|
|
diags = decodeHclBody(body, parseCtx.EvalCtx, parseCtx, resource)
|
|
// handle any resulting diags, which may specify dependencies
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// decode sql args and params
|
|
res.Merge(decodeQueryProviderBlocks(block, body, resource, parseCtx))
|
|
|
|
// now decode child blocks
|
|
if len(body.Blocks) > 0 {
|
|
blocksRes := decodeNodeAndEdgeProviderBlocks(body, nodeAndEdgeProvider, parseCtx)
|
|
res.Merge(blocksRes)
|
|
}
|
|
|
|
return resource, res
|
|
}
|
|
|
|
func decodeNodeAndEdgeProviderBlocks(content *hclsyntax.Body, nodeAndEdgeProvider modconfig.NodeAndEdgeProvider, parseCtx *ModParseContext) *DecodeResult {
|
|
var res = newDecodeResult()
|
|
|
|
for _, b := range content.Blocks {
|
|
block := b.AsHCLBlock()
|
|
switch block.Type {
|
|
case modconfig.BlockTypeCategory:
|
|
// decode block
|
|
category, blockRes := decodeBlock(block, parseCtx)
|
|
res.Merge(blockRes)
|
|
if !blockRes.Success() {
|
|
continue
|
|
}
|
|
|
|
// add the category to the nodeAndEdgeProvider
|
|
res.addDiags(nodeAndEdgeProvider.AddCategory(category.(*modconfig.DashboardCategory)))
|
|
|
|
// DO NOT add the category to the mod
|
|
|
|
case modconfig.BlockTypeNode, modconfig.BlockTypeEdge:
|
|
child, childRes := decodeQueryProvider(block, parseCtx)
|
|
|
|
// TACTICAL if child has any runtime dependencies, claim them
|
|
// this is to ensure if this resource is used as base, we can be correctly identified
|
|
// as the publisher of the runtime dependencies
|
|
for _, r := range child.GetRuntimeDependencies() {
|
|
r.Provider = nodeAndEdgeProvider
|
|
}
|
|
|
|
// populate metadata, set references and call OnDecoded
|
|
handleModDecodeResult(child, childRes, block, parseCtx)
|
|
res.Merge(childRes)
|
|
if res.Success() {
|
|
moreDiags := nodeAndEdgeProvider.AddChild(child)
|
|
res.addDiags(moreDiags)
|
|
}
|
|
case modconfig.BlockTypeWith:
|
|
with, withRes := decodeBlock(block, parseCtx)
|
|
res.Merge(withRes)
|
|
if res.Success() {
|
|
moreDiags := nodeAndEdgeProvider.AddWith(with.(*modconfig.DashboardWith))
|
|
res.addDiags(moreDiags)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func decodeDashboard(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.Dashboard, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
dashboard := modconfig.NewDashboard(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*modconfig.Dashboard)
|
|
|
|
// do a partial decode using an empty schema - use to pull out all body content in the remain block
|
|
_, r, diags := block.Body.PartialContent(&hcl.BodySchema{})
|
|
body := r.(*hclsyntax.Body)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// decode the body into 'dashboardContainer' to populate all properties that can be automatically decoded
|
|
diags = decodeHclBody(body, parseCtx.EvalCtx, parseCtx, dashboard)
|
|
// handle any resulting diags, which may specify dependencies
|
|
res.handleDecodeDiags(diags)
|
|
|
|
if dashboard.Base != nil && len(dashboard.Base.ChildNames) > 0 {
|
|
supportedChildren := []string{modconfig.BlockTypeContainer, modconfig.BlockTypeChart, modconfig.BlockTypeControl, modconfig.BlockTypeCard, modconfig.BlockTypeFlow, modconfig.BlockTypeGraph, modconfig.BlockTypeHierarchy, modconfig.BlockTypeImage, modconfig.BlockTypeInput, modconfig.BlockTypeTable, modconfig.BlockTypeText}
|
|
// TACTICAL: we should be passing in the block for the Base resource - but this is only used for diags
|
|
// and we do not expect to get any (as this function has already succeeded when the base was originally parsed)
|
|
children, _ := resolveChildrenFromNames(dashboard.Base.ChildNames, block, supportedChildren, parseCtx)
|
|
dashboard.Base.SetChildren(children)
|
|
}
|
|
if !res.Success() {
|
|
return dashboard, res
|
|
}
|
|
|
|
// now decode child blocks
|
|
if len(body.Blocks) > 0 {
|
|
blocksRes := decodeDashboardBlocks(body, dashboard, parseCtx)
|
|
res.Merge(blocksRes)
|
|
}
|
|
|
|
return dashboard, res
|
|
}
|
|
|
|
func decodeDashboardBlocks(content *hclsyntax.Body, dashboard *modconfig.Dashboard, parseCtx *ModParseContext) *DecodeResult {
|
|
var res = newDecodeResult()
|
|
// set dashboard as parent on the run context - this is used when generating names for anonymous blocks
|
|
parseCtx.PushParent(dashboard)
|
|
defer func() {
|
|
parseCtx.PopParent()
|
|
}()
|
|
|
|
for _, b := range content.Blocks {
|
|
block := b.AsHCLBlock()
|
|
|
|
// decode block
|
|
resource, blockRes := decodeBlock(block, parseCtx)
|
|
res.Merge(blockRes)
|
|
if !blockRes.Success() {
|
|
continue
|
|
}
|
|
|
|
// we expect either inputs or child report nodes
|
|
// add the resource to the mod
|
|
res.addDiags(addResourceToMod(resource, block, parseCtx))
|
|
// add to the dashboard children
|
|
// (we expect this cast to always succeed)
|
|
if child, ok := resource.(modconfig.ModTreeItem); ok {
|
|
dashboard.AddChild(child)
|
|
}
|
|
|
|
}
|
|
|
|
moreDiags := dashboard.InitInputs()
|
|
res.addDiags(moreDiags)
|
|
|
|
return res
|
|
}
|
|
|
|
func decodeDashboardContainer(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.DashboardContainer, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
container := modconfig.NewDashboardContainer(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*modconfig.DashboardContainer)
|
|
|
|
// do a partial decode using an empty schema - use to pull out all body content in the remain block
|
|
_, r, diags := block.Body.PartialContent(&hcl.BodySchema{})
|
|
body := r.(*hclsyntax.Body)
|
|
res.handleDecodeDiags(diags)
|
|
if !res.Success() {
|
|
return nil, res
|
|
}
|
|
|
|
// decode the body into 'dashboardContainer' to populate all properties that can be automatically decoded
|
|
diags = decodeHclBody(body, parseCtx.EvalCtx, parseCtx, container)
|
|
// handle any resulting diags, which may specify dependencies
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// now decode child blocks
|
|
if len(body.Blocks) > 0 {
|
|
blocksRes := decodeDashboardContainerBlocks(body, container, parseCtx)
|
|
res.Merge(blocksRes)
|
|
}
|
|
|
|
return container, res
|
|
}
|
|
|
|
func decodeDashboardContainerBlocks(content *hclsyntax.Body, dashboardContainer *modconfig.DashboardContainer, parseCtx *ModParseContext) *DecodeResult {
|
|
var res = newDecodeResult()
|
|
|
|
// set container as parent on the run context - this is used when generating names for anonymous blocks
|
|
parseCtx.PushParent(dashboardContainer)
|
|
defer func() {
|
|
parseCtx.PopParent()
|
|
}()
|
|
|
|
for _, b := range content.Blocks {
|
|
block := b.AsHCLBlock()
|
|
resource, blockRes := decodeBlock(block, parseCtx)
|
|
res.Merge(blockRes)
|
|
if !blockRes.Success() {
|
|
continue
|
|
}
|
|
|
|
// special handling for inputs
|
|
if b.Type == modconfig.BlockTypeInput {
|
|
input := resource.(*modconfig.DashboardInput)
|
|
dashboardContainer.Inputs = append(dashboardContainer.Inputs, input)
|
|
dashboardContainer.AddChild(input)
|
|
// the input will be added to the mod by the parent dashboard
|
|
|
|
} else {
|
|
// for all other children, add to mod and children
|
|
res.addDiags(addResourceToMod(resource, block, parseCtx))
|
|
if child, ok := resource.(modconfig.ModTreeItem); ok {
|
|
dashboardContainer.AddChild(child)
|
|
}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func decodeBenchmark(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.Benchmark, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
benchmark := modconfig.NewBenchmark(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*modconfig.Benchmark)
|
|
content, diags := block.Body.Content(BenchmarkBlockSchema)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "children", &benchmark.ChildNames, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "description", &benchmark.Description, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "documentation", &benchmark.Documentation, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "tags", &benchmark.Tags, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "title", &benchmark.Title, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "type", &benchmark.Type, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
diags = decodeProperty(content, "display", &benchmark.Display, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// now add children
|
|
if res.Success() {
|
|
supportedChildren := []string{modconfig.BlockTypeBenchmark, modconfig.BlockTypeControl}
|
|
children, diags := resolveChildrenFromNames(benchmark.ChildNames.StringList(), block, supportedChildren, parseCtx)
|
|
res.handleDecodeDiags(diags)
|
|
|
|
// now set children and child name strings
|
|
benchmark.SetChildren(children)
|
|
benchmark.ChildNameStrings = getChildNameStringsFromModTreeItem(children)
|
|
}
|
|
|
|
diags = decodeProperty(content, "base", &benchmark.Base, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
if benchmark.Base != nil && len(benchmark.Base.ChildNames) > 0 {
|
|
supportedChildren := []string{modconfig.BlockTypeBenchmark, modconfig.BlockTypeControl}
|
|
// TACTICAL: we should be passing in the block for the Base resource - but this is only used for diags
|
|
// and we do not expect to get any (as this function has already succeeded when the base was originally parsed)
|
|
children, _ := resolveChildrenFromNames(benchmark.Base.ChildNameStrings, block, supportedChildren, parseCtx)
|
|
benchmark.Base.SetChildren(children)
|
|
}
|
|
diags = decodeProperty(content, "width", &benchmark.Width, parseCtx.EvalCtx)
|
|
res.handleDecodeDiags(diags)
|
|
return benchmark, res
|
|
}
|
|
|
|
func decodeProperty(content *hcl.BodyContent, property string, dest interface{}, evalCtx *hcl.EvalContext) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
if attr, ok := content.Attributes[property]; ok {
|
|
diags = gohcl.DecodeExpression(attr.Expr, evalCtx, dest)
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// handleModDecodeResult
|
|
// if decode was successful:
|
|
// - generate and set resource metadata
|
|
// - add resource to ModParseContext (which adds it to the mod)handleModDecodeResult
|
|
func handleModDecodeResult(resource modconfig.HclResource, res *DecodeResult, block *hcl.Block, parseCtx *ModParseContext) {
|
|
if !res.Success() {
|
|
if len(res.Depends) > 0 {
|
|
moreDiags := parseCtx.AddDependencies(block, resource.GetUnqualifiedName(), res.Depends)
|
|
res.addDiags(moreDiags)
|
|
}
|
|
return
|
|
}
|
|
// set whether this is a top level resource
|
|
resource.SetTopLevel(parseCtx.IsTopLevelBlock(block))
|
|
|
|
// call post decode hook
|
|
// NOTE: must do this BEFORE adding resource to run context to ensure we respect the base property
|
|
moreDiags := resource.OnDecoded(block, parseCtx)
|
|
res.addDiags(moreDiags)
|
|
|
|
// add references
|
|
moreDiags = AddReferences(resource, block, parseCtx)
|
|
res.addDiags(moreDiags)
|
|
|
|
// validate the resource
|
|
moreDiags = validateResource(resource)
|
|
res.addDiags(moreDiags)
|
|
// if we failed validation, return
|
|
if !res.Success() {
|
|
return
|
|
}
|
|
|
|
// if resource is NOT anonymous, and this is a TOP LEVEL BLOCK, add into the run context
|
|
// NOTE: we can only reference resources defined in a top level block
|
|
if !resourceIsAnonymous(resource) && resource.IsTopLevel() {
|
|
moreDiags = parseCtx.AddResource(resource)
|
|
res.addDiags(moreDiags)
|
|
}
|
|
|
|
// if resource supports metadata, save it
|
|
if resourceWithMetadata, ok := resource.(modconfig.ResourceWithMetadata); ok {
|
|
body := block.Body.(*hclsyntax.Body)
|
|
moreDiags = addResourceMetadata(resourceWithMetadata, body.SrcRange, parseCtx)
|
|
res.addDiags(moreDiags)
|
|
}
|
|
}
|
|
|
|
func resourceIsAnonymous(resource modconfig.HclResource) bool {
|
|
// (if a resource anonymous it must support ResourceWithMetadata)
|
|
resourceWithMetadata, ok := resource.(modconfig.ResourceWithMetadata)
|
|
anonymousResource := ok && resourceWithMetadata.IsAnonymous()
|
|
return anonymousResource
|
|
}
|
|
|
|
func addResourceMetadata(resourceWithMetadata modconfig.ResourceWithMetadata, srcRange hcl.Range, parseCtx *ModParseContext) hcl.Diagnostics {
|
|
metadata, err := GetMetadataForParsedResource(resourceWithMetadata.Name(), srcRange, parseCtx.FileData, parseCtx.CurrentMod)
|
|
if err != nil {
|
|
return hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: err.Error(),
|
|
Subject: &srcRange,
|
|
}}
|
|
}
|
|
// set on resource
|
|
resourceWithMetadata.SetMetadata(metadata)
|
|
return nil
|
|
}
|
|
|
|
func validateName(block *hcl.Block) hcl.Diagnostics {
|
|
if len(block.Labels) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if !hclsyntax.ValidIdentifier(block.Labels[0]) {
|
|
return hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate all blocks and attributes are supported
|
|
// We use partial decoding so that we can automatically decode as many properties as possible
|
|
// and only manually decode properties requiring special logic.
|
|
// The problem is the partial decode does not return errors for invalid attributes/blocks, so we must implement our own
|
|
func validateHcl(blockType string, body *hclsyntax.Body, schema *hcl.BodySchema) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
// identify any blocks specified by hcl tags
|
|
var supportedBlocks = make(map[string]struct{})
|
|
var supportedAttributes = make(map[string]struct{})
|
|
for _, b := range schema.Blocks {
|
|
supportedBlocks[b.Type] = struct{}{}
|
|
}
|
|
for _, b := range schema.Attributes {
|
|
supportedAttributes[b.Name] = struct{}{}
|
|
}
|
|
|
|
// now check for invalid blocks
|
|
for _, block := range body.Blocks {
|
|
if _, ok := supportedBlocks[block.Type]; !ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf(`Unsupported block type: Blocks of type '%s' are not expected here.`, block.Type),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
}
|
|
}
|
|
for _, attribute := range body.Attributes {
|
|
if _, ok := supportedAttributes[attribute.Name]; !ok {
|
|
// special case code for deprecated properties
|
|
subject := attribute.Range()
|
|
if isDeprecated(attribute, blockType) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: fmt.Sprintf(`Deprecated attribute: '%s' is deprecated for '%s' blocks and will be ignored.`, attribute.Name, blockType),
|
|
Subject: &subject,
|
|
})
|
|
} else {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf(`Unsupported attribute: '%s' not expected here.`, attribute.Name),
|
|
Subject: &subject,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func isDeprecated(attribute *hclsyntax.Attribute, blockType string) bool {
|
|
switch attribute.Name {
|
|
case "search_path", "search_path_prefix":
|
|
return blockType == modconfig.BlockTypeQuery || blockType == modconfig.BlockTypeControl
|
|
default:
|
|
return false
|
|
}
|
|
}
|