Files
steampipe/pkg/steampipeconfig/parse/decode.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
}
}