1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-04-08 18:15:48 +00:00

Fix #1506; Resolve path reference in entire rule instead of just pattern/overlay

Signed-off-by: Max Goncharenko <kacejot@fex.net>
This commit is contained in:
Max Goncharenko 2021-03-11 22:06:04 +02:00
parent af4b85d3a8
commit 24c4f06ecd
22 changed files with 982 additions and 467 deletions

View file

@ -1,5 +1,10 @@
package common
import (
"path"
"strings"
)
// IsAnchor is a function handler
type IsAnchor func(str string) bool
@ -72,6 +77,24 @@ func RemoveAnchor(key string) (string, string) {
return key, ""
}
// RemoveAnchorsFromPath removes all anchor from path string
func RemoveAnchorsFromPath(str string) string {
components := strings.Split(str, "/")
if components[0] == "" {
components = components[1:]
}
for i, component := range components {
components[i], _ = RemoveAnchor(component)
}
newPath := path.Join(components...)
if path.IsAbs(str) {
newPath = "/" + newPath
}
return newPath
}
// AddAnchor adds an anchor with the supplied prefix.
// The suffix is assumed to be ")".
func AddAnchor(key, anchorPrefix string) string {

View file

@ -56,3 +56,13 @@ func TestIsExistenceAnchor_OnlyHat(t *testing.T) {
func TestIsExistenceAnchor_ConditionAnchor(t *testing.T) {
assert.Assert(t, !IsExistenceAnchor("(abc)"))
}
func TestRemoveAnchorsFromPath_WorksWithAbsolutePath(t *testing.T) {
newPath := RemoveAnchorsFromPath("/path/(to)/X(anchors)")
assert.Equal(t, newPath, "/path/to/anchors")
}
func TestRemoveAnchorsFromPath_WorksWithRelativePath(t *testing.T) {
newPath := RemoveAnchorsFromPath("path/(to)/X(anchors)")
assert.Equal(t, newPath, "path/to/anchors")
}

View file

@ -0,0 +1,19 @@
package common
// CopyMap creates a full copy of the target map
func CopyMap(m map[string]interface{}) map[string]interface{} {
mapCopy := make(map[string]interface{})
for k, v := range m {
mapCopy[k] = v
}
return mapCopy
}
// CopySlice creates a full copy of the target slice
func CopySlice(s []interface{}) []interface{} {
sliceCopy := make([]interface{}, len(s))
copy(sliceCopy, s)
return sliceCopy
}

View file

@ -0,0 +1,37 @@
package common
import (
"testing"
"gotest.tools/assert"
)
func Test_OriginalMapMustNotBeChanged(t *testing.T) {
// no variables
originalMap := map[string]interface{}{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
mapCopy := CopyMap(originalMap)
mapCopy["r"] = 1
assert.Equal(t, originalMap["r"], 2138)
}
func Test_OriginalSliceMustNotBeChanged(t *testing.T) {
// no variables
originalSlice := []interface{}{
3711,
2138,
1908,
912,
}
sliceCopy := CopySlice(originalSlice)
sliceCopy[0] = 1
assert.Equal(t, originalSlice[0], 3711)
}

View file

@ -1,9 +1,7 @@
package engine
import (
"encoding/json"
"fmt"
"regexp"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
@ -63,17 +61,15 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour
continue
}
rule, err = variables.SubstituteAllForceMutate(log.Log, ctx, rule)
if err != nil {
return unstructured.Unstructured{}, err
}
mutation := rule.Mutation.DeepCopy()
if mutation.Overlay != nil {
overlay := mutation.Overlay
if ctx != nil {
if overlay, err = variables.SubstituteVars(log.Log, ctx, overlay); err != nil {
return unstructured.Unstructured{}, err
}
} else {
overlay = replaceSubstituteVariables(overlay)
}
resource, err = mutateResourceWithOverlay(resource, overlay)
if err != nil {
@ -93,27 +89,3 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour
return resource, nil
}
func replaceSubstituteVariables(overlay interface{}) interface{} {
overlayRaw, err := json.Marshal(overlay)
if err != nil {
return overlay
}
regex := regexp.MustCompile(`\{\{([^{}]*)\}\}`)
for {
if len(regex.FindAllStringSubmatch(string(overlayRaw), -1)) > 0 {
overlayRaw = regex.ReplaceAll(overlayRaw, []byte(`placeholderValue`))
} else {
break
}
}
var output interface{}
err = json.Unmarshal(overlayRaw, &output)
if err != nil {
return overlay
}
return output
}

View file

@ -0,0 +1,150 @@
package engine
import (
"encoding/json"
"testing"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/utils"
"gotest.tools/assert"
)
var rawPolicy = []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "add-label"
},
"spec": {
"rules": [
{
"name": "add-name-label",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"labels": {
"appname": "{{request.object.metadata.name}}"
}
}
}
}
}
]
}
}
`)
var rawResource = []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user"
},
"spec": {
"containers": [
{
"name": "check-root-user",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
`)
func Test_ForceMutateSubstituteVars(t *testing.T) {
expectedRawResource := []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user",
"labels": {
"appname": "check-root-user"
}
},
"spec": {
"containers": [
{
"name": "check-root-user",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
`)
var expectedResource interface{}
assert.NilError(t, json.Unmarshal(expectedRawResource, &expectedResource))
var policy kyverno.ClusterPolicy
err := json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
resourceUnstructured, err := utils.ConvertToUnstructured(rawResource)
assert.NilError(t, err)
ctx := context.NewContext()
err = ctx.AddResource(rawResource)
assert.NilError(t, err)
mutatedResource, err := ForceMutate(ctx, policy, *resourceUnstructured)
assert.NilError(t, err)
assert.DeepEqual(t, expectedResource, mutatedResource.UnstructuredContent())
}
func Test_ForceMutateSubstituteVarsWithNilContext(t *testing.T) {
expectedRawResource := []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user",
"labels": {
"appname": "placeholderValue"
}
},
"spec": {
"containers": [
{
"name": "check-root-user",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
`)
var expectedResource interface{}
assert.NilError(t, json.Unmarshal(expectedRawResource, &expectedResource))
var policy kyverno.ClusterPolicy
err := json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
resourceUnstructured, err := utils.ConvertToUnstructured(rawResource)
assert.NilError(t, err)
mutatedResource, err := ForceMutate(nil, policy, *resourceUnstructured)
assert.NilError(t, err)
assert.DeepEqual(t, expectedResource, mutatedResource.UnstructuredContent())
}

View file

@ -0,0 +1,94 @@
package json_utils
import (
"strconv"
"github.com/kyverno/kyverno/pkg/engine/common"
)
// ActionData represents data available for action on current element
type ActionData struct {
Document interface{}
Element interface{}
Path string
}
// Action encapsulates the logic that must be performed for each
// JSON element
type Action func(data *ActionData) (interface{}, error)
// OnlyForLeafs is an action modifier - apply action only for leafs
func OnlyForLeafs(action Action) Action {
return func(data *ActionData) (interface{}, error) {
switch data.Element.(type) {
case map[string]interface{}, []interface{}: // skip arrays and maps
return data.Element, nil
default: // leaf detected
return action(data)
}
}
}
// Traversal is a type that encapsulates JSON traversal algorithm
// It traverses entire JSON structure applying some logic to its elements
type Traversal struct {
document interface{}
action Action
}
// NewTraversal creates JSON Traversal object
func NewTraversal(document interface{}, action Action) *Traversal {
return &Traversal{
document,
action,
}
}
// TraverseJSON performs a traverse of JSON document and applying
// action for each JSON element
func (t *Traversal) TraverseJSON() (interface{}, error) {
return t.traverseJSON(t.document, "")
}
func (t *Traversal) traverseJSON(element interface{}, path string) (interface{}, error) {
// perform an action
element, err := t.action(&ActionData{t.document, element, path})
if err != nil {
return element, err
}
// traverse further
switch typed := element.(type) {
case map[string]interface{}:
return t.traverseObject(common.CopyMap(typed), path)
case []interface{}:
return t.traverseList(common.CopySlice(typed), path)
default:
return element, nil
}
}
func (t *Traversal) traverseObject(object map[string]interface{}, path string) (map[string]interface{}, error) {
for key, element := range object {
value, err := t.traverseJSON(element, path+"/"+key)
if err != nil {
return nil, err
}
object[key] = value
}
return object, nil
}
func (t *Traversal) traverseList(list []interface{}, path string) ([]interface{}, error) {
for idx, element := range list {
value, err := t.traverseJSON(element, path+"/"+strconv.Itoa(idx))
if err != nil {
return nil, err
}
list[idx] = value
}
return list, nil
}

View file

@ -0,0 +1,73 @@
package json_utils
import (
"encoding/json"
"testing"
"gotest.tools/assert"
)
var document = []byte(`
{
"kind": "{{request.object.metadata.name1}}",
"name": "ns-owner-{{request.object.metadata.name}}",
"data": {
"rules": [
{
"apiGroups": [
"{{request.object.metadata.name}}"
],
"resources": [
"namespaces"
],
"verbs": [
"*"
]
}
]
}
}
`)
func Test_TraverseLeafsCheckIfTheyHit(t *testing.T) {
hitMap := map[string]int{
"{{request.object.metadata.name1}}": 0,
"ns-owner-{{request.object.metadata.name}}": 0,
"{{request.object.metadata.name}}": 0,
"namespaces": 0,
"*": 0,
}
var originalJSON interface{}
err := json.Unmarshal(document, &originalJSON)
assert.NilError(t, err)
traversal := NewTraversal(originalJSON, OnlyForLeafs(func(data *ActionData) (interface{}, error) {
hitMap[data.Element.(string)]++
return data.Element, nil
}))
_, err = traversal.TraverseJSON()
for _, v := range hitMap {
assert.Equal(t, v, 1)
}
}
func Test_PathMustBeCorrectEveryTime(t *testing.T) {
expectedValue := "ns-owner-{{request.object.metadata.name}}"
expectedPath := "/name"
var originalJSON interface{}
err := json.Unmarshal(document, &originalJSON)
assert.NilError(t, err)
traversal := NewTraversal(originalJSON, OnlyForLeafs(func(data *ActionData) (interface{}, error) {
if data.Element.(string) == expectedValue {
assert.Equal(t, expectedPath, data.Path)
}
return data.Element, nil
}))
_, err = traversal.TraverseJSON()
}

View file

@ -101,7 +101,7 @@ func fetchAPIData(log logr.Logger, entry kyverno.ContextEntry, ctx *PolicyContex
return nil, fmt.Errorf("missing APICall in context entry %s %v", entry.Name, entry.APICall)
}
path, err := variables.SubstituteVars(log, ctx.JSONContext, entry.APICall.URLPath)
path, err := variables.SubstituteAll(log, ctx.JSONContext, entry.APICall.URLPath)
if err != nil {
return nil, fmt.Errorf("failed to substitute variables in context entry %s %s: %v", entry.Name, entry.APICall.URLPath, err)
}
@ -168,12 +168,12 @@ func loadConfigMap(logger logr.Logger, entry kyverno.ContextEntry, lister dynami
func fetchConfigMap(logger logr.Logger, entry kyverno.ContextEntry, lister dynamiclister.Lister, jsonContext *context.Context) ([]byte, error) {
contextData := make(map[string]interface{})
name, err := variables.SubstituteVars(logger, jsonContext, entry.ConfigMap.Name)
name, err := variables.SubstituteAll(logger, jsonContext, entry.ConfigMap.Name)
if err != nil {
return nil, fmt.Errorf("failed to substitute variables in context %s configMap.name %s: %v", entry.Name, entry.ConfigMap.Name, err)
}
namespace, err := variables.SubstituteVars(logger, jsonContext, entry.ConfigMap.Namespace)
namespace, err := variables.SubstituteAll(logger, jsonContext, entry.ConfigMap.Namespace)
if err != nil {
return nil, fmt.Errorf("failed to substitute variables in context %s configMap.namespace %s: %v", entry.Name, entry.ConfigMap.Namespace, err)
}

View file

@ -62,7 +62,7 @@ func (h patchStrategicMergeHandler) Handle() (response.RuleResponse, unstructure
// substitute the variables
var err error
if PatchStrategicMerge, err = variables.SubstituteVars(log, h.evalCtx, PatchStrategicMerge); err != nil {
if PatchStrategicMerge, err = variables.SubstituteAll(log, h.evalCtx, PatchStrategicMerge); err != nil {
// variable subsitution failed
ruleResponse.Success = false
ruleResponse.Message = err.Error()
@ -140,7 +140,7 @@ func (h overlayHandler) Handle() (response.RuleResponse, unstructured.Unstructur
// substitute the variables
var err error
if overlay, err = variables.SubstituteVars(h.logger, h.evalCtx, overlay); err != nil {
if overlay, err = variables.SubstituteAll(h.logger, h.evalCtx, overlay); err != nil {
// variable substitution failed
ruleResponse.Success = false
ruleResponse.Message = err.Error()

View file

@ -3,10 +3,7 @@ package validate
import (
"errors"
"fmt"
"path"
"reflect"
"strconv"
"strings"
"github.com/go-logr/logr"
"github.com/kyverno/kyverno/pkg/engine/anchor"
@ -39,7 +36,6 @@ func ValidateResourceWithPattern(log logr.Logger, resource, pattern interface{})
// and calls corresponding handler
// Pattern tree and resource tree can have different structure. In this case validation fails
func validateResourceElement(log logr.Logger, resourceElement, patternElement, originPattern interface{}, path string, ac *common.AnchorKey) (string, error) {
var err error
switch typedPatternElement := patternElement.(type) {
// map
case map[string]interface{}:
@ -62,14 +58,6 @@ func validateResourceElement(log logr.Logger, resourceElement, patternElement, o
// elementary values
case string, float64, int, int64, bool, nil:
/*Analyze pattern */
if checkedPattern := reflect.ValueOf(patternElement); checkedPattern.Kind() == reflect.String {
if isStringIsReference(checkedPattern.String()) { //check for $ anchor
patternElement, err = actualizePattern(log, originPattern, checkedPattern.String(), path)
if err != nil {
return path, err
}
}
}
if !ValidateValueWithPattern(log, resourceElement, patternElement) {
return path, fmt.Errorf("Validation rule failed at '%s' to validate value '%v' with pattern '%v'", path, resourceElement, patternElement)
@ -162,73 +150,6 @@ func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, o
return "", nil
}
func actualizePattern(log logr.Logger, origPattern interface{}, referencePattern, absolutePath string) (interface{}, error) {
var foundValue interface{}
referencePattern = strings.Trim(referencePattern, "$()")
operatorVariable := operator.GetOperatorFromStringPattern(referencePattern)
referencePattern = referencePattern[len(operatorVariable):]
if len(referencePattern) == 0 {
return nil, errors.New("Expected path. Found empty reference")
}
// Check for variables
// substitute it from Context
// remove absolute path
// {{ }}
// value :=
actualPath := formAbsolutePath(referencePattern, absolutePath)
valFromReference, err := getValueFromReference(log, origPattern, actualPath)
if err != nil {
return err, nil
}
//TODO validate this
if operatorVariable == operator.Equal { //if operator does not exist return raw value
return valFromReference, nil
}
foundValue, err = valFromReferenceToString(valFromReference, string(operatorVariable))
if err != nil {
return "", err
}
return string(operatorVariable) + foundValue.(string), nil
}
//Parse value to string
func valFromReferenceToString(value interface{}, operator string) (string, error) {
switch typed := value.(type) {
case string:
return typed, nil
case int, int64:
return fmt.Sprintf("%d", value), nil
case float64:
return fmt.Sprintf("%f", value), nil
default:
return "", fmt.Errorf("Incorrect expression. Operator %s does not match with value: %v", operator, value)
}
}
// returns absolute path
func formAbsolutePath(referencePath, absolutePath string) string {
if path.IsAbs(referencePath) {
return referencePath
}
return path.Join(absolutePath, referencePath)
}
//Prepares original pattern, path to value, and call traverse function
func getValueFromReference(log logr.Logger, origPattern interface{}, reference string) (interface{}, error) {
originalPatternMap := origPattern.(map[string]interface{})
reference = reference[1:]
statements := strings.Split(reference, "/")
return getValueFromPattern(log, originalPatternMap, statements, 0)
}
func getValueFromPattern(log logr.Logger, patternMap map[string]interface{}, keys []string, currentKeyIndex int) (interface{}, error) {
for key, pattern := range patternMap {

View file

@ -6,6 +6,7 @@ import (
"testing"
"github.com/kyverno/kyverno/pkg/engine/common"
"github.com/kyverno/kyverno/pkg/engine/variables"
"gotest.tools/assert"
"sigs.k8s.io/controller-runtime/pkg/log"
)
@ -788,6 +789,9 @@ func TestValidateMap_CorrectRelativePathInConfig(t *testing.T) {
assert.Assert(t, json.Unmarshal(rawPattern, &pattern))
assert.Assert(t, json.Unmarshal(rawMap, &resource))
pattern, err := variables.SubstituteAll(log.Log, nil, pattern)
assert.NilError(t, err)
path, err := validateResourceElement(log.Log, resource, pattern, pattern, "/", common.NewAnchorMap())
assert.Equal(t, path, "")
assert.NilError(t, err)
@ -1004,6 +1008,9 @@ func TestValidateMap_RelativePathWithParentheses(t *testing.T) {
assert.Assert(t, json.Unmarshal(rawPattern, &pattern))
assert.Assert(t, json.Unmarshal(rawMap, &resource))
pattern, err := variables.SubstituteAll(log.Log, nil, pattern)
assert.NilError(t, err)
path, err := validateResourceElement(log.Log, resource, pattern, pattern, "/", common.NewAnchorMap())
assert.Equal(t, path, "")
assert.NilError(t, err)
@ -1112,6 +1119,9 @@ func TestValidateMap_AbosolutePathExists(t *testing.T) {
assert.Assert(t, json.Unmarshal(rawPattern, &pattern))
assert.Assert(t, json.Unmarshal(rawMap, &resource))
pattern, err := variables.SubstituteAll(log.Log, nil, pattern)
assert.NilError(t, err)
path, err := validateResourceElement(log.Log, resource, pattern, pattern, "/", common.NewAnchorMap())
assert.Equal(t, path, "")
assert.Assert(t, err == nil)
@ -1195,6 +1205,9 @@ func TestValidateMap_AbsolutePathToMetadata_fail(t *testing.T) {
assert.Assert(t, json.Unmarshal(rawPattern, &pattern))
assert.Assert(t, json.Unmarshal(rawMap, &resource))
pattern, err := variables.SubstituteAll(log.Log, nil, pattern)
assert.NilError(t, err)
path, err := validateResourceElement(log.Log, resource, pattern, pattern, "/", common.NewAnchorMap())
assert.Equal(t, path, "/spec/containers/0/image/")
assert.Assert(t, err != nil)
@ -1254,75 +1267,6 @@ func TestValidateMap_AbosolutePathDoesNotExists(t *testing.T) {
assert.Assert(t, err != nil)
}
func TestActualizePattern_GivenRelativePathThatExists(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "$(<=./../../limits/memory)"
rawPattern := []byte(`{
"spec":{
"containers":[
{
"name":"*",
"resources":{
"requests":{
"memory":"$(<=./../../limits/memory)"
},
"limits":{
"memory":"2048Mi"
}
}
}
]
}
}`)
var pattern interface{}
assert.Assert(t, json.Unmarshal(rawPattern, &pattern))
pattern, err := actualizePattern(log.Log, pattern, referencePath, absolutePath)
assert.Assert(t, err == nil)
}
func TestFormAbsolutePath_RelativePathExists(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "./../../limits/memory"
expectedString := "/spec/containers/0/resources/limits/memory"
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == expectedString)
}
func TestFormAbsolutePath_RelativePathWithBackToTopInTheBegining(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "../../limits/memory"
expectedString := "/spec/containers/0/resources/limits/memory"
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == expectedString)
}
func TestFormAbsolutePath_AbsolutePathExists(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "/spec/containers/0/resources/limits/memory"
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == referencePath)
}
func TestFormAbsolutePath_EmptyPath(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := ""
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == absolutePath)
}
func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) {
rawPattern := []byte(`[
{

View file

@ -61,7 +61,7 @@ func buildResponse(logger logr.Logger, ctx *PolicyContext, resp *response.Engine
}
for i := range resp.PolicyResponse.Rules {
messageInterface, err := variables.SubstituteVars(logger, ctx.JSONContext, resp.PolicyResponse.Rules[i].Message)
messageInterface, err := variables.SubstituteAll(logger, ctx.JSONContext, resp.PolicyResponse.Rules[i].Message)
if err != nil {
logger.V(4).Info("failed to substitute variables", "error", err.Error())
continue
@ -119,7 +119,6 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo
continue
}
// evaluate pre-conditions
// - handle variable substitutions
if !variables.EvaluateConditions(log, ctx.JSONContext, preconditionsCopy) {
log.V(4).Info("resource fails the preconditions")
continue
@ -226,15 +225,16 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
logger.V(4).Info("finished processing rule", "processingTime", resp.RuleStats.ProcessingTime.String())
}()
var err error
if rule, err = variables.SubstituteAllInRule(logger, ctx, rule); err != nil {
resp.Success = false
resp.Message = fmt.Sprintf("variable substitution failed for rule %s: %s", rule.Name, err.Error())
return resp
}
validationRule := rule.Validation.DeepCopy()
if validationRule.Pattern != nil {
pattern := validationRule.Pattern
var err error
if pattern, err = variables.SubstituteVars(logger, ctx, pattern); err != nil {
resp.Success = false
resp.Message = fmt.Sprintf("variable substitution failed for rule %s: %s", rule.Name, err.Error())
return resp
}
if path, err := validate.ValidateResourceWithPattern(logger, resource.Object, pattern); err != nil {
logger.V(3).Info("validation failed", "path", path, "error", err.Error())
@ -250,7 +250,6 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
}
if validationRule.AnyPattern != nil {
var failedSubstitutionsErrors []error
var failedAnyPatternsErrors []error
var err error
@ -262,11 +261,6 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
}
for idx, pattern := range anyPatterns {
if pattern, err = variables.SubstituteVars(logger, ctx, pattern); err != nil {
failedSubstitutionsErrors = append(failedSubstitutionsErrors, err)
continue
}
path, err := validate.ValidateResourceWithPattern(logger, resource.Object, pattern)
if err == nil {
resp.Success = true
@ -279,13 +273,6 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
failedAnyPatternsErrors = append(failedAnyPatternsErrors, patternErr)
}
// Substitution failures
if len(failedSubstitutionsErrors) > 0 {
resp.Success = false
resp.Message = fmt.Sprintf("failed to substitute variables: %v", failedSubstitutionsErrors)
return resp
}
// Any Pattern validation errors
if len(failedAnyPatternsErrors) > 0 {
var errorStr []string

View file

@ -1322,10 +1322,10 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
er := Validate(policyContext)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message,
"variable substitution failed for rule test-path-not-exist: variable request.object.metadata.name1 not resolved at path /spec/containers/0/name")
"variable substitution failed for rule test-path-not-exist: variable request.object.metadata.name1 not resolved at path /validate/pattern/spec/containers/0/name")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *testing.T) {
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSubstitutionFails(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
@ -1412,8 +1412,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *t
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "validation rule 'test-path-not-exist' anyPattern[1] passed.")
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed for rule test-path-not-exist: variable request.object.metadata.name1 not resolved at path /validate/anyPattern/0/spec/template/spec/containers/0/name")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *testing.T) {
@ -1504,7 +1504,7 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *test
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "failed to substitute variables: [variable request.object.metadata.name1 not resolved at path /spec/template/spec/containers/0/name variable request.object.metadata.name2 not resolved at path /spec/template/spec/containers/0/name]")
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed for rule test-path-not-exist: variable request.object.metadata.name1 not resolved at path /validate/anyPattern/0/spec/template/spec/containers/0/name")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatternSatisfy(t *testing.T) {

View file

@ -10,7 +10,7 @@ import (
//Evaluate evaluates the condition
func Evaluate(log logr.Logger, ctx context.EvalInterface, condition kyverno.Condition) bool {
// get handler for the operator
handle := operator.CreateOperatorHandler(log, ctx, condition.Operator, SubstituteVars)
handle := operator.CreateOperatorHandler(log, ctx, condition.Operator, SubstituteAll)
if handle == nil {
return false
}

View file

@ -85,7 +85,7 @@ func Test_variablesub1(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
@ -175,7 +175,7 @@ func Test_variablesub_multiple(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
@ -262,7 +262,7 @@ func Test_variablesubstitution(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
@ -323,7 +323,7 @@ func Test_variableSubstitutionValue(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
@ -381,7 +381,7 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
@ -440,7 +440,7 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err == nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err == nil {
t.Log("expected to fails")
t.Fail()
}
@ -498,7 +498,7 @@ func Test_variableSubstitutionObject(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
@ -562,7 +562,7 @@ func Test_variableSubstitutionObjectOperatorNotEqualFail(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err == nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err == nil {
t.Error(err)
}
@ -621,7 +621,7 @@ func Test_variableSubstitutionMultipleObject(t *testing.T) {
t.Error(err)
}
if patternCopy, err = SubstituteVars(log.Log, ctx, patternCopy); err != nil {
if patternCopy, err = SubstituteAll(log.Log, ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)

View file

@ -1,81 +1,47 @@
package variables
import (
"encoding/json"
"errors"
"fmt"
"path"
"regexp"
"strconv"
"strings"
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/anchor/common"
"github.com/kyverno/kyverno/pkg/engine/context"
ju "github.com/kyverno/kyverno/pkg/engine/json-utils"
"github.com/kyverno/kyverno/pkg/engine/operator"
)
var regexVariables = regexp.MustCompile(`\{\{[^{}]*\}\}`)
var regexReferences = regexp.MustCompile(`\$\(.[^\ ]*\)`)
//IsVariable returns true if the element contains a 'valid' variable {{}}
func IsVariable(element string) bool {
groups := regexVariables.FindAllStringSubmatch(element, -1)
// IsVariable returns true if the element contains a 'valid' variable {{}}
func IsVariable(value string) bool {
groups := regexVariables.FindAllStringSubmatch(value, -1)
return len(groups) != 0
}
// IsReference returns true if the element contains a 'valid' reference $()
func IsReference(value string) bool {
groups := regexReferences.FindAllStringSubmatch(value, -1)
return len(groups) != 0
}
//SubstituteVars replaces the variables with the values defined in the context
// - if any variable is invalid or has nil value, it is considered as a failed variable substitution
func SubstituteVars(log logr.Logger, ctx context.EvalInterface, pattern interface{}) (interface{}, error) {
pattern, err := subVars(log, ctx, pattern, "")
if err != nil {
return pattern, err
}
return pattern, nil
func substituteVars(log logr.Logger, ctx context.EvalInterface, rule interface{}) (interface{}, error) {
return ju.NewTraversal(rule, substituteVariablesIfAny(log, ctx)).TraverseJSON()
}
func subVars(log logr.Logger, ctx context.EvalInterface, pattern interface{}, path string) (interface{}, error) {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
mapCopy := make(map[string]interface{})
for k, v := range typedPattern {
mapCopy[k] = v
}
return subMap(log, ctx, mapCopy, path)
case []interface{}:
sliceCopy := make([]interface{}, len(typedPattern))
copy(sliceCopy, typedPattern)
return subArray(log, ctx, sliceCopy, path)
case string:
return subValR(log, ctx, typedPattern, path)
default:
return pattern, nil
}
func substituteReferences(log logr.Logger, rule interface{}) (interface{}, error) {
return ju.NewTraversal(rule, substituteReferencesIfAny(log)).TraverseJSON()
}
func subMap(log logr.Logger, ctx context.EvalInterface, patternMap map[string]interface{}, path string) (map[string]interface{}, error) {
for key, patternElement := range patternMap {
curPath := path + "/" + key
value, err := subVars(log, ctx, patternElement, curPath)
if err != nil {
return nil, err
}
patternMap[key] = value
}
return patternMap, nil
}
func subArray(log logr.Logger, ctx context.EvalInterface, patternList []interface{}, path string) ([]interface{}, error) {
for idx, patternElement := range patternList {
curPath := path + "/" + strconv.Itoa(idx)
value, err := subVars(log, ctx, patternElement, curPath)
if err != nil {
return nil, err
}
patternList[idx] = value
}
return patternList, nil
}
// NotFoundVariableErr ...
// NotFoundVariableErr is returned when it is impossible to resolve the variable
type NotFoundVariableErr struct {
variable string
path string
@ -85,49 +51,302 @@ func (n NotFoundVariableErr) Error() string {
return fmt.Sprintf("variable %s not resolved at path %s", n.variable, n.path)
}
// subValR resolves the variables if defined
func subValR(log logr.Logger, ctx context.EvalInterface, valuePattern string, path string) (interface{}, error) {
originalPattern := valuePattern
vars := regexVariables.FindAllString(valuePattern, -1)
for len(vars) > 0 {
for _, v := range vars {
variable := strings.ReplaceAll(v, "{{", "")
variable = strings.ReplaceAll(variable, "}}", "")
variable = strings.TrimSpace(variable)
substitutedVar, err := ctx.Query(variable)
// NotFoundVariableErr is returned when it is impossible to resolve the variable
type NotResolvedReferenceErr struct {
reference string
path string
}
func (n NotResolvedReferenceErr) Error() string {
return fmt.Sprintf("reference %s not resolved at path %s", n.reference, n.path)
}
func substituteReferencesIfAny(log logr.Logger) ju.Action {
return ju.OnlyForLeafs(func(data *ju.ActionData) (interface{}, error) {
value, ok := data.Element.(string)
if !ok {
return data.Element, nil
}
for _, v := range regexReferences.FindAllString(value, -1) {
resolvedReference, err := resolveReference(log, data.Document, v, data.Path)
if err != nil {
switch err.(type) {
case context.InvalidVariableErr:
return nil, err
return data.Element, err
default:
return nil, fmt.Errorf("failed to resolve %v at path %s", variable, path)
return data.Element, fmt.Errorf("failed to resolve %v at path %s", v, data.Path)
}
}
log.V(3).Info("variable substituted", "variable", v, "value", substitutedVar, "path", path)
if resolvedReference == nil {
return data.Element, fmt.Errorf("failed to resolve %v at path %s", v, data.Path)
}
if val, ok := substitutedVar.(string); ok {
valuePattern = strings.Replace(valuePattern, v, val, -1)
log.V(3).Info("reference resolved", "reference", v, "value", resolvedReference, "path", data.Path)
if val, ok := resolvedReference.(string); ok {
value = strings.Replace(value, v, val, -1)
continue
}
if substitutedVar != nil {
if originalPattern == v {
return substitutedVar, nil
}
return nil, fmt.Errorf("failed to resolve %v at path %s", variable, path)
}
return nil, NotFoundVariableErr{
variable: variable,
path: path,
return data.Element, NotResolvedReferenceErr{
reference: v,
path: data.Path,
}
}
// check for nested variables in strings
vars = regexVariables.FindAllString(valuePattern, -1)
return value, nil
})
}
func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface) ju.Action {
return ju.OnlyForLeafs(func(data *ju.ActionData) (interface{}, error) {
value, ok := data.Element.(string)
if !ok {
return data.Element, nil
}
originalPattern := value
vars := regexVariables.FindAllString(value, -1)
for len(vars) > 0 {
for _, v := range vars {
variable := strings.ReplaceAll(v, "{{", "")
variable = strings.ReplaceAll(variable, "}}", "")
variable = strings.TrimSpace(variable)
substitutedVar, err := ctx.Query(variable)
if err != nil {
switch err.(type) {
case context.InvalidVariableErr:
return nil, err
default:
return nil, fmt.Errorf("failed to resolve %v at path %s", variable, data.Path)
}
}
log.V(3).Info("variable substituted", "variable", v, "value", substitutedVar, "path", data.Path)
if val, ok := substitutedVar.(string); ok {
value = strings.Replace(value, v, val, -1)
continue
}
if substitutedVar != nil {
if originalPattern == v {
return substitutedVar, nil
}
return nil, fmt.Errorf("failed to resolve %v at path %s", variable, data.Path)
}
return nil, NotFoundVariableErr{
variable: variable,
path: data.Path,
}
}
// check for nested variables in strings
vars = regexVariables.FindAllString(value, -1)
}
return value, nil
})
}
func resolveReference(log logr.Logger, fullDocument interface{}, reference, absolutePath string) (interface{}, error) {
var foundValue interface{}
path := strings.Trim(reference, "$()")
operation := operator.GetOperatorFromStringPattern(path)
path = path[len(operation):]
if len(path) == 0 {
return nil, errors.New("Expected path. Found empty reference")
}
return valuePattern, nil
path = formAbsolutePath(path, absolutePath)
valFromReference, err := getValueFromReference(fullDocument, path)
if err != nil {
return err, nil
}
//TODO validate this
if operation == operator.Equal { //if operator does not exist return raw value
return valFromReference, nil
}
foundValue, err = valFromReferenceToString(valFromReference, string(operation))
if err != nil {
return "", err
}
return string(operation) + foundValue.(string), nil
}
//Parse value to string
func valFromReferenceToString(value interface{}, operator string) (string, error) {
switch typed := value.(type) {
case string:
return typed, nil
case int, int64:
return fmt.Sprintf("%d", value), nil
case float64:
return fmt.Sprintf("%f", value), nil
default:
return "", fmt.Errorf("Incorrect expression. Operator %s does not match with value: %v", operator, value)
}
}
func FindAndShiftReferences(log logr.Logger, value, shift, pivot string) string {
for _, reference := range regexReferences.FindAllString(value, -1) {
index := strings.Index(reference, pivot)
if index == -1 {
log.Error(fmt.Errorf(`Failed to shit reference. Pivot value "%s" was not found`, pivot), "pivot search failed")
}
// try to get rule index from the reference
if pivot == "anyPattern" {
ruleIndex := strings.Split(reference[index+len(pivot)+1:], "/")[0]
pivot = pivot + "/" + ruleIndex
}
shiftedReference := strings.Replace(reference, pivot, pivot+"/"+shift, 1)
value = strings.Replace(value, reference, shiftedReference, -1)
}
return value
}
func formAbsolutePath(referencePath, absolutePath string) string {
if path.IsAbs(referencePath) {
return referencePath
}
return path.Join(absolutePath, referencePath)
}
func getValueFromReference(fullDocument interface{}, path string) (interface{}, error) {
var element interface{}
ju.NewTraversal(fullDocument, ju.OnlyForLeafs(
func(data *ju.ActionData) (interface{}, error) {
if common.RemoveAnchorsFromPath(data.Path) == path {
element = data.Element
}
return data.Element, nil
})).TraverseJSON()
return element, nil
}
func RuleToUntyped(rule kyverno.Rule) (interface{}, error) {
jsonRule, err := json.Marshal(rule)
if err != nil {
return nil, err
}
var untyped interface{}
err = json.Unmarshal(jsonRule, &untyped)
if err != nil {
return nil, err
}
return untyped, nil
}
func UntypedToRule(untyped interface{}) (kyverno.Rule, error) {
jsonRule, err := json.Marshal(untyped)
if err != nil {
return kyverno.Rule{}, err
}
var rule kyverno.Rule
err = json.Unmarshal(jsonRule, &rule)
if err != nil {
return kyverno.Rule{}, err
}
return rule, nil
}
func SubstituteAllInRule(log logr.Logger, ctx context.EvalInterface, typedRule kyverno.Rule) (_ kyverno.Rule, err error) {
var rule interface{}
rule, err = RuleToUntyped(typedRule)
if err != nil {
return typedRule, err
}
rule, err = substituteReferences(log, rule)
if err != nil {
return typedRule, err
}
rule, err = substituteVars(log, ctx, rule)
if err != nil {
return typedRule, err
}
return UntypedToRule(rule)
}
func SubstituteAll(log logr.Logger, ctx context.EvalInterface, document interface{}) (_ interface{}, err error) {
document, err = substituteReferences(log, document)
if err != nil {
return kyverno.Rule{}, err
}
return substituteVars(log, ctx, document)
}
func SubstituteAllForceMutate(log logr.Logger, ctx context.EvalInterface, typedRule kyverno.Rule) (_ kyverno.Rule, err error) {
var rule interface{}
rule, err = RuleToUntyped(typedRule)
if err != nil {
return kyverno.Rule{}, err
}
rule, err = substituteReferences(log, rule)
if err != nil {
return kyverno.Rule{}, err
}
if ctx == nil {
rule = replaceSubstituteVariables(rule)
} else {
rule, err = substituteVars(log, ctx, rule)
if err != nil {
return kyverno.Rule{}, err
}
}
return UntypedToRule(rule)
}
func replaceSubstituteVariables(document interface{}) interface{} {
rawDocument, err := json.Marshal(document)
if err != nil {
return document
}
regex := regexp.MustCompile(`\{\{([^{}]*)\}\}`)
for {
if len(regex.FindAllStringSubmatch(string(rawDocument), -1)) > 0 {
rawDocument = regex.ReplaceAll(rawDocument, []byte(`placeholderValue`))
} else {
break
}
}
var output interface{}
err = json.Unmarshal(rawDocument, &output)
if err != nil {
return document
}
return output
}

View file

@ -2,9 +2,11 @@ package variables
import (
"encoding/json"
"strings"
"testing"
"github.com/kyverno/kyverno/pkg/engine/context"
ju "github.com/kyverno/kyverno/pkg/engine/json-utils"
"gotest.tools/assert"
"sigs.k8s.io/controller-runtime/pkg/log"
)
@ -65,7 +67,7 @@ func Test_subVars_success(t *testing.T) {
t.Error(err)
}
if _, err := SubstituteVars(log.Log, ctx, pattern); err != nil {
if _, err := SubstituteAll(log.Log, ctx, pattern); err != nil {
t.Error(err)
}
}
@ -126,7 +128,7 @@ func Test_subVars_failed(t *testing.T) {
t.Error(err)
}
if _, err := SubstituteVars(log.Log, ctx, pattern); err == nil {
if _, err := SubstituteAll(log.Log, ctx, pattern); err == nil {
t.Error("error is expected")
}
}
@ -154,7 +156,13 @@ func Test_SubstituteSuccess(t *testing.T) {
var pattern interface{}
patternRaw := []byte(`"{{request.object.metadata.annotations.test}}"`)
assert.Assert(t, json.Unmarshal(patternRaw, &pattern))
results, err := subValR(log.Log, ctx, string(patternRaw), "/")
action := substituteVariablesIfAny(log.Log, ctx)
results, err := action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
Path: "/"})
if err != nil {
t.Errorf("substitution failed: %v", err.Error())
return
@ -172,14 +180,26 @@ func Test_SubstituteRecursiveErrors(t *testing.T) {
var pattern interface{}
patternRaw := []byte(`"{{request.object.metadata.{{request.object.metadata.annotations.test2}}}}"`)
assert.Assert(t, json.Unmarshal(patternRaw, &pattern))
results, err := subValR(log.Log, ctx, string(patternRaw), "/")
action := substituteVariablesIfAny(log.Log, ctx)
results, err := action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
Path: "/"})
if err == nil {
t.Errorf("expected error but received: %v", results)
}
patternRaw = []byte(`"{{request.object.metadata2.{{request.object.metadata.annotations.test}}}}"`)
assert.Assert(t, json.Unmarshal(patternRaw, &pattern))
results, err = subValR(log.Log, ctx, string(patternRaw), "/")
action = substituteVariablesIfAny(log.Log, ctx)
results, err = action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
Path: "/"})
if err == nil {
t.Errorf("expected error but received: %v", results)
}
@ -192,7 +212,13 @@ func Test_SubstituteRecursive(t *testing.T) {
var pattern interface{}
patternRaw := []byte(`"{{request.object.metadata.{{request.object.metadata.annotations.test}}}}"`)
assert.Assert(t, json.Unmarshal(patternRaw, &pattern))
results, err := subValR(log.Log, ctx, string(patternRaw), "/")
action := substituteVariablesIfAny(log.Log, ctx)
results, err := action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
Path: "/"})
if err != nil {
t.Errorf("substitution failed: %v", err.Error())
return
@ -223,6 +249,144 @@ func Test_policyContextValidation(t *testing.T) {
ctx := context.NewContext("request.object")
_, err = SubstituteVars(log.Log, ctx, contextMap)
_, err = SubstituteAll(log.Log, ctx, contextMap)
assert.Assert(t, err != nil, err)
}
func Test_ReferenceSubstitution(t *testing.T) {
jsonRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1",
"annotations": {
"test": "$(../../../../spec/namespace)"
}
},
"(spec)": {
"namespace": "n1",
"name": "temp1"
}
}`)
expectedJSON := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1",
"annotations": {
"test": "n1"
}
},
"(spec)": {
"namespace": "n1",
"name": "temp1"
}
}`)
var document interface{}
err := json.Unmarshal(jsonRaw, &document)
assert.NilError(t, err)
var expectedDocument interface{}
err = json.Unmarshal(expectedJSON, &expectedDocument)
assert.NilError(t, err)
ctx := context.NewContext()
err = ctx.AddResource(jsonRaw)
assert.NilError(t, err)
actualDocument, err := SubstituteAll(log.Log, ctx, document)
assert.NilError(t, err)
assert.DeepEqual(t, expectedDocument, actualDocument)
}
func TestFormAbsolutePath_RelativePathExists(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "./../../limits/memory"
expectedString := "/spec/containers/0/resources/limits/memory"
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == expectedString)
}
func TestFormAbsolutePath_RelativePathWithBackToTopInTheBegining(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "../../limits/memory"
expectedString := "/spec/containers/0/resources/limits/memory"
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == expectedString)
}
func TestFormAbsolutePath_AbsolutePathExists(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "/spec/containers/0/resources/limits/memory"
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == referencePath)
}
func TestFormAbsolutePath_EmptyPath(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := ""
result := formAbsolutePath(referencePath, absolutePath)
assert.Assert(t, result == absolutePath)
}
func TestActualizePattern_GivenRelativePathThatExists(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "$(<=./../../limits/memory)"
rawPattern := []byte(`{
"spec":{
"containers":[
{
"name":"*",
"resources":{
"requests":{
"memory":"$(<=./../../limits/memory)"
},
"limits":{
"memory":"2048Mi"
}
}
}
]
}
}`)
resolvedReference := "<=2048Mi"
var pattern interface{}
assert.NilError(t, json.Unmarshal(rawPattern, &pattern))
// pattern, err := actualizePattern(log.Log, pattern, referencePath, absolutePath)
pattern, err := resolveReference(log.Log, pattern, referencePath, absolutePath)
assert.NilError(t, err)
assert.DeepEqual(t, resolvedReference, pattern)
}
func TestFindAndShiftReferences_PositiveCase(t *testing.T) {
message := "Message with $(./../../pattern/spec/containers/0/image) reference inside. Or maybe even two $(./../../pattern/spec/containers/0/image), but they are same."
expectedMessage := strings.Replace(message, "$(./../../pattern/spec/containers/0/image)", "$(./../../pattern/spec/jobTemplate/spec/containers/0/image)", -1)
actualMessage := FindAndShiftReferences(log.Log, message, "spec/jobTemplate", "pattern")
assert.Equal(t, expectedMessage, actualMessage)
}
func TestFindAndShiftReferences_AnyPatternPositiveCase(t *testing.T) {
message := "Message with $(./../../anyPattern/0/spec/containers/0/image)."
expectedMessage := strings.Replace(message, "$(./../../anyPattern/0/spec/containers/0/image)", "$(./../../anyPattern/0/spec/jobTemplate/spec/containers/0/image)", -1)
actualMessage := FindAndShiftReferences(log.Log, message, "spec/jobTemplate", "anyPattern")
assert.Equal(t, expectedMessage, actualMessage)
}

View file

@ -316,7 +316,7 @@ func applyRule(log logr.Logger, client *dclient.Client, rule kyverno.Rule, resou
// format : {{<variable_name}}
// - if there is variables that are not defined the context -> results in error and rule is not applied
// - valid variables are replaced with the values
object, err := variables.SubstituteVars(log, ctx, genUnst.Object)
object, err := variables.SubstituteAll(log, ctx, genUnst.Object)
if err != nil {
return noGenResource, err
}

View file

@ -1,8 +1,8 @@
package policy
import (
"encoding/json"
"fmt"
"reflect"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
@ -27,29 +27,17 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
filterVars := []string{"request.object", "request.namespace"}
ctx := context.NewContext(filterVars...)
for contextIdx, contextEntry := range rule.Context {
if rule, err = variables.SubstituteAllInRule(log.Log, ctx, rule); !checkNotFoundErr(err) {
return fmt.Errorf("variable substitution failed for rule %s: %s", rule.Name, err.Error())
}
for _, contextEntry := range rule.Context {
if contextEntry.APICall != nil {
ctx.AddBuiltInVars(contextEntry.Name)
if _, err := variables.SubstituteVars(log.Log, ctx, contextEntry.APICall.URLPath); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/apiCall/urlPath: %s", idx, contextIdx, err.Error())
}
if _, err := variables.SubstituteVars(log.Log, ctx, contextEntry.APICall.JMESPath); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/apiCall/jmesPath: %s", idx, contextIdx, err.Error())
}
}
if contextEntry.ConfigMap != nil {
ctx.AddBuiltInVars(contextEntry.Name)
if _, err = variables.SubstituteVars(log.Log, ctx, contextEntry.ConfigMap.Name); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/configMap/name: %s", idx, contextIdx, err.Error())
}
if _, err = variables.SubstituteVars(log.Log, ctx, contextEntry.ConfigMap.Namespace); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/configMap/namespace: %s", idx, contextIdx, err.Error())
}
}
}
@ -59,64 +47,11 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
}
}
if rule.Mutation.Overlay != nil {
if rule.Mutation.Overlay, err = variables.SubstituteVars(log.Log, ctx, rule.Mutation.Overlay); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/mutate/overlay: %s", idx, err.Error())
}
}
if rule.Mutation.PatchStrategicMerge != nil {
if rule.Mutation.Overlay, err = variables.SubstituteVars(log.Log, ctx, rule.Mutation.PatchStrategicMerge); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/mutate/patchStrategicMerge: %s", idx, err.Error())
}
}
if rule.Validation.Pattern != nil {
if rule.Validation.Pattern, err = variables.SubstituteVars(log.Log, ctx, rule.Validation.Pattern); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/validate/pattern: %s", idx, err.Error())
}
}
anyPattern, err := rule.Validation.DeserializeAnyPattern()
if err != nil {
return fmt.Errorf("failed to deserialize anyPattern, expect array: %s", err.Error())
}
for idx2, pattern := range anyPattern {
if anyPattern[idx2], err = variables.SubstituteVars(log.Log, ctx, pattern); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/validate/anyPattern[%d]: %s", idx, idx2, err.Error())
}
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Validation.Message); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/validate/message: %s", idx, err.Error())
}
if rule.Validation.Deny != nil {
if err = validateDenyConditions(idx, ctx, rule.Validation.Deny.AnyAllConditions); err != nil {
return err
}
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Name); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/name: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Namespace); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/name: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Data); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/data: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Clone.Name); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/clone/name: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Clone.Namespace); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/clone/namespace: %v", idx, err)
}
}
return nil
@ -124,96 +59,31 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
func validatePreConditions(idx int, ctx context.EvalInterface, anyAllConditions apiextensions.JSON) error {
var err error
// conditions are currently in the form of []interface{}
kyvernoAnyAllConditions, err := utils.ApiextensionsJsonToKyvernoConditions(anyAllConditions)
anyAllConditions, err = substituteVarsInJSON(ctx, anyAllConditions)
if err != nil {
return err
}
switch typedPreConditions := kyvernoAnyAllConditions.(type) {
case kyverno.AnyAllConditions:
if !reflect.DeepEqual(typedPreConditions, kyverno.AnyAllConditions{}) && typedPreConditions.AnyConditions != nil {
for condIdx, condition := range typedPreConditions.AnyConditions {
if condition.Key, err = variables.SubstituteVars(log.Log, ctx, condition.Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/any/condition[%d]/key", condition.Key, idx, condIdx)
}
if condition.Value, err = variables.SubstituteVars(log.Log, ctx, condition.Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid %s variable used at spec/rules[%d]/any/condition[%d]/value", condition.Value, idx, condIdx)
}
}
}
if !reflect.DeepEqual(typedPreConditions, kyverno.AnyAllConditions{}) && typedPreConditions.AllConditions != nil {
for condIdx, condition := range typedPreConditions.AllConditions {
if condition.Key, err = variables.SubstituteVars(log.Log, ctx, condition.Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/all/condition[%d]/key", condition.Key, idx, condIdx)
}
if condition.Value, err = variables.SubstituteVars(log.Log, ctx, condition.Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid %s variable used at spec/rules[%d]/all/condition[%d]/value", condition.Value, idx, condIdx)
}
}
}
case []kyverno.Condition: //backwards compatibility
for condIdx, condition := range typedPreConditions {
if condition.Key, err = variables.SubstituteVars(log.Log, ctx, condition.Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/condition[%d]/key", condition.Key, idx, condIdx)
}
if condition.Value, err = variables.SubstituteVars(log.Log, ctx, condition.Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid %s variable used at spec/rules[%d]/condition[%d]/value", condition.Value, idx, condIdx)
}
}
_, err = utils.ApiextensionsJsonToKyvernoConditions(anyAllConditions)
if err != nil {
return err
}
return nil
}
func validateDenyConditions(idx int, ctx context.EvalInterface, denyConditions apiextensions.JSON) error {
// conditions are currently in the form of []interface{}
kyvernoDenyConditions, err := utils.ApiextensionsJsonToKyvernoConditions(denyConditions)
var err error
denyConditions, err = substituteVarsInJSON(ctx, denyConditions)
if err != nil {
return err
}
switch typedDenyConditions := kyvernoDenyConditions.(type) {
case kyverno.AnyAllConditions:
// validating validate.deny.any.conditions
if !reflect.DeepEqual(typedDenyConditions, kyverno.AnyAllConditions{}) && typedDenyConditions.AnyConditions != nil {
for i := range typedDenyConditions.AnyConditions {
if _, err := variables.SubstituteVars(log.Log, ctx, typedDenyConditions.AnyConditions[i].Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/validate/deny/any/conditions[%d]/key: %v",
typedDenyConditions.AnyConditions[i].Key, idx, i, err)
}
if _, err := variables.SubstituteVars(log.Log, ctx, typedDenyConditions.AnyConditions[i].Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/validate/deny/any/conditions[%d]/value: %v",
typedDenyConditions.AnyConditions[i].Value, idx, i, err)
}
}
}
// validating validate.deny.all.conditions
if !reflect.DeepEqual(typedDenyConditions, kyverno.AnyAllConditions{}) && typedDenyConditions.AllConditions != nil {
for i := range typedDenyConditions.AllConditions {
if _, err := variables.SubstituteVars(log.Log, ctx, typedDenyConditions.AllConditions[i].Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/validate/deny/all/conditions[%d]/key: %v",
typedDenyConditions.AllConditions[i].Key, idx, i, err)
}
if _, err := variables.SubstituteVars(log.Log, ctx, typedDenyConditions.AllConditions[i].Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/validate/deny/all/conditions[%d]/value: %v",
typedDenyConditions.AllConditions[i].Value, idx, i, err)
}
}
}
case []kyverno.Condition: // backwards compatibility
// validating validate.deny.conditions
for i := range typedDenyConditions {
if _, err := variables.SubstituteVars(log.Log, ctx, typedDenyConditions[i].Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/validate/deny/conditions[%d]/key: %v",
typedDenyConditions[i].Key, idx, i, err)
}
if _, err := variables.SubstituteVars(log.Log, ctx, typedDenyConditions[i].Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/validate/deny/conditions[%d]/value: %v",
typedDenyConditions[i].Value, idx, i, err)
}
}
_, err = utils.ApiextensionsJsonToKyvernoConditions(denyConditions)
if err != nil {
return err
}
return nil
@ -247,3 +117,33 @@ func userInfoDefined(ui kyverno.UserInfo) string {
}
return ""
}
func substituteVarsInJSON(ctx context.EvalInterface, document apiextensions.JSON) (apiextensions.JSON, error) {
jsonByte, err := json.Marshal(document)
if err != nil {
return nil, err
}
var jsonInterface interface{}
err = json.Unmarshal(jsonByte, &jsonInterface)
if err != nil {
return nil, err
}
jsonInterface, err = variables.SubstituteAll(log.Log, ctx, jsonInterface)
if err != nil {
return nil, err
}
jsonByte, err = json.Marshal(jsonInterface)
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonByte, &document)
if err != nil {
return nil, err
}
return document, nil
}

View file

@ -8,6 +8,7 @@ import (
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/engine/variables"
)
func generateCronJobRule(rule kyverno.Rule, controllers string, log logr.Logger) kyvernoRule {
@ -65,7 +66,7 @@ func generateCronJobRule(rule kyverno.Rule, controllers string, log logr.Logger)
if (jobRule.Validation != nil) && (jobRule.Validation.Pattern != nil) {
newValidate := &kyverno.Validation{
Message: rule.Validation.Message,
Message: variables.FindAndShiftReferences(log, rule.Validation.Message, "spec/jobTemplate/spec/template", "pattern"),
Pattern: map[string]interface{}{
"spec": map[string]interface{}{
"jobTemplate": jobRule.Validation.Pattern,
@ -94,7 +95,7 @@ func generateCronJobRule(rule kyverno.Rule, controllers string, log logr.Logger)
}
cronJobRule.Validation = &kyverno.Validation{
Message: rule.Validation.Message,
Message: variables.FindAndShiftReferences(log, rule.Validation.Message, "spec/jobTemplate/spec/template", "anyPattern"),
AnyPattern: patterns,
}
return *cronJobRule

View file

@ -13,6 +13,7 @@ import (
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/common"
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/utils"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
@ -480,7 +481,7 @@ func generateRuleForControllers(rule kyverno.Rule, controllers string, log logr.
if rule.Validation.Pattern != nil {
newValidate := &kyverno.Validation{
Message: rule.Validation.Message,
Message: variables.FindAndShiftReferences(log, rule.Validation.Message, "spec/template", "pattern"),
Pattern: map[string]interface{}{
"spec": map[string]interface{}{
"template": rule.Validation.Pattern,
@ -509,7 +510,7 @@ func generateRuleForControllers(rule kyverno.Rule, controllers string, log logr.
}
controllerRule.Validation = &kyverno.Validation{
Message: rule.Validation.Message,
Message: variables.FindAndShiftReferences(log, rule.Validation.Message, "spec/template", "anyPattern"),
AnyPattern: patterns,
}
return *controllerRule