2020-09-01 21:42:05 +05:30
package mutate
import (
2020-10-07 11:12:31 -07:00
anchor "github.com/kyverno/kyverno/pkg/engine/anchor/common"
2020-09-01 21:42:05 +05:30
"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
}
for _ , key := range sfields {
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
}
2020-09-19 00:48:13 +05:30
2020-09-01 21:42:05 +05:30
// 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 )
2020-09-14 22:55:00 +05:30
// 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 )
}
2020-09-01 21:42:05 +05:30
}
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
}
}
2020-09-19 00:48:13 +05:30
} 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
}
}
2020-09-01 21:42:05 +05:30
}
}
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 resouce 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 sequence with anchors
err = removeAnchorSequence ( pattern , resource )
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 sequence 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 removeAnchorSequence ( pattern , resource * yaml . RNode ) error {
patternElements , err := pattern . Elements ( )
if err != nil {
return err
}
for index , patternElement := range patternElements {
if hasAnchors ( patternElement ) {
sfields , _ , err := getAnchorSortedFields ( patternElement )
if err != nil {
return err
}
for _ , key := range sfields {
if anchor . IsConditionAnchor ( key ) {
pattern . YNode ( ) . Content = append ( pattern . YNode ( ) . Content [ : index ] , pattern . YNode ( ) . Content [ index + 1 : ] ... )
}
}
}
}
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 {
2020-09-19 00:48:13 +05:30
if anchor . IsConditionAnchor ( key ) || anchor . IsAddingAnchor ( key ) {
2020-09-01 21:42:05 +05:30
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 copyData ( resource * yaml . RNode ) ( * yaml . RNode , error ) {
newNodeString , err := resource . String ( )
if err != nil {
return resource , err
}
newNode , err := yaml . Parse ( newNodeString )
return newNode , err
}