mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-13 19:28:55 +00:00
127: Implemented usage of result package in validation and mutation functions.
This commit is contained in:
parent
55fe28d5bf
commit
e571f730b2
19 changed files with 625 additions and 343 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
certs
|
||||
Gopkg.lock
|
||||
.vscode
|
||||
kyverno
|
||||
gh-pages/public
|
||||
_output
|
||||
_output
|
||||
|
|
|
@ -46,7 +46,7 @@ func Test_ResourceDescription_EmptyNameAndSelector(t *testing.T) {
|
|||
Kinds: []string{"Deployment"},
|
||||
}
|
||||
err := resourceDescription.Validate()
|
||||
assert.Assert(t, err != nil)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func Test_Patch_EmptyPath(t *testing.T) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
event "github.com/nirmata/kyverno/pkg/event"
|
||||
"github.com/nirmata/kyverno/pkg/event"
|
||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||
violation "github.com/nirmata/kyverno/pkg/violation"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
// The each function returns the changes that need to be applied on the resource
|
||||
// the caller is responsible to apply the changes to the resource
|
||||
|
||||
// ProcessExisting checks for mutation and validation violations of existing resources
|
||||
func ProcessExisting(policy types.Policy, rawResource []byte) ([]violation.Info, []event.Info, error) {
|
||||
var violations []violation.Info
|
||||
var events []event.Info
|
||||
|
|
|
@ -1,52 +1,59 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Mutate performs mutation. Overlay first and then mutation patches
|
||||
// TODO: return events and violations
|
||||
func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([]PatchBytes, []byte) {
|
||||
var policyPatches []PatchBytes
|
||||
var processedPatches []PatchBytes
|
||||
var err error
|
||||
func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([]PatchBytes, result.Result) {
|
||||
var allPatches []PatchBytes
|
||||
|
||||
patchedDocument := rawResource
|
||||
policyResult := result.NewPolicyApplicationResult(policy.Name)
|
||||
|
||||
for _, rule := range policy.Spec.Rules {
|
||||
if rule.Mutation == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ruleApplicationResult := result.NewRuleApplicationResult(rule.Name)
|
||||
|
||||
ok := ResourceMeetsDescription(rawResource, rule.ResourceDescription, gvk)
|
||||
if !ok {
|
||||
glog.Infof("Rule \"%s\" is not applicable to resource\n", rule.Name)
|
||||
ruleApplicationResult.AddMessagef("Rule %s is not applicable to resource\n", rule.Name)
|
||||
policyResult = result.Append(policyResult, &ruleApplicationResult)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process Overlay
|
||||
|
||||
if rule.Mutation.Overlay != nil {
|
||||
overlayPatches, err := ProcessOverlay(policy, rawResource, gvk)
|
||||
if err != nil {
|
||||
glog.Warningf("Overlay application has failed for rule %s in policy %s, err: %v\n", rule.Name, policy.ObjectMeta.Name, err)
|
||||
overlayPatches, ruleResult := ProcessOverlay(rule.Mutation.Overlay, rawResource, gvk)
|
||||
if result.Success != ruleResult.GetReason() {
|
||||
ruleApplicationResult.MergeWith(&ruleResult)
|
||||
ruleApplicationResult.AddMessagef("Overlay application has failed for rule %s in policy %s\n", rule.Name, policy.ObjectMeta.Name)
|
||||
} else {
|
||||
policyPatches = append(policyPatches, overlayPatches...)
|
||||
ruleApplicationResult.AddMessagef("Success")
|
||||
allPatches = append(allPatches, overlayPatches...)
|
||||
}
|
||||
}
|
||||
|
||||
// Process Patches
|
||||
|
||||
if rule.Mutation.Patches != nil {
|
||||
processedPatches, patchedDocument, err = ProcessPatches(rule.Mutation.Patches, patchedDocument)
|
||||
if err != nil {
|
||||
glog.Warningf("Patches application has failed for rule %s in policy %s, err: %v\n", rule.Name, policy.ObjectMeta.Name, err)
|
||||
rulePatches, ruleResult := ProcessPatches(rule.Mutation.Patches, patchedDocument)
|
||||
|
||||
if result.Success != ruleResult.GetReason() {
|
||||
ruleApplicationResult.MergeWith(&ruleResult)
|
||||
ruleApplicationResult.AddMessagef("Patches application has failed for rule %s in policy %s\n", rule.Name, policy.ObjectMeta.Name)
|
||||
} else {
|
||||
policyPatches = append(policyPatches, processedPatches...)
|
||||
ruleApplicationResult.AddMessagef("Success")
|
||||
allPatches = append(allPatches, rulePatches...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return policyPatches, patchedDocument
|
||||
return allPatches, policyResult
|
||||
}
|
||||
|
|
|
@ -7,57 +7,42 @@ import (
|
|||
"strconv"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/golang/glog"
|
||||
|
||||
kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// ProcessOverlay handles validating admission request
|
||||
// Checks the target resourse for rules defined in the policy
|
||||
func ProcessOverlay(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([]PatchBytes, error) {
|
||||
func ProcessOverlay(overlay interface{}, rawResource []byte, gvk metav1.GroupVersionKind) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var resource interface{}
|
||||
var appliedPatches []PatchBytes
|
||||
json.Unmarshal(rawResource, &resource)
|
||||
|
||||
var appliedPatches []PatchBytes
|
||||
|
||||
for _, rule := range policy.Spec.Rules {
|
||||
if rule.Mutation == nil || rule.Mutation.Overlay == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ok := ResourceMeetsDescription(rawResource, rule.ResourceDescription, gvk)
|
||||
if !ok {
|
||||
glog.Infof("Rule \"%s\" is not applicable to resource\n", rule.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
overlay := *rule.Mutation.Overlay
|
||||
patch, err := applyOverlay(resource, overlay, "/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Overlay application failed: %v", err.Error())
|
||||
}
|
||||
overlayApplicationResult := result.NewRuleApplicationResult("")
|
||||
if overlay == nil {
|
||||
return nil, overlayApplicationResult
|
||||
}
|
||||
|
||||
patch := applyOverlay(resource, overlay, "/", &overlayApplicationResult)
|
||||
if overlayApplicationResult.GetReason() == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch...)
|
||||
}
|
||||
|
||||
return appliedPatches, nil
|
||||
return appliedPatches, overlayApplicationResult
|
||||
}
|
||||
|
||||
// goes down through overlay and resource trees and applies overlay
|
||||
func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, error) {
|
||||
func applyOverlay(resource, overlay interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
|
||||
var appliedPatches []PatchBytes
|
||||
|
||||
// resource item exists but has different type - replace
|
||||
// all subtree within this path by overlay
|
||||
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
|
||||
patch, err := replaceSubtree(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := replaceSubtree(overlay, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
return appliedPatches, nil
|
||||
return appliedPatches
|
||||
}
|
||||
|
||||
switch typedOverlay := overlay.(type) {
|
||||
|
@ -72,17 +57,15 @@ func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, err
|
|||
resourcePart, ok := typedResource[key]
|
||||
|
||||
if ok {
|
||||
patches, err := applyOverlay(resourcePart, value, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patches := applyOverlay(resourcePart, value, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
|
||||
} else {
|
||||
patch, err := insertSubtree(value, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := insertSubtree(value, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
|
@ -90,44 +73,38 @@ func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, err
|
|||
}
|
||||
case []interface{}:
|
||||
typedResource := resource.([]interface{})
|
||||
patches, err := applyOverlayToArray(typedResource, typedOverlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patches := applyOverlayToArray(typedResource, typedOverlay, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
case string, float64, int64, bool:
|
||||
patch, err := replaceSubtree(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := replaceSubtree(overlay, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
default:
|
||||
return nil, fmt.Errorf("Overlay has unsupported type: %T", overlay)
|
||||
res.FailWithMessagef("Overlay has unsupported type: %T", overlay)
|
||||
return nil
|
||||
}
|
||||
|
||||
return appliedPatches, nil
|
||||
return appliedPatches
|
||||
}
|
||||
|
||||
// for each overlay and resource array elements and applies overlay
|
||||
func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchBytes, error) {
|
||||
func applyOverlayToArray(resource, overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
|
||||
var appliedPatches []PatchBytes
|
||||
if len(overlay) == 0 {
|
||||
return nil, fmt.Errorf("overlay does not support empty arrays")
|
||||
res.FailWithMessagef("Empty array detected in the overlay")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(resource) == 0 {
|
||||
patches, err := fillEmptyArray(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return patches, nil
|
||||
return fillEmptyArray(overlay, path, res)
|
||||
}
|
||||
|
||||
if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) {
|
||||
return nil, fmt.Errorf("overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
res.FailWithMessagef("overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
switch overlay[0].(type) {
|
||||
|
@ -141,53 +118,49 @@ func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchB
|
|||
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
if !skipArrayObject(typedResource, anchors) {
|
||||
patches, err := applyOverlay(resourceElement, overlayElement, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patches := applyOverlay(resourceElement, overlayElement, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
|
||||
}
|
||||
} else if hasNestedAnchors(overlayElement) {
|
||||
for i, resourceElement := range resource {
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
patches, err := applyOverlay(resourceElement, overlayElement, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patches := applyOverlay(resourceElement, overlayElement, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
} else {
|
||||
currentPath := path + "0/"
|
||||
patch, err := insertSubtree(overlayElement, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := insertSubtree(overlayElement, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
default:
|
||||
path += "0/"
|
||||
for _, value := range overlay {
|
||||
patch, err := insertSubtree(value, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := insertSubtree(value, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches, nil
|
||||
return appliedPatches
|
||||
}
|
||||
|
||||
// In case of empty resource array
|
||||
// append all non-anchor items to front
|
||||
func fillEmptyArray(overlay []interface{}, path string) ([]PatchBytes, error) {
|
||||
func fillEmptyArray(overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
|
||||
var appliedPatches []PatchBytes
|
||||
if len(overlay) == 0 {
|
||||
return nil, fmt.Errorf("overlay does not support empty arrays")
|
||||
res.FailWithMessagef("Empty array detected in the overlay")
|
||||
return nil
|
||||
}
|
||||
|
||||
path += "0/"
|
||||
|
@ -199,55 +172,33 @@ func fillEmptyArray(overlay []interface{}, path string) ([]PatchBytes, error) {
|
|||
anchors := GetAnchorsFromMap(typedOverlay)
|
||||
|
||||
if len(anchors) == 0 {
|
||||
patch, err := insertSubtree(overlayElement, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := insertSubtree(overlayElement, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
default:
|
||||
for _, overlayElement := range overlay {
|
||||
patch, err := insertSubtree(overlayElement, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patch := insertSubtree(overlayElement, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches, nil
|
||||
return appliedPatches
|
||||
}
|
||||
|
||||
// Checks if array object matches anchors. If not - skip - return true
|
||||
func skipArrayObject(object, anchors map[string]interface{}) bool {
|
||||
for key, pattern := range anchors {
|
||||
key = key[1 : len(key)-1]
|
||||
|
||||
value, ok := object[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
func insertSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte {
|
||||
return processSubtree(overlay, path, "add", res)
|
||||
}
|
||||
|
||||
func insertSubtree(overlay interface{}, path string) ([]byte, error) {
|
||||
return processSubtree(overlay, path, "add")
|
||||
func replaceSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte {
|
||||
return processSubtree(overlay, path, "replace", res)
|
||||
}
|
||||
|
||||
func replaceSubtree(overlay interface{}, path string) ([]byte, error) {
|
||||
return processSubtree(overlay, path, "replace")
|
||||
}
|
||||
|
||||
func processSubtree(overlay interface{}, path string, op string) ([]byte, error) {
|
||||
func processSubtree(overlay interface{}, path string, op string, res *result.RuleApplicationResult) PatchBytes {
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
|
@ -262,10 +213,11 @@ func processSubtree(overlay interface{}, path string, op string) ([]byte, error)
|
|||
// check the patch
|
||||
_, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
res.FailWithMessagef("Failed to make '%s' patch from an overlay for path %s", op, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
return []byte(patchStr), nil
|
||||
return PatchBytes(patchStr)
|
||||
}
|
||||
|
||||
// TODO: Overlay is already in JSON, remove this code
|
||||
|
|
|
@ -2,12 +2,21 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func compareJsonAsMap(t *testing.T, expected, actual []byte) {
|
||||
var expectedMap, actualMap map[string]interface{}
|
||||
assert.NilError(t, json.Unmarshal(expected, &expectedMap))
|
||||
assert.NilError(t, json.Unmarshal(actual, &actualMap))
|
||||
assert.Assert(t, reflect.DeepEqual(expectedMap, actualMap))
|
||||
}
|
||||
|
||||
func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
|
||||
resourceRaw := []byte(`
|
||||
{
|
||||
|
@ -57,8 +66,9 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, err)
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
patch := JoinPatches(patches)
|
||||
|
@ -71,7 +81,7 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
|
|||
assert.Assert(t, patched != nil)
|
||||
|
||||
expectedResult := []byte(`{"apiVersion":"v1","kind":"Endpoints","metadata":{"name":"test-endpoint","labels":{"label":"test"}},"subsets":[{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":444.000000,"protocol":"UDP"}]}]}`)
|
||||
assert.Equal(t, string(expectedResult), string(patched))
|
||||
compareJsonAsMap(t, expectedResult, patched)
|
||||
}
|
||||
|
||||
func TestApplyOverlay_InsertIntoArray(t *testing.T) {
|
||||
|
@ -130,8 +140,9 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, err)
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
patch := JoinPatches(patches)
|
||||
|
@ -145,7 +156,7 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) {
|
|||
assert.Assert(t, patched != nil)
|
||||
|
||||
expectedResult := []byte(`{"apiVersion":"v1","kind":"Endpoints","metadata":{"name":"test-endpoint","labels":{"label":"test"}},"subsets":[{"addresses":[{"ip":"192.168.10.172"},{"ip":"192.168.10.173"}],"ports":[{"name":"insecure-connection","port":80.000000,"protocol":"UDP"}]},{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":443,"protocol":"TCP"}]}]}`)
|
||||
assert.Equal(t, string(expectedResult), string(patched))
|
||||
compareJsonAsMap(t, expectedResult, patched)
|
||||
}
|
||||
|
||||
func TestApplyOverlay_TestInsertToArray(t *testing.T) {
|
||||
|
@ -208,8 +219,9 @@ func TestApplyOverlay_TestInsertToArray(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, err)
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
patch := JoinPatches(patches)
|
||||
|
|
|
@ -2,28 +2,35 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/golang/glog"
|
||||
kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
)
|
||||
|
||||
// PatchBytes stands for []byte
|
||||
type PatchBytes []byte
|
||||
|
||||
// ProcessPatches Returns array from separate patches that can be applied to the document
|
||||
// Returns error ONLY in case when creation of resource should be denied.
|
||||
func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, []byte, error) {
|
||||
if len(resource) == 0 {
|
||||
return nil, nil, errors.New("Source document for patching is empty")
|
||||
func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
res := result.RuleApplicationResult{
|
||||
Reason: result.Success,
|
||||
}
|
||||
|
||||
var appliedPatches []PatchBytes
|
||||
if len(resource) == 0 {
|
||||
res.AddMessagef("Source document for patching is empty")
|
||||
res.Reason = result.Failed
|
||||
return nil, res
|
||||
}
|
||||
|
||||
var allPatches []PatchBytes
|
||||
patchedDocument := resource
|
||||
for i, patch := range patches {
|
||||
patchRaw, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
}
|
||||
|
||||
patchedDocument, err = applyPatch(patchedDocument, patchRaw)
|
||||
|
@ -32,13 +39,14 @@ func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes,
|
|||
if patch.Operation == "remove" {
|
||||
continue
|
||||
}
|
||||
glog.Warningf("Patch failed: patch number = %d, patch Operation = %s, err: %v", i, patch.Operation, err)
|
||||
message := fmt.Sprintf("Patch failed: patch number = %d, patch Operation = %s, err: %v", i, patch.Operation, err)
|
||||
res.Messages = append(res.Messages, message)
|
||||
continue
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patchRaw)
|
||||
allPatches = append(allPatches, patchRaw)
|
||||
}
|
||||
return appliedPatches, patchedDocument, nil
|
||||
return allPatches, res
|
||||
}
|
||||
|
||||
// JoinPatches joins array of serialized JSON patches to the single JSONPatch array
|
||||
|
@ -59,12 +67,16 @@ func JoinPatches(patches []PatchBytes) PatchBytes {
|
|||
return result
|
||||
}
|
||||
|
||||
// ApplyPatch applies patch for resource, returns patched resource.
|
||||
// applyPatch applies patch for resource, returns patched resource.
|
||||
func applyPatch(resource []byte, patchRaw []byte) ([]byte, error) {
|
||||
patchRaw = append([]byte{'['}, patchRaw...) // push [ forward
|
||||
patchRaw = append(patchRaw, ']') // push ] back
|
||||
patchesList := []PatchBytes{patchRaw}
|
||||
return ApplyPatches(resource, patchesList)
|
||||
}
|
||||
|
||||
patch, err := jsonpatch.DecodePatch(patchRaw)
|
||||
// ApplyPatches patches given resource with given patches and returns patched document
|
||||
func ApplyPatches(resource []byte, patches []PatchBytes) ([]byte, error) {
|
||||
joinedPatches := JoinPatches(patches)
|
||||
patch, err := jsonpatch.DecodePatch(joinedPatches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -35,8 +35,8 @@ const endpointsDocument string = `{
|
|||
|
||||
func TestProcessPatches_EmptyPatches(t *testing.T) {
|
||||
var empty []types.Patch
|
||||
patches, _, err := ProcessPatches(empty, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patches, res := ProcessPatches(empty, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patches) == 0)
|
||||
}
|
||||
|
||||
|
@ -51,14 +51,14 @@ func makeAddIsMutatedLabelPatch() types.Patch {
|
|||
func TestProcessPatches_EmptyDocument(t *testing.T) {
|
||||
var patches []types.Patch
|
||||
patches = append(patches, makeAddIsMutatedLabelPatch())
|
||||
patchesBytes, _, err := ProcessPatches(patches, nil)
|
||||
assert.Assert(t, err != nil)
|
||||
patchesBytes, res := ProcessPatches(patches, nil)
|
||||
assert.Assert(t, res.ToError() != nil)
|
||||
assert.Assert(t, len(patchesBytes) == 0)
|
||||
}
|
||||
|
||||
func TestProcessPatches_AllEmpty(t *testing.T) {
|
||||
patchesBytes, _, err := ProcessPatches(nil, nil)
|
||||
assert.Assert(t, err != nil)
|
||||
patchesBytes, res := ProcessPatches(nil, nil)
|
||||
assert.Assert(t, res.ToError() != nil)
|
||||
assert.Assert(t, len(patchesBytes) == 0)
|
||||
}
|
||||
|
||||
|
@ -66,16 +66,16 @@ func TestProcessPatches_AddPathDoesntExist(t *testing.T) {
|
|||
patch := makeAddIsMutatedLabelPatch()
|
||||
patch.Path = "/metadata/additional/is-mutated"
|
||||
patches := []types.Patch{patch}
|
||||
patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patchesBytes, res := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patchesBytes) == 0)
|
||||
}
|
||||
|
||||
func TestProcessPatches_RemovePathDoesntExist(t *testing.T) {
|
||||
patch := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"}
|
||||
patches := []types.Patch{patch}
|
||||
patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patchesBytes, res := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patchesBytes) == 0)
|
||||
}
|
||||
|
||||
|
@ -83,8 +83,8 @@ func TestProcessPatches_AddAndRemovePathsDontExist_EmptyResult(t *testing.T) {
|
|||
patch1 := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"}
|
||||
patch2 := types.Patch{Path: "/spec/labels/label3", Operation: "add", Value: "label3Value"}
|
||||
patches := []types.Patch{patch1, patch2}
|
||||
patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patchesBytes, res := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patchesBytes) == 0)
|
||||
}
|
||||
|
||||
|
@ -93,8 +93,8 @@ func TestProcessPatches_AddAndRemovePathsDontExist_ContinueOnError_NotEmptyResul
|
|||
patch2 := types.Patch{Path: "/spec/labels/label2", Operation: "remove", Value: "label2Value"}
|
||||
patch3 := types.Patch{Path: "/metadata/labels/label3", Operation: "add", Value: "label3Value"}
|
||||
patches := []types.Patch{patch1, patch2, patch3}
|
||||
patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patchesBytes, res := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patchesBytes) == 1)
|
||||
assertEqStringAndData(t, `{"path":"/metadata/labels/label3","op":"add","value":"label3Value"}`, patchesBytes[0])
|
||||
}
|
||||
|
@ -102,8 +102,8 @@ func TestProcessPatches_AddAndRemovePathsDontExist_ContinueOnError_NotEmptyResul
|
|||
func TestProcessPatches_RemovePathDoesntExist_EmptyResult(t *testing.T) {
|
||||
patch := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"}
|
||||
patches := []types.Patch{patch}
|
||||
patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patchesBytes, res := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patchesBytes) == 0)
|
||||
}
|
||||
|
||||
|
@ -111,8 +111,8 @@ func TestProcessPatches_RemovePathDoesntExist_NotEmptyResult(t *testing.T) {
|
|||
patch1 := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"}
|
||||
patch2 := types.Patch{Path: "/metadata/labels/label2", Operation: "add", Value: "label2Value"}
|
||||
patches := []types.Patch{patch1, patch2}
|
||||
patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, err)
|
||||
patchesBytes, res := ProcessPatches(patches, []byte(endpointsDocument))
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patchesBytes) == 1)
|
||||
assertEqStringAndData(t, `{"path":"/metadata/labels/label2","op":"add","value":"label2Value"}`, patchesBytes[0])
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
@ -166,6 +165,7 @@ func validateString(value interface{}, pattern string, operator Operator) bool {
|
|||
}
|
||||
|
||||
func validateNumberWithStr(value interface{}, patternNumber, patternStr string, operator Operator) bool {
|
||||
// pattern has suffix
|
||||
if "" != patternStr {
|
||||
typedValue, ok := value.(string)
|
||||
if !ok {
|
||||
|
@ -281,14 +281,6 @@ func getNumberAndStringPartsFromPattern(pattern string) (number, str string) {
|
|||
return match[1], match[3]
|
||||
}
|
||||
|
||||
func checkForWildcard(value, pattern string) error {
|
||||
if !wildcard.Match(pattern, value) {
|
||||
return fmt.Errorf("wildcard check has failed. Pattern: \"%s\". Value: \"%s\"", pattern, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseNumber(number string) (interface{}, error) {
|
||||
var err error
|
||||
|
||||
|
|
|
@ -125,3 +125,29 @@ func findKind(kinds []string, kindGVK string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func wrappedWithParentheses(str string) bool {
|
||||
if len(str) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return (str[0] == '(' && str[len(str)-1] == ')')
|
||||
}
|
||||
|
||||
// Checks if array object matches anchors. If not - skip - return true
|
||||
func skipArrayObject(object, anchors map[string]interface{}) bool {
|
||||
for key, pattern := range anchors {
|
||||
key = key[1 : len(key)-1]
|
||||
|
||||
value, ok := object[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -2,166 +2,148 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang/glog"
|
||||
kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// TODO: Refactor using State pattern
|
||||
// TODO: Return Events and pass all checks to get all validation errors (not )
|
||||
|
||||
// Validate handles validating admission request
|
||||
// Checks the target resourse for rules defined in the policy
|
||||
func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) error {
|
||||
func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) result.Result {
|
||||
var resource interface{}
|
||||
json.Unmarshal(rawResource, &resource)
|
||||
|
||||
policyResult := result.NewPolicyApplicationResult(policy.Name)
|
||||
|
||||
for _, rule := range policy.Spec.Rules {
|
||||
if rule.Validation == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ruleApplicationResult := result.NewRuleApplicationResult(rule.Name)
|
||||
|
||||
ok := ResourceMeetsDescription(rawResource, rule.ResourceDescription, gvk)
|
||||
if !ok {
|
||||
glog.Infof("Rule \"%s\" is not applicable to resource\n", rule.Name)
|
||||
ruleApplicationResult.AddMessagef("Rule %s is not applicable to resource\n", rule.Name)
|
||||
policyResult = result.Append(policyResult, &ruleApplicationResult)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateMap(resource, rule.Validation.Pattern); err != nil {
|
||||
message := *rule.Validation.Message
|
||||
if len(message) == 0 {
|
||||
message = fmt.Sprintf("%v", err)
|
||||
} else {
|
||||
message = fmt.Sprintf("%s, %s", message, err.Error())
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: %s", *rule.Validation.Message, err.Error())
|
||||
validationResult := validateResourceWithPattern(resource, rule.Validation.Pattern)
|
||||
if result.Success != validationResult.Reason {
|
||||
ruleApplicationResult.MergeWith(&validationResult)
|
||||
ruleApplicationResult.AddMessagef(*rule.Validation.Message)
|
||||
} else {
|
||||
ruleApplicationResult.AddMessagef("Success")
|
||||
}
|
||||
|
||||
policyResult = result.Append(policyResult, &ruleApplicationResult)
|
||||
}
|
||||
|
||||
return nil
|
||||
return policyResult
|
||||
}
|
||||
|
||||
func validateMap(resourcePart, patternPart interface{}) error {
|
||||
pattern, ok := patternPart.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected map, found %T", patternPart)
|
||||
}
|
||||
func validateResourceWithPattern(resource, pattern interface{}) result.RuleApplicationResult {
|
||||
return validateResourceElement(resource, pattern, "/")
|
||||
}
|
||||
|
||||
resource, ok := resourcePart.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected map, found %T", resourcePart)
|
||||
}
|
||||
func validateResourceElement(value, pattern interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
// TODO: Move similar message templates to message package
|
||||
|
||||
for key, value := range pattern {
|
||||
switch typedPattern := pattern.(type) {
|
||||
case map[string]interface{}:
|
||||
typedValue, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", pattern, value, path)
|
||||
return res
|
||||
}
|
||||
|
||||
return validateMap(typedValue, typedPattern, path)
|
||||
case []interface{}:
|
||||
typedValue, ok := value.([]interface{})
|
||||
if !ok {
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", pattern, value, path)
|
||||
return res
|
||||
}
|
||||
|
||||
return validateArray(typedValue, typedPattern, path)
|
||||
case string, float64, int, int64, bool:
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path)
|
||||
}
|
||||
|
||||
return res
|
||||
default:
|
||||
res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", pattern, path)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func validateMap(valueMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
|
||||
for key, pattern := range patternMap {
|
||||
if wrappedWithParentheses(key) {
|
||||
key = key[1 : len(key)-1]
|
||||
}
|
||||
|
||||
if value == "*" && resource[key] != nil {
|
||||
if pattern == "*" && valueMap[key] != nil {
|
||||
continue
|
||||
} else if value == "*" && resource[key] == nil {
|
||||
return fmt.Errorf("validating error: field %s must be present", key)
|
||||
} else if pattern == "*" && valueMap[key] == nil {
|
||||
res.FailWithMessagef("Field %s is not present", key)
|
||||
} else {
|
||||
if err := validateMapElement(resource[key], value); err != nil {
|
||||
return err
|
||||
elementResult := validateResourceElement(valueMap[key], pattern, path+key+"/")
|
||||
if result.Failed == elementResult.Reason {
|
||||
res.Reason = elementResult.Reason
|
||||
res.Messages = append(res.Messages, elementResult.Messages...)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
return res
|
||||
}
|
||||
|
||||
func validateArray(resourcePart, patternPart interface{}) error {
|
||||
patternArray, ok := patternPart.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected array, found %T", patternPart)
|
||||
}
|
||||
func validateArray(resourceArray, patternArray []interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
|
||||
resourceArray, ok := resourcePart.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected array, found %T", resourcePart)
|
||||
if 0 == len(patternArray) {
|
||||
return res
|
||||
}
|
||||
|
||||
switch pattern := patternArray[0].(type) {
|
||||
case map[string]interface{}:
|
||||
anchors := GetAnchorsFromMap(pattern)
|
||||
|
||||
for _, value := range resourceArray {
|
||||
for i, value := range resourceArray {
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
resource, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected array, found %T", resourcePart)
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", pattern, value, currentPath)
|
||||
return res
|
||||
}
|
||||
|
||||
if skipValidatingObject(resource, anchors) {
|
||||
if skipArrayObject(resource, anchors) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateMap(resource, pattern); err != nil {
|
||||
return err
|
||||
mapValidationResult := validateMap(resource, pattern, currentPath)
|
||||
if result.Failed == mapValidationResult.Reason {
|
||||
res.Reason = mapValidationResult.Reason
|
||||
res.Messages = append(res.Messages, mapValidationResult.Messages...)
|
||||
}
|
||||
}
|
||||
default:
|
||||
case string, float64, int, int64, bool:
|
||||
for _, value := range resourceArray {
|
||||
if !ValidateValueWithPattern(value, patternArray[0]) {
|
||||
return fmt.Errorf("Failed validate %v with %v", value, patternArray[0])
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMapElement(resourcePart, patternPart interface{}) error {
|
||||
switch pattern := patternPart.(type) {
|
||||
case map[string]interface{}:
|
||||
dictionary, ok := resourcePart.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected %T, found %T", patternPart, resourcePart)
|
||||
}
|
||||
|
||||
return validateMap(dictionary, pattern)
|
||||
case []interface{}:
|
||||
array, ok := resourcePart.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected %T, found %T", patternPart, resourcePart)
|
||||
}
|
||||
|
||||
return validateArray(array, pattern)
|
||||
case string, float64, int, int64, bool, nil:
|
||||
if !ValidateValueWithPattern(resourcePart, patternPart) {
|
||||
return fmt.Errorf("Failed validate %v with %v", resourcePart, patternPart)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("validating error: unknown type in map: %T", patternPart)
|
||||
res.FailWithMessagef("Array element pattern of unknown type %T. Path: %s", pattern, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func skipValidatingObject(object, anchors map[string]interface{}) bool {
|
||||
for key, pattern := range anchors {
|
||||
key = key[1 : len(key)-1]
|
||||
|
||||
value, ok := object[key]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func wrappedWithParentheses(str string) bool {
|
||||
if len(str) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return (str[0] == '(' && str[len(str)-1] == ')')
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -44,46 +44,46 @@ func TestWrappedWithParentheses_Empty(t *testing.T) {
|
|||
assert.Assert(t, !wrappedWithParentheses(str))
|
||||
}
|
||||
|
||||
func TestCheckForWildcard_AsteriskTest(t *testing.T) {
|
||||
func TestValidateString_AsteriskTest(t *testing.T) {
|
||||
pattern := "*"
|
||||
value := "anything"
|
||||
empty := ""
|
||||
|
||||
assert.Assert(t, checkForWildcard(value, pattern))
|
||||
assert.Assert(t, checkForWildcard(empty, pattern))
|
||||
assert.Assert(t, validateString(value, pattern, Equal))
|
||||
assert.Assert(t, validateString(empty, pattern, Equal))
|
||||
}
|
||||
|
||||
func TestCheckForWildcard_LeftAsteriskTest(t *testing.T) {
|
||||
func TestValidateString_LeftAsteriskTest(t *testing.T) {
|
||||
pattern := "*right"
|
||||
value := "leftright"
|
||||
right := "right"
|
||||
|
||||
assert.Assert(t, checkForWildcard(value, pattern))
|
||||
assert.Assert(t, checkForWildcard(right, pattern))
|
||||
assert.Assert(t, validateString(value, pattern, Equal))
|
||||
assert.Assert(t, validateString(right, pattern, Equal))
|
||||
|
||||
value = "leftmiddle"
|
||||
middle := "middle"
|
||||
|
||||
assert.Assert(t, checkForWildcard(value, pattern) != nil)
|
||||
assert.Assert(t, checkForWildcard(middle, pattern) != nil)
|
||||
assert.Assert(t, !validateString(value, pattern, Equal))
|
||||
assert.Assert(t, !validateString(middle, pattern, Equal))
|
||||
}
|
||||
|
||||
func TestCheckForWildcard_MiddleAsteriskTest(t *testing.T) {
|
||||
func TestValidateString_MiddleAsteriskTest(t *testing.T) {
|
||||
pattern := "ab*ba"
|
||||
value := "abbeba"
|
||||
assert.NilError(t, checkForWildcard(value, pattern))
|
||||
assert.Assert(t, validateString(value, pattern, Equal))
|
||||
|
||||
value = "abbca"
|
||||
assert.Assert(t, checkForWildcard(value, pattern) != nil)
|
||||
assert.Assert(t, !validateString(value, pattern, Equal))
|
||||
}
|
||||
|
||||
func TestCheckForWildcard_QuestionMark(t *testing.T) {
|
||||
func TestValidateString_QuestionMark(t *testing.T) {
|
||||
pattern := "ab?ba"
|
||||
value := "abbba"
|
||||
assert.NilError(t, checkForWildcard(value, pattern))
|
||||
assert.Assert(t, validateString(value, pattern, Equal))
|
||||
|
||||
value = "abbbba"
|
||||
assert.Assert(t, checkForWildcard(value, pattern) != nil)
|
||||
assert.Assert(t, !validateString(value, pattern, Equal))
|
||||
}
|
||||
|
||||
func TestSkipArrayObject_OneAnchor(t *testing.T) {
|
||||
|
@ -142,7 +142,7 @@ func TestSkipArrayObject_OneNumberAnchorPass(t *testing.T) {
|
|||
func TestSkipArrayObject_TwoAnchorsPass(t *testing.T) {
|
||||
rawAnchors := []byte(`{
|
||||
"(name)":"nirmata-*",
|
||||
"(namespace)":"kube-?olicy"
|
||||
"(namespace)":"kyv?rno"
|
||||
}`)
|
||||
rawResource := []byte(`{
|
||||
"name":"nirmata-resource",
|
||||
|
@ -320,11 +320,12 @@ func TestValidateMap(t *testing.T) {
|
|||
}
|
||||
}`)
|
||||
|
||||
var pattern, resource interface{}
|
||||
var pattern, resource map[string]interface{}
|
||||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.NilError(t, validateMap(resource, pattern))
|
||||
res := validateMap(resource, pattern, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
||||
func TestValidateMap_AsteriskForInt(t *testing.T) {
|
||||
|
@ -414,11 +415,12 @@ func TestValidateMap_AsteriskForInt(t *testing.T) {
|
|||
}
|
||||
`)
|
||||
|
||||
var pattern, resource interface{}
|
||||
var pattern, resource map[string]interface{}
|
||||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.NilError(t, validateMap(resource, pattern))
|
||||
res := validateMap(resource, pattern, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
||||
func TestValidateMap_AsteriskForMap(t *testing.T) {
|
||||
|
@ -505,11 +507,12 @@ func TestValidateMap_AsteriskForMap(t *testing.T) {
|
|||
}
|
||||
}`)
|
||||
|
||||
var pattern, resource interface{}
|
||||
var pattern, resource map[string]interface{}
|
||||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.NilError(t, validateMap(resource, pattern))
|
||||
res := validateMap(resource, pattern, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
||||
func TestValidateMap_AsteriskForArray(t *testing.T) {
|
||||
|
@ -591,11 +594,12 @@ func TestValidateMap_AsteriskForArray(t *testing.T) {
|
|||
}
|
||||
}`)
|
||||
|
||||
var pattern, resource interface{}
|
||||
var pattern, resource map[string]interface{}
|
||||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.NilError(t, validateMap(resource, pattern))
|
||||
res := validateMap(resource, pattern, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
||||
func TestValidateMap_AsteriskFieldIsMissing(t *testing.T) {
|
||||
|
@ -680,11 +684,12 @@ func TestValidateMap_AsteriskFieldIsMissing(t *testing.T) {
|
|||
}
|
||||
}`)
|
||||
|
||||
var pattern, resource interface{}
|
||||
var pattern, resource map[string]interface{}
|
||||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.Assert(t, validateMap(resource, pattern) != nil)
|
||||
res := validateMap(resource, pattern, "/")
|
||||
assert.Assert(t, res.ToError() != nil)
|
||||
}
|
||||
|
||||
func TestValidateMapElement_TwoElementsInArrayOnePass(t *testing.T) {
|
||||
|
@ -724,7 +729,8 @@ func TestValidateMapElement_TwoElementsInArrayOnePass(t *testing.T) {
|
|||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.NilError(t, validateMapElement(resource, pattern))
|
||||
res := validateResourceElement(resource, pattern, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
||||
func TestValidateMapElement_OneElementInArrayPass(t *testing.T) {
|
||||
|
@ -755,7 +761,8 @@ func TestValidateMapElement_OneElementInArrayPass(t *testing.T) {
|
|||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.NilError(t, validateMapElement(resource, pattern))
|
||||
res := validateResourceElement(resource, pattern, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
||||
func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) {
|
||||
|
@ -786,7 +793,8 @@ func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) {
|
|||
json.Unmarshal(rawPattern, &pattern)
|
||||
json.Unmarshal(rawMap, &resource)
|
||||
|
||||
assert.Assert(t, validateMapElement(resource, pattern) != nil)
|
||||
res := validateResourceElement(resource, pattern, "/")
|
||||
assert.Assert(t, res.ToError() != nil)
|
||||
}
|
||||
|
||||
func TestValidate_ServiceTest(t *testing.T) {
|
||||
|
@ -977,5 +985,6 @@ func TestValidate_MapHasFloats(t *testing.T) {
|
|||
Kind: "Deployment",
|
||||
}
|
||||
|
||||
assert.NilError(t, Validate(policy, rawResource, gvk))
|
||||
res := Validate(policy, rawResource, gvk)
|
||||
assert.NilError(t, res.ToError())
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
policyscheme "github.com/nirmata/kyverno/pkg/client/clientset/versioned/scheme"
|
||||
v1alpha1 "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -157,7 +158,7 @@ func (c *controller) SyncHandler(key Info) error {
|
|||
}
|
||||
|
||||
//NewEvent returns a new event
|
||||
func NewEvent(kind string, resource string, reason Reason, message MsgKey, args ...interface{}) Info {
|
||||
func NewEvent(kind string, resource string, reason result.Reason, message MsgKey, args ...interface{}) Info {
|
||||
msgText, err := getEventMsg(message, args)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
|
|
|
@ -95,12 +95,18 @@ func applyPolicy(policy *kubepolicy.Policy, resources []*resourceInfo) (output s
|
|||
}
|
||||
|
||||
func applyPolicyOnRaw(policy *kubepolicy.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]byte, error) {
|
||||
_, patchedDocument := engine.Mutate(*policy, rawResource, *gvk)
|
||||
patches, result := engine.Mutate(*policy, rawResource, *gvk)
|
||||
|
||||
if err := engine.Validate(*policy, patchedDocument, *gvk); err != nil {
|
||||
return nil, err
|
||||
err := result.ToError()
|
||||
var patchedResource []byte
|
||||
if err == nil {
|
||||
patchedResource, err = engine.ApplyPatches(rawResource, patches)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to apply mutation patches:\n%v", err)
|
||||
}
|
||||
err = engine.Validate(*policy, patchedResource, *gvk).ToError()
|
||||
}
|
||||
return patchedDocument, nil
|
||||
return patchedResource, err
|
||||
}
|
||||
|
||||
func extractPolicy(fileDir string) (*kubepolicy.Policy, error) {
|
||||
|
|
21
pkg/result/reason.go
Normal file
21
pkg/result/reason.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package result
|
||||
|
||||
//Reason types of Result Reasons
|
||||
type Reason int
|
||||
|
||||
const (
|
||||
//PolicyViolation there is a violation of policy
|
||||
Success Reason = iota
|
||||
//Success policy applied
|
||||
Violation
|
||||
//Failed the request to create/update the resource was blocked(generated from admission-controller)
|
||||
Failed
|
||||
)
|
||||
|
||||
func (r Reason) String() string {
|
||||
return [...]string{
|
||||
"Success",
|
||||
"Violation",
|
||||
"Failed",
|
||||
}[r]
|
||||
}
|
175
pkg/result/result.go
Normal file
175
pkg/result/result.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Indent acts for indenting in result hierarchy
|
||||
type Indent string
|
||||
|
||||
const (
|
||||
// SpaceIndent means 4 spaces
|
||||
SpaceIndent Indent = " "
|
||||
// TabIndent is a tab symbol
|
||||
TabIndent Indent = "\t"
|
||||
)
|
||||
|
||||
// Result is an interface that is used for result polymorphic behavio
|
||||
type Result interface {
|
||||
String() string
|
||||
StringWithIndent(indent string) string
|
||||
GetReason() Reason
|
||||
ToError() error
|
||||
}
|
||||
|
||||
// CompositeResult is used for result hierarchy
|
||||
type CompositeResult struct {
|
||||
Message string
|
||||
Reason Reason
|
||||
Children []Result
|
||||
}
|
||||
|
||||
// RuleApplicationResult represents elementary result that is produced by PolicyEngine
|
||||
// TODO: It can be used to create Kubernetes Results, so make method for this
|
||||
type RuleApplicationResult struct {
|
||||
PolicyRule string
|
||||
Reason Reason
|
||||
Messages []string
|
||||
}
|
||||
|
||||
func NewRuleApplicationResult(ruleName string) RuleApplicationResult {
|
||||
return RuleApplicationResult{
|
||||
PolicyRule: ruleName,
|
||||
Reason: Success,
|
||||
Messages: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// StringWithIndent makes result string where each
|
||||
// line is prepended with specified indent
|
||||
func (e *RuleApplicationResult) StringWithIndent(indent string) string {
|
||||
message := fmt.Sprintf("%s* %s: policy rule - %s:\n", indent, e.Reason.String(), e.PolicyRule)
|
||||
childrenIndent := indent + string(SpaceIndent)
|
||||
for i, m := range e.Messages {
|
||||
message += fmt.Sprintf("%s%d. %s\n", childrenIndent, i+1, m)
|
||||
}
|
||||
|
||||
// remove last line feed
|
||||
if 0 != len(message) {
|
||||
message = message[:len(message)-1]
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// String makes result string
|
||||
// for writing it to logs
|
||||
func (e *RuleApplicationResult) String() string {
|
||||
return e.StringWithIndent("")
|
||||
}
|
||||
|
||||
func (e *RuleApplicationResult) ToError() error {
|
||||
if e.Reason != Success {
|
||||
return fmt.Errorf(e.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *RuleApplicationResult) GetReason() Reason {
|
||||
return e.Reason
|
||||
}
|
||||
|
||||
// Adds formatted message to this result
|
||||
func (rar *RuleApplicationResult) AddMessagef(message string, a ...interface{}) {
|
||||
rar.Messages = append(rar.Messages, fmt.Sprintf(message, a...))
|
||||
}
|
||||
|
||||
// Sets the Reason Failed and adds formatted message to this result
|
||||
func (rar *RuleApplicationResult) FailWithMessagef(message string, a ...interface{}) {
|
||||
rar.Reason = Failed
|
||||
rar.AddMessagef(message, a...)
|
||||
}
|
||||
|
||||
// Takes messages and higher reason from another RuleApplicationResult
|
||||
func (e *RuleApplicationResult) MergeWith(other *RuleApplicationResult) {
|
||||
if other != nil {
|
||||
e.Messages = append(e.Messages, other.Messages...)
|
||||
}
|
||||
if other.Reason > e.Reason {
|
||||
e.Reason = other.Reason
|
||||
}
|
||||
}
|
||||
|
||||
// StringWithIndent makes result string where each
|
||||
// line is prepended with specified indent
|
||||
func (e *CompositeResult) StringWithIndent(indent string) string {
|
||||
childrenIndent := indent + string(SpaceIndent)
|
||||
message := fmt.Sprintf("%s- %s: %s\n", indent, e.Reason, e.Message)
|
||||
for _, res := range e.Children {
|
||||
message += (res.StringWithIndent(childrenIndent) + "\n")
|
||||
}
|
||||
|
||||
// remove last line feed
|
||||
if 0 != len(message) {
|
||||
message = message[:len(message)-1]
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// String makes result string
|
||||
// for writing it to logs
|
||||
func (e *CompositeResult) String() string {
|
||||
return e.StringWithIndent("")
|
||||
}
|
||||
|
||||
func (e *CompositeResult) ToError() error {
|
||||
if e.Reason != Success {
|
||||
return fmt.Errorf(e.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *CompositeResult) GetReason() Reason {
|
||||
return e.Reason
|
||||
}
|
||||
|
||||
func NewPolicyApplicationResult(policyName string) Result {
|
||||
return &CompositeResult{
|
||||
Message: fmt.Sprintf("policy - %s:", policyName),
|
||||
Reason: Success,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdmissionResult(requestUID string) Result {
|
||||
return &CompositeResult{
|
||||
Message: fmt.Sprintf("For resource with UID - %s:", requestUID),
|
||||
Reason: Success,
|
||||
}
|
||||
}
|
||||
|
||||
// Append returns CompositeResult with target and source
|
||||
// Or appends source to target if it is composite result
|
||||
// If the source reason is more important than target reason,
|
||||
// target takes the reason of the source.
|
||||
func Append(target Result, source Result) Result {
|
||||
targetReason := target.GetReason()
|
||||
if targetReason < source.GetReason() {
|
||||
targetReason = source.GetReason()
|
||||
}
|
||||
|
||||
if composite, ok := target.(*CompositeResult); ok {
|
||||
composite.Children = append(composite.Children, source)
|
||||
composite.Reason = targetReason
|
||||
return composite
|
||||
}
|
||||
|
||||
composite := &CompositeResult{
|
||||
Children: []Result{
|
||||
target,
|
||||
source,
|
||||
},
|
||||
Reason: targetReason,
|
||||
}
|
||||
|
||||
return composite
|
||||
}
|
54
pkg/result/result_test.go
Normal file
54
pkg/result/result_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package result
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestAppend_TwoResultObjects(t *testing.T) {
|
||||
firstRuleApplicationResult := RuleApplicationResult{
|
||||
Reason: Failed,
|
||||
Messages: []string{
|
||||
"1. Test",
|
||||
"2. Toast",
|
||||
},
|
||||
}
|
||||
|
||||
secondRuleApplicationResult := RuleApplicationResult{
|
||||
Reason: Success,
|
||||
Messages: []string{
|
||||
"1. Kyverno",
|
||||
"2. KubePolicy",
|
||||
},
|
||||
}
|
||||
|
||||
result := Append(&firstRuleApplicationResult, &secondRuleApplicationResult)
|
||||
composite, ok := result.(*CompositeResult)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, len(composite.Children), 2)
|
||||
|
||||
RuleApplicationResult, ok := composite.Children[0].(*RuleApplicationResult)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, RuleApplicationResult.Messages[1], "2. Toast")
|
||||
}
|
||||
|
||||
func TestAppend_FirstObjectIsComposite(t *testing.T) {
|
||||
composite := &CompositeResult{}
|
||||
|
||||
firstRuleApplicationResult := RuleApplicationResult{
|
||||
Reason: Failed,
|
||||
Messages: []string{
|
||||
"1. Test",
|
||||
"2. Toast",
|
||||
},
|
||||
}
|
||||
|
||||
result := Append(composite, &firstRuleApplicationResult)
|
||||
composite, ok := result.(*CompositeResult)
|
||||
assert.Equal(t, len(composite.Children), 1)
|
||||
|
||||
RuleApplicationResult, ok := composite.Children[0].(*RuleApplicationResult)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, RuleApplicationResult.Messages[1], "2. Toast")
|
||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/nirmata/kyverno/pkg/config"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
engine "github.com/nirmata/kyverno/pkg/engine"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||
tlsutils "github.com/nirmata/kyverno/pkg/tls"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
|
@ -131,13 +132,19 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be
|
|||
return nil
|
||||
}
|
||||
|
||||
admissionResult := result.NewAdmissionResult(string(request.UID))
|
||||
var allPatches []engine.PatchBytes
|
||||
for _, policy := range policies {
|
||||
|
||||
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
||||
|
||||
policyPatches, _ := engine.Mutate(*policy, request.Object.Raw, request.Kind)
|
||||
policyPatches, mutationResult := engine.Mutate(*policy, request.Object.Raw, request.Kind)
|
||||
allPatches = append(allPatches, policyPatches...)
|
||||
admissionResult = result.Append(admissionResult, mutationResult)
|
||||
|
||||
if mutationError := mutationResult.ToError(); mutationError != nil {
|
||||
glog.Warningf(mutationError.Error())
|
||||
}
|
||||
|
||||
if len(policyPatches) > 0 {
|
||||
namespace := engine.ParseNamespaceFromObject(request.Object.Raw)
|
||||
|
@ -146,11 +153,23 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be
|
|||
}
|
||||
}
|
||||
|
||||
patchType := v1beta1.PatchTypeJSONPatch
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: engine.JoinPatches(allPatches),
|
||||
PatchType: &patchType,
|
||||
message := admissionResult.String()
|
||||
glog.Info(message)
|
||||
|
||||
if admissionResult.GetReason() == result.Success {
|
||||
patchType := v1beta1.PatchTypeJSONPatch
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: engine.JoinPatches(allPatches),
|
||||
PatchType: &patchType,
|
||||
}
|
||||
} else {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,30 +184,42 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1
|
|||
return nil
|
||||
}
|
||||
|
||||
admissionResult := result.NewAdmissionResult(string(request.UID))
|
||||
for _, policy := range policies {
|
||||
// validation
|
||||
glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
||||
validationResult := engine.Validate(*policy, request.Object.Raw, request.Kind)
|
||||
admissionResult = result.Append(admissionResult, validationResult)
|
||||
|
||||
if err := engine.Validate(*policy, request.Object.Raw, request.Kind); err != nil {
|
||||
message := fmt.Sprintf("validation has failed: %s", err.Error())
|
||||
glog.Warning(message)
|
||||
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
if validationError := validationResult.ToError(); validationError != nil {
|
||||
glog.Warningf(validationError.Error())
|
||||
}
|
||||
|
||||
// generation
|
||||
engine.Generate(ws.client, *policy, request.Object.Raw, request.Kind)
|
||||
}
|
||||
glog.Info("Validation is successful")
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
|
||||
message := admissionResult.String()
|
||||
glog.Info(message)
|
||||
|
||||
// Generation loop after all validation succeeded
|
||||
var response *v1beta1.AdmissionResponse
|
||||
|
||||
if admissionResult.GetReason() == result.Success {
|
||||
for _, policy := range policies {
|
||||
engine.Generate(ws.client, *policy, request.Object.Raw, request.Kind)
|
||||
}
|
||||
glog.Info("Validation is successful")
|
||||
|
||||
response = &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
} else {
|
||||
response = &v1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// bodyToAdmissionReview creates AdmissionReview object from request body
|
||||
|
|
Loading…
Add table
Reference in a new issue