mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-30 19:35:06 +00:00
conditional anchor preprocessing for patch strategic merge (#1090)
* conditional anchor preprocessing for patch strategic merge * modified sequence pre processing and added unit test * merged master * go fmt * corrected mistake and added error handling to policy validate
This commit is contained in:
parent
e0f617b383
commit
3690bf5fff
7 changed files with 603 additions and 29 deletions
5
go.mod
5
go.mod
|
@ -38,7 +38,7 @@ require (
|
|||
google.golang.org/appengine v1.6.5 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2
|
||||
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71
|
||||
gotest.tools v2.2.0+incompatible
|
||||
k8s.io/api v0.17.4
|
||||
k8s.io/apiextensions-apiserver v0.17.4
|
||||
|
@ -48,9 +48,8 @@ require (
|
|||
k8s.io/klog v1.0.0
|
||||
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6
|
||||
sigs.k8s.io/controller-runtime v0.5.0
|
||||
sigs.k8s.io/kustomize v2.0.3+incompatible
|
||||
sigs.k8s.io/kustomize/api v0.5.1
|
||||
sigs.k8s.io/kustomize/kyaml v0.4.1
|
||||
sigs.k8s.io/kustomize/kyaml v0.6.1
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
|
|
7
go.sum
7
go.sum
|
@ -910,6 +910,7 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y
|
|||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
|
@ -1055,6 +1056,7 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -1266,6 +1268,8 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
|||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -1318,12 +1322,15 @@ mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZI
|
|||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
sigs.k8s.io/controller-runtime v0.5.0 h1:CbqIy5fbUX+4E9bpnBFd204YAzRYlM9SWW77BbrcDQo=
|
||||
sigs.k8s.io/controller-runtime v0.5.0/go.mod h1:REiJzC7Y00U+2YkMbT8wxgrsX5USpXKGhb2sCtAXiT8=
|
||||
sigs.k8s.io/kustomize v1.0.11 h1:Yb+6DDt9+aR2AvQApvUaKS/ugteeG4MPyoFeUHiPOjk=
|
||||
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
|
||||
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
|
||||
sigs.k8s.io/kustomize/api v0.5.1 h1:iHGTs5LcnJGqHstUSxWD/kX6XZgmd82x79LLlZwDU0I=
|
||||
sigs.k8s.io/kustomize/api v0.5.1/go.mod h1:LGqJ9ZWOnWDqlECqrFgNUyEqSJc6ooA9ZiWZ4KFZv+I=
|
||||
sigs.k8s.io/kustomize/kyaml v0.4.1 h1:NEqA/35upoAjb+I5vh1ODUqxoX4DOrezeQa9BhhG5Co=
|
||||
sigs.k8s.io/kustomize/kyaml v0.4.1/go.mod h1:XJL84E6sOFeNrQ7CADiemc1B0EjIxHo3OhW4o1aJYNw=
|
||||
sigs.k8s.io/kustomize/kyaml v0.6.1 h1:mwffj5vt3MPdbWV3fZnnwol8SO7sUoGdgejBlvseyak=
|
||||
sigs.k8s.io/kustomize/kyaml v0.6.1/go.mod h1:bEzbO5pN9OvlEeCLvFHo8Pu7SA26Herc2m60UeWZBdI=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
|
|
|
@ -109,7 +109,7 @@ func NewConfigData(rclient kubernetes.Interface, cmInformer informers.ConfigMapI
|
|||
if excludeGroupRole != "" {
|
||||
cd.log.Info("init configuration from commandline arguments for excludeGroupRole")
|
||||
cd.initRbac("excludeRoles", excludeGroupRole)
|
||||
}else{
|
||||
} else {
|
||||
cd.initRbac("excludeRoles", "")
|
||||
}
|
||||
|
||||
|
@ -187,9 +187,9 @@ func (cd *ConfigData) load(cm v1.ConfigMap) {
|
|||
defer cd.mux.Unlock()
|
||||
// get resource filters
|
||||
filters, ok := cm.Data["resourceFilters"]
|
||||
if !ok {
|
||||
if !ok {
|
||||
logger.V(4).Info("configuration: No resourceFilters defined in ConfigMap")
|
||||
}else{
|
||||
} else {
|
||||
newFilters := parseKinds(filters)
|
||||
if reflect.DeepEqual(newFilters, cd.filters) {
|
||||
logger.V(4).Info("resourceFilters did not change")
|
||||
|
@ -202,7 +202,7 @@ func (cd *ConfigData) load(cm v1.ConfigMap) {
|
|||
|
||||
// get resource filters
|
||||
excludeGroupRole, ok := cm.Data["excludeGroupRole"]
|
||||
if !ok {
|
||||
if !ok {
|
||||
logger.V(4).Info("configuration: No excludeGroupRole defined in ConfigMap")
|
||||
}
|
||||
newExcludeGroupRoles := parseRbac(excludeGroupRole)
|
||||
|
@ -217,9 +217,9 @@ func (cd *ConfigData) load(cm v1.ConfigMap) {
|
|||
|
||||
// get resource filters
|
||||
excludeUsername, ok := cm.Data["excludeUsername"]
|
||||
if !ok {
|
||||
if !ok {
|
||||
logger.V(4).Info("configuration: No excludeUsername defined in ConfigMap")
|
||||
}else{
|
||||
} else {
|
||||
excludeUsernames := parseRbac(excludeUsername)
|
||||
if reflect.DeepEqual(excludeUsernames, cd.excludeUsername) {
|
||||
logger.V(4).Info("excludeGroupRole did not change")
|
||||
|
|
|
@ -3,7 +3,9 @@ package mutate
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
@ -27,6 +29,26 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
|
|||
logger.V(4).Info("finished applying strategicMerge patch", "processingTime", resp.RuleStats.ProcessingTime.String())
|
||||
}()
|
||||
|
||||
// ====== Meet Conditions =======
|
||||
if path, overlayerr := meetConditions(log, resource.UnstructuredContent(), overlay); !reflect.DeepEqual(overlayerr, overlayError{}) {
|
||||
switch overlayerr.statusCode {
|
||||
// anchor key does not exist in the resource, skip applying policy
|
||||
case conditionNotPresent:
|
||||
log.V(4).Info("skip applying policy", "path", path, "error", overlayerr)
|
||||
log.V(3).Info("skip applying rule", "reason", "conditionNotPresent")
|
||||
resp.Success = true
|
||||
return resp, resource
|
||||
// anchor key is not satisfied in the resource, skip applying policy
|
||||
case conditionFailure:
|
||||
log.V(4).Info("failed to validate condition", "path", path, "error", overlayerr)
|
||||
log.V(3).Info("skip applying rule", "reason", "conditionFailure")
|
||||
resp.Success = true
|
||||
resp.Message = overlayerr.ErrorMsg()
|
||||
return resp, resource
|
||||
}
|
||||
}
|
||||
// ============================
|
||||
|
||||
overlayBytes, err := json.Marshal(overlay)
|
||||
if err != nil {
|
||||
resp.Success = false
|
||||
|
@ -42,7 +64,6 @@ 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))
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed to apply patchStrategicMerge: %v", err)
|
||||
|
@ -78,14 +99,26 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
|
|||
}
|
||||
|
||||
func strategicMergePatch(base, overlay string) ([]byte, error) {
|
||||
patch := yaml.MustParse(overlay)
|
||||
|
||||
patch := yaml.MustParse(overlay)
|
||||
preprocessedYaml, err := preProcessStrategicMergePatch(overlay, base)
|
||||
if err != nil {
|
||||
return []byte{}, errors.New(fmt.Sprintf("failed to preProcess rule : %+v", err))
|
||||
}
|
||||
patch = preprocessedYaml
|
||||
f := patchstrategicmerge.Filter{
|
||||
Patch: patch,
|
||||
}
|
||||
|
||||
baseObj := buffer{Buffer: bytes.NewBufferString(base)}
|
||||
err := filtersutil.ApplyToJSON(f, baseObj)
|
||||
err = filtersutil.ApplyToJSON(f, baseObj)
|
||||
|
||||
return baseObj.Bytes(), err
|
||||
}
|
||||
|
||||
func preProcessStrategicMergePatch(pattern, resource string) (*yaml.RNode, error) {
|
||||
patternNode := yaml.MustParse(pattern)
|
||||
resourceNode := yaml.MustParse(resource)
|
||||
err := preProcessPattern(patternNode, resourceNode)
|
||||
return patternNode, err
|
||||
}
|
||||
|
|
506
pkg/engine/mutate/strategicPreprocessing.go
Normal file
506
pkg/engine/mutate/strategicPreprocessing.go
Normal file
|
@ -0,0 +1,506 @@
|
|||
package mutate
|
||||
|
||||
import (
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
anchor "github.com/nirmata/kyverno/pkg/engine/anchor/common"
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
if anchor.IsConditionAnchor(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 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
|
||||
}
|
26
pkg/engine/mutate/strategicPreprocessing_test.go
Normal file
26
pkg/engine/mutate/strategicPreprocessing_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package mutate
|
||||
|
||||
import (
|
||||
assertnew "github.com/stretchr/testify/assert"
|
||||
"gotest.tools/assert"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_preProcessStrategicMergePatch(t *testing.T) {
|
||||
rawPolicy := []byte(`{"metadata":{"+(annotations)":{"+(annotation1)":"atest1"},"labels":{"+(label1)":"test1"}},"spec":{"(volumes)":[{"(hostPath)":{"path":"/var/run/docker.sock"}}],"containers":[{"(image)":"*:latest","command":["ls"],"imagePullPolicy":"Always"}]}}`)
|
||||
|
||||
rawResource := []byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{"annotation1":"atest2"},"labels":{"label1":"test2","label2":"test2"},"name":"check-root-user"},"spec":{"containers":[{"command":["ll"],"image":"nginx:latest","imagePullPolicy":"Never","name":"nginx"},{"image":"busybox:latest","imagePullPolicy":"Never","name":"busybox"}],"volumes":[{"hostPath":{"path":"/var/run/docker.sock"},"name":"test-volume"}]}}`)
|
||||
|
||||
expected := `{"metadata": {"annotations": {"annotation1": "atest1"}, "labels": {"label1": "test1"}},"spec": {"containers": [{"command": ["ls", "ll"], "imagePullPolicy": "Always", "name": "nginx"},{"command": ["ls"], "imagePullPolicy": "Always", "name": "busybox"}]}}`
|
||||
|
||||
preProcessedPolicy, err := preProcessStrategicMergePatch(string(rawPolicy), string(rawResource))
|
||||
assert.NilError(t, err)
|
||||
output, err := preProcessedPolicy.String()
|
||||
assert.NilError(t, err)
|
||||
re := regexp.MustCompile("\\n")
|
||||
if !assertnew.Equal(t, strings.ReplaceAll(expected, " ", ""), strings.ReplaceAll(re.ReplaceAllString(output, ""), " ", "")) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ func Validate(policyRaw []byte, client *dclient.Client, mock bool, openAPIContro
|
|||
return fmt.Errorf("failed to unmarshal policy admission request err %v", err)
|
||||
}
|
||||
|
||||
if common.PolicyHasVariables(p) && common.PolicyHasNonAllowedVariables(p){
|
||||
if common.PolicyHasVariables(p) && common.PolicyHasNonAllowedVariables(p) {
|
||||
return fmt.Errorf("policy contains non allowed variables")
|
||||
}
|
||||
|
||||
|
@ -55,27 +55,30 @@ func Validate(policyRaw []byte, client *dclient.Client, mock bool, openAPIContro
|
|||
// validate Cluster Resources in namespaced cluster policy
|
||||
// For namespaced cluster policy, ClusterResource type field and values are not allowed in match and exclude
|
||||
if !mock && p.ObjectMeta.Namespace != "" {
|
||||
var Empty struct{}
|
||||
clusterResourcesMap := make(map[string]*struct{})
|
||||
// Get all the cluster type kind supported by cluster
|
||||
res, _ := client.GetDiscoveryCache().ServerPreferredResources()
|
||||
for _, resList := range res {
|
||||
for _, r := range resList.APIResources {
|
||||
if r.Namespaced == false {
|
||||
if clusterResourcesMap[r.Kind] != nil {
|
||||
clusterResourcesMap[r.Kind] = &Empty
|
||||
}
|
||||
var Empty struct{}
|
||||
clusterResourcesMap := make(map[string]*struct{})
|
||||
// Get all the cluster type kind supported by cluster
|
||||
res, err := client.GetDiscoveryCache().ServerPreferredResources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, resList := range res {
|
||||
for _, r := range resList.APIResources {
|
||||
if r.Namespaced == false {
|
||||
if clusterResourcesMap[r.Kind] != nil {
|
||||
clusterResourcesMap[r.Kind] = &Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clusterResources := make([]string, 0, len(clusterResourcesMap))
|
||||
for k := range clusterResourcesMap {
|
||||
clusterResources = append(clusterResources, k)
|
||||
}
|
||||
return checkClusterResourceInMatchAndExclude(rule, clusterResources)
|
||||
}
|
||||
|
||||
clusterResources := make([]string, 0, len(clusterResourcesMap))
|
||||
for k := range clusterResourcesMap {
|
||||
clusterResources = append(clusterResources, k)
|
||||
}
|
||||
return checkClusterResourceInMatchAndExclude(rule, clusterResources)
|
||||
}
|
||||
|
||||
if doesMatchAndExcludeConflict(rule) {
|
||||
return fmt.Errorf("path: spec.rules[%v]: rule is matching an empty set", rule.Name)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue