From 2289720ba049f0ca0b4aacf766b0893a8b823159 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Tue, 8 Oct 2024 12:43:04 -0700 Subject: [PATCH] add support for shallow substitution (#11058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add support for shallow substitution Signed-off-by: Jim Bugwadia * linter issue Signed-off-by: Jim Bugwadia * exclude EphemeralReport and ClusterEphemeralReport Signed-off-by: Jim Bugwadia * update codegen Signed-off-by: Jim Bugwadia --------- Signed-off-by: Jim Bugwadia Co-authored-by: Charles-Edouard Brétéché --- charts/kyverno/values.yaml | 2 ++ config/install-latest-testing.yaml | 2 ++ pkg/engine/variables/regex/vars.go | 5 +--- pkg/engine/variables/vars.go | 42 +++++++++++++++++----------- pkg/engine/variables/vars_test.go | 44 ++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/charts/kyverno/values.yaml b/charts/kyverno/values.yaml index fd8cb70779..106585a1e6 100644 --- a/charts/kyverno/values.yaml +++ b/charts/kyverno/values.yaml @@ -224,6 +224,8 @@ config: - '[Pod/binding,*,*]' - '[ReplicaSet,*,*]' - '[ReplicaSet/*,*,*]' + - '[EphemeralReport,*,*]' + - '[ClusterEphemeralReport,*,*]' # exclude resources from the chart - '[ClusterRole,*,{{ template "kyverno.admission-controller.roleName" . }}]' - '[ClusterRole,*,{{ template "kyverno.admission-controller.roleName" . }}:core]' diff --git a/config/install-latest-testing.yaml b/config/install-latest-testing.yaml index bc217768d2..6a94ef434e 100644 --- a/config/install-latest-testing.yaml +++ b/config/install-latest-testing.yaml @@ -86,6 +86,8 @@ data: [Pod/binding,*,*] [ReplicaSet,*,*] [ReplicaSet/*,*,*] + [EphemeralReport,*,*] + [ClusterEphemeralReport,*,*] [ClusterRole,*,kyverno:admission-controller] [ClusterRole,*,kyverno:admission-controller:core] [ClusterRole,*,kyverno:admission-controller:additional] diff --git a/pkg/engine/variables/regex/vars.go b/pkg/engine/variables/regex/vars.go index a91f6154f6..215543ad34 100644 --- a/pkg/engine/variables/regex/vars.go +++ b/pkg/engine/variables/regex/vars.go @@ -3,10 +3,9 @@ package regex import "regexp" var ( + // RegexVariables is the Regex for '{{...}}' at the beginning of the string, and 'x{{...}}' where 'x' is not '\' RegexVariables = regexp.MustCompile(`(^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`) - RegexEscpVariables = regexp.MustCompile(`\\\{\{(\{[^{}]*\}|[^{}])*\}\}`) - // RegexReferences is the Regex for '$(...)' at the beginning of the string, and 'x$(...)' where 'x' is not '\' RegexReferences = regexp.MustCompile(`^\$\(.[^\ ]*\)|[^\\]\$\(.[^\ ]*\)`) @@ -16,6 +15,4 @@ var ( RegexVariableInit = regexp.MustCompile(`^\{\{(\{[^{}]*\}|[^{}])*\}\}`) RegexElementIndex = regexp.MustCompile(`{{\s*elementIndex\d*\s*}}`) - - RegexVariableKey = regexp.MustCompile(`\{{(.*?)\}}`) ) diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go index 93c6a0b964..2b65f83568 100644 --- a/pkg/engine/variables/vars.go +++ b/pkg/engine/variables/vars.go @@ -222,7 +222,7 @@ func validateElementInForEach() jsonUtils.Action { v = v[1:] } - variable := replaceBracesAndTrimSpaces(v) + variable, _ := replaceBracesAndTrimSpaces(v) isElementVar := strings.HasPrefix(variable, "element") || variable == "elementIndex" if isElementVar && !strings.Contains(data.Path, "/foreach/") { return nil, fmt.Errorf("variable '%v' present outside of foreach at path %s", variable, data.Path) @@ -308,7 +308,7 @@ func DefaultVariableResolver(ctx context.EvalInterface, variable string) (interf return ctx.Query(variable) } -func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr VariableResolver) jsonUtils.Action { +func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, lookupVar VariableResolver) jsonUtils.Action { isDeleteRequest := isDeleteRequest(ctx) return jsonUtils.OnlyForLeafsAndKeys(func(data *jsonUtils.ActionData) (interface{}, error) { value, ok := data.Element.(string) @@ -319,16 +319,16 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var vars := regex.RegexVariables.FindAllString(value, -1) for len(vars) > 0 { originalPattern := value + shallowSubstitution := false + var variable string for _, v := range vars { initial := len(regex.RegexVariableInit.FindAllString(v, -1)) > 0 old := v - if !initial { v = v[1:] } - variable := replaceBracesAndTrimSpaces(v) - + variable, shallowSubstitution = replaceBracesAndTrimSpaces(v) if variable == "@" { pathPrefix := "target" if _, err := ctx.Query("target"); err != nil { @@ -339,7 +339,6 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var // Skip 2 elements (e.g. mutate.overlay | validate.pattern) plus "foreach" if it is part of the pointer. // Prefix the pointer with pathPrefix. val := jsonpointer.ParsePath(data.Path).SkipPast("foreach").SkipN(2).Prepend(strings.Split(pathPrefix, ".")...).JMESPath() - variable = strings.Replace(variable, "@", val, -1) } @@ -347,7 +346,7 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var variable = strings.ReplaceAll(variable, "request.object", "request.oldObject") } - substitutedVar, err := vr(ctx, variable) + substitutedVar, err := lookupVar(ctx, variable) if err != nil { switch err.(type) { case context.InvalidVariableError, gojmespath.NotFoundError: @@ -368,6 +367,10 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var prefix = string(old[0]) } + if shallowSubstitution { + substitutedVar = strings.ReplaceAll(substitutedVar.(string), "{{", "\\{{") + } + if value, err = substituteVarInPattern(prefix, value, v, substitutedVar); err != nil { return nil, fmt.Errorf("failed to resolve %v at path %s: %s", variable, data.Path, err.Error()) } @@ -375,14 +378,16 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var continue } - // check for nested variables in strings - vars = regex.RegexVariables.FindAllString(value, -1) - } - - for _, v := range regex.RegexEscpVariables.FindAllString(value, -1) { - value = strings.Replace(value, v, v[1:], -1) + if shallowSubstitution { + vars = []string{} + } else { + // check for nested variables in strings + vars = regex.RegexVariables.FindAllString(value, -1) + } } + // Unescape escaped braces + value = strings.ReplaceAll(value, "\\{{", "{{") return value, nil }) } @@ -418,11 +423,16 @@ func substituteVarInPattern(prefix, pattern, variable string, value interface{}) return strings.Replace(pattern, variable, stringToSubstitute, 1), nil } -func replaceBracesAndTrimSpaces(v string) string { - variable := strings.ReplaceAll(v, "{{", "") +func replaceBracesAndTrimSpaces(v string) (variable string, isShallow bool) { + variable = strings.ReplaceAll(v, "{{", "") variable = strings.ReplaceAll(variable, "}}", "") variable = strings.TrimSpace(variable) - return variable + if strings.HasPrefix(variable, "-") { + variable = strings.TrimSpace(variable[1:]) + return variable, true + } + + return variable, false } func resolveReference(fullDocument interface{}, reference, absolutePath string) (interface{}, error) { diff --git a/pkg/engine/variables/vars_test.go b/pkg/engine/variables/vars_test.go index 1b16f2dbc0..daa881333b 100644 --- a/pkg/engine/variables/vars_test.go +++ b/pkg/engine/variables/vars_test.go @@ -544,6 +544,50 @@ func Test_SubstituteRecursive(t *testing.T) { } } +func Test_SubstituteShallow(t *testing.T) { + ctx := context.NewContext(jp) + data := map[string]interface{}{ + "variableWithVariables": "{{ DO_NOT_SUBSTITUTE_ME {{OR_ME}} }}", + "foo": "bar", + "foo2": "bar2", + "variablesNested": "{{foo2}}", + } + + assert.NilError(t, context.AddJSONObject(ctx, data)) + + patternRaw := []byte(`"{{- variableWithVariables }} {{foo}} {{variablesNested}}"`) + action := substituteVariablesIfAny(logr.Discard(), ctx, DefaultVariableResolver) + results, err := action(&ju.ActionData{ + Document: nil, + Element: string(patternRaw), + Path: "/", + }) + + assert.NilError(t, err) + assert.Equal(t, results.(string), "\"{{ DO_NOT_SUBSTITUTE_ME {{OR_ME}} }} bar bar2\"") + + patternRaw = []byte(`"{{foo}} {{- variableWithVariables }} {{variablesNested}}"`) + action = substituteVariablesIfAny(logr.Discard(), ctx, DefaultVariableResolver) + results, err = action(&ju.ActionData{ + Document: nil, + Element: string(patternRaw), + Path: "/", + }) + + assert.NilError(t, err) + assert.Equal(t, results.(string), "\"bar {{ DO_NOT_SUBSTITUTE_ME {{OR_ME}} }} bar2\"") + + patternRaw = []byte(`"{{- variableWithVariables {{foo}} {{variablesNested}} }}"`) + action = substituteVariablesIfAny(logr.Discard(), ctx, DefaultVariableResolver) + _, err = action(&ju.ActionData{ + Document: nil, + Element: string(patternRaw), + Path: "/", + }) + + assert.ErrorContains(t, err, "failed to resolve variableWithVariables bar bar2") +} + func Test_policyContextValidation(t *testing.T) { policyContext := []byte(` {