github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/decode_args.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/zclconf/go-cty/cty"
    13  	"github.com/zclconf/go-cty/cty/gocty"
    14  )
    15  
    16  func decodeArgs(attr *hcl.Attribute, evalCtx *hcl.EvalContext, resource modconfig.QueryProvider) (*modconfig.QueryArgs, []*modconfig.RuntimeDependency, hcl.Diagnostics) {
    17  	var runtimeDependencies []*modconfig.RuntimeDependency
    18  	var args = modconfig.NewQueryArgs()
    19  	var diags hcl.Diagnostics
    20  
    21  	v, valDiags := attr.Expr.Value(evalCtx)
    22  	ty := v.Type()
    23  	// determine which diags are runtime dependencies (which we allow) and which are not
    24  	if valDiags.HasErrors() {
    25  		for _, diag := range diags {
    26  			dependency := diagsToDependency(diag)
    27  			if dependency == nil || !dependency.IsRuntimeDependency() {
    28  				diags = append(diags, diag)
    29  			}
    30  		}
    31  	}
    32  	// now diags contains all diags which are NOT runtime dependencies
    33  	if diags.HasErrors() {
    34  		return nil, nil, diags
    35  	}
    36  
    37  	var err error
    38  
    39  	switch {
    40  	case ty.IsObjectType():
    41  		var argMap map[string]any
    42  		argMap, runtimeDependencies, err = ctyObjectToArgMap(attr, v, evalCtx)
    43  		if err == nil {
    44  			err = args.SetArgMap(argMap)
    45  		}
    46  	case ty.IsTupleType():
    47  		var argList []any
    48  		argList, runtimeDependencies, err = ctyTupleToArgArray(attr, v)
    49  		if err == nil {
    50  			err = args.SetArgList(argList)
    51  		}
    52  	default:
    53  		err = fmt.Errorf("'params' property must be either a map or an array")
    54  	}
    55  
    56  	if err != nil {
    57  		diags = append(diags, &hcl.Diagnostic{
    58  			Severity: hcl.DiagError,
    59  			Summary:  fmt.Sprintf("%s has invalid parameter config", resource.Name()),
    60  			Detail:   err.Error(),
    61  			Subject:  &attr.Range,
    62  		})
    63  	}
    64  	return args, runtimeDependencies, diags
    65  }
    66  
    67  func ctyTupleToArgArray(attr *hcl.Attribute, val cty.Value) ([]any, []*modconfig.RuntimeDependency, error) {
    68  	// convert the attribute to a slice
    69  	values := val.AsValueSlice()
    70  
    71  	// build output array
    72  	res := make([]any, len(values))
    73  	var runtimeDependencies []*modconfig.RuntimeDependency
    74  
    75  	for idx, v := range values {
    76  		// if the value is unknown, this is a runtime dependency
    77  		if !v.IsKnown() {
    78  			runtimeDependency, err := identifyRuntimeDependenciesFromArray(attr, idx, modconfig.AttributeArgs)
    79  			if err != nil {
    80  				return nil, nil, err
    81  			}
    82  
    83  			runtimeDependencies = append(runtimeDependencies, runtimeDependency)
    84  		} else {
    85  			// decode the value into a go type
    86  			val, err := hclhelpers.CtyToGo(v)
    87  			if err != nil {
    88  				err := fmt.Errorf("invalid value provided for arg #%d: %v", idx, err)
    89  				return nil, nil, err
    90  			}
    91  
    92  			res[idx] = val
    93  		}
    94  	}
    95  	return res, runtimeDependencies, nil
    96  }
    97  
    98  func ctyObjectToArgMap(attr *hcl.Attribute, val cty.Value, evalCtx *hcl.EvalContext) (map[string]any, []*modconfig.RuntimeDependency, error) {
    99  	res := make(map[string]any)
   100  	var runtimeDependencies []*modconfig.RuntimeDependency
   101  	it := val.ElementIterator()
   102  	for it.Next() {
   103  		k, v := it.Element()
   104  
   105  		// decode key
   106  		var key string
   107  		if err := gocty.FromCtyValue(k, &key); err != nil {
   108  			return nil, nil, err
   109  		}
   110  
   111  		// if the value is unknown, this is a runtime dependency
   112  		if !v.IsKnown() {
   113  			runtimeDependency, err := identifyRuntimeDependenciesFromObject(attr, key, modconfig.AttributeArgs, evalCtx)
   114  			if err != nil {
   115  				return nil, nil, err
   116  			}
   117  			runtimeDependencies = append(runtimeDependencies, runtimeDependency)
   118  		} else if getWrappedUnknownVal(v) {
   119  			runtimeDependency, err := identifyRuntimeDependenciesFromObject(attr, key, modconfig.AttributeArgs, evalCtx)
   120  			if err != nil {
   121  				return nil, nil, err
   122  			}
   123  			runtimeDependencies = append(runtimeDependencies, runtimeDependency)
   124  		} else {
   125  			// decode the value into a go type
   126  			val, err := hclhelpers.CtyToGo(v)
   127  			if err != nil {
   128  				err := fmt.Errorf("invalid value provided for param '%s': %v", key, err)
   129  				return nil, nil, err
   130  			}
   131  			res[key] = val
   132  		}
   133  	}
   134  
   135  	return res, runtimeDependencies, nil
   136  }
   137  
   138  // TACTICAL - is the cty value an array with a single unknown value
   139  func getWrappedUnknownVal(v cty.Value) bool {
   140  	ty := v.Type()
   141  
   142  	switch {
   143  
   144  	case ty.IsTupleType():
   145  		values := v.AsValueSlice()
   146  		if len(values) == 1 && !values[0].IsKnown() {
   147  			return true
   148  		}
   149  	}
   150  	return false
   151  }
   152  
   153  func identifyRuntimeDependenciesFromObject(attr *hcl.Attribute, targetProperty, parentProperty string, evalCtx *hcl.EvalContext) (*modconfig.RuntimeDependency, error) {
   154  	// find the expression for this key
   155  	argsExpr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr)
   156  	if !ok {
   157  		return nil, fmt.Errorf("could not extract runtime dependency for arg %s", targetProperty)
   158  	}
   159  	for _, item := range argsExpr.Items {
   160  		nameCty, valDiags := item.KeyExpr.Value(evalCtx)
   161  		if valDiags.HasErrors() {
   162  			return nil, fmt.Errorf("could not extract runtime dependency for arg %s", targetProperty)
   163  		}
   164  		var name string
   165  		if err := gocty.FromCtyValue(nameCty, &name); err != nil {
   166  			return nil, err
   167  		}
   168  		if name == targetProperty {
   169  			dep, err := getRuntimeDepFromExpression(item.ValueExpr, targetProperty, parentProperty)
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  
   174  			return dep, nil
   175  		}
   176  	}
   177  	return nil, fmt.Errorf("could not extract runtime dependency for arg %s - not found in attribute map", targetProperty)
   178  }
   179  
   180  func getRuntimeDepFromExpression(expr hcl.Expression, targetProperty, parentProperty string) (*modconfig.RuntimeDependency, error) {
   181  	isArray, propertyPath, err := propertyPathFromExpression(expr)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	if propertyPath.ItemType == modconfig.BlockTypeInput {
   187  		// tactical: validate input dependency
   188  		if err := validateInputRuntimeDependency(propertyPath); err != nil {
   189  			return nil, err
   190  		}
   191  	}
   192  	ret := &modconfig.RuntimeDependency{
   193  		PropertyPath:       propertyPath,
   194  		ParentPropertyName: parentProperty,
   195  		TargetPropertyName: &targetProperty,
   196  		IsArray:            isArray,
   197  	}
   198  	return ret, nil
   199  }
   200  
   201  func propertyPathFromExpression(expr hcl.Expression) (bool, *modconfig.ParsedPropertyPath, error) {
   202  	var propertyPathStr string
   203  	var isArray bool
   204  
   205  dep_loop:
   206  	for {
   207  		switch e := expr.(type) {
   208  		case *hclsyntax.ScopeTraversalExpr:
   209  			propertyPathStr = hclhelpers.TraversalAsString(e.Traversal)
   210  			break dep_loop
   211  		case *hclsyntax.SplatExpr:
   212  			root := hclhelpers.TraversalAsString(e.Source.(*hclsyntax.ScopeTraversalExpr).Traversal)
   213  			var suffix string
   214  			// if there is a property path, add it
   215  			if each, ok := e.Each.(*hclsyntax.RelativeTraversalExpr); ok {
   216  				suffix = fmt.Sprintf(".%s", hclhelpers.TraversalAsString(each.Traversal))
   217  			}
   218  			propertyPathStr = fmt.Sprintf("%s.*%s", root, suffix)
   219  			break dep_loop
   220  		case *hclsyntax.TupleConsExpr:
   221  			// TACTICAL
   222  			// handle the case where an arg value is given as a runtime dependency inside an array, for example
   223  			// arns = [input.arn]
   224  			// this is a common pattern where a runtime depdency gives a scalar value, but an array is needed for the arg
   225  			// NOTE: this code only supports a SINGLE item in the array
   226  			if len(e.Exprs) != 1 {
   227  				return false, nil, fmt.Errorf("unsupported runtime dependency expression - only a single runtime depdency item may be wrapped in an array")
   228  			}
   229  			isArray = true
   230  			expr = e.Exprs[0]
   231  			// fall through to rerun loop with updated expr
   232  		default:
   233  			// unhandled expression type
   234  			return false, nil, fmt.Errorf("unexpected runtime dependency expression type")
   235  		}
   236  	}
   237  
   238  	propertyPath, err := modconfig.ParseResourcePropertyPath(propertyPathStr)
   239  	if err != nil {
   240  		return false, nil, err
   241  	}
   242  	return isArray, propertyPath, nil
   243  }
   244  
   245  func identifyRuntimeDependenciesFromArray(attr *hcl.Attribute, idx int, parentProperty string) (*modconfig.RuntimeDependency, error) {
   246  	// find the expression for this key
   247  	argsExpr, ok := attr.Expr.(*hclsyntax.TupleConsExpr)
   248  	if !ok {
   249  		return nil, fmt.Errorf("could not extract runtime dependency for arg #%d", idx)
   250  	}
   251  	for i, item := range argsExpr.Exprs {
   252  		if i == idx {
   253  			isArray, propertyPath, err := propertyPathFromExpression(item)
   254  			if err != nil {
   255  				return nil, err
   256  			}
   257  			// tactical: validate input dependency
   258  			if propertyPath.ItemType == modconfig.BlockTypeInput {
   259  				if err := validateInputRuntimeDependency(propertyPath); err != nil {
   260  					return nil, err
   261  				}
   262  			}
   263  			ret := &modconfig.RuntimeDependency{
   264  				PropertyPath:        propertyPath,
   265  				ParentPropertyName:  parentProperty,
   266  				TargetPropertyIndex: &idx,
   267  				IsArray:             isArray,
   268  			}
   269  
   270  			return ret, nil
   271  		}
   272  	}
   273  	return nil, fmt.Errorf("could not extract runtime dependency for arg %d - not found in attribute list", idx)
   274  }
   275  
   276  // tactical - if runtime dependency is an input, validate it is of correct format
   277  // TODO - include this with the main runtime dependency validation, when it is rewritten https://github.com/turbot/steampipe/issues/2925
   278  func validateInputRuntimeDependency(propertyPath *modconfig.ParsedPropertyPath) error {
   279  	// input references must be of form self.input.<input_name>.value
   280  	if propertyPath.Scope != modconfig.RuntimeDependencyDashboardScope {
   281  		return fmt.Errorf("could not resolve runtime dependency resource %s", propertyPath.Original)
   282  	}
   283  	return nil
   284  }
   285  
   286  func decodeParam(block *hcl.Block, parseCtx *ModParseContext) (*modconfig.ParamDef, []*modconfig.RuntimeDependency, hcl.Diagnostics) {
   287  	def := modconfig.NewParamDef(block)
   288  	var runtimeDependencies []*modconfig.RuntimeDependency
   289  	content, diags := block.Body.Content(ParamDefBlockSchema)
   290  
   291  	if attr, exists := content.Attributes["description"]; exists {
   292  		moreDiags := gohcl.DecodeExpression(attr.Expr, parseCtx.EvalCtx, &def.Description)
   293  		diags = append(diags, moreDiags...)
   294  	}
   295  	if attr, exists := content.Attributes["default"]; exists {
   296  		defaultValue, deps, moreDiags := decodeParamDefault(attr, parseCtx, def.UnqualifiedName)
   297  		diags = append(diags, moreDiags...)
   298  		if !helpers.IsNil(defaultValue) {
   299  			def.SetDefault(defaultValue)
   300  		}
   301  		runtimeDependencies = deps
   302  	}
   303  	return def, runtimeDependencies, diags
   304  }
   305  
   306  func decodeParamDefault(attr *hcl.Attribute, parseCtx *ModParseContext, paramName string) (any, []*modconfig.RuntimeDependency, hcl.Diagnostics) {
   307  	v, diags := attr.Expr.Value(parseCtx.EvalCtx)
   308  
   309  	if v.IsKnown() {
   310  		// convert the raw default into a string representation
   311  		val, err := hclhelpers.CtyToGo(v)
   312  		if err != nil {
   313  			diags = append(diags, &hcl.Diagnostic{
   314  				Severity: hcl.DiagError,
   315  				Summary:  fmt.Sprintf("%s has invalid default config", paramName),
   316  				Detail:   err.Error(),
   317  				Subject:  &attr.Range,
   318  			})
   319  			return nil, nil, diags
   320  		}
   321  		return val, nil, nil
   322  	}
   323  
   324  	// so value not known - is there a runtime dependency?
   325  
   326  	// check for a runtime dependency
   327  	runtimeDependency, err := getRuntimeDepFromExpression(attr.Expr, "default", paramName)
   328  	if err != nil {
   329  		diags = append(diags, &hcl.Diagnostic{
   330  			Severity: hcl.DiagError,
   331  			Summary:  fmt.Sprintf("%s has invalid parameter default config", paramName),
   332  			Detail:   err.Error(),
   333  			Subject:  &attr.Range,
   334  		})
   335  		return nil, nil, diags
   336  	}
   337  	if runtimeDependency == nil {
   338  		// return the original diags
   339  		return nil, nil, diags
   340  	}
   341  
   342  	// so we have a runtime dependency
   343  	return nil, []*modconfig.RuntimeDependency{runtimeDependency}, nil
   344  }