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  }