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 }