mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-06 16:06:56 +00:00
Merge pull request #430 from nirmata/158_array_validation
158 array validation
This commit is contained in:
commit
bdb677abf6
13 changed files with 455 additions and 199 deletions
|
@ -53,16 +53,16 @@ For conditional anchors, the child element is considered to be part of the "if"
|
|||
|
||||
````yaml
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
allow-docker: true
|
||||
spec:
|
||||
metadata:
|
||||
labels:
|
||||
allow-docker: true
|
||||
(volumes):
|
||||
(hostPath):
|
||||
path: "/var/run/docker.sock"
|
||||
````
|
||||
|
||||
This reads as "If a hostPath volume exists and the path it equals /var/run/docker.sock, then a label "allow-docker" must be specified with a value of true."
|
||||
This reads as "If a hostPath volume exists and the path equals /var/run/docker.sock, then a label "allow-docker" must be specified with a value of true."
|
||||
|
||||
For equality anchors, a child element is considered to be part of the "then" clause. Consider this pattern:
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ func processOverlay(rule kyverno.Rule, resource unstructured.Unstructured) (resp
|
|||
patches, err := processOverlayPatches(resource.UnstructuredContent(), rule.Mutation.Overlay)
|
||||
// resource does not satisfy the overlay pattern, we dont apply this rule
|
||||
if err != nil && strings.Contains(err.Error(), "Conditions are not met") {
|
||||
glog.V(4).Infof("Resource %s/%s/%s does not meet the conditions in the rule %s with overlay pattern %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Name, rule.Mutation.Overlay)
|
||||
glog.Errorf("Resource %s/%s/%s does not meet the conditions in the rule %s with overlay pattern %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Name, rule.Mutation.Overlay)
|
||||
//TODO: send zero response and not consider this as applied?
|
||||
return RuleResponse{}, resource
|
||||
}
|
||||
|
@ -69,15 +69,17 @@ func processOverlay(rule kyverno.Rule, resource unstructured.Unstructured) (resp
|
|||
|
||||
// rule application succesfuly
|
||||
response.Success = true
|
||||
response.Message = fmt.Sprintf("succesfully process overlay")
|
||||
response.Message = fmt.Sprintf("successfully processed overlay")
|
||||
response.Patches = patches
|
||||
// apply the patches to the resource
|
||||
return response, patchedResource
|
||||
}
|
||||
|
||||
func processOverlayPatches(resource, overlay interface{}) ([][]byte, error) {
|
||||
|
||||
if !meetConditions(resource, overlay) {
|
||||
return nil, errors.New("Conditions are not met")
|
||||
if path, err := meetConditions(resource, overlay); err != nil {
|
||||
glog.V(4).Infof("Mutate rule: failed to validate condition at %s, err: %v", path, err)
|
||||
return nil, fmt.Errorf("Conditions are not met at %s, %v", path, err)
|
||||
}
|
||||
|
||||
return mutateResourceWithOverlay(resource, overlay)
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/nirmata/kyverno/pkg/engine/anchor"
|
||||
)
|
||||
|
||||
func meetConditions(resource, overlay interface{}) bool {
|
||||
func meetConditions(resource, overlay interface{}) (string, error) {
|
||||
return checkConditions(resource, overlay, "/")
|
||||
}
|
||||
|
||||
// resource and overlay should be the same type
|
||||
func checkConditions(resource, overlay interface{}, path string) (string, error) {
|
||||
// overlay has no anchor, return true
|
||||
if !hasNestedAnchors(overlay) {
|
||||
return true
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// resource item exists but has different type
|
||||
|
@ -18,157 +25,181 @@ func meetConditions(resource, overlay interface{}) bool {
|
|||
// conditon never be true in this case
|
||||
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
|
||||
if hasNestedAnchors(overlay) {
|
||||
glog.V(3).Infof("Found anchor on different types of element: overlay %T, resource %T\nSkip processing overlay.", overlay, resource)
|
||||
return false
|
||||
glog.V(4).Infof("Found anchor on different types of element at path %s: overlay %T, resource %T", path, overlay, resource)
|
||||
return path, fmt.Errorf("Found anchor on different types of element at path %s: overlay %T %v, resource %T %v", path, overlay, overlay, resource, resource)
|
||||
|
||||
}
|
||||
return true
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return checkConditions(resource, overlay)
|
||||
}
|
||||
|
||||
func checkConditions(resource, overlay interface{}) bool {
|
||||
switch typedOverlay := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
typedResource := resource.(map[string]interface{})
|
||||
return checkConditionOnMap(typedResource, typedOverlay)
|
||||
return checkConditionOnMap(typedResource, typedOverlay, path)
|
||||
case []interface{}:
|
||||
typedResource := resource.([]interface{})
|
||||
return checkConditionOnArray(typedResource, typedOverlay)
|
||||
return checkConditionOnArray(typedResource, typedOverlay, path)
|
||||
default:
|
||||
return true
|
||||
// anchor on non map/array is invalid:
|
||||
// - anchor defined on values
|
||||
glog.Warningln("Found invalid conditional anchor: anchor defined on values")
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkConditionOnMap(resourceMap, overlayMap map[string]interface{}) bool {
|
||||
anchors := getAnchorsFromMap(overlayMap)
|
||||
if len(anchors) > 0 {
|
||||
if !isConditionMetOnMap(resourceMap, anchors) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
func checkConditionOnMap(resourceMap, overlayMap map[string]interface{}, path string) (string, error) {
|
||||
anchors, overlayWithoutAnchor := getAnchorAndElementsFromMap(overlayMap)
|
||||
|
||||
// validate resource with conditions
|
||||
if newPath, err := validateConditionAnchorMap(resourceMap, anchors, path); err != nil {
|
||||
return newPath, err
|
||||
}
|
||||
|
||||
for key, value := range overlayMap {
|
||||
resourcePart, ok := resourceMap[key]
|
||||
|
||||
if ok && !anchor.IsAddingAnchor(key) {
|
||||
if !meetConditions(resourcePart, value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// traverse overlay pattern to further validate conditions
|
||||
if newPath, err := validateNonAnchorOverlayMap(resourceMap, overlayWithoutAnchor, path); err != nil {
|
||||
return newPath, err
|
||||
}
|
||||
|
||||
// key does not exist or isAddingAnchor
|
||||
return true
|
||||
// empty overlayMap
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func checkConditionOnArray(resource, overlay []interface{}) bool {
|
||||
if 0 == len(resource) {
|
||||
return false
|
||||
}
|
||||
|
||||
func checkConditionOnArray(resource, overlay []interface{}, path string) (string, error) {
|
||||
if 0 == len(overlay) {
|
||||
return true
|
||||
glog.Infof("Mutate overlay pattern is empty, path %s", path)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) {
|
||||
glog.Warningf("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
return false
|
||||
glog.V(4).Infof("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
return path, fmt.Errorf("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
}
|
||||
|
||||
return checkConditionsOnArrayOfSameTypes(resource, overlay)
|
||||
return checkConditionsOnArrayOfSameTypes(resource, overlay, path)
|
||||
}
|
||||
|
||||
func checkConditionsOnArrayOfSameTypes(resource, overlay []interface{}) bool {
|
||||
switch overlay[0].(type) {
|
||||
case map[string]interface{}:
|
||||
return checkConditionsOnArrayOfMaps(resource, overlay)
|
||||
default:
|
||||
glog.Warningf("Anchors not supported in overlay of array type %T\n", overlay[0])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func checkConditionsOnArrayOfMaps(resource, overlay []interface{}) bool {
|
||||
for _, overlayElement := range overlay {
|
||||
typedOverlay := overlayElement.(map[string]interface{})
|
||||
anchors, overlayWithoutAnchor := getElementsFromMap(typedOverlay)
|
||||
|
||||
if len(anchors) > 0 {
|
||||
if !validAnchorMap(anchors) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isConditionMet(resource, anchors) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for key, val := range overlayWithoutAnchor {
|
||||
if hasNestedAnchors(val) {
|
||||
for _, resourceElement := range resource {
|
||||
typedResource := resourceElement.(map[string]interface{})
|
||||
|
||||
if resourcePart, ok := typedResource[key]; ok {
|
||||
if !meetConditions(resourcePart, val) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validAnchorMap(anchors map[string]interface{}) bool {
|
||||
for _, val := range anchors {
|
||||
switch val.(type) {
|
||||
case map[string]interface{}, []interface{}:
|
||||
glog.Warning("Maps and arrays as patterns are not supported")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isConditionMet(resource []interface{}, anchors map[string]interface{}) bool {
|
||||
for _, resourceElement := range resource {
|
||||
typedResource := resourceElement.(map[string]interface{})
|
||||
for key, pattern := range anchors {
|
||||
key = key[1 : len(key)-1]
|
||||
|
||||
value, ok := typedResource[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(resource) == 1 {
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
ValidateValueWithPattern(value, pattern)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isConditionMetOnMap(resource, anchors map[string]interface{}) bool {
|
||||
for key, pattern := range anchors {
|
||||
key = key[1 : len(key)-1]
|
||||
|
||||
value, ok := resource[key]
|
||||
if !ok {
|
||||
func validateConditionAnchorMap(resourceMap, anchors map[string]interface{}, path string) (string, error) {
|
||||
for key, overlayValue := range anchors {
|
||||
// skip if key does not have condition anchor
|
||||
if !anchor.IsConditionAnchor(key) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
return false
|
||||
// validate condition anchor map
|
||||
noAnchorKey := removeAnchor(key)
|
||||
curPath := path + noAnchorKey + "/"
|
||||
if resourceValue, ok := resourceMap[noAnchorKey]; ok {
|
||||
// compare entire resourceValue block
|
||||
// return immediately on err since condition fails on this block
|
||||
if newPath, err := compareOverlay(resourceValue, overlayValue, curPath); err != nil {
|
||||
return newPath, err
|
||||
}
|
||||
} else {
|
||||
// noAnchorKey doesn't exist in resource
|
||||
return curPath, fmt.Errorf("resource field %s is not present", noAnchorKey)
|
||||
}
|
||||
}
|
||||
return true
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// compareOverlay compare values in anchormap and resourcemap
|
||||
// i.e. check if B1 == B2
|
||||
// overlay - (A): B1
|
||||
// resource - A: B2
|
||||
func compareOverlay(resource, overlay interface{}, path string) (string, error) {
|
||||
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
|
||||
glog.Errorf("Found anchor on different types of element: overlay %T, resource %T\nSkip processing overlay.", overlay, resource)
|
||||
return path, fmt.Errorf("")
|
||||
}
|
||||
|
||||
switch typedOverlay := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
typedResource := resource.(map[string]interface{})
|
||||
for key, overlayVal := range typedOverlay {
|
||||
noAnchorKey := removeAnchor(key)
|
||||
curPath := path + noAnchorKey + "/"
|
||||
resourceVal, ok := typedResource[noAnchorKey]
|
||||
if !ok {
|
||||
return curPath, fmt.Errorf("Field %s is not present", noAnchorKey)
|
||||
}
|
||||
if newPath, err := compareOverlay(resourceVal, overlayVal, curPath); err != nil {
|
||||
return newPath, err
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
typedResource := resource.([]interface{})
|
||||
for _, overlayElement := range typedOverlay {
|
||||
for _, resourceElement := range typedResource {
|
||||
if newPath, err := compareOverlay(resourceElement, overlayElement, path); err != nil {
|
||||
return newPath, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case string, float64, int, int64, bool, nil:
|
||||
if !ValidateValueWithPattern(resource, overlay) {
|
||||
glog.V(4).Infof("Mutate rule: failed validating value %v with overlay %v", resource, overlay)
|
||||
return path, fmt.Errorf("failed validating value %v with overlay %v", resource, overlay)
|
||||
}
|
||||
default:
|
||||
return path, fmt.Errorf("overlay has unknown type %T, value %v", overlay, overlay)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// validateNonAnchorOverlayMap validate anchor condition in overlay block without anchor
|
||||
func validateNonAnchorOverlayMap(resourceMap, overlayWithoutAnchor map[string]interface{}, path string) (string, error) {
|
||||
// validate resource map (anchors could exist in resource)
|
||||
for key, overlayValue := range overlayWithoutAnchor {
|
||||
curPath := path + key + "/"
|
||||
resourceValue, ok := resourceMap[key]
|
||||
if !ok {
|
||||
// policy: "(image)": "*:latest",
|
||||
// "imagePullPolicy": "IfNotPresent",
|
||||
// resource: "(image)": "*:latest",
|
||||
// the above case should be allowed
|
||||
continue
|
||||
}
|
||||
if newPath, err := checkConditions(resourceValue, overlayValue, curPath); err != nil {
|
||||
return newPath, err
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func checkConditionsOnArrayOfSameTypes(resource, overlay []interface{}, path string) (string, error) {
|
||||
switch overlay[0].(type) {
|
||||
case map[string]interface{}:
|
||||
return checkConditionsOnArrayOfMaps(resource, overlay, path)
|
||||
default:
|
||||
for i, overlayElement := range overlay {
|
||||
curPath := path + strconv.Itoa(i) + "/"
|
||||
path, err := checkConditions(resource[i], overlayElement, curPath)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func checkConditionsOnArrayOfMaps(resource, overlay []interface{}, path string) (string, error) {
|
||||
var newPath string
|
||||
var err error
|
||||
|
||||
for i, overlayElement := range overlay {
|
||||
for _, resourceMap := range resource {
|
||||
curPath := path + strconv.Itoa(i) + "/"
|
||||
newPath, err = checkConditionOnMap(resourceMap.(map[string]interface{}), overlayElement.(map[string]interface{}), curPath)
|
||||
// when resource has multiple same blocks of the overlay block
|
||||
// return true if there is one resource block meet the overlay pattern
|
||||
// reference: TestMeetConditions_AtleastOneExist
|
||||
if err == nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// report last error
|
||||
return newPath, err
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
|
@ -26,11 +27,11 @@ func TestMeetConditions_NoAnchor(t *testing.T) {
|
|||
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(nil, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err := meetConditions(nil, overlay)
|
||||
assert.Assert(t, err == nil)
|
||||
}
|
||||
|
||||
func TestMeetConditions_invalidConditionalAnchor(t *testing.T) {
|
||||
func TestMeetConditions_conditionalAnchorOnMap(t *testing.T) {
|
||||
resourceRaw := []byte(`
|
||||
{
|
||||
"apiVersion":"v1",
|
||||
|
@ -79,8 +80,8 @@ func TestMeetConditions_invalidConditionalAnchor(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, !res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.Assert(t, err != nil)
|
||||
|
||||
overlayRaw = []byte(`
|
||||
{
|
||||
|
@ -89,8 +90,8 @@ func TestMeetConditions_invalidConditionalAnchor(t *testing.T) {
|
|||
"ports":[
|
||||
{
|
||||
"name":"secure-connection",
|
||||
"port":444,
|
||||
"protocol":"UDP"
|
||||
"port":443,
|
||||
"(protocol)":"TCP"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -99,28 +100,24 @@ func TestMeetConditions_invalidConditionalAnchor(t *testing.T) {
|
|||
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res = meetConditions(resource, overlay)
|
||||
assert.Assert(t, !res)
|
||||
_, err = meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestMeetConditions_DifferentTypes(t *testing.T) {
|
||||
resourceRaw := []byte(`
|
||||
{
|
||||
"apiVersion":"v1",
|
||||
"kind":"Endpoints",
|
||||
"metadata":{
|
||||
"name":"test-endpoint",
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Endpoints",
|
||||
"metadata": {
|
||||
"name": "test-endpoint"
|
||||
},
|
||||
"subsets":[
|
||||
{
|
||||
"addresses":[
|
||||
{
|
||||
"ip":"192.168.10.171"
|
||||
}
|
||||
],
|
||||
"subsets": {
|
||||
"addresses": {
|
||||
"ip": "192.168.10.171"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
}
|
||||
}`)
|
||||
|
||||
overlayRaw := []byte(`
|
||||
{
|
||||
|
@ -142,8 +139,8 @@ func TestMeetConditions_DifferentTypes(t *testing.T) {
|
|||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
// anchor exist
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, !res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.Assert(t, strings.Contains(err.Error(), "Found anchor on different types of element at path /subsets/"))
|
||||
}
|
||||
|
||||
func TestMeetConditions_anchosInSameObject(t *testing.T) {
|
||||
|
@ -195,9 +192,8 @@ func TestMeetConditions_anchosInSameObject(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
// no anchor
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, !res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.Error(t, err, "failed validating value 443 with overlay 444")
|
||||
}
|
||||
|
||||
func TestMeetConditions_anchorOnPeer(t *testing.T) {
|
||||
|
@ -254,8 +250,8 @@ func TestMeetConditions_anchorOnPeer(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestMeetConditions_anchorsOnMetaAndSpec(t *testing.T) {
|
||||
|
@ -331,8 +327,8 @@ func TestMeetConditions_anchorsOnMetaAndSpec(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
var resourceRawAnchorOnPeers = []byte(`{
|
||||
|
@ -412,8 +408,8 @@ func TestMeetConditions_anchorsOnPeer_single(t *testing.T) {
|
|||
json.Unmarshal(resourceRawAnchorOnPeers, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestMeetConditions_anchorsOnPeer_two(t *testing.T) {
|
||||
|
@ -425,7 +421,7 @@ func TestMeetConditions_anchorsOnPeer_two(t *testing.T) {
|
|||
{
|
||||
"(image)": "*/nginx-unprivileged",
|
||||
"securityContext": {
|
||||
"(runAsNonRoot)": true,
|
||||
"(runAsNonRoot)": false,
|
||||
"allowPrivilegeEscalation": false
|
||||
},
|
||||
"env": [
|
||||
|
@ -446,8 +442,8 @@ func TestMeetConditions_anchorsOnPeer_two(t *testing.T) {
|
|||
json.Unmarshal(resourceRawAnchorOnPeers, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.Error(t, err, "failed validating value true with overlay false")
|
||||
|
||||
overlayRaw = []byte(`{
|
||||
"spec": {
|
||||
|
@ -475,8 +471,8 @@ func TestMeetConditions_anchorsOnPeer_two(t *testing.T) {
|
|||
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res = meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err = meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
|
||||
overlayRaw = []byte(`{
|
||||
"spec": {
|
||||
|
@ -504,8 +500,8 @@ func TestMeetConditions_anchorsOnPeer_two(t *testing.T) {
|
|||
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res = meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err = meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestMeetConditions_anchorsOnPeer_multiple(t *testing.T) {
|
||||
|
@ -538,8 +534,8 @@ func TestMeetConditions_anchorsOnPeer_multiple(t *testing.T) {
|
|||
json.Unmarshal(resourceRawAnchorOnPeers, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err := meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
|
||||
overlayRaw = []byte(`{
|
||||
"spec": {
|
||||
|
@ -567,8 +563,8 @@ func TestMeetConditions_anchorsOnPeer_multiple(t *testing.T) {
|
|||
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res = meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
_, err = meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
|
||||
overlayRaw = []byte(`{
|
||||
"spec": {
|
||||
|
@ -581,10 +577,10 @@ func TestMeetConditions_anchorsOnPeer_multiple(t *testing.T) {
|
|||
"runAsNonRoot": true,
|
||||
"(allowPrivilegeEscalation)": false
|
||||
},
|
||||
"env": [
|
||||
"(env)": [
|
||||
{
|
||||
"(name)": "ENV_KEY",
|
||||
"(value)": "ENV_VALUE"
|
||||
"name": "ENV_KEY",
|
||||
"value": "ENV_VALUE1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -596,7 +592,66 @@ func TestMeetConditions_anchorsOnPeer_multiple(t *testing.T) {
|
|||
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res = meetConditions(resource, overlay)
|
||||
assert.Assert(t, res)
|
||||
|
||||
_, err = meetConditions(resource, overlay)
|
||||
assert.Error(t, err, "failed validating value ENV_VALUE with overlay ENV_VALUE1")
|
||||
}
|
||||
|
||||
func TestMeetConditions_AtleastOneExist(t *testing.T) {
|
||||
overlayRaw := []byte(`
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": true
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"volumes": [
|
||||
{
|
||||
"(emptyDir)": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
// validate when resource has multiple same blocks
|
||||
resourceRaw := []byte(`
|
||||
{
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "k8s.gcr.io/test-webserver",
|
||||
"name": "test-container",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/cache",
|
||||
"name": "cache-volume"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "cache-volume1",
|
||||
"emptyDir": 1
|
||||
},
|
||||
{
|
||||
"name": "cache-volume2",
|
||||
"emptyDir": 2
|
||||
},
|
||||
{
|
||||
"name": "cache-volume3",
|
||||
"emptyDir": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
var resource, overlay interface{}
|
||||
|
||||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
path, err := meetConditions(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, len(path) == 0)
|
||||
}
|
||||
|
|
|
@ -494,7 +494,7 @@ func TestProcessOverlayPatches_ImagePullPolicy(t *testing.T) {
|
|||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err = processOverlayPatches(resource, overlay)
|
||||
assert.Error(t, err, "Conditions are not met")
|
||||
assert.Error(t, err, "Conditions are not met at /spec/template/metadata/labels/app/, failed validating value nginx with overlay nginx1")
|
||||
assert.Assert(t, len(patches) == 0)
|
||||
}
|
||||
|
||||
|
@ -590,10 +590,10 @@ func TestProcessOverlayPatches_AddingAnchorInsideListElement(t *testing.T) {
|
|||
"imagePullPolicy":"Always"
|
||||
},
|
||||
{
|
||||
"image":"debian:10"
|
||||
"image":"debian:latest"
|
||||
},
|
||||
{
|
||||
"image":"ubuntu:18.04",
|
||||
"image":"ubuntu:latest",
|
||||
"imagePullPolicy":"Always"
|
||||
}
|
||||
]
|
||||
|
@ -647,10 +647,11 @@ func TestProcessOverlayPatches_AddingAnchorInsideListElement(t *testing.T) {
|
|||
"imagePullPolicy":"Always"
|
||||
},
|
||||
{
|
||||
"image":"debian:10"
|
||||
"image":"debian:latest",
|
||||
"imagePullPolicy":"IfNotPresent"
|
||||
},
|
||||
{
|
||||
"image":"ubuntu:18.04",
|
||||
"image":"ubuntu:latest",
|
||||
"imagePullPolicy":"Always"
|
||||
}
|
||||
]
|
||||
|
@ -806,7 +807,7 @@ func TestProcessOverlayPatches_anchorOnPeer(t *testing.T) {
|
|||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err = processOverlayPatches(resource, overlay)
|
||||
assert.Error(t, err, "Conditions are not met")
|
||||
assert.Error(t, err, "Conditions are not met at /subsets/0/ports/0/port/, failed validating value 443 with overlay 444")
|
||||
assert.Assert(t, len(patches) == 0)
|
||||
}
|
||||
|
||||
|
@ -946,3 +947,96 @@ func TestProcessOverlayPatches_insertWithCondition(t *testing.T) {
|
|||
|
||||
compareJSONAsMap(t, expectedResult, doc)
|
||||
}
|
||||
|
||||
func TestProcessOverlayPatches_InsertIfNotPresentWithConditions(t *testing.T) {
|
||||
overlayRaw := []byte(`
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": true
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"volumes": [
|
||||
{
|
||||
"(emptyDir)": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
resourceRaw := []byte(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "pod-with-emptydir"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "k8s.gcr.io/test-webserver",
|
||||
"name": "test-container",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/cache",
|
||||
"name": "cache-volume"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "cache-volume",
|
||||
"emptyDir": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
var resource, overlay interface{}
|
||||
|
||||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err := processOverlayPatches(resource, overlay)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, len(patches) != 0)
|
||||
|
||||
doc, err := ApplyPatches(resourceRaw, patches)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expectedResult := []byte(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "pod-with-emptydir",
|
||||
"annotations": {
|
||||
"cluster-autoscaler.kubernetes.io/safe-to-evict": true
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "k8s.gcr.io/test-webserver",
|
||||
"name": "test-container",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/cache",
|
||||
"name": "cache-volume"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "cache-volume",
|
||||
"emptyDir": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
compareJSONAsMap(t, expectedResult, doc)
|
||||
}
|
||||
|
|
|
@ -210,12 +210,12 @@ func ParseNamespaceFromObject(bytes []byte) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// Validation
|
||||
// getAnchorsFromMap gets the conditional anchor map
|
||||
func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, value := range anchorsMap {
|
||||
if anchor.IsConditionAnchor(key) || anchor.IsExistanceAnchor(key) {
|
||||
if anchor.IsConditionAnchor(key) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
@ -223,12 +223,12 @@ func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{}
|
|||
return result
|
||||
}
|
||||
|
||||
// Mutation
|
||||
func getElementsFromMap(anchorsMap map[string]interface{}) (map[string]interface{}, map[string]interface{}) {
|
||||
// getAnchorAndElementsFromMap gets the condition anchor map and resource map without anchor
|
||||
func getAnchorAndElementsFromMap(anchorsMap map[string]interface{}) (map[string]interface{}, map[string]interface{}) {
|
||||
anchors := make(map[string]interface{})
|
||||
elementsWithoutanchor := make(map[string]interface{})
|
||||
for key, value := range anchorsMap {
|
||||
if anchor.IsConditionAnchor(key) || anchor.IsExistanceAnchor(key) {
|
||||
if anchor.IsConditionAnchor(key) {
|
||||
anchors[key] = value
|
||||
} else if !anchor.IsAddingAnchor(key) {
|
||||
elementsWithoutanchor[key] = value
|
||||
|
|
|
@ -283,9 +283,8 @@ func compareResourceSpec(t *testing.T, resource engine.ResourceSpec, expectedRes
|
|||
func compareRules(t *testing.T, rule engine.RuleResponse, expectedRule engine.RuleResponse) {
|
||||
// name
|
||||
if rule.Name != expectedRule.Name {
|
||||
t.Errorf("rule name: expected %s, recieved %s", expectedRule.Name, rule.Name)
|
||||
t.Errorf("rule name: expected %s, recieved %+v", expectedRule.Name, rule.Name)
|
||||
// as the rule names dont match no need to compare the rest of the information
|
||||
return
|
||||
}
|
||||
// type
|
||||
if rule.Type != expectedRule.Type {
|
||||
|
@ -443,11 +442,11 @@ func loadPolicy(t *testing.T, path string) *kyverno.ClusterPolicy {
|
|||
}
|
||||
|
||||
func testScenario(t *testing.T, path string) {
|
||||
//load scenario
|
||||
scenario, err := loadScenario(t, path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
runScenario(t, scenario)
|
||||
}
|
||||
|
|
|
@ -126,3 +126,7 @@ func Test_validate_disallow_docker_sock_mount(t *testing.T) {
|
|||
func Test_validate_disallow_helm_tiller(t *testing.T) {
|
||||
testScenario(t, "test/scenarios/samples/best_practices/scenario_validate_disallow_helm_tiller.yaml")
|
||||
}
|
||||
|
||||
func Test_mutate_add_safe_to_evict_annotation(t *testing.T) {
|
||||
testScenario(t, "test/scenarios/samples/best_practices/scenario_mutate_safe-to-evict.yaml")
|
||||
}
|
||||
|
|
22
samples/best_practices/add_safe-to-evict_annotation.yaml
Normal file
22
samples/best_practices/add_safe-to-evict_annotation.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
apiVersion: "kyverno.io/v1alpha1"
|
||||
kind: "ClusterPolicy"
|
||||
metadata:
|
||||
name: "annotate-emptyDir"
|
||||
annotations:
|
||||
policies.kyverno.io/category: AutoScaling
|
||||
policies.kyverno.io/description:
|
||||
spec:
|
||||
rules:
|
||||
- name: "add-safe-to-evict-annotation"
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- "Pod"
|
||||
mutate:
|
||||
overlay:
|
||||
metadata:
|
||||
annotations:
|
||||
+(cluster-autoscaler.kubernetes.io/safe-to-evict): true
|
||||
spec:
|
||||
volumes:
|
||||
- (emptyDir): {}
|
16
test/output/pod-with-emptydir.yaml
Normal file
16
test/output/pod-with-emptydir.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod-with-emptydir
|
||||
annotations:
|
||||
cluster-autoscaler.kubernetes.io/safe-to-evict: true
|
||||
spec:
|
||||
containers:
|
||||
- image: k8s.gcr.io/test-webserver
|
||||
name: test-container
|
||||
volumeMounts:
|
||||
- mountPath: /cache
|
||||
name: cache-volume
|
||||
volumes:
|
||||
- name: cache-volume
|
||||
emptyDir: {}
|
14
test/resources/pod-with-emptydir.yaml
Normal file
14
test/resources/pod-with-emptydir.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod-with-emptydir
|
||||
spec:
|
||||
containers:
|
||||
- image: k8s.gcr.io/test-webserver
|
||||
name: test-container
|
||||
volumeMounts:
|
||||
- mountPath: /cache
|
||||
name: cache-volume
|
||||
volumes:
|
||||
- name: cache-volume
|
||||
emptyDir: {}
|
|
@ -1,4 +1,4 @@
|
|||
# file path relative to project root
|
||||
# file path is relative to project root
|
||||
input:
|
||||
policy: test/policy/mutate/policy_mutate_validate_qos.yaml
|
||||
resource: test/resources/resource_mutate_validate_qos.yaml
|
||||
|
@ -16,7 +16,7 @@ expected:
|
|||
- name: add-memory-limit
|
||||
type: Mutation
|
||||
success: true
|
||||
message: succesfully process overlay
|
||||
message: successfully processed overlay
|
||||
validation:
|
||||
policyresponse:
|
||||
policy: policy-qos
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# file path is relative to project root
|
||||
input:
|
||||
policy: samples/best_practices/add_safe-to-evict_annotation.yaml
|
||||
resource: test/resources/pod-with-emptydir.yaml
|
||||
expected:
|
||||
mutation:
|
||||
patchedresource: test/output/pod-with-emptydir.yaml
|
||||
policyresponse:
|
||||
policy: annotate-emptyDir
|
||||
resource:
|
||||
kind: Pod
|
||||
apiVersion: v1
|
||||
namespace: ''
|
||||
name: pod-with-emptydir
|
||||
rules:
|
||||
- name: add-safe-to-evict-annotation
|
||||
type: Mutation
|
||||
success: true
|
||||
message: "successfully processed overlay"
|
Loading…
Add table
Reference in a new issue