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:
parent
af4b85d3a8
commit
24c4f06ecd
22 changed files with 982 additions and 467 deletions
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
19
pkg/engine/common/utils.go
Normal file
19
pkg/engine/common/utils.go
Normal 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
|
||||
}
|
37
pkg/engine/common/utils_test.go
Normal file
37
pkg/engine/common/utils_test.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
150
pkg/engine/forceMutate_test.go
Normal file
150
pkg/engine/forceMutate_test.go
Normal 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())
|
||||
}
|
94
pkg/engine/json-utils/traverse.go
Normal file
94
pkg/engine/json-utils/traverse.go
Normal 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
|
||||
}
|
73
pkg/engine/json-utils/traverse_test.go
Normal file
73
pkg/engine/json-utils/traverse_test.go
Normal 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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(`[
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue