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/hclhelpers" "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" "github.com/turbot/steampipe/pkg/type_conversion" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" ) func decodeArgs(attr *hcl.Attribute, evalCtx *hcl.EvalContext, resource modconfig.QueryProvider) (*modconfig.QueryArgs, []*modconfig.RuntimeDependency, hcl.Diagnostics) { var runtimeDependencies []*modconfig.RuntimeDependency var args = modconfig.NewQueryArgs() var diags hcl.Diagnostics v, valDiags := attr.Expr.Value(evalCtx) ty := v.Type() // determine which diags are runtime dependencies (which we allow) and which are not if valDiags.HasErrors() { for _, diag := range diags { dependency := diagsToDependency(diag) if dependency == nil || !dependency.IsRuntimeDependency() { diags = append(diags, diag) } } } // now diags contains all diags which are NOT runtime dependencies if diags.HasErrors() { return nil, nil, diags } var err error switch { case ty.IsObjectType(): var argMap map[string]any argMap, runtimeDependencies, err = ctyObjectToArgMap(attr, v, evalCtx) if err == nil { err = args.SetArgMap(argMap) } case ty.IsTupleType(): var argList []any argList, runtimeDependencies, err = ctyTupleToArgArray(attr, v) if err == nil { err = args.SetArgList(argList) } default: err = fmt.Errorf("'params' property must be either a map or an array") } if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("%s has invalid parameter config", resource.Name()), Detail: err.Error(), Subject: &attr.Range, }) } return args, runtimeDependencies, diags } func ctyTupleToArgArray(attr *hcl.Attribute, val cty.Value) ([]any, []*modconfig.RuntimeDependency, error) { // convert the attribute to a slice values := val.AsValueSlice() // build output array res := make([]any, len(values)) var runtimeDependencies []*modconfig.RuntimeDependency for idx, v := range values { // if the value is unknown, this is a runtime dependency if !v.IsKnown() { runtimeDependency, err := identifyRuntimeDependenciesFromArray(attr, idx, modconfig.AttributeArgs) if err != nil { return nil, nil, err } runtimeDependencies = append(runtimeDependencies, runtimeDependency) } else { // decode the value into a go type val, err := type_conversion.CtyToGo(v) if err != nil { err := fmt.Errorf("invalid value provided for arg #%d: %v", idx, err) return nil, nil, err } res[idx] = val } } return res, runtimeDependencies, nil } func ctyObjectToArgMap(attr *hcl.Attribute, val cty.Value, evalCtx *hcl.EvalContext) (map[string]any, []*modconfig.RuntimeDependency, error) { res := make(map[string]any) var runtimeDependencies []*modconfig.RuntimeDependency it := val.ElementIterator() for it.Next() { k, v := it.Element() // decode key var key string if err := gocty.FromCtyValue(k, &key); err != nil { return nil, nil, err } // if the value is unknown, this is a runtime dependency if !v.IsKnown() { runtimeDependency, err := identifyRuntimeDependenciesFromObject(attr, key, modconfig.AttributeArgs, evalCtx) if err != nil { return nil, nil, err } runtimeDependencies = append(runtimeDependencies, runtimeDependency) } else if getWrappedUnknownVal(v) { runtimeDependency, err := identifyRuntimeDependenciesFromObject(attr, key, modconfig.AttributeArgs, evalCtx) if err != nil { return nil, nil, err } runtimeDependencies = append(runtimeDependencies, runtimeDependency) } else { // decode the value into a go type val, err := type_conversion.CtyToGo(v) if err != nil { err := fmt.Errorf("invalid value provided for param '%s': %v", key, err) return nil, nil, err } res[key] = val } } return res, runtimeDependencies, nil } // TACTICAL - is the cty value an array with a single unknown value func getWrappedUnknownVal(v cty.Value) bool { ty := v.Type() switch { case ty.IsTupleType(): values := v.AsValueSlice() if len(values) == 1 && !values[0].IsKnown() { return true } } return false } func identifyRuntimeDependenciesFromObject(attr *hcl.Attribute, targetProperty, parentProperty string, evalCtx *hcl.EvalContext) (*modconfig.RuntimeDependency, error) { // find the expression for this key argsExpr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr) if !ok { return nil, fmt.Errorf("could not extract runtime dependency for arg %s", targetProperty) } for _, item := range argsExpr.Items { nameCty, valDiags := item.KeyExpr.Value(evalCtx) if valDiags.HasErrors() { return nil, fmt.Errorf("could not extract runtime dependency for arg %s", targetProperty) } var name string if err := gocty.FromCtyValue(nameCty, &name); err != nil { return nil, err } if name == targetProperty { dep, err := getRuntimeDepFromExpression(item.ValueExpr, targetProperty, parentProperty) if err != nil { return nil, err } return dep, nil } } return nil, fmt.Errorf("could not extract runtime dependency for arg %s - not found in attribute map", targetProperty) } func getRuntimeDepFromExpression(expr hcl.Expression, targetProperty, parentProperty string) (*modconfig.RuntimeDependency, error) { isArray, propertyPath, err := propertyPathFromExpression(expr) if err != nil { return nil, err } if propertyPath.ItemType == modconfig.BlockTypeInput { // tactical: validate input dependency if err := validateInputRuntimeDependency(propertyPath); err != nil { return nil, err } } ret := &modconfig.RuntimeDependency{ PropertyPath: propertyPath, ParentPropertyName: parentProperty, TargetPropertyName: &targetProperty, IsArray: isArray, } return ret, nil } func propertyPathFromExpression(expr hcl.Expression) (bool, *modconfig.ParsedPropertyPath, error) { var propertyPathStr string var isArray bool dep_loop: for { switch e := expr.(type) { case *hclsyntax.ScopeTraversalExpr: propertyPathStr = hclhelpers.TraversalAsString(e.Traversal) break dep_loop case *hclsyntax.SplatExpr: root := hclhelpers.TraversalAsString(e.Source.(*hclsyntax.ScopeTraversalExpr).Traversal) var suffix string // if there is a property path, add it if each, ok := e.Each.(*hclsyntax.RelativeTraversalExpr); ok { suffix = fmt.Sprintf(".%s", hclhelpers.TraversalAsString(each.Traversal)) } propertyPathStr = fmt.Sprintf("%s.*%s", root, suffix) break dep_loop case *hclsyntax.TupleConsExpr: // TACTICAL // handle the case where an arg value is given as a runtime dependency inside an array, for example // arns = [input.arn] // this is a common pattern where a runtime depdency gives a scalar value, but an array is needed for the arg // NOTE: this code only supports a SINGLE item in the array if len(e.Exprs) != 1 { return false, nil, fmt.Errorf("unsupported runtime dependency expression - only a single runtime depdency item may be wrapped in an array") } isArray = true expr = e.Exprs[0] // fall through to rerun loop with updated expr default: // unhandled expression type return false, nil, fmt.Errorf("unexpected runtime dependency expression type") } } propertyPath, err := modconfig.ParseResourcePropertyPath(propertyPathStr) if err != nil { return false, nil, err } return isArray, propertyPath, nil } func identifyRuntimeDependenciesFromArray(attr *hcl.Attribute, idx int, parentProperty string) (*modconfig.RuntimeDependency, error) { // find the expression for this key argsExpr, ok := attr.Expr.(*hclsyntax.TupleConsExpr) if !ok { return nil, fmt.Errorf("could not extract runtime dependency for arg #%d", idx) } for i, item := range argsExpr.Exprs { if i == idx { isArray, propertyPath, err := propertyPathFromExpression(item) if err != nil { return nil, err } // tactical: validate input dependency if propertyPath.ItemType == modconfig.BlockTypeInput { if err := validateInputRuntimeDependency(propertyPath); err != nil { return nil, err } } ret := &modconfig.RuntimeDependency{ PropertyPath: propertyPath, ParentPropertyName: parentProperty, TargetPropertyIndex: &idx, IsArray: isArray, } return ret, nil } } return nil, fmt.Errorf("could not extract runtime dependency for arg %d - not found in attribute list", idx) } // tactical - if runtime dependency is an input, validate it is of correct format // TODO - include this with the main runtime dependency validation, when it is rewritten https://github.com/turbot/steampipe/issues/2925 func validateInputRuntimeDependency(propertyPath *modconfig.ParsedPropertyPath) error { // input references must be of form self.input..value if propertyPath.Scope != modconfig.RuntimeDependencyDashboardScope { return fmt.Errorf("could not resolve runtime dependency resource %s", propertyPath.Original) } return nil } func decodeParam(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.ParamDef, []*modconfig.RuntimeDependency, hcl.Diagnostics) { def := modconfig.NewParamDef(block) var runtimeDependencies []*modconfig.RuntimeDependency content, diags := block.Body.Content(ParamDefBlockSchema) if attr, exists := content.Attributes["description"]; exists { moreDiags := gohcl.DecodeExpression(attr.Expr, parseCtx.EvalCtx, &def.Description) diags = append(diags, moreDiags...) } if attr, exists := content.Attributes["default"]; exists { defaultValue, deps, moreDiags := decodeParamDefault(attr, parseCtx, def.UnqualifiedName) diags = append(diags, moreDiags...) if !helpers.IsNil(defaultValue) { def.SetDefault(defaultValue) } runtimeDependencies = deps } return def, runtimeDependencies, diags } func decodeParamDefault(attr *hcl.Attribute, parseCtx *ModParseContext, paramName string) (any, []*modconfig.RuntimeDependency, hcl.Diagnostics) { v, diags := attr.Expr.Value(parseCtx.EvalCtx) if v.IsKnown() { // convert the raw default into a string representation val, err := type_conversion.CtyToGo(v) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("%s has invalid default config", paramName), Detail: err.Error(), Subject: &attr.Range, }) return nil, nil, diags } return val, nil, nil } // so value not known - is there a runtime dependency? // check for a runtime dependency runtimeDependency, err := getRuntimeDepFromExpression(attr.Expr, "default", paramName) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("%s has invalid parameter default config", paramName), Detail: err.Error(), Subject: &attr.Range, }) return nil, nil, diags } if runtimeDependency == nil { // return the original diags return nil, nil, diags } // so we have a runtime dependency return nil, []*modconfig.RuntimeDependency{runtimeDependency}, nil }