1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

Patch strategic merge preprocessing: implement anchor handling (#2156)

* finished walkMap

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* added validation to the patchStrategicMerge

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* finished fixing tests

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* fixed part of old tests

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* patchStrategicMerge anchor preprocessing is finished

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* fix #1915 and #1896

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* fix lint errors

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* removed debug logs

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* added failing test

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>

* Fix unnecessary deletion

Signed-off-by: Maxim Goncharenko <goncharenko.maxim@apriorit.com>
This commit is contained in:
Max Goncharenko 2021-07-23 20:53:37 +03:00 committed by GitHub
parent 0a38f1c8ec
commit 4c7ca97eac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1448 additions and 660 deletions

View file

@ -63,6 +63,12 @@ func IsExistenceAnchor(str string) bool {
return (str[:len(left)] == left && str[len(str)-len(right):] == right)
}
// IsNonAnchor checks that key does not have any anchor
func IsNonAnchor(str string) bool {
key, _ := RemoveAnchor(str)
return str == key
}
// RemoveAnchor remove anchor from the given key. It returns
// the anchor-free tag value and the prefix of the anchor.
func RemoveAnchor(key string) (string, string) {

View file

@ -88,6 +88,14 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour
}
}
if rule.Mutation.PatchStrategicMerge != nil {
var resp response.RuleResponse
resp, resource = mutate.ProcessStrategicMergePatch(rule.Name, rule.Mutation.PatchStrategicMerge, resource, logger.WithValues("rule", rule.Name))
if !resp.Success {
return unstructured.Unstructured{}, fmt.Errorf(resp.Message)
}
}
if rule.Mutation.PatchesJSON6902 != "" {
var resp response.RuleResponse
jsonPatches, err := yaml.YAMLToJSON([]byte(rule.Mutation.PatchesJSON6902))

View file

@ -254,3 +254,89 @@ func Test_ForceMutateSubstituteVarsWithPatchesJson6902(t *testing.T) {
assert.DeepEqual(t, expectedResource.UnstructuredContent(), mutatedResource.UnstructuredContent())
}
func Test_ForceMutateSubstituteVarsWithPatchStrategicMerge(t *testing.T) {
rawPolicy := []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "strategic-merge-patch"
},
"spec": {
"rules": [
{
"name": "set-image-pull-policy-add-command",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"patchStrategicMerge": {
"spec": {
"volumes": [
{
"emptyDir": {
"medium": "Memory"
},
"name": "cache-volume"
}
]
}
}
}
}
]
}
}
`)
rawResource := []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user"
},
"spec": {
"volumes": [
{
"name": "cache-volume",
"emptyDir": { }
},
{
"name": "cache-volume2",
"emptyDir": {
"medium": "Memory"
}
}
]
}
}
`)
expectedRawResource := []byte(`
{"apiVersion":"v1","kind":"Pod","metadata":{"name":"check-root-user"},"spec":{"volumes":[{"emptyDir":{"medium":"Memory"},"name":"cache-volume"},{"emptyDir":{"medium":"Memory"},"name":"cache-volume2"}]}}
`)
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())
}

View file

