mirror of
https://github.com/turbot/steampipe.git
synced 2026-05-11 09:03:13 -04:00
`steampipe mod update` now updates transitive mods It is now be possible to set values for variables in the current mod using fully qualified variable names. Only variables for root mod and top level dependency mods can be set by user Closes #3533. Closes #3547. Closes #3548. Closes #3549
303 lines
9.2 KiB
Go
303 lines
9.2 KiB
Go
package parse
|
|
|
|
import (
|
|
"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/hclhelpers"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
func decodeHclBody(body hcl.Body, evalCtx *hcl.EvalContext, resourceProvider modconfig.ResourceMapsProvider, resource modconfig.HclResource) (diags hcl.Diagnostics) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "unexpected error in decodeHclBody",
|
|
Detail: helpers.ToError(r).Error()})
|
|
}
|
|
}()
|
|
|
|
nestedStructs, moreDiags := getNestedStructValsRecursive(resource)
|
|
diags = append(diags, moreDiags...)
|
|
// get the schema for this resource
|
|
schema := getResourceSchema(resource, nestedStructs)
|
|
// handle invalid block types
|
|
moreDiags = validateHcl(resource.BlockType(), body.(*hclsyntax.Body), schema)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
moreDiags = decodeHclBodyIntoStruct(body, evalCtx, resourceProvider, resource)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
for _, nestedStruct := range nestedStructs {
|
|
moreDiags := decodeHclBodyIntoStruct(body, evalCtx, resourceProvider, nestedStruct)
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func decodeHclBodyIntoStruct(body hcl.Body, evalCtx *hcl.EvalContext, resourceProvider modconfig.ResourceMapsProvider, resource any) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
// call decodeHclBodyIntoStruct to do actual decode
|
|
moreDiags := gohcl.DecodeBody(body, evalCtx, resource)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
// resolve any resource references using the resource map, rather than relying on the EvalCtx
|
|
// (which does not work with nested struct vals)
|
|
moreDiags = resolveReferences(body, resourceProvider, resource)
|
|
diags = append(diags, moreDiags...)
|
|
return diags
|
|
}
|
|
|
|
// build the hcl schema for this resource
|
|
func getResourceSchema(resource modconfig.HclResource, nestedStructs []any) *hcl.BodySchema {
|
|
t := reflect.TypeOf(helpers.DereferencePointer(resource))
|
|
typeName := t.Name()
|
|
|
|
if cachedSchema, ok := resourceSchemaCache[typeName]; ok {
|
|
return cachedSchema
|
|
}
|
|
var res = &hcl.BodySchema{}
|
|
|
|
// ensure we cache before returning
|
|
defer func() {
|
|
resourceSchemaCache[typeName] = res
|
|
}()
|
|
|
|
var schemas []*hcl.BodySchema
|
|
|
|
// build schema for top level object
|
|
schemas = append(schemas, getSchemaForStruct(t))
|
|
|
|
// now get schemas for any nested structs (using cache)
|
|
for _, nestedStruct := range nestedStructs {
|
|
t := reflect.TypeOf(helpers.DereferencePointer(nestedStruct))
|
|
typeName := t.Name()
|
|
|
|
// is this cached?
|
|
nestedStructSchema, schemaCached := resourceSchemaCache[typeName]
|
|
if !schemaCached {
|
|
nestedStructSchema = getSchemaForStruct(t)
|
|
resourceSchemaCache[typeName] = nestedStructSchema
|
|
}
|
|
|
|
// add to our list of schemas
|
|
schemas = append(schemas, nestedStructSchema)
|
|
}
|
|
|
|
// TODO handle duplicates and required/optional
|
|
// now merge the schemas
|
|
for _, s := range schemas {
|
|
for _, b := range s.Blocks {
|
|
res.Blocks = append(res.Blocks, b)
|
|
}
|
|
for _, a := range s.Attributes {
|
|
res.Attributes = append(res.Attributes, a)
|
|
}
|
|
}
|
|
|
|
// special cases for manually parsed attributes and blocks
|
|
switch resource.BlockType() {
|
|
case modconfig.BlockTypeMod:
|
|
res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: modconfig.BlockTypeRequire})
|
|
case modconfig.BlockTypeDashboard, modconfig.BlockTypeContainer:
|
|
res.Blocks = append(res.Blocks,
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeControl},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeBenchmark},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeCard},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeChart},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeContainer},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeFlow},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeGraph},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeHierarchy},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeImage},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeInput},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeTable},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeText},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeWith},
|
|
)
|
|
case modconfig.BlockTypeQuery:
|
|
// remove `Query` from attributes
|
|
var querySchema = &hcl.BodySchema{}
|
|
for _, a := range res.Attributes {
|
|
if a.Name != modconfig.AttributeQuery {
|
|
querySchema.Attributes = append(querySchema.Attributes, a)
|
|
}
|
|
}
|
|
res = querySchema
|
|
}
|
|
|
|
if _, ok := resource.(modconfig.QueryProvider); ok {
|
|
res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: modconfig.BlockTypeParam})
|
|
// if this is NOT query, add args
|
|
if resource.BlockType() != modconfig.BlockTypeQuery {
|
|
res.Attributes = append(res.Attributes, hcl.AttributeSchema{Name: modconfig.AttributeArgs})
|
|
}
|
|
}
|
|
if _, ok := resource.(modconfig.NodeAndEdgeProvider); ok {
|
|
res.Blocks = append(res.Blocks,
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeCategory},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeNode},
|
|
hcl.BlockHeaderSchema{Type: modconfig.BlockTypeEdge})
|
|
}
|
|
if _, ok := resource.(modconfig.WithProvider); ok {
|
|
res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: modconfig.BlockTypeWith})
|
|
}
|
|
return res
|
|
}
|
|
|
|
func getSchemaForStruct(t reflect.Type) *hcl.BodySchema {
|
|
var schema = &hcl.BodySchema{}
|
|
// get all hcl tags
|
|
for i := 0; i < t.NumField(); i++ {
|
|
tag := t.FieldByIndex([]int{i}).Tag.Get("hcl")
|
|
if tag == "" {
|
|
continue
|
|
}
|
|
if idx := strings.LastIndex(tag, ",block"); idx != -1 {
|
|
blockName := tag[:idx]
|
|
schema.Blocks = append(schema.Blocks, hcl.BlockHeaderSchema{Type: blockName})
|
|
} else {
|
|
attributeName := strings.Split(tag, ",")[0]
|
|
if attributeName != "" {
|
|
schema.Attributes = append(schema.Attributes, hcl.AttributeSchema{Name: attributeName})
|
|
}
|
|
}
|
|
}
|
|
return schema
|
|
}
|
|
|
|
// rather than relying on the evaluation context to resolve resource references
|
|
// (which has the issue that when deserializing from cty we do not receive all base struct values)
|
|
// instead resolve the reference by parsing the resource name and finding the resource in the ResourceMap
|
|
// and use this resource to set the target property
|
|
func resolveReferences(body hcl.Body, resourceMapsProvider modconfig.ResourceMapsProvider, val any) (diags hcl.Diagnostics) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if r := recover(); r != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "unexpected error in resolveReferences",
|
|
Detail: helpers.ToError(r).Error()})
|
|
}
|
|
}
|
|
}()
|
|
attributes := body.(*hclsyntax.Body).Attributes
|
|
rv := reflect.ValueOf(val)
|
|
for rv.Type().Kind() == reflect.Pointer {
|
|
rv = rv.Elem()
|
|
}
|
|
ty := rv.Type()
|
|
if ty.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
|
|
ct := ty.NumField()
|
|
for i := 0; i < ct; i++ {
|
|
field := ty.Field(i)
|
|
fieldVal := rv.Field(i)
|
|
// get hcl attribute tag (if any) tag
|
|
hclAttribute := getHclAttributeTag(field)
|
|
if hclAttribute == "" {
|
|
continue
|
|
}
|
|
if fieldVal.Type().Kind() == reflect.Pointer && !fieldVal.IsNil() {
|
|
fieldVal = fieldVal.Elem()
|
|
}
|
|
if fieldVal.Kind() == reflect.Struct {
|
|
v := fieldVal.Addr().Interface()
|
|
if _, ok := v.(modconfig.HclResource); ok {
|
|
if hclVal, ok := attributes[hclAttribute]; ok {
|
|
if scopeTraversal, ok := hclVal.Expr.(*hclsyntax.ScopeTraversalExpr); ok {
|
|
path := hclhelpers.TraversalAsString(scopeTraversal.Traversal)
|
|
if parsedName, err := modconfig.ParseResourceName(path); err == nil {
|
|
if r, ok := resourceMapsProvider.GetResource(parsedName); ok {
|
|
f := rv.FieldByName(field.Name)
|
|
if f.IsValid() && f.CanSet() {
|
|
targetVal := reflect.ValueOf(r)
|
|
f.Set(targetVal)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getHclAttributeTag(field reflect.StructField) string {
|
|
tag := field.Tag.Get("hcl")
|
|
if tag == "" {
|
|
return ""
|
|
}
|
|
|
|
comma := strings.Index(tag, ",")
|
|
var name, kind string
|
|
if comma != -1 {
|
|
name = tag[:comma]
|
|
kind = tag[comma+1:]
|
|
} else {
|
|
name = tag
|
|
kind = "attr"
|
|
}
|
|
|
|
switch kind {
|
|
case "attr":
|
|
return name
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func getNestedStructValsRecursive(val any) ([]any, hcl.Diagnostics) {
|
|
nested, diags := getNestedStructVals(val)
|
|
res := nested
|
|
|
|
for _, n := range nested {
|
|
nestedVals, moreDiags := getNestedStructValsRecursive(n)
|
|
diags = append(diags, moreDiags...)
|
|
res = append(res, nestedVals...)
|
|
}
|
|
return res, diags
|
|
|
|
}
|
|
|
|
// GetNestedStructVals return a slice of any nested structs within val
|
|
func getNestedStructVals(val any) (_ []any, diags hcl.Diagnostics) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if r := recover(); r != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "unexpected error in resolveReferences",
|
|
Detail: helpers.ToError(r).Error()})
|
|
}
|
|
}
|
|
}()
|
|
|
|
rv := reflect.ValueOf(val)
|
|
for rv.Type().Kind() == reflect.Pointer {
|
|
rv = rv.Elem()
|
|
}
|
|
ty := rv.Type()
|
|
if ty.Kind() != reflect.Struct {
|
|
return nil, nil
|
|
}
|
|
ct := ty.NumField()
|
|
var res []any
|
|
for i := 0; i < ct; i++ {
|
|
field := ty.Field(i)
|
|
fieldVal := rv.Field(i)
|
|
if field.Anonymous && fieldVal.Kind() == reflect.Struct {
|
|
res = append(res, fieldVal.Addr().Interface())
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|