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 }