1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00
kyverno/pkg/engine/mutate/strategicPreprocessing.go
shuting 27e2b9abd5
Fix mutation panic (#1462)
* fix #1454

* - add unit tests; - rename method
2021-01-08 16:45:39 -08:00

552 lines
15 KiB
Go

package mutate
import (
anchor "github.com/kyverno/kyverno/pkg/engine/anchor/common"
"github.com/minio/minio/pkg/wildcard"
yaml "sigs.k8s.io/kustomize/kyaml/yaml"
)
// preProcessPattern - Dynamically preProcess the yaml
// 1> For conditional anchor remove anchors from the pattern.
// 2> For Adding anchors remove anchor tags.
// The whole yaml is structured as a pointer tree.
// 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".
// https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/yaml/rnode.go
func preProcessPattern(pattern, resource *yaml.RNode) error {
switch pattern.YNode().Kind {
case yaml.MappingNode:
err := walkMap(pattern, resource)
if err != nil {
return err
}
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 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
*/
/* 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)
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
}
// 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
}
}
} 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
}
}
}
}
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()
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()
if err != nil {
return fields, fields, 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
}
}
nonAnchors = append(nonAnchors, key)
}
anchors = append(anchors, nestedAnchors...)
return append(anchors, nonAnchors...), fields, nil
}
func hasAnchors(pattern *yaml.RNode) bool {
switch pattern.YNode().Kind {
case yaml.MappingNode:
fields, err := pattern.Fields()
if err != nil {
return false
}
for _, key := range fields {
if anchor.IsConditionAnchor(key) || anchor.IsAddingAnchor(key) {
return true
}
patternMapNode := pattern.Field(key)
if !patternMapNode.IsNilOrEmpty() {
if hasAnchors(patternMapNode.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])
}
return preservedPatterns
}
func getIndexToBeRemoved(patternElements []*yaml.RNode) (removedIndex []int, err error) {
for index, patternElement := range patternElements {
if hasAnchors(patternElement) {
sfields, _, err := getAnchorSortedFields(patternElement)
if err != nil {
return nil, err
}
for _, key := range sfields {
if anchor.IsConditionAnchor(key) {
removedIndex = append(removedIndex, index)
break
}
}
}
}
return
}