@ -7,11 +7,12 @@ import (
"github.com/mattbaird/jsonpatch"
assertnew "github.com/stretchr/testify/assert"
"gotest.tools/assert"
"sigs.k8s.io/controller-runtime/pkg/log"
)
func Test_GeneratePatches(t *testing.T) {
out, err := strategicMergePatch(string(baseBytes), string(overlayBytes))
out, err := strategicMergePatch(log.Log, string(baseBytes), string(overlayBytes))
assert.NilError(t, err)
expectedPatches := map[string]bool{

View file

@ -65,7 +65,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
resp.Message = fmt.Sprintf("failed to process patchStrategicMerge: %v", err)
return resp, resource
}
patchedBytes, err := strategicMergePatch(string(base), string(overlayBytes))
patchedBytes, err := strategicMergePatch(logger, string(base), string(overlayBytes))
if err != nil {
log.Error(err, "failed to apply patchStrategicMerge")
msg := fmt.Sprintf("failed to apply patchStrategicMerge: %v", err)
@ -103,9 +103,8 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
return resp, patchedResource
}
func strategicMergePatch(base, overlay string) ([]byte, error) {
preprocessedYaml, err := preProcessStrategicMergePatch(overlay, base)
func strategicMergePatch(logger logr.Logger, base, overlay string) ([]byte, error) {
preprocessedYaml, err := preProcessStrategicMergePatch(logger, overlay, base)
if err != nil {
return []byte{}, fmt.Errorf("failed to preProcess rule: %+v", err)
}
@ -120,9 +119,9 @@ func strategicMergePatch(base, overlay string) ([]byte, error) {
return baseObj.Bytes(), err
}
func preProcessStrategicMergePatch(pattern, resource string) (*yaml.RNode, error) {
func preProcessStrategicMergePatch(logger logr.Logger, pattern, resource string) (*yaml.RNode, error) {
patternNode := yaml.MustParse(pattern)
resourceNode := yaml.MustParse(resource)
err := preProcessPattern(patternNode, resourceNode)
err := preProcessPattern(logger, patternNode, resourceNode)
return patternNode, err
}

View file

@ -9,6 +9,7 @@ import (
assertnew "github.com/stretchr/testify/assert"
"gotest.tools/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/log"
)
func TestMergePatch(t *testing.T) {
@ -39,7 +40,7 @@ func TestMergePatch(t *testing.T) {
for i, test := range testCases {
// out
out, err := strategicMergePatch(string(test.rawResource), string(test.rawPolicy))
out, err := strategicMergePatch(log.Log, string(test.rawResource), string(test.rawPolicy))
assert.NilError(t, err)
// expect
@ -132,7 +133,7 @@ func Test_PolicyDeserilize(t *testing.T) {
patchString, err := json.Marshal(overlayPatches)
assert.NilError(t, err)
out, err := strategicMergePatch(string(baseBytes), string(patchString))
out, err := strategicMergePatch(log.Log, string(baseBytes), string(patchString))
assert.NilError(t, err)
var ep unstructured.Unstructured

View file

@ -1,11 +1,27 @@
package mutate
import (
"encoding/json"
"fmt"
"github.com/go-logr/logr"
anchor "github.com/kyverno/kyverno/pkg/engine/anchor/common"
"github.com/minio/pkg/wildcard"
"github.com/kyverno/kyverno/pkg/engine/validate"
yaml "sigs.k8s.io/kustomize/kyaml/yaml"
)
type ConditionError struct {
errorChain error
}
func (ce ConditionError) Error() string {
return fmt.Sprintf("Condition failed: %s", ce.errorChain.Error())
}
func NewConditionError(err error) error {
return ConditionError{err}
}
// preProcessPattern - Dynamically preProcess the yaml
// 1> For conditional anchor remove anchors from the pattern.
// 2> For Adding anchors remove anchor tags.
@ -14,539 +30,430 @@ import (
// https://godoc.org/gopkg.in/yaml.v3#Node
// A single Node contains Tag to identify it as MappingNode (map[string]interface{}), Sequence ([]interface{}), ScalarNode (string, int, float bool etc.)
// A parent node having MappingNode keeps the data as <keyNode>, <ValueNode> inside it's Content field and Tag field as "!!map".
// A parent node having MappingNode keeps the data as array of Node inside Content field and a Tag field as "!!seq".
// A parent node having Sequence keeps the data as array of Node inside Content field and a Tag field as "!!seq".
// https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/yaml/rnode.go
func preProcessPattern(pattern, resource *yaml.RNode) error {
func preProcessPattern(logger logr.Logger, pattern, resource *yaml.RNode) error {
err := preProcessRecursive(logger, pattern, resource)
if err != nil {
return err
}
return deleteConditionElements(pattern)
}
func preProcessRecursive(logger logr.Logger, pattern, resource *yaml.RNode) error {
switch pattern.YNode().Kind {
case yaml.MappingNode:
err := walkMap(pattern, resource)
if err != nil {
return err
}
return walkMap(logger, pattern, resource)
case yaml.SequenceNode:
err := walkArray(pattern, resource)
if err != nil {
return err
}
case yaml.ScalarNode:
if pattern.YNode().Value != resource.YNode().Value {
if wildcard.Match(pattern.YNode().Value, resource.YNode().Value) {
}
}
return walkList(logger, pattern, resource)
}
return nil
}
// getIndex - get the index of the key from the fields.
var getIndex = func(k string, list []string) int {
for i, v := range list {
if v == k {
return 2 * i
}
}
return -1
}
// removeAnchorNode - removes anchor nodes from yaml
func removeAnchorNode(targetNode *yaml.RNode, index int) {
targetNode.YNode().Content = append(targetNode.YNode().Content[:index], targetNode.YNode().Content[index+2:]...)
}
func removeKeyFromFields(key string, fields []string) []string {
for i, v := range fields {
if v == key {
return append(fields[:i], fields[i+1:]...)
}
}
return fields
}
// walkMap - walk through the MappingNode
/* 1> For conditional anchor remove anchors from the pattern, patchStrategicMerge will add the anchors as a new patch,
so it is necessary to remove the anchor mapsfrom the pattern before calling patchStrategicMerge.
| (volumes):
| - (hostPath):
| path: "/var/run/docker.sock"
walkMap will remove the node containing (volumes) from the yaml
*/
func walkMap(logger logr.Logger, pattern, resource *yaml.RNode) error {
var err error
/* 2> For Adding anchors remove anchor tags.
annotations:
- "+(annotation1)": "atest1"
will remove "+(" and ")" chars from pattern.
*/
func walkMap(pattern, resource *yaml.RNode) error {
sfields, fields, err := getAnchorSortedFields(pattern)
err = validateConditions(logger, pattern, resource)
if err != nil {
return err
}
sfieldsCopy := make([]string, len(sfields))
copy(sfieldsCopy, sfields)
for _, key := range sfieldsCopy {
if anchor.IsConditionAnchor(key) {
// remove anchor node from yaml
// In a MappingNode, yaml.Node store <keyNode>:<valueNode> pairs as an array of Node inside Content field,
// <valueNode> further can be a MappingNode, SequenceNode or ScalarNode.
// for a mapping node with single key value pair then key is in position index 0 and value in position 1 and
// the next <keyNode>:<valueNode> pairs in position 2 and 3 respectively.
ind := getIndex(key, fields)
if ind == -1 {
continue
}
// remove anchor from the map and update fields
removeAnchorNode(pattern, ind)
sfields = removeKeyFromFields(key, sfields)
fields = removeKeyFromFields(key, fields)
continue
}
if anchor.IsAddingAnchor(key) {
ind := getIndex(key, fields)
if ind == -1 {
continue
}
err = handleAddings(logger, pattern, resource)
if err != nil {
return err
}
// remove anchor tags from value
// A MappingNode contains keyNode and Value node
// keyNode contains it's key value in it's Value field, So remove anchor tags from Value field
pattern.YNode().Content[ind].Value = removeAnchor(key)
// If the field exists in resource, then remove the field from pattern
_, resFields, err := getAnchorSortedFields(resource)
if err != nil {
return err
}
rInd := getIndex(removeAnchor(key), resFields)
if rInd != -1 {
// remove anchor field from the map and update fields
removeAnchorNode(pattern, ind)
sfields = removeKeyFromFields(key, sfields)
fields = removeKeyFromFields(key, fields)
}
}
noAnchorKey := removeAnchor(key)
patternMapNode := pattern.Field(noAnchorKey)
resourceMapNode := resource.Field(noAnchorKey)
if resourceMapNode != nil {
if !patternMapNode.IsNilOrEmpty() {
err := preProcessPattern(patternMapNode.Value, resourceMapNode.Value)
if err != nil {
return err
}
}
fields, err := pattern.Fields()
if err != nil {
return err
}
for _, field := range fields {
var resourceNode *yaml.RNode
if resource == nil || resource.Field(field) == nil {
// In case if we have pattern, but not corresponding resource part,
// just walk down and remove all anchors. nil here indicates that
// resourceNode is empty
resourceNode = nil
} else {
// remove anchors from patterns where there is no specific key exists in resource.
// Ex :-
// pattern : {"annotations": {"+(add-annotation)":"true" }}
// resource : No "annotations" key
if hasAnchors(pattern) {
err := preProcessPattern(patternMapNode.Value, resource)
if err != nil {
return err
}
}
resourceNode = resource.Field(field).Value
}
}
return nil
}
// walkArray - walk through array elements
// 1> processNonAssocSequence - process array of basic types. Ex:- {command: ["ls", "ls -l"]}
// 2> processAssocSequence - process array having MappingNode. like containers, volumes etc.
func walkArray(pattern, resource *yaml.RNode) error {
pafs, err := pattern.Elements()
if err != nil {
return err
}
if len(pafs) == 0 {
return nil
}
switch pafs[0].YNode().Kind {
case yaml.MappingNode:
return processAssocSequence(pattern, resource)
case yaml.ScalarNode:
return processNonAssocSequence(pattern, resource)
}
return nil
}
// processAssocSequence - process arrays
// in many cases like containers, volumes kustomize uses name field to match resource for processing
// 1> If any conditional anchor match resource field and if the pattern doesn't contains "name" field and
// resource contains "name" field then copy the name field from resource to pattern.
// 2> If the resource doesn't contains "name" field then just remove anchor field from yaml.
/*
Policy:
"spec": {
"containers": [{
"(image)": "*:latest",
"imagePullPolicy": "Always"
}]}
Resource:
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:latest",
"imagePullPolicy": "Never"
}]
}
After Preprocessing:
"spec": {
"containers": [{
"name": "nginx",
"imagePullPolicy": "Always"
}]}
kustomize uses name field to match resource for processing. So if containers doesn't contains name field then it will be skipped.
So if a conditional anchor image matches resource then remove "(image)" field from yaml and add the matching names from the resource.
*/
func processAssocSequence(pattern, resource *yaml.RNode) error {
patternElements, err := pattern.Elements()
if err != nil {
return err
}
for _, patternElement := range patternElements {
if hasAnchors(patternElement) {
err := processAnchorSequence(patternElement, resource, pattern)
if err != nil {
return err
}
}
}
// remove the elements with anchors
err = removeAnchorElements(pattern)
if err != nil {
return err
}
return preProcessArrayPattern(pattern, resource)
}
func preProcessArrayPattern(pattern, resource *yaml.RNode) error {
patternElements, err := pattern.Elements()
if err != nil {
return err
}
resourceElements, err := resource.Elements()
if err != nil {
return err
}
for _, patternElement := range patternElements {
patternNameField := patternElement.Field("name")
if patternNameField != nil {
patternNameValue, err := patternNameField.Value.String()
if err != nil {
return err
}
for _, resourceElement := range resourceElements {
resourceNameField := resourceElement.Field("name")
if resourceNameField != nil {
resourceNameValue, err := resourceNameField.Value.String()
if err != nil {
return err
}
if patternNameValue == resourceNameValue {
err := preProcessPattern(patternElement, resourceElement)
if err != nil {
return err
}
}
}
}
}
}
return nil
}
/*
removeAnchorSequence :- removes element containing conditional anchor
Pattern:
"spec": {
"containers": [{
"(image)": "*:latest",
"imagePullPolicy": "Always"
},
{
"name": "nginx",
"imagePullPolicy": "Always"
}]}
After Removing Conditional Sequence:
"spec": {
"containers": [{
"name": "nginx",
"imagePullPolicy": "Always"
}]}
*/
func removeAnchorElements(pattern *yaml.RNode) error {
patternElements, err := pattern.Elements()
if err != nil {
return err
}
removedIndex, err := getIndexToBeRemoved(patternElements)
if err != nil {
return err
}
if len(removedIndex) == 0 {
return nil
}
preservedPatterns := removeByIndex(pattern, removedIndex)
pattern.YNode().Content = preservedPatterns
return nil
}
func processAnchorSequence(pattern, resource, arrayPattern *yaml.RNode) error {
resourceElements, err := resource.Elements()
if err != nil {
return err
}
switch pattern.YNode().Kind {
case yaml.MappingNode:
for _, resourceElement := range resourceElements {
err := processAnchorMap(pattern, resourceElement, arrayPattern)
if err != nil {
return err
}
}
}
return nil
}
// processAnchorMap - process arrays
// in many cases like containers, volumes kustomize uses name field to match resource for processing
// 1> If any conditional anchor match resource field and if the pattern doesn't contains "name" field and
// resource contains "name" field then copy the name field from resource to pattern.
// 2> If the resource doesn't contains "name" field then just remove anchor field from yaml.
/*
Policy:
"spec": {
"containers": [{
"(image)": "*:latest",
"imagePullPolicy": "Always"
}]}
Resource:
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:latest",
"imagePullPolicy": "Never"
}]
}
After Preprocessing:
"spec": {
"containers": [{
"(image)": "*:latest",
"imagePullPolicy": "Always"
},
{
"name": "nginx",
"imagePullPolicy": "Always"
}]}
kustomize uses name field to match resource for processing. So if containers doesn't contains name field then it will be skipped.
So if a conditional anchor image matches resouce then remove "(image)" field from yaml and add the matching names from the resource.
*/
func processAnchorMap(pattern, resource, arrayPattern *yaml.RNode) error {
sfields, fields, err := getAnchorSortedFields(pattern)
if err != nil {
return err
}
for _, key := range sfields {
if anchor.IsConditionAnchor(key) {
_, efields, err := getAnchorSortedFields(resource)
if err != nil {
return err
}
noAnchorKey := removeAnchor(key)
eind := getIndex("name", efields)
if eind != -1 && getIndex("name", fields) == -1 {
patternMapNode := pattern.Field(key)
resourceMapNode := resource.Field(noAnchorKey)
if resourceMapNode != nil {
pval, err := patternMapNode.Value.String()
if err != nil {
return err
}
eval, err := resourceMapNode.Value.String()
if err != nil {
return err
}
if wildcard.Match(pval, eval) {
newNodeString, err := pattern.String()
if err != nil {
return err
}
newNode, err := yaml.Parse(newNodeString)
if err != nil {
return err
}
for i, ekey := range efields {
if ekey == noAnchorKey {
pind := getIndex(key, fields)
if pind == -1 {
continue
}
removeAnchorNode(newNode, pind)
sfields = removeKeyFromFields(key, sfields)
fields = removeKeyFromFields(key, fields)
if ekey == "name" {
newNode.YNode().Content = append(newNode.YNode().Content, resource.YNode().Content[2*i])
newNode.YNode().Content = append(newNode.YNode().Content, resource.YNode().Content[2*i+1])
}
} else if ekey == "name" {
newNode.YNode().Content = append(newNode.YNode().Content, resource.YNode().Content[2*i])
newNode.YNode().Content = append(newNode.YNode().Content, resource.YNode().Content[2*i+1])
}
}
arrayPattern.YNode().Content = append(arrayPattern.YNode().Content, newNode.YNode())
}
}
} else {
ind := getIndex(key, fields)
if ind == -1 {
continue
}
removeAnchorNode(pattern, ind)
sfields = removeKeyFromFields(key, sfields)
fields = removeKeyFromFields(key, fields)
}
continue
}
}
return nil
}
func processNonAssocSequence(pattern, resource *yaml.RNode) error {
pafs, err := pattern.Elements()
if err != nil {
return err
}
rafs, err := resource.Elements()
if err != nil {
return err
}
for _, sa := range rafs {
des, err := sa.String()
err := preProcessRecursive(logger, pattern.Field(field).Value, resourceNode)
if err != nil {
return err
}
ok := false
for _, ra := range pafs {
src, err := ra.String()
if err != nil {
return err
}
if des == src {
ok = true
break
}
}
if !ok {
pattern.YNode().Content = append(pattern.YNode().Content, sa.YNode())
}
}
return nil
}
// getAnchorSortedFields - get all the keys from a MappingNode sorted by anchor field
func getAnchorSortedFields(pattern *yaml.RNode) ([]string, []string, error) {
anchors := make([]string, 0)
nonAnchors := make([]string, 0)
nestedAnchors := make([]string, 0)
fields, err := pattern.Fields()
// walkList - walk through array elements
func walkList(logger logr.Logger, pattern, resource *yaml.RNode) error {
elements, err := pattern.Elements()
if err != nil {
return fields, fields, err
return err
}
for _, key := range fields {
if anchor.IsConditionAnchor(key) {
anchors = append(anchors, key)
continue
}
patternMapNode := pattern.Field(key)
if !patternMapNode.IsNilOrEmpty() {
if hasAnchors(patternMapNode.Value) {
nestedAnchors = append(nestedAnchors, key)
continue
if len(elements) == 0 {
return nil
}
if elements[0].YNode().Kind == yaml.MappingNode {
return processListOfMaps(logger, pattern, resource)
}
return nil
}
// processListOfMaps - process arrays
// in many cases like containers, volumes kustomize uses name field to match resource for processing
// If any conditional anchor match resource field and if the pattern doesn't contain "name" field and
// resource contains "name" field, then copy the name field from resource to pattern.
func processListOfMaps(logger logr.Logger, pattern, resource *yaml.RNode) error {
patternElements, err := pattern.Elements()
if err != nil {
return err
}
resourceElements, err := resource.Elements()
if err != nil {
return err
}
for _, patternElement := range patternElements {
// If pattern has conditions, look for matching elements and process them
if hasAnchors(patternElement) {
for _, resourceElement := range resourceElements {
err := preProcessRecursive(logger, patternElement, resourceElement)
if err != nil {
if _, ok := err.(ConditionError); ok {
// Skip element, if condition has failed
continue
}
return err
} else {
// 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()
err := deleteConditionsFromNestedMaps(newNode)
if err != nil {
return err
}
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
}
}
}
}
nonAnchors = append(nonAnchors, key)
}
anchors = append(anchors, nestedAnchors...)
return append(anchors, nonAnchors...), fields, nil
return nil
}
// validateConditions checks all conditions from current map.
// 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 map, it must stop processing and skip entire rule.
func validateConditions(logger logr.Logger, pattern, resource *yaml.RNode) error {
conditions, err := filterKeys(pattern, anchor.IsConditionAnchor)
if err != nil {
return err
}
for _, condition := range conditions {
conditionKey := removeAnchor(condition)
if resource == nil || resource.Field(conditionKey) == nil {
continue
}
err = checkCondition(logger, pattern.Field(condition).Value, resource.Field(conditionKey).Value)
if err != nil {
return err
}
}
return nil
}
// handleAddings handles adding anchors.
// Remove anchor from pattern, if field already exists.
// Remove anchor wrapping from key, if field does not exist in the resource.
func handleAddings(logger logr.Logger, pattern, resource *yaml.RNode) error {
addings, err := filterKeys(pattern, anchor.IsAddingAnchor)
if err != nil {
return err
}
for _, adding := range addings {
key, _ := anchor.RemoveAnchor(adding)
if resource != nil && resource.Field(key) != nil {
// Resource already has this field.
// Delete the field with adding anchor from patch.
err = pattern.PipeE(yaml.Clear(adding))
if err != nil {
return err
}
continue
}
// Remove anchor wrap from patch field.
renameField(adding, key, pattern)
}
return nil
}
func filterKeys(pattern *yaml.RNode, condition func(string) bool) ([]string, error) {
keys := make([]string, 0)
fields, err := pattern.Fields()
if err != nil {
return keys, err
}
for _, key := range fields {
if condition(key) {
keys = append(keys, key)
continue
}
}
return keys, nil
}
func hasAnchors(pattern *yaml.RNode) bool {
switch pattern.YNode().Kind {
case yaml.MappingNode:
if yaml.MappingNode == pattern.YNode().Kind {
fields, err := pattern.Fields()
if err != nil {
return false
}
for _, key := range fields {
for _, key := range fields {
if anchor.IsConditionAnchor(key) || anchor.IsAddingAnchor(key) {
return true
}
patternMapNode := pattern.Field(key)
if !patternMapNode.IsNilOrEmpty() {
if hasAnchors(patternMapNode.Value) {
patternNode := pattern.Field(key)
if !patternNode.IsNilOrEmpty() {
if hasAnchors(patternNode.Value) {
return true
}
}
}
case yaml.SequenceNode:
pafs, err := pattern.Elements()
if err != nil {
return false
}
for _, pa := range pafs {
if hasAnchors(pa) {
return true
}
}
}
return false
}
func removeByIndex(pattern *yaml.RNode, removedIndex []int) []*yaml.Node {
preservedPatterns := make([]*yaml.Node, 0)
i := 0
for index := 0; index < (len(pattern.YNode().Content)); index++ {
if i < len(removedIndex) && index == removedIndex[i] {
i++
continue
}
preservedPatterns = append(preservedPatterns, pattern.YNode().Content[index])
func renameField(name, newName string, pattern *yaml.RNode) {
field := pattern.Field(name)
if field == nil {
return
}
return preservedPatterns
field.Key.YNode().Value = newName
}
func getIndexToBeRemoved(patternElements []*yaml.RNode) (removedIndex []int, err error) {
for index, patternElement := range patternElements {
if hasAnchors(patternElement) {
sfields, _, err := getAnchorSortedFields(patternElement)
func convertRNodeToInterface(document *yaml.RNode) (interface{}, error) {
if document.YNode().Kind == yaml.ScalarNode {
return document.YNode().Value, nil
}
rawDocument, err := document.MarshalJSON()
if err != nil {
return nil, err
}
var documentInterface interface{}
err = json.Unmarshal(rawDocument, &documentInterface)
if err != nil {
return nil, err
}
return documentInterface, nil
}
func checkCondition(logger logr.Logger, pattern *yaml.RNode, resource *yaml.RNode) error {
patternInterface, err := convertRNodeToInterface(pattern)
if err != nil {
return err
}
resourceInterface, err := convertRNodeToInterface(resource)
if err != nil {
return err
}
_, err = validate.ValidateResourceWithPattern(logger, resourceInterface, patternInterface)
if err != nil {
return NewConditionError(err)
}
return nil
}
func deleteConditionsFromNestedMaps(pattern *yaml.RNode) error {
if pattern.YNode().Kind != yaml.MappingNode {
return nil
}
fields, err := pattern.Fields()
if err != nil {
return err
}
for _, field := range fields {
if anchor.IsConditionAnchor(field) {
err = pattern.PipeE(yaml.Clear(field))
if err != nil {
return nil, err
return err
}
for _, key := range sfields {
if anchor.IsConditionAnchor(key) {
removedIndex = append(removedIndex, index)
break
} else {
child := pattern.Field(field).Value
if child != nil {
err = deleteConditionsFromNestedMaps(child)
if err != nil {
return err
}
}
}
}
return
return nil
}
func deleteConditionElements(pattern *yaml.RNode) error {
fields, err := pattern.Fields()
if err != nil {
return err
}
for _, field := range fields {
ok, err := deleteAnchors(pattern.Field(field).Value)
if err != nil {
return err
}
if ok {
err = pattern.PipeE(yaml.Clear(field))
if err != nil {
return err
}
}
}
return nil
}
// deleteAnchors deletes all the anchors and returns true,
// if this node must be deleted from patch.
// Node is considered to be deleted, if there were only
// anchors elemets. After anchors elements are removed,
// we have patch with nil values which could cause
// unnecessary resource elements deletion.
func deleteAnchors(node *yaml.RNode) (bool, error) {
switch node.YNode().Kind {
case yaml.MappingNode:
return deleteAnchorsInMap(node)
case yaml.SequenceNode:
return deleteAnchorsInList(node)
}
return false, nil
}
func deleteAnchorsInMap(node *yaml.RNode) (bool, error) {
conditions, err := filterKeys(node, anchor.IsConditionAnchor)
if err != nil {
return false, err
}
// Remove all conditions first.
for _, condition := range conditions {
err = node.PipeE(yaml.Clear(condition))
if err != nil {
return false, err
}
}
fields, err := node.Fields()
if err != nil {
return false, err
}
// Go further through the map elements.
for _, field := range fields {
ok, err := deleteAnchors(node.Field(field).Value)
if err != nil {
return false, err
}
// If we have at least one element without annchor,
// then we don't need to delete this element.
if !ok {
return false, nil
}
}
return true, nil
}
func deleteAnchorsInList(node *yaml.RNode) (bool, error) {
elements, err := node.Elements()
if err != nil {
return false, err
}
for i, element := range elements {
if hasAnchors(element) {
deleteListElement(node, i)
} else {
// This element also could have some conditions
// inside sub-arrays. Delete them too.
ok, err := deleteAnchors(element)
if err != nil {
return false, err
}
if err != nil {
return false, err
}
if ok {
deleteListElement(node, i)
}
}
}
elements, err = node.Elements()
if err != nil {
return false, err
}
if len(elements) == 0 {
return true, nil
}
return false, nil
}
func deleteListElement(list *yaml.RNode, i int) {
content := list.YNode().Content
list.YNode().Content = append(content[:i], content[i+1:]...)
}

File diff suppressed because it is too large Load diff

View file

@ -15,10 +15,10 @@ import (
// ValidateResourceWithPattern is a start of element-by-element validation process
// It assumes that validation is started from root, so "/" is passed
func ValidateResourceWithPattern(log logr.Logger, resource, pattern interface{}) (string, error) {
func ValidateResourceWithPattern(logger logr.Logger, resource, pattern interface{}) (string, error) {
// newAnchorMap - to check anchor key has values
ac := common.NewAnchorMap()
elemPath, err := validateResourceElement(log, resource, pattern, pattern, "/", ac)
elemPath, err := validateResourceElement(logger, resource, pattern, pattern, "/", ac)
if err != nil {
if common.IsConditionalAnchorError(err.Error()) {
return "", nil