1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-05 15:37:19 +00:00

fix mutate preprocessing for anchors (#3052)

* fix mutate preprocessing for anchors

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* make fmt

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

Co-authored-by: shuting <shutting06@gmail.com>
This commit is contained in:
Jim Bugwadia 2022-01-23 05:54:22 -08:00 committed by GitHub
parent bd50291848
commit bb06901119
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 442 additions and 193 deletions

View file

@ -21,7 +21,7 @@ package v1alpha1
import ( import (
policyreportv1alpha1 "github.com/kyverno/kyverno/api/policyreport/v1alpha1" policyreportv1alpha1 "github.com/kyverno/kyverno/api/policyreport/v1alpha1"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )

View file

@ -21,7 +21,7 @@ package v1alpha2
import ( import (
policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2" policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )

View file

@ -20,7 +20,7 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )

View file

@ -20,7 +20,7 @@ limitations under the License.
package v1alpha2 package v1alpha2
import ( import (
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )

1
go.mod
View file

@ -56,6 +56,7 @@ require (
require ( require (
github.com/aquilax/truncate v1.0.0 github.com/aquilax/truncate v1.0.0
github.com/blang/semver/v4 v4.0.0 github.com/blang/semver/v4 v4.0.0
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249
gopkg.in/inf.v0 v0.9.1 gopkg.in/inf.v0 v0.9.1
) )

2
go.sum
View file

@ -1268,6 +1268,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=

View file

@ -25,8 +25,9 @@ func IsGlobalAnchor(str string) bool {
return false return false
} }
//TODO: trim spaces ? leftMatch := strings.TrimSpace(str[:len(left)]) == left
return (str[:len(left)] == left && str[len(str)-len(right):] == right) rightMatch := strings.TrimSpace(str[len(str)-len(right):]) == right
return leftMatch && rightMatch
} }
//ContainsCondition returns true, if str is either condition anchor or //ContainsCondition returns true, if str is either condition anchor or
@ -46,8 +47,8 @@ func IsNegationAnchor(str string) bool {
return (str[:len(left)] == left && str[len(str)-len(right):] == right) return (str[:len(left)] == left && str[len(str)-len(right):] == right)
} }
// IsAddingAnchor checks for addition anchor // IsAddIfNotPresentAnchor checks for addition anchor
func IsAddingAnchor(key string) bool { func IsAddIfNotPresentAnchor(key string) bool {
const left = "+(" const left = "+("
const right = ")" const right = ")"
@ -94,7 +95,7 @@ func RemoveAnchor(key string) (string, string) {
return key[1 : len(key)-1], key[0:1] return key[1 : len(key)-1], key[0:1]
} }
if IsExistenceAnchor(key) || IsAddingAnchor(key) || IsEqualityAnchor(key) || IsNegationAnchor(key) || IsGlobalAnchor(key) { if IsExistenceAnchor(key) || IsAddIfNotPresentAnchor(key) || IsEqualityAnchor(key) || IsNegationAnchor(key) || IsGlobalAnchor(key) {
return key[2 : len(key)-1], key[0:2] return key[2 : len(key)-1], key[0:2]
} }

View file

@ -151,6 +151,10 @@ func validateValueWithNilPattern(log logr.Logger, value interface{}) bool {
// Handler for pattern values during validation process // Handler for pattern values during validation process
func validateValueWithStringPatterns(log logr.Logger, value interface{}, pattern string) bool { func validateValueWithStringPatterns(log logr.Logger, value interface{}, pattern string) bool {
if value == pattern {
return true
}
conditions := strings.Split(pattern, "|") conditions := strings.Split(pattern, "|")
for _, condition := range conditions { for _, condition := range conditions {
condition = strings.Trim(condition, " ") condition = strings.Trim(condition, " ")

View file

@ -97,6 +97,8 @@ func strategicMergePatch(logger logr.Logger, base, overlay string) ([]byte, erro
} }
} }
patchStr, _ := preprocessedYaml.String()
logger.V(3).Info("applying strategic merge patch", "patch", patchStr)
f := patchstrategicmerge.Filter{ f := patchstrategicmerge.Filter{
Patch: preprocessedYaml, Patch: preprocessedYaml,
} }

View file

@ -164,9 +164,7 @@ func TestMergePatch(t *testing.T) {
t.Logf("Running test %d...", i+1) t.Logf("Running test %d...", i+1)
out, err := strategicMergePatch(log.Log, string(test.rawResource), string(test.rawPolicy)) out, err := strategicMergePatch(log.Log, string(test.rawResource), string(test.rawPolicy))
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, toJSON(t, test.expected), toJSON(t, out))
// has assertions inside
areEqualJSONs(t, test.expected, out)
} }
} }

