github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/decode_body.go (about)

     1  package parse
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/gohcl"
     9  	"github.com/hashicorp/hcl/v2/hclsyntax"
    10  	"github.com/turbot/go-kit/helpers"
    11  	"github.com/turbot/pipe-fittings/hclhelpers"
    12  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    13  )
    14  
    15  func decodeHclBody(body hcl.Body, evalCtx *hcl.EvalContext, resourceProvider modconfig.ResourceMapsProvider, resource modconfig.HclResource) (diags hcl.Diagnostics) {
    16  	defer func() {
    17  		if r := recover(); r != nil {
    18  			diags = append(diags, &hcl.Diagnostic{
    19  				Severity: hcl.DiagError,
    20  				Summary:  "unexpected error in decodeHclBody",
    21  				Detail:   helpers.ToError(r).Error()})
    22  		}
    23  	}()
    24  
    25  	nestedStructs, moreDiags := getNestedStructValsRecursive(resource)
    26  	diags = append(diags, moreDiags...)
    27  	// get the schema for this resource
    28  	schema := getResourceSchema(resource, nestedStructs)
    29  	// handle invalid block types
    30  	moreDiags = validateHcl(resource.BlockType(), body.(*hclsyntax.Body), schema)
    31  	diags = append(diags, moreDiags...)
    32  
    33  	moreDiags = decodeHclBodyIntoStruct(body, evalCtx, resourceProvider, resource)
    34  	diags = append(diags, moreDiags...)
    35  
    36  	for _, nestedStruct := range nestedStructs {
    37  		moreDiags := decodeHclBodyIntoStruct(body, evalCtx, resourceProvider, nestedStruct)
    38  		diags = append(diags, moreDiags...)
    39  	}
    40  
    41  	return diags
    42  }
    43  
    44  func decodeHclBodyIntoStruct(body hcl.Body, evalCtx *hcl.EvalContext, resourceProvider modconfig.ResourceMapsProvider, resource any) hcl.Diagnostics {
    45  	var diags hcl.Diagnostics
    46  	// call decodeHclBodyIntoStruct to do actual decode
    47  	moreDiags := gohcl.DecodeBody(body, evalCtx, resource)
    48  	diags = append(diags, moreDiags...)
    49  
    50  	// resolve any resource references using the resource map, rather than relying on the EvalCtx
    51  	// (which does not work with nested struct vals)
    52  	moreDiags = resolveReferences(body, resourceProvider, resource)
    53  	diags = append(diags, moreDiags...)
    54  	return diags
    55  }
    56  
    57  // build the hcl schema for this resource
    58  func getResourceSchema(resource modconfig.HclResource, nestedStructs []any) *hcl.BodySchema {
    59  	t := reflect.TypeOf(helpers.DereferencePointer(resource))
    60  	typeName := t.Name()
    61  
    62  	if cachedSchema, ok := resourceSchemaCache[typeName]; ok {
    63  		return cachedSchema
    64  	}
    65  	var res = &hcl.BodySchema{}
    66  
    67  	// ensure we cache before returning
    68  	defer func() {
    69  		resourceSchemaCache[typeName] = res
    70  	}()
    71  
    72  	var schemas []*hcl.BodySchema
    73  
    74  	// build schema for top level object
    75  	schemas = append(schemas, getSchemaForStruct(t))
    76  
    77  	// now get schemas for any nested structs (using cache)
    78  	for _, nestedStruct := range nestedStructs {
    79  		t := reflect.TypeOf(helpers.DereferencePointer(nestedStruct))
    80  		typeName := t.Name()
    81  
    82  		// is this cached?
    83  		nestedStructSchema, schemaCached := resourceSchemaCache[typeName]
    84  		if !schemaCached {
    85  			nestedStructSchema = getSchemaForStruct(t)
    86  			resourceSchemaCache[typeName] = nestedStructSchema
    87  		}
    88  
    89  		// add to our list of schemas
    90  		schemas = append(schemas, nestedStructSchema)
    91  	}
    92  
    93  	// TODO handle duplicates and required/optional
    94  	// now merge the schemas
    95  	for _, s := range schemas {
    96  		res.Blocks = append(res.Blocks, s.Blocks...)
    97  		res.Attributes = append(res.Attributes, s.Attributes...)
    98  	}
    99  
   100  	// special cases for manually parsed attributes and blocks
   101  	switch resource.BlockType() {
   102  	case modconfig.BlockTypeMod:
   103  		res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: modconfig.BlockTypeRequire})
   104  	case modconfig.BlockTypeDashboard, modconfig.BlockTypeContainer:
   105  		res.Blocks = append(res.Blocks,
   106  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeDashboard},
   107  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeControl},
   108  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeBenchmark},
   109  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeCard},
   110  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeChart},
   111  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeContainer},
   112  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeFlow},
   113  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeGraph},
   114  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeHierarchy},
   115  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeImage},
   116  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeInput},
   117  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeTable},
   118  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeText},
   119  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeWith},
   120  		)
   121  	case modconfig.BlockTypeQuery:
   122  		// remove `Query` from attributes
   123  		var querySchema = &hcl.BodySchema{}
   124  		for _, a := range res.Attributes {
   125  			if a.Name != modconfig.AttributeQuery {
   126  				querySchema.Attributes = append(querySchema.Attributes, a)
   127  			}
   128  		}
   129  		res = querySchema
   130  	}
   131  
   132  	if _, ok := resource.(modconfig.QueryProvider); ok {
   133  		res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: modconfig.BlockTypeParam})
   134  		// if this is NOT query, add args
   135  		if resource.BlockType() != modconfig.BlockTypeQuery {
   136  			res.Attributes = append(res.Attributes, hcl.AttributeSchema{Name: modconfig.AttributeArgs})
   137  		}
   138  	}
   139  	if _, ok := resource.(modconfig.NodeAndEdgeProvider); ok {
   140  		res.Blocks = append(res.Blocks,
   141  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeCategory},
   142  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeNode},
   143  			hcl.BlockHeaderSchema{Type: modconfig.BlockTypeEdge})
   144  	}
   145  	if _, ok := resource.(modconfig.WithProvider); ok {
   146  		res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: modconfig.BlockTypeWith})
   147  	}
   148  	return res
   149  }
   150  
   151  func getSchemaForStruct(t reflect.Type) *hcl.BodySchema {
   152  	var schema = &hcl.BodySchema{}
   153  	// get all hcl tags
   154  	for i := 0; i < t.NumField(); i++ {
   155  		tag := t.FieldByIndex([]int{i}).Tag.Get("hcl")
   156  		if tag == "" {
   157  			continue
   158  		}
   159  		if idx := strings.LastIndex(tag, ",block"); idx != -1 {
   160  			blockName := tag[:idx]
   161  			schema.Blocks = append(schema.Blocks, hcl.BlockHeaderSchema{Type: blockName})
   162  		} else {
   163  			attributeName := strings.Split(tag, ",")[0]
   164  			if attributeName != "" {
   165  				schema.Attributes = append(schema.Attributes, hcl.AttributeSchema{Name: attributeName})
   166  			}
   167  		}
   168  	}
   169  	return schema
   170  }
   171  
   172  // rather than relying on the evaluation context to resolve resource references
   173  // (which has the issue that when deserializing from cty we do not receive all base struct values)
   174  // instead resolve the reference by parsing the resource name and finding the resource in the ResourceMap
   175  // and use this resource to set the target property
   176  func resolveReferences(body hcl.Body, resourceMapsProvider modconfig.ResourceMapsProvider, val any) (diags hcl.Diagnostics) {
   177  	defer func() {
   178  		if r := recover(); r != nil {
   179  			if r := recover(); r != nil {
   180  				diags = append(diags, &hcl.Diagnostic{
   181  					Severity: hcl.DiagError,
   182  					Summary:  "unexpected error in resolveReferences",
   183  					Detail:   helpers.ToError(r).Error()})
   184  			}
   185  		}
   186  	}()
   187  	attributes := body.(*hclsyntax.Body).Attributes
   188  	rv := reflect.ValueOf(val)
   189  	for rv.Type().Kind() == reflect.Pointer {
   190  		rv = rv.Elem()
   191  	}
   192  	ty := rv.Type()
   193  	if ty.Kind() != reflect.Struct {
   194  		return
   195  	}
   196  
   197  	ct := ty.NumField()
   198  	for i := 0; i < ct; i++ {
   199  		field := ty.Field(i)
   200  		fieldVal := rv.Field(i)
   201  		// get hcl attribute tag (if any) tag
   202  		hclAttribute := getHclAttributeTag(field)
   203  		if hclAttribute == "" {
   204  			continue
   205  		}
   206  		if fieldVal.Type().Kind() == reflect.Pointer && !fieldVal.IsNil() {
   207  			fieldVal = fieldVal.Elem()
   208  		}
   209  		if fieldVal.Kind() == reflect.Struct {
   210  			v := fieldVal.Addr().Interface()
   211  			if _, ok := v.(modconfig.HclResource); ok {
   212  				if hclVal, ok := attributes[hclAttribute]; ok {
   213  					if scopeTraversal, ok := hclVal.Expr.(*hclsyntax.ScopeTraversalExpr); ok {
   214  						path := hclhelpers.TraversalAsString(scopeTraversal.Traversal)
   215  						if parsedName, err := modconfig.ParseResourceName(path); err == nil {
   216  							if r, ok := resourceMapsProvider.GetResource(parsedName); ok {
   217  								f := rv.FieldByName(field.Name)
   218  								if f.IsValid() && f.CanSet() {
   219  									targetVal := reflect.ValueOf(r)
   220  									f.Set(targetVal)
   221  								}
   222  							}
   223  						}
   224  					}
   225  				}
   226  			}
   227  		}
   228  	}
   229  	return nil
   230  }
   231  
   232  func getHclAttributeTag(field reflect.StructField) string {
   233  	tag := field.Tag.Get("hcl")
   234  	if tag == "" {
   235  		return ""
   236  	}
   237  
   238  	comma := strings.Index(tag, ",")
   239  	var name, kind string
   240  	if comma != -1 {
   241  		name = tag[:comma]
   242  		kind = tag[comma+1:]
   243  	} else {
   244  		name = tag
   245  		kind = "attr"
   246  	}
   247  
   248  	switch kind {
   249  	case "attr":
   250  		return name
   251  	default:
   252  		return ""
   253  	}
   254  }
   255  
   256  func getNestedStructValsRecursive(val any) ([]any, hcl.Diagnostics) {
   257  	nested, diags := getNestedStructVals(val)
   258  	res := nested
   259  
   260  	for _, n := range nested {
   261  		nestedVals, moreDiags := getNestedStructValsRecursive(n)
   262  		diags = append(diags, moreDiags...)
   263  		res = append(res, nestedVals...)
   264  	}
   265  	return res, diags
   266  
   267  }
   268  
   269  // GetNestedStructVals return a slice of any nested structs within val
   270  func getNestedStructVals(val any) (_ []any, diags hcl.Diagnostics) {
   271  	defer func() {
   272  		if r := recover(); r != nil {
   273  			if r := recover(); r != nil {
   274  				diags = append(diags, &hcl.Diagnostic{
   275  					Severity: hcl.DiagError,
   276  					Summary:  "unexpected error in resolveReferences",
   277  					Detail:   helpers.ToError(r).Error()})
   278  			}
   279  		}
   280  	}()
   281  
   282  	rv := reflect.ValueOf(val)
   283  	for rv.Type().Kind() == reflect.Pointer {
   284  		rv = rv.Elem()
   285  	}
   286  	ty := rv.Type()
   287  	if ty.Kind() != reflect.Struct {
   288  		return nil, nil
   289  	}
   290  	ct := ty.NumField()
   291  	var res []any
   292  	for i := 0; i < ct; i++ {
   293  		field := ty.Field(i)
   294  		fieldVal := rv.Field(i)
   295  		if field.Anonymous && fieldVal.Kind() == reflect.Struct {
   296  			res = append(res, fieldVal.Addr().Interface())
   297  		}
   298  	}
   299  	return res, nil
   300  }