diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 0f17d68195..0f575748ea 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -31,16 +31,25 @@ func Validate(policyContext PolicyContext) (resp response.EngineResponse) { if reflect.DeepEqual(resp, response.EngineResponse{}) { return } - startResultResponse(&resp, policy, newR) - endResultResponse(logger, &resp, startTime) + var resource unstructured.Unstructured if reflect.DeepEqual(resp.PatchedResource, unstructured.Unstructured{}) { // for delete requests patched resource will be oldR since newR is empty if reflect.DeepEqual(newR, unstructured.Unstructured{}) { - resp.PatchedResource = oldR + resource = oldR } else { - resp.PatchedResource = newR + resource = newR } } + for i := range resp.PolicyResponse.Rules { + messageInterface, err := variables.SubstituteVars(logger, ctx, resp.PolicyResponse.Rules[i].Message) + if err != nil { + continue + } + resp.PolicyResponse.Rules[i].Message, _ = messageInterface.(string) + } + resp.PatchedResource = resource + startResultResponse(&resp, policy, resource) + endResultResponse(logger, &resp, startTime) }() // If request is delete, newR will be empty diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go index 89d1a93714..0b0125a00d 100644 --- a/pkg/engine/variables/vars.go +++ b/pkg/engine/variables/vars.go @@ -11,8 +11,7 @@ import ( ) const ( - variableRegex = `\{\{([^{}]*)\}\}` - singleVarRegex = `^\{\{([^{}]*)\}\}$` + variableRegex = `\{\{([^{}]*)\}\}` ) //SubstituteVars replaces the variables with the values defined in the context @@ -42,7 +41,7 @@ func subVars(log logr.Logger, ctx context.EvalInterface, pattern interface{}, pa return subArray(log, ctx, sliceCopy, path, errs) case string: - return subValR(log, ctx, typedPattern, path, errs) + return subValR(ctx, typedPattern, path, errs) default: return pattern } @@ -68,108 +67,46 @@ func subArray(log logr.Logger, ctx context.EvalInterface, patternList []interfac } // subValR resolves the variables if defined -func subValR(log logr.Logger, ctx context.EvalInterface, valuePattern string, path string, errs *[]error) interface{} { +func subValR(ctx context.EvalInterface, valuePattern string, path string, errs *[]error) interface{} { + originalPattern := valuePattern + var failedVars []interface{} - // variable values can be scalar values(string,int, float) or they can be obects(map,slice) - // - {{variable}} - // there is a single variable resolution so the value can be scalar or object - // - {{variable1--{{variable2}}}}} - // variable2 is evaluted first as an individual variable and can be have scalar or object values - // but resolving the outer variable, {{variable--}} - // if is scalar then it can replaced, but for object types its tricky - // as object cannot be directy replaced, if the object is stringyfied then it loses it structure. - // since this might be a potential place for error, required better error reporting and handling + defer func() { + if len(failedVars) > 0 { + *errs = append(*errs, fmt.Errorf("failed to resolve %v at path %s", failedVars, path)) + } + }() - // object values are only suported for single variable substitution - if ok, retVal := processIfSingleVariable(log, ctx, valuePattern, path, errs); ok { - return retVal - } - // var emptyInterface interface{} - var failedVars []string - // process type string + regex := regexp.MustCompile(`\{\{([^{}]*)\}\}`) for { - valueStr := valuePattern - if len(failedVars) != 0 { - log.Info("failed to resolve variablesl short-circuiting") + if vars := regex.FindAllString(valuePattern, -1); len(vars) > 0 { + for _, variable := range vars { + underlyingVariable := strings.ReplaceAll(strings.ReplaceAll(variable, "}}", ""), "{{", "") + substitutedVar, err := ctx.Query(underlyingVariable) + if err != nil { + failedVars = append(failedVars, underlyingVariable) + return nil + } + if val, ok := substitutedVar.(string); ok { + if val == "" { + failedVars = append(failedVars, underlyingVariable) + return nil + } + valuePattern = strings.Replace(valuePattern, variable, val, -1) + } else { + if substitutedVar != nil { + if originalPattern == variable { + return substitutedVar + } + } + failedVars = append(failedVars, underlyingVariable) + return nil + } + } + } else { break } - // get variables at this level - validRegex := regexp.MustCompile(variableRegex) - groups := validRegex.FindAllStringSubmatch(valueStr, -1) - if len(groups) == 0 { - // there was no match - // not variable defined - break - } - subs := map[string]interface{}{} - for _, group := range groups { - if _, ok := subs[group[0]]; ok { - // value has already been substituted - continue - } - // here we do the querying of the variables from the context - variable, err := ctx.Query(group[1]) - if err != nil { - // error while evaluating - failedVars = append(failedVars, group[1]) - continue - } - // path not found in context and value stored in null/nill - if variable == nil { - failedVars = append(failedVars, group[1]) - continue - } - // get values for each and replace - subs[group[0]] = variable - } - // perform substitutions - newVal := valueStr - for k, v := range subs { - // if value is of type string then cast else consider it as direct replacement - if val, ok := v.(string); ok { - newVal = strings.Replace(newVal, k, val, -1) - continue - } - // if type is not scalar then consider this as a failed variable - log.Info("variable resolves to non-scalar value. Non-Scalar values are not supported for nested variables", "variable", k, "value", v) - failedVars = append(failedVars, k) - } - valuePattern = newVal - } - // update errors if any - if len(failedVars) > 0 { - *errs = append(*errs, fmt.Errorf("failed to resolve %v at path %s", failedVars, path)) } return valuePattern } - -// processIfSingleVariable will process the evaluation of single variables -// {{variable-{{variable}}}} -> compound/nested variables -// {{variable}}{{variable}} -> multiple variables -// {{variable}} -> single variable -// if the value can be evaluted return the value -// -> return value can be scalar or object type -// -> if the variable is not present in the context then add an error and dont process further -func processIfSingleVariable(log logr.Logger, ctx context.EvalInterface, valuePattern interface{}, path string, errs *[]error) (bool, interface{}) { - valueStr, ok := valuePattern.(string) - if !ok { - log.Info("failed to convert to string", "pattern", valuePattern) - return false, nil - } - // get variables at this level - validRegex := regexp.MustCompile(singleVarRegex) - groups := validRegex.FindAllStringSubmatch(valueStr, -1) - if len(groups) == 0 { - return false, nil - } - // as there will be exactly one variable based on the above regex - group := groups[0] - variable, err := ctx.Query(group[1]) - if err != nil || variable == nil { - *errs = append(*errs, fmt.Errorf("failed to resolve %v at path %s", group[1], path)) - // return the same value pattern, and add un-resolvable variable error - return true, valuePattern - } - return true, variable -} diff --git a/pkg/engine/variables/vars_test.go b/pkg/engine/variables/vars_test.go index aae517a082..85d4537cd8 100644 --- a/pkg/engine/variables/vars_test.go +++ b/pkg/engine/variables/vars_test.go @@ -153,5 +153,5 @@ func Test_SubvarRecursive(t *testing.T) { ctx := context.NewContext() assert.Assert(t, ctx.AddResource(resourceRaw)) errs := []error{} - subValR(log.Log, ctx, string(patternRaw), "/", &errs) + subValR(ctx, string(patternRaw), "/", &errs) }