View file

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/kyverno/kyverno/pkg/engine/anchor" "github.com/kyverno/kyverno/pkg/engine/anchor"
"github.com/kyverno/kyverno/pkg/engine/validate" "github.com/kyverno/kyverno/pkg/engine/validate"
@ -65,31 +67,26 @@ func preProcessRecursive(logger logr.Logger, pattern, resource *yaml.RNode) erro
} }
func walkMap(logger logr.Logger, pattern, resource *yaml.RNode) error { func walkMap(logger logr.Logger, pattern, resource *yaml.RNode) error {
var err error if _, err := handleAddIfNotPresentAnchor(pattern, resource); err != nil {
return errors.Wrap(err, "failed to process addIfNotPresent anchor")
err = validateConditions(logger, pattern, resource)
if err != nil {
return err
} }
err = handleAddings(pattern, resource) if err := validateConditions(logger, pattern, resource); err != nil {
if err != nil { return err // do not wrap condition errors
return err
} }
nonAnchors, err := filterKeys(pattern, func(key string) bool { isNotAnchor := func(key string) bool {
return !hasAnchor(key) return !hasAnchor(key)
}) }
nonAnchors, err := filterKeys(pattern, isNotAnchor)
if err != nil { if err != nil {
return err return err
} }
var resourceValue *yaml.RNode
for _, field := range nonAnchors { for _, field := range nonAnchors {
if resource == nil || resource.Field(field) == nil { var resourceValue *yaml.RNode
resourceValue = nil if resource != nil && resource.Field(field) != nil {
} else {
resourceValue = resource.Field(field).Value resourceValue = resource.Field(field).Value
} }
@ -139,57 +136,30 @@ func processListOfMaps(logger logr.Logger, pattern, resource *yaml.RNode) error
hasAnyAnchor := hasAnchors(patternElement, hasAnchor) hasAnyAnchor := hasAnchors(patternElement, hasAnchor)
hasGlobalConditions := hasAnchors(patternElement, anchor.IsGlobalAnchor) hasGlobalConditions := hasAnchors(patternElement, anchor.IsGlobalAnchor)
if hasAnyAnchor { if hasAnyAnchor {
anyGlobalConditionPassed := false anyGlobalConditionPassed := false
var lastGlobalAnchorError error = nil var lastGlobalAnchorError error = nil
for _, resourceElement := range resourceElements { for _, resourceElement := range resourceElements {
err := preProcessRecursive(logger, patternElement, resourceElement) if err := preProcessRecursive(logger, patternElement, resourceElement); err != nil {
if err != nil { logger.V(3).Info("anchor mismatch", "reason", err.Error())
switch err.(type) { if isConditionError(err) {
case ConditionError:
// Skip element, if condition has failed
continue continue
case GlobalConditionError: }
if isGlobalConditionError(err) {
lastGlobalAnchorError = err lastGlobalAnchorError = err
continue continue
} }
return err return err
}
if hasGlobalConditions {
// global anchor has passed, there is no need to return an error
anyGlobalConditionPassed = true
} else { } else {
if hasGlobalConditions { if err := handlePatternName(pattern, patternElement, resourceElement); err != nil {
// global anchor has passed, there is no need to return an error return errors.Wrap(err, "failed to update name in pattern")
anyGlobalConditionPassed = true
}
// If condition is satisfied, create new pattern list element based on patternElement
// but related with current resource element by name.
// Resource element must have name. Without name kustomize won't be able to update this element.
// In case if element does not have name, skip it.
resourceElementName := resourceElement.Field("name")
if resourceElementName.IsNilOrEmpty() {
continue
}
newNode := patternElement.Copy()
empty, err := deleteConditionsFromNestedMaps(newNode)
if err != nil {
return err
}
// Do not add an empty element to the patch
if empty {
continue
}
err = newNode.PipeE(yaml.SetField("name", resourceElementName.Value))
if err != nil {
return err
}
err = pattern.PipeE(yaml.Append(newNode.YNode()))
if err != nil {
return err
} }
} }
} }
@ -203,6 +173,50 @@ func processListOfMaps(logger logr.Logger, pattern, resource *yaml.RNode) error
return nil return nil
} }
func handlePatternName(pattern, patternElement, resourceElement *yaml.RNode) error {
// If condition is satisfied, create new pattern list element based on patternElement
// but related with current resource element by name.
// Resource element must have name. Without name kustomize won't be able to update this element.
// In case if element does not have name, skip it.
resourceElementName := resourceElement.Field("name")
if resourceElementName.IsNilOrEmpty() {
return nil
}
newNode := patternElement.Copy()
empty, err := deleteAnchors(newNode, true, false)
if err != nil {
return err
}
// Do not add an empty element to the patch
if empty {
return nil
}
err = newNode.PipeE(yaml.SetField("name", resourceElementName.Value))
if err != nil {
return err
}
err = pattern.PipeE(yaml.Append(newNode.YNode()))
if err != nil {
return err
}
return nil
}
func isConditionError(err error) bool {
_, ok := err.(ConditionError)
return ok
}
func isGlobalConditionError(err error) bool {
_, ok := err.(GlobalConditionError)
return ok
}
// validateConditions checks all conditions from current map. // validateConditions checks all conditions from current map.
// If at least one condition fails, return error. // If at least one condition fails, return error.
// If caller handles list of maps and gets an error, it must skip element. // If caller handles list of maps and gets an error, it must skip element.
@ -223,35 +237,38 @@ func validateConditions(logger logr.Logger, pattern, resource *yaml.RNode) error
return nil return nil
} }
// handleAddings handles adding anchors. // handleAddIfNotPresentAnchor handles adding anchors.
// Remove anchor from pattern, if field already exists. // Remove anchor from pattern, if field already exists.
// Remove anchor wrapping from key, if field does not exist in the resource. // Remove anchor wrapping from key, if field does not exist in the resource.
func handleAddings(pattern, resource *yaml.RNode) error { func handleAddIfNotPresentAnchor(pattern, resource *yaml.RNode) (int, error) {
addings, err := filterKeys(pattern, anchor.IsAddingAnchor) anchors, err := filterKeys(pattern, anchor.IsAddIfNotPresentAnchor)
if err != nil { if err != nil {
return err return 0, err
} }
for _, adding := range addings { for _, a := range anchors {
key, _ := anchor.RemoveAnchor(adding) key, _ := anchor.RemoveAnchor(a)
if resource != nil && resource.Field(key) != nil { if resource != nil && resource.Field(key) != nil {
// Resource already has this field. // Resource already has this field.
// Delete the field with adding anchor from patch. // Delete the field with addIfNotPresent anchor from patch.
err = pattern.PipeE(yaml.Clear(adding)) err = pattern.PipeE(yaml.Clear(a))
if err != nil { if err != nil {
return err return 0, err
} }
continue } else {
// Remove anchor tags from patch field key.
renameField(a, key, pattern)
} }
// Remove anchor wrap from patch field.
renameField(adding, key, pattern)
} }
return nil return len(anchors), nil
} }
func filterKeys(pattern *yaml.RNode, condition func(string) bool) ([]string, error) { func filterKeys(pattern *yaml.RNode, condition func(string) bool) ([]string, error) {
if !isMappingNode(pattern) {
return nil, nil
}
keys := make([]string, 0) keys := make([]string, 0)
fields, err := pattern.Fields() fields, err := pattern.Fields()
if err != nil { if err != nil {
@ -267,12 +284,23 @@ func filterKeys(pattern *yaml.RNode, condition func(string) bool) ([]string, err
return keys, nil return keys, nil
} }
func isMappingNode(node *yaml.RNode) bool {
if err := yaml.ErrorIfInvalid(node, yaml.MappingNode); err != nil {
return false
}
return true
}
func hasAnchor(key string) bool { func hasAnchor(key string) bool {
return anchor.ContainsCondition(key) || anchor.IsAddingAnchor(key) return anchor.ContainsCondition(key) || anchor.IsAddIfNotPresentAnchor(key)
} }
func hasAnchors(pattern *yaml.RNode, isAnchor func(key string) bool) bool { func hasAnchors(pattern *yaml.RNode, isAnchor func(key string) bool) bool {
if yaml.MappingNode == pattern.YNode().Kind { ynode := pattern.YNode()
kind := ynode.Kind
if kind == yaml.MappingNode {
fields, err := pattern.Fields() fields, err := pattern.Fields()
if err != nil { if err != nil {
return false return false
@ -290,6 +318,19 @@ func hasAnchors(pattern *yaml.RNode, isAnchor func(key string) bool) bool {
} }
} }
} }
} else if kind == yaml.ScalarNode {
v := ynode.Value
return anchor.ContainsCondition(v)
} else if kind == yaml.SequenceNode {
elements, _ := pattern.Elements()
for _, e := range elements {
if hasAnchors(e, isAnchor) {
return true
}
}
return false
} }
return false return false
@ -343,52 +384,6 @@ func checkCondition(logger logr.Logger, pattern *yaml.RNode, resource *yaml.RNod
return nil return nil
} }
func deleteConditionsFromNestedMaps(pattern *yaml.RNode) (bool, error) {
if pattern.YNode().Kind != yaml.MappingNode {
return false, nil
}
fields, err := pattern.Fields()
if err != nil {
return false, err
}
for _, field := range fields {
if anchor.ContainsCondition(field) {
err = pattern.PipeE(yaml.Clear(field))
if err != nil {
return false, err
}
} else {
child := pattern.Field(field).Value
if child != nil {
empty, err := deleteConditionsFromNestedMaps(child)
if err != nil {
return false, err
}
if empty {
err = pattern.PipeE(yaml.Clear(field))
if err != nil {
return false, err
}
}
}
}
}
fields, err = pattern.Fields()
if err != nil {
return false, err
}
if len(fields) == 0 {
return true, nil
}
return false, nil
}
func deleteConditionElements(pattern *yaml.RNode) error { func deleteConditionElements(pattern *yaml.RNode) error {
fields, err := pattern.Fields() fields, err := pattern.Fields()
if err != nil { if err != nil {
@ -396,11 +391,12 @@ func deleteConditionElements(pattern *yaml.RNode) error {
} }
for _, field := range fields { for _, field := range fields {
ok, err := deleteAnchors(pattern.Field(field).Value) deleteScalar := anchor.ContainsCondition(field)
canDelete, err := deleteAnchors(pattern.Field(field).Value, deleteScalar, false)
if err != nil { if err != nil {
return err return err
} }
if ok { if canDelete {
err = pattern.PipeE(yaml.Clear(field)) err = pattern.PipeE(yaml.Clear(field))
if err != nil { if err != nil {
return err return err
@ -414,32 +410,50 @@ func deleteConditionElements(pattern *yaml.RNode) error {
// deleteAnchors deletes all the anchors and returns true, // deleteAnchors deletes all the anchors and returns true,
// if this node must be deleted from patch. // if this node must be deleted from patch.
// Node is considered to be deleted, if there were only // Node is considered to be deleted, if there were only
// anchors elemets. After anchors elements are removed, // anchors elements. After anchors elements are removed,
// we have patch with nil values which could cause // A patch with nil values which could cause
// unnecessary resource elements deletion. // unnecessary resource elements deletion.
func deleteAnchors(node *yaml.RNode) (bool, error) { func deleteAnchors(node *yaml.RNode, deleteScalar, traverseMappingNodes bool) (bool, error) {
switch node.YNode().Kind { switch node.YNode().Kind {
case yaml.MappingNode: case yaml.MappingNode:
return deleteAnchorsInMap(node) return deleteAnchorsInMap(node, traverseMappingNodes)
case yaml.SequenceNode: case yaml.SequenceNode:
return deleteAnchorsInList(node) return deleteAnchorsInList(node, traverseMappingNodes)
case yaml.ScalarNode:
return deleteScalar, nil
} }
return false, nil return false, nil
} }
func deleteAnchorsInMap(node *yaml.RNode) (bool, error) { func deleteAnchorsInMap(node *yaml.RNode, traverseMappingNodes bool) (bool, error) {
conditions, err := filterKeys(node, anchor.ContainsCondition) conditions, err := filterKeys(node, anchor.ContainsCondition)
if err != nil { if err != nil {
return false, err return false, err
} }
// Remove all conditions first. // remove all conditional anchors with no child nodes first
anchorsExist := false
for _, condition := range conditions { for _, condition := range conditions {
err = node.PipeE(yaml.Clear(condition)) field := node.Field(condition)
shouldDelete, err := deleteAnchors(field.Value, true, traverseMappingNodes)
if err != nil { if err != nil {
return false, err return false, err
} }
if shouldDelete {
if err := node.PipeE(yaml.Clear(condition)); err != nil {
return false, err
}
} else {
anchorsExist = true
}
}
if anchorsExist {
if err := stripAnchorsFromNode(node, ""); err != nil {
return false, errors.Wrap(err, "failed to remove anchor tags")
}
} }
fields, err := node.Fields() fields, err := node.Fields()
@ -448,15 +462,13 @@ func deleteAnchorsInMap(node *yaml.RNode) (bool, error) {
} }
needToDelete := true needToDelete := true
// Go further through the map elements.
for _, field := range fields { for _, field := range fields {
ok, err := deleteAnchors(node.Field(field).Value) canDelete, err := deleteAnchors(node.Field(field).Value, false, traverseMappingNodes)
if err != nil { if err != nil {
return false, err return false, err
} }
if ok { if canDelete {
err = node.PipeE(yaml.Clear(field)) err = node.PipeE(yaml.Clear(field))
if err != nil { if err != nil {
return false, err return false, err
@ -471,26 +483,53 @@ func deleteAnchorsInMap(node *yaml.RNode) (bool, error) {
return needToDelete, nil return needToDelete, nil
} }
func deleteAnchorsInList(node *yaml.RNode) (bool, error) { // stripAnchorFromNode strips one or more anchor fields from the node.
// If key is "" all anchor fields are stripped. Otherwise, only the matching
// field is stripped.
func stripAnchorsFromNode(node *yaml.RNode, key string) error {
anchors, err := filterKeys(node, anchor.ContainsCondition)
if err != nil {
return err
}
for _, a := range anchors {
k, _ := anchor.RemoveAnchor(a)
if key == "" || k == key {
renameField(a, k, node)
}
}
return nil
}
func deleteAnchorsInList(node *yaml.RNode, traverseMappingNodes bool) (bool, error) {
elements, err := node.Elements() elements, err := node.Elements()
if err != nil { if err != nil {
return false, err return false, err
} }
wasEmpty := len(elements) == 0 wasEmpty := len(elements) == 0
for i, element := range elements { for i, element := range elements {
if hasAnchors(element, hasAnchor) { if hasAnchors(element, hasAnchor) {
deleteListElement(node, i) shouldDelete := true
if traverseMappingNodes && isMappingNode(element) {
shouldDelete, err = deleteAnchors(element, true, traverseMappingNodes)
if err != nil {
return false, errors.Wrap(err, "failed to delete anchors")
}
}
if shouldDelete {
deleteListElement(node, i)
}
} else { } else {
// This element also could have some conditions // This element also could have some conditions
// inside sub-arrays. Delete them too. // inside sub-arrays. Delete them too.
canDelete, err := deleteAnchors(element, false, traverseMappingNodes)
ok, err := deleteAnchors(element)
if err != nil { if err != nil {
return false, err return false, errors.Wrap(err, "failed to delete anchors")
} }
if ok { if canDelete {
deleteListElement(node, i) deleteListElement(node, i)
} }
} }
@ -524,8 +563,15 @@ func validateConditionsInternal(logger logr.Logger, pattern, resource *yaml.RNod
return fmt.Errorf("could not found \"%s\" key in the resource", conditionKey) return fmt.Errorf("could not found \"%s\" key in the resource", conditionKey)
} }
err = checkCondition(logger, pattern.Field(condition).Value, resource.Field(conditionKey).Value) patternValue := pattern.Field(condition).Value
if err != nil { resourceValue := resource.Field(conditionKey).Value
if count, err := handleAddIfNotPresentAnchor(patternValue, resourceValue); err != nil {
return err
} else if count > 0 {
continue
}
if err := checkCondition(logger, patternValue, resourceValue); err != nil {
return err return err
} }
} }

View file

@ -5,25 +5,11 @@ import (
"testing" "testing"
"github.com/kyverno/kyverno/pkg/engine/anchor" "github.com/kyverno/kyverno/pkg/engine/anchor"
"gotest.tools/assert" "gotest.tools/assert"
"sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log"
yaml "sigs.k8s.io/kustomize/kyaml/yaml" yaml "sigs.k8s.io/kustomize/kyaml/yaml"
) )
func areEqualJSONs(t *testing.T, s1, s2 []byte) {
var o1 interface{}
var o2 interface{}
var err error
err = json.Unmarshal(s1, &o1)
assert.NilError(t, err)
err = json.Unmarshal(s2, &o2)
assert.NilError(t, err)
assert.DeepEqual(t, o1, o2)
}
func Test_preProcessStrategicMergePatch_multipleAnchors(t *testing.T) { func Test_preProcessStrategicMergePatch_multipleAnchors(t *testing.T) {
testCases := []struct { testCases := []struct {
rawPolicy []byte rawPolicy []byte
@ -341,7 +327,7 @@ func Test_preProcessStrategicMergePatch_multipleAnchors(t *testing.T) {
"spec": { "spec": {
"volumes": [ "volumes": [
{ {
"(hostPath)": { "<(hostPath)": {
"path": "*" "path": "*"
} }
} }
@ -400,7 +386,7 @@ func Test_preProcessStrategicMergePatch_multipleAnchors(t *testing.T) {
"spec": { "spec": {
"volumes": [ "volumes": [
{ {
"(hostPath)": { "<(hostPath)": {
"path": "*" "path": "*"
} }
} }
@ -855,19 +841,25 @@ func Test_preProcessStrategicMergePatch_multipleAnchors(t *testing.T) {
} }
for i, test := range testCases { for i, test := range testCases {
t.Logf("Running test %d...", i)
t.Logf("Running test %d...", i+1)
preProcessedPolicy, err := preProcessStrategicMergePatch(log.Log, string(test.rawPolicy), string(test.rawResource)) preProcessedPolicy, err := preProcessStrategicMergePatch(log.Log, string(test.rawPolicy), string(test.rawResource))
assert.NilError(t, err) assert.NilError(t, err)
output, err := preProcessedPolicy.MarshalJSON() output, err := preProcessedPolicy.MarshalJSON()
assert.NilError(t, err) assert.NilError(t, err)
// has assertions inside assert.DeepEqual(t, toJSON(t, []byte(test.expectedPatch)), toJSON(t, output))
areEqualJSONs(t, test.expectedPatch, output)
} }
} }
func toJSON(t *testing.T, b []byte) interface{} {
var i interface{}
var err error
err = json.Unmarshal(b, &i)
assert.NilError(t, err)
return i
}
func Test_FilterKeys_NoConditions(t *testing.T) { func Test_FilterKeys_NoConditions(t *testing.T) {
patternRaw := []byte(`{ patternRaw := []byte(`{
"key1": "value1", "key1": "value1",
@ -1056,7 +1048,7 @@ func Test_DeleteConditions(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, len(containers), 2) assert.Equal(t, len(containers), 2)
_, err = deleteAnchors(pattern) _, err = deleteAnchors(pattern, false, false)
assert.NilError(t, err) assert.NilError(t, err)
containers, err = pattern.Field("spec").Value.Field("containers").Value.Elements() containers, err = pattern.Field("spec").Value.Field("containers").Value.Elements()
@ -1150,7 +1142,20 @@ func Test_NonExistingKeyMustFailPreprocessing(t *testing.T) {
pattern := yaml.MustParse(string(rawPattern)) pattern := yaml.MustParse(string(rawPattern))
resource := yaml.MustParse(string(rawResource)) resource := yaml.MustParse(string(rawResource))
err := preProcessPattern(log.Log, pattern, resource) err := preProcessPattern(log.Log, pattern, resource)
assert.Error(t, err, "condition failed: could not found \"key1\" key in the resource") assert.Error(t, err, "condition failed: could not found \"key1\" key in the resource")
} }
func Test_NestedConditionals(t *testing.T) {
rawPattern := `{"spec":{"template":{"spec":{"volumes":[{"(emptyDir)":{"+(sizeLimit)":"20Mi"},"name":"*"}]}}}}`
rawResource := `{"spec":{"template":{"spec":{"volumes":[{"emptyDir":{},"name":"vol02"}]}}}}`
expectedPattern := `{"spec":{"template":{"spec":{"volumes":[{"emptyDir":{"sizeLimit":"20Mi"},"name":"vol02"}]}}}}`
pattern := yaml.MustParse(rawPattern)
resource := yaml.MustParse(rawResource)
err := preProcessPattern(log.Log, pattern, resource)
assert.NilError(t, err)
resultPattern, _ := pattern.String()
assert.DeepEqual(t, toJSON(t, []byte(expectedPattern)), toJSON(t, []byte(resultPattern)))
}

View file

@ -11,7 +11,7 @@ func getAnchorAndElementsFromMap(anchorsMap map[string]interface{}) (map[string]
for key, value := range anchorsMap { for key, value := range anchorsMap {
if commonAnchors.IsConditionAnchor(key) { if commonAnchors.IsConditionAnchor(key) {
anchors[key] = value anchors[key] = value
} else if !commonAnchors.IsAddingAnchor(key) { } else if !commonAnchors.IsAddIfNotPresentAnchor(key) {
elementsWithoutanchor[key] = value elementsWithoutanchor[key] = value
} }
} }

View file

@ -277,7 +277,7 @@ func (v *validator) validateElements(foreach *kyverno.ForEachValidation, element
v.log.Info("skip rule", "reason", r.Message) v.log.Info("skip rule", "reason", r.Message)
continue continue
} else if r.Status != response.RuleStatusPass { } else if r.Status != response.RuleStatusPass {
msg := fmt.Sprintf("validation failed in foreach rule for %v", r.Message) msg := fmt.Sprintf("validation failure: %v", r.Message)
return ruleResponse(v.rule, utils.Validation, msg, r.Status), applyCount return ruleResponse(v.rule, utils.Validation, msg, r.Status), applyCount
} }

View file

@ -15,7 +15,6 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-billy/v5/memfs"
"github.com/go-logr/logr"
"github.com/kataras/tablewriter" "github.com/kataras/tablewriter"
report "github.com/kyverno/kyverno/api/policyreport/v1alpha2" report "github.com/kyverno/kyverno/api/policyreport/v1alpha2"
client "github.com/kyverno/kyverno/pkg/dclient" client "github.com/kyverno/kyverno/pkg/dclient"
@ -391,11 +390,11 @@ func getLocalDirTestFiles(fs billy.Filesystem, path, fileName, valuesFile string
return errors return errors
} }
func buildPolicyResults(resps []*response.EngineResponse, testResults []TestResults, infos []policyreport.Info, policyResourcePath string, fs billy.Filesystem, isGit bool) (map[string]report.PolicyReportResult, []TestResults) { func buildPolicyResults(engineResponses []*response.EngineResponse, testResults []TestResults, infos []policyreport.Info, policyResourcePath string, fs billy.Filesystem, isGit bool) (map[string]report.PolicyReportResult, []TestResults) {
results := make(map[string]report.PolicyReportResult) results := make(map[string]report.PolicyReportResult)
now := metav1.Timestamp{Seconds: time.Now().Unix()} now := metav1.Timestamp{Seconds: time.Now().Unix()}
for _, resp := range resps { for _, resp := range engineResponses {
policyName := resp.PolicyResponse.Policy.Name policyName := resp.PolicyResponse.Policy.Name
resourceName := resp.PolicyResponse.Resource.Name resourceName := resp.PolicyResponse.Resource.Name
resourceKind := resp.PolicyResponse.Resource.Kind resourceKind := resp.PolicyResponse.Resource.Kind
@ -417,7 +416,7 @@ func buildPolicyResults(resps []*response.EngineResponse, testResults []TestResu
Message: buildMessage(resp), Message: buildMessage(resp),
} }
var patcheResourcePath []string var patchedResourcePath []string
for i, test := range testResults { for i, test := range testResults {
var userDefinedPolicyNamespace string var userDefinedPolicyNamespace string
var userDefinedPolicyName string var userDefinedPolicyName string
@ -457,7 +456,7 @@ func buildPolicyResults(resps []*response.EngineResponse, testResults []TestResu
} }
} }
patcheResourcePath = append(patcheResourcePath, test.PatchedResource) patchedResourcePath = append(patchedResourcePath, test.PatchedResource)
if _, ok := results[resultsKey]; !ok { if _, ok := results[resultsKey]; !ok {
results[resultsKey] = result results[resultsKey] = result
} }
@ -472,13 +471,12 @@ func buildPolicyResults(resps []*response.EngineResponse, testResults []TestResu
var resultsKey []string var resultsKey []string
var resultKey string var resultKey string
var result report.PolicyReportResult var result report.PolicyReportResult
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name, resourceNamespace, resourceKind, resourceName) resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name, resourceNamespace, resourceKind, resourceName)
for _, resultK := range resultsKey { for _, key := range resultsKey {
if val, ok := results[resultK]; ok { if val, ok := results[key]; ok {
result = val result = val
resultKey = resultK resultKey = key
} else { } else {
continue continue
} }
@ -491,7 +489,7 @@ func buildPolicyResults(resps []*response.EngineResponse, testResults []TestResu
} else { } else {
var x string var x string
for _, path := range patcheResourcePath { for _, path := range patchedResourcePath {
result.Result = report.StatusFail result.Result = report.StatusFail
x = getAndComparePatchedResource(path, resp.PatchedResource, isGit, policyResourcePath, fs) x = getAndComparePatchedResource(path, resp.PatchedResource, isGit, policyResourcePath, fs)
if x == "pass" { if x == "pass" {
@ -538,12 +536,12 @@ func buildPolicyResults(resps []*response.EngineResponse, testResults []TestResu
return results, testResults return results, testResults
} }
func GetAllPossibleResultsKey(policyNs, policy, rule, resourceNsnamespace, kind, resource string) []string { func GetAllPossibleResultsKey(policyNamespace, policy, rule, resourceNamespace, kind, resource string) []string {
var resultsKey []string var resultsKey []string
resultKey1 := fmt.Sprintf("%s-%s-%s-%s", policy, rule, kind, resource) resultKey1 := fmt.Sprintf("%s-%s-%s-%s", policy, rule, kind, resource)
resultKey2 := fmt.Sprintf("%s-%s-%s-%s-%s", policy, rule, resourceNsnamespace, kind, resource) resultKey2 := fmt.Sprintf("%s-%s-%s-%s-%s", policy, rule, resourceNamespace, kind, resource)
resultKey3 := fmt.Sprintf("%s-%s-%s-%s-%s", policyNs, policy, rule, kind, resource) resultKey3 := fmt.Sprintf("%s-%s-%s-%s-%s", policyNamespace, policy, rule, kind, resource)
resultKey4 := fmt.Sprintf("%s-%s-%s-%s-%s-%s", policyNs, policy, rule, resourceNsnamespace, kind, resource) resultKey4 := fmt.Sprintf("%s-%s-%s-%s-%s-%s", policyNamespace, policy, rule, resourceNamespace, kind, resource)
resultsKey = append(resultsKey, resultKey1, resultKey2, resultKey3, resultKey4) resultsKey = append(resultsKey, resultKey1, resultKey2, resultKey3, resultKey4)
return resultsKey return resultsKey
} }
@ -584,12 +582,12 @@ func getAndComparePatchedResource(path string, enginePatchedResource unstructure
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
var log logr.Logger matched, err := generate.ValidateResourceWithPattern(log.Log, enginePatchedResource.UnstructuredContent(), patchedResources.UnstructuredContent())
matched, err := generate.ValidateResourceWithPattern(log, enginePatchedResource.UnstructuredContent(), patchedResources.UnstructuredContent())
if err != nil { if err != nil {
log.Log.Info("patched resource mismatch", "error", err.Error())
status = "fail" status = "fail"
} }
if matched == "" { if matched == "" {
status = "pass" status = "pass"
} }

View file

@ -0,0 +1,35 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-sizelimit-test
namespace: default
spec:
selector:
matchLabels:
test: svc-sizelimit-test
replicas: 1
template:
metadata:
labels:
test: svc-sizelimit-test
spec:
securityContext:
runAsUser: 65000
runAsGroup: 65000
containers:
- name: pause
image: k8s.gcr.io/pause:3.3
resources:
limits:
cpu: 10m
memory: 32Mi
requests:
cpu: 10m
memory: 32Mi
volumeMounts:
- name: vol02
mountPath: /opt02
volumes:
- name: vol02
emptyDir:
sizeLimit: 20Mi

View file

@ -0,0 +1,12 @@
name: foreach-mutate
policies:
- policies.yaml
resources:
- resources.yaml
results:
- policy: mutate-emptydir
rule: setDefault
resource: svc-sizelimit-test
patchedResource: deploy-patched.yaml
kind: Deployment
result: pass

View file

@ -0,0 +1,22 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: mutate-emptydir
spec:
rules:
- name: setDefault
match:
resources:
kinds:
- Deployment
mutate:
foreach:
- list: "request.object.spec.template.spec.volumes"
patchStrategicMerge:
spec:
template:
spec:
volumes:
- name: "{{ element.name }}"
(emptyDir):
+(sizeLimit): "20Mi"

View file

@ -0,0 +1,35 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-sizelimit-test
namespace: default
spec:
selector:
matchLabels:
test: svc-sizelimit-test
replicas: 1
template:
metadata:
labels:
test: svc-sizelimit-test
spec:
securityContext:
runAsUser: 65000
runAsGroup: 65000
containers:
- name: pause
image: k8s.gcr.io/pause:3.3
resources:
limits:
cpu: 10m
memory: 32Mi
requests:
cpu: 10m
memory: 32Mi
volumeMounts:
- name: vol02
mountPath: /opt02
volumes:
- name: vol02
emptyDir: {}

View file

@ -0,0 +1,12 @@
name: foreach-mutate
policies:
- policies.yaml
resources:
- resources.yaml
results:
- policy: replace-image-registry-containers
rule: set-default
resource: test-patched-image
patchedResource: pod-patched.yaml
kind: Pod
result: pass

View file

@ -0,0 +1,27 @@
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: test-patched-image
spec:
containers:
- args:
- --web.listen-address=127.0.0.1:9100
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/host/root
- --no-collector.wifi
- --no-collector.hwmon
- --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/)
- --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
image: test/test3.2
imagePullPolicy: IfNotPresent
name: node-exporter
- args:
- --logtostderr
- --secure-listen-address=[$(IP)]:9100
- --tls-cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
- --upstream=http://127.0.0.1:9100/
image: test/test3.2
imagePullPolicy: IfNotPresent
name: kube-rbac-proxy

View file

@ -0,0 +1,22 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: replace-image-registry-containers
annotations:
pod-policies.kyverno.io/autogen-controllers: "none"
spec:
rules:
- name: set-default
match:
all:
- resources:
kinds:
- Pod
mutate:
foreach:
- list: "request.object.spec.containers"
patchStrategicMerge:
spec:
containers:
- (name): "{{ element.name }}"
image: test/test3.2

View file

@ -0,0 +1,27 @@
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: test-patched-image
spec:
containers:
- args:
- --web.listen-address=127.0.0.1:9100
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/host/root
- --no-collector.wifi
- --no-collector.hwmon
- "--collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/)"
- "--collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$"
image: docker.io/prom/node-exporter:v0.18.1
imagePullPolicy: IfNotPresent
name: node-exporter
- args:
- --logtostderr
- --secure-listen-address=[$(IP)]:9100
- --tls-cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
- --upstream=http://127.0.0.1:9100/
image: kubesphere/kube-rbac-proxy:v0.8.0
imagePullPolicy: IfNotPresent
name: kube-rbac-proxy