1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

Merge pull request #601 from nirmata/add_testscenario

Setting validationFailureAction to enforce is going to enforce it for every Policy
This commit is contained in:
shuting 2020-01-10 18:37:15 -08:00 committed by GitHub
commit 28ccff0eb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1656 additions and 907 deletions

View file

@ -304,13 +304,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
@ -383,13 +376,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition

View file

@ -304,13 +304,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
@ -383,13 +376,6 @@ spec:
type: string
message:
type: string
managedResource:
type: object
properties:
kind:
type: string
creationBlocked:
type: boolean
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition

View file

@ -278,19 +278,9 @@ type ResourceSpec struct {
// ViolatedRule stores the information regarding the rule
type ViolatedRule struct {
Name string `json:"name"`
Type string `json:"type"`
Message string `json:"message"`
ManagedResource ManagedResourceSpec `json:"managedResource,omitempty"`
}
// ManagedResourceSpec is used when the violations is created on resource owner
// to determing the kind of child resource that caused the violation
type ManagedResourceSpec struct {
Kind string `json:"kind,omitempty"`
// Is not used in processing, but will is present for backward compatablitiy
Namespace string `json:"namespace,omitempty"`
CreationBlocked bool `json:"creationBlocked,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Message string `json:"message"`
}
//PolicyViolationStatus provides information regarding policyviolation status

View file

@ -319,22 +319,6 @@ func (in *Generation) DeepCopy() *Generation {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ManagedResourceSpec) DeepCopyInto(out *ManagedResourceSpec) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedResourceSpec.
func (in *ManagedResourceSpec) DeepCopy() *ManagedResourceSpec {
if in == nil {
return nil
}
out := new(ManagedResourceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MatchResources) DeepCopyInto(out *MatchResources) {
*out = *in
@ -714,7 +698,6 @@ func (in *Validation) DeepCopy() *Validation {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ViolatedRule) DeepCopyInto(out *ViolatedRule) {
*out = *in
out.ManagedResource = in.ManagedResource
return
}

View file

@ -2,9 +2,10 @@ package context
import (
"encoding/json"
"fmt"
"github.com/golang/glog"
"github.com/jmespath/go-jmespath"
jmespath "github.com/jmespath/go-jmespath"
)
//Query the JSON context with JMESPATH search path
@ -14,7 +15,7 @@ func (ctx *Context) Query(query string) (interface{}, error) {
queryPath, err := jmespath.Compile(query)
if err != nil {
glog.V(4).Infof("incorrect query %s: %v", query, err)
return emptyResult, err
return emptyResult, fmt.Errorf("incorrect query %s: %v", query, err)
}
// search
ctx.mu.RLock()
@ -22,14 +23,14 @@ func (ctx *Context) Query(query string) (interface{}, error) {
var data interface{}
if err := json.Unmarshal(ctx.jsonRaw, &data); err != nil {
glog.V(4).Infof("failed to unmarshall context")
return emptyResult, err
glog.V(4).Infof("failed to unmarshall context: %v", err)
return emptyResult, fmt.Errorf("failed to unmarshall context: %v", err)
}
result, err := queryPath.Search(data)
if err != nil {
glog.V(4).Infof("failed to search query %s: %v", query, err)
return emptyResult, err
return emptyResult, fmt.Errorf("failed to search query %s: %v", query, err)
}
return result, nil
}

View file

@ -1,16 +1,22 @@
package engine
import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
//GenerateNew returns the list of rules that are applicable on this policy and resource
// GenerateNew
// 1. validate variables to be susbtitute in the general ruleInfo (match,exclude,condition)
// - the caller has to check the ruleResponse to determine whether the path exist
// 2. returns the list of rules that are applicable on this policy and resource, if 1 succeed
func GenerateNew(policyContext PolicyContext) (resp response.EngineResponse) {
policy := policyContext.Policy
resource := policyContext.NewResource
@ -55,6 +61,13 @@ func filterRules(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
}
for _, rule := range policy.Spec.Rules {
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in generate rule %s, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Mutation.String(), fmt.Sprintf("path not present: %s", paths)))
continue
}
if ruleResp := filterRule(rule, resource, admissionInfo, ctx); ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
}

View file

@ -22,7 +22,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/variables"
)
// processOverlay processes validation patterns on the resource
// processOverlay processes mutation overlay on the resource
func ProcessOverlay(ctx context.EvalInterface, rule kyverno.Rule, resource unstructured.Unstructured) (resp response.RuleResponse, patchedResource unstructured.Unstructured) {
startTime := time.Now()
glog.V(4).Infof("started applying overlay rule %q (%v)", rule.Name, startTime)
@ -32,6 +32,16 @@ func ProcessOverlay(ctx context.EvalInterface, rule kyverno.Rule, resource unstr
resp.RuleStats.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("finished applying overlay rule %q (%v)", resp.Name, resp.RuleStats.ProcessingTime)
}()
// if referenced path not present, we skip processing the rule and report violation
if invalidPaths := variables.ValidateVariables(ctx, rule.Mutation.Overlay); len(invalidPaths) != 0 {
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", invalidPaths)
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
return resp, resource
}
// substitute variables
// first pass we substitute all the JMESPATH substitution for the variable
// variable: {{<JMESPATH>}}

View file

@ -1,6 +1,7 @@
package engine
import (
"fmt"
"reflect"
"strings"
"time"
@ -10,6 +11,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/mutate"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
@ -55,6 +57,13 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
continue
}
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in rule %s, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Mutation.String(), fmt.Sprintf("path not present in rule info: %s", paths)))
continue
}
startTime := time.Now()
if !rbac.MatchAdmissionInfo(rule, policyContext.AdmissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
@ -82,11 +91,20 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
if rule.Mutation.Overlay != nil {
var ruleResponse response.RuleResponse
ruleResponse, patchedResource = mutate.ProcessOverlay(ctx, rule, patchedResource)
if ruleResponse.Success == true && ruleResponse.Patches == nil {
// overlay pattern does not match the resource conditions
glog.V(4).Infof(ruleResponse.Message)
continue
} else if ruleResponse.Success == true {
if ruleResponse.Success == true {
// - variable substitution path is not present
if ruleResponse.PathNotPresent {
glog.V(4).Infof(ruleResponse.Message)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse)
continue
}
// - overlay pattern does not match the resource conditions
if ruleResponse.Patches == nil {
glog.V(4).Infof(ruleResponse.Message)
continue
}
glog.Infof("Mutate overlay in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
}

View file

@ -3,6 +3,7 @@ package engine
import (
"encoding/json"
"reflect"
"strings"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -88,3 +89,115 @@ func Test_VariableSubstitutionOverlay(t *testing.T) {
t.Error("patches dont match")
}
}
func Test_variableSubstitutionPathNotExist(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user"
},
"spec": {
"containers": [
{
"name": "check-root-user",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"spec": {
"name": "{{request.object.metadata.name1}}"
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
json.Unmarshal(policyraw, &policy)
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_variableSubstitutionPathNotExist_InRuleInfo(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "check-root-user"
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-validate-variables"
},
"spec": {
"rules": [
{
"name": "test-match",
"match": {
"resources": {
"kinds": [
"{{request.kind}}"
]
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
assert.Assert(t, strings.Contains(er.PolicyResponse.Rules[0].Message, "path not present in rule info"))
}

View file

@ -65,6 +65,8 @@ type RuleResponse struct {
Success bool `json:"success"`
// statistics
RuleStats `json:",inline"`
// PathNotPresent indicates whether referenced path in variable substitution exist
PathNotPresent bool `json:"pathNotPresent"`
}
//ToString ...
@ -119,3 +121,13 @@ func (er EngineResponse) getRules(success bool) []string {
}
return rules
}
// IsPathNotPresent checks if the referenced path(in variable substitution) exist
func (er EngineResponse) IsPathNotPresent() bool {
for _, r := range er.PolicyResponse.Rules {
if r.PathNotPresent {
return true
}
}
return false
}

View file

@ -9,7 +9,10 @@ import (
"github.com/minio/minio/pkg/wildcard"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/operator"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/variables"
"github.com/nirmata/kyverno/pkg/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -228,3 +231,39 @@ type resourceInfo struct {
Resource unstructured.Unstructured
Gvk *metav1.GroupVersionKind
}
// validateGeneralRuleInfoVariables validate variable subtition defined in
// - MatchResources
// - ExcludeResources
// - Conditions
func validateGeneralRuleInfoVariables(ctx context.EvalInterface, rule kyverno.Rule) string {
var tempRule kyverno.Rule
var tempRulePattern interface{}
tempRule.MatchResources = rule.MatchResources
tempRule.ExcludeResources = rule.ExcludeResources
tempRule.Conditions = rule.Conditions
raw, err := json.Marshal(tempRule)
if err != nil {
glog.Infof("failed to serilize rule info while validating variable substitution: %v", err)
return ""
}
if err := json.Unmarshal(raw, &tempRulePattern); err != nil {
glog.Infof("failed to serilize rule info while validating variable substitution: %v", err)
return ""
}
return variables.ValidateVariables(ctx, tempRulePattern)
}
func newPathNotPresentRuleResponse(rname, rtype, msg string) response.RuleResponse {
return response.RuleResponse{
Name: rname,
Type: rtype,
Message: msg,
Success: true,
PathNotPresent: true,
}
}

View file

@ -1,11 +1,15 @@
package engine
import (
"encoding/json"
"fmt"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
context "github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/utils"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -392,3 +396,106 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
assert.Assert(t, !MatchesResourceDescription(*resource, rule))
}
func Test_validateGeneralRuleInfoVariables(t *testing.T) {
rawResource := []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "image-with-hostpath",
"labels": {
"app.type": "prod",
"namespace": "my-namespace"
}
},
"spec": {
"containers": [
{
"name": "image-with-hostpath",
"image": "docker.io/nautiker/curl",
"volumeMounts": [
{
"name": "var-lib-etcd",
"mountPath": "/var/lib"
}
]
}
],
"volumes": [
{
"name": "var-lib-etcd",
"emptyDir": {}
}
]
}
}
`)
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-validate-variables"
},
"spec": {
"rules": [
{
"name": "test-match",
"match": {
"Subjects": [
{
"kind": "User",
"name": "{{request.userInfo.username1}}}"
}
],
"resources": {
"kind": "{{request.object.kind}}"
}
}
},
{
"name": "test-exclude",
"match": {
"resources": {
"namespaces": [
"{{request.object.namespace}}"
]
}
}
},
{
"name": "test-condition",
"preconditions": [
{
"key": "{{serviceAccountName}}",
"operator": "NotEqual",
"value": "testuser"
}
]
}
]
}
}`)
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyRaw, &policy))
ctx := context.NewContext()
ctx.AddResource(rawResource)
ctx.AddUserInfo(userReqInfo)
ctx.AddSA("system:serviceaccount:test:testuser")
expectPaths := []string{"request.userInfo.username1", "request.object.namespace", ""}
for i, rule := range policy.Spec.Rules {
invalidPaths := validateGeneralRuleInfoVariables(ctx, rule)
assert.Assert(t, invalidPaths == expectPaths[i], fmt.Sprintf("result not match, got invalidPaths %s", invalidPaths))
}
}

View file

@ -7,6 +7,13 @@ import (
"github.com/nirmata/kyverno/pkg/engine/operator"
)
type ValidationFailureReason int
const (
PathNotPresent ValidationFailureReason = iota
Rulefailure
)
func isStringIsReference(str string) bool {
if len(str) < len(operator.ReferenceSign) {
return false
@ -66,3 +73,13 @@ func getRawKeyIfWrappedWithAttributes(str string) string {
return str
}
}
type ValidationError struct {
StatusCode ValidationFailureReason
ErrorMsg string
}
// newValidatePatternError returns an validation error using the ValidationFailureReason and errorMsg
func newValidatePatternError(reason ValidationFailureReason, msg string) ValidationError {
return ValidationError{StatusCode: reason, ErrorMsg: msg}
}

View file

@ -17,13 +17,22 @@ import (
// validateResourceWithPattern is a start of element-by-element validation process
// It assumes that validation is started from root, so "/" is passed
//TODO: for failure, we return the path at which it failed along with error
func ValidateResourceWithPattern(ctx context.EvalInterface, resource, pattern interface{}) (string, error) {
func ValidateResourceWithPattern(ctx context.EvalInterface, resource, pattern interface{}) (string, ValidationError) {
// if referenced path is not present, we skip processing the rule and report violation
if invalidPaths := variables.ValidateVariables(ctx, pattern); len(invalidPaths) != 0 {
return "", newValidatePatternError(PathNotPresent, invalidPaths)
}
// first pass we substitute all the JMESPATH substitution for the variable
// variable: {{<JMESPATH>}}
// if a JMESPATH fails, we dont return error but variable is substitured with nil and error log
pattern = variables.SubstituteVariables(ctx, pattern)
return validateResourceElement(resource, pattern, pattern, "/")
path, err := validateResourceElement(resource, pattern, pattern, "/")
if err != nil {
return path, newValidatePatternError(Rulefailure, err.Error())
}
return "", ValidationError{}
}
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)

View file

@ -1,6 +1,7 @@
package engine
import (
"errors"
"fmt"
"reflect"
"strings"
@ -17,29 +18,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func startResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, newR unstructured.Unstructured) {
// set policy information
resp.PolicyResponse.Policy = policy.Name
// resource details
resp.PolicyResponse.Resource.Name = newR.GetName()
resp.PolicyResponse.Resource.Namespace = newR.GetNamespace()
resp.PolicyResponse.Resource.Kind = newR.GetKind()
resp.PolicyResponse.Resource.APIVersion = newR.GetAPIVersion()
resp.PolicyResponse.ValidationFailureAction = policy.Spec.ValidationFailureAction
}
func endResultResponse(resp *response.EngineResponse, startTime time.Time) {
resp.PolicyResponse.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("Finished applying validation rules policy %v (%v)", resp.PolicyResponse.Policy, resp.PolicyResponse.ProcessingTime)
glog.V(4).Infof("Validation Rules appplied succesfully count %v for policy %q", resp.PolicyResponse.RulesAppliedCount, resp.PolicyResponse.Policy)
}
func incrementAppliedCount(resp *response.EngineResponse) {
// rules applied succesfully count
resp.PolicyResponse.RulesAppliedCount++
}
//Validate applies validation rules from policy on the resource
func Validate(policyContext PolicyContext) (resp response.EngineResponse) {
startTime := time.Now()
@ -87,6 +65,29 @@ func Validate(policyContext PolicyContext) (resp response.EngineResponse) {
return response.EngineResponse{}
}
func startResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, newR unstructured.Unstructured) {
// set policy information
resp.PolicyResponse.Policy = policy.Name
// resource details
resp.PolicyResponse.Resource.Name = newR.GetName()
resp.PolicyResponse.Resource.Namespace = newR.GetNamespace()
resp.PolicyResponse.Resource.Kind = newR.GetKind()
resp.PolicyResponse.Resource.APIVersion = newR.GetAPIVersion()
resp.PolicyResponse.ValidationFailureAction = policy.Spec.ValidationFailureAction
}
func endResultResponse(resp *response.EngineResponse, startTime time.Time) {
resp.PolicyResponse.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("Finished applying validation rules policy %v (%v)", resp.PolicyResponse.Policy, resp.PolicyResponse.ProcessingTime)
glog.V(4).Infof("Validation Rules appplied succesfully count %v for policy %q", resp.PolicyResponse.RulesAppliedCount, resp.PolicyResponse.Policy)
}
func incrementAppliedCount(resp *response.EngineResponse) {
// rules applied succesfully count
resp.PolicyResponse.RulesAppliedCount++
}
func validateResource(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, admissionInfo kyverno.RequestInfo) *response.EngineResponse {
resp := &response.EngineResponse{}
for _, rule := range policy.Spec.Rules {
@ -94,6 +95,14 @@ func validateResource(ctx context.EvalInterface, policy kyverno.ClusterPolicy, r
continue
}
startTime := time.Now()
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in rule %s/, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Validation.String(), fmt.Sprintf("path not present: %s", paths)))
continue
}
if !rbac.MatchAdmissionInfo(rule, admissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), admissionInfo)
@ -178,14 +187,23 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
// either pattern or anyPattern can be specified in Validation rule
if rule.Validation.Pattern != nil {
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, rule.Validation.Pattern)
if err != nil {
// rule application failed
glog.V(4).Infof("Validation rule '%s' failed at '%s' for resource %s/%s/%s. %s: %v", rule.Name, path, resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Validation.Message, err)
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
if !reflect.DeepEqual(err, validate.ValidationError{}) {
switch err.StatusCode {
case validate.PathNotPresent:
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", err.ErrorMsg)
glog.V(4).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
case validate.Rulefailure:
// rule application failed
glog.V(4).Infof("Validation rule '%s' failed at '%s' for resource %s/%s/%s. %s: %v", rule.Name, path, resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Validation.Message, err)
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
}
return resp
}
// rule application succesful
glog.V(4).Infof("rule %s pattern validated succesfully on resource %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
resp.Success = true
@ -194,39 +212,54 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
}
// using anyPattern we can define multiple patterns and only one of them has to be succesfully validated
// return directly if one pattern succeed
// if none succeed, report violation / policyerror(TODO)
if rule.Validation.AnyPattern != nil {
var errs []error
var failedPaths []string
var ruleFailureErrs []error
var failedPaths, invalidPaths []string
for index, pattern := range rule.Validation.AnyPattern {
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, pattern)
if err == nil {
// this pattern was succesfully validated
// this pattern was succesfully validated
if reflect.DeepEqual(err, validate.ValidationError{}) {
glog.V(4).Infof("anyPattern %v succesfully validated on resource %s/%s/%s", pattern, resource.GetKind(), resource.GetNamespace(), resource.GetName())
resp.Success = true
resp.Message = fmt.Sprintf("Validation rule '%s' anyPattern[%d] succeeded.", rule.Name, index)
return resp
}
if err != nil {
switch err.StatusCode {
case validate.PathNotPresent:
invalidPaths = append(invalidPaths, err.ErrorMsg)
case validate.Rulefailure:
glog.V(4).Infof("Validation error: %s; Validation rule %s anyPattern[%d] failed at path %s for %s/%s/%s",
rule.Validation.Message, rule.Name, index, path, resource.GetKind(), resource.GetNamespace(), resource.GetName())
errs = append(errs, err)
ruleFailureErrs = append(ruleFailureErrs, errors.New(err.ErrorMsg))
failedPaths = append(failedPaths, path)
}
}
// If none of the anyPatterns are validated
if len(errs) > 0 {
glog.V(4).Infof("none of anyPattern were processed: %v", errs)
resp.Success = false
var errorStr []string
for index, err := range errs {
glog.V(4).Infof("anyPattern[%d] failed at path %s: %v", index, failedPaths[index], err)
str := fmt.Sprintf("Validation rule %s anyPattern[%d] failed at path %s.", rule.Name, index, failedPaths[index])
errorStr = append(errorStr, str)
}
resp.Message = fmt.Sprintf("Validation error: %s; %s", rule.Validation.Message, strings.Join(errorStr, ";"))
// PathNotPresent
if len(invalidPaths) != 0 {
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", strings.Join(invalidPaths, ";"))
glog.V(4).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
return resp
}
// none of the anyPatterns succeed: len(ruleFailureErrs) > 0
glog.V(4).Infof("none of anyPattern comply with resource: %v", ruleFailureErrs)
resp.Success = false
var errorStr []string
for index, err := range ruleFailureErrs {
glog.V(4).Infof("anyPattern[%d] failed at path %s: %v", index, failedPaths[index], err)
str := fmt.Sprintf("Validation rule %s anyPattern[%d] failed at path %s.", rule.Name, index, failedPaths[index])
errorStr = append(errorStr, str)
}
resp.Message = fmt.Sprintf("Validation error: %s; %s", rule.Validation.Message, strings.Join(errorStr, " "))
return resp
}
return response.RuleResponse{}
}

View file

@ -5,6 +5,7 @@ import (
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/utils"
"gotest.tools/assert"
)
@ -483,7 +484,7 @@ func TestValidate_Fail_anyPattern(t *testing.T) {
resourceUnstructured, err := utils.ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: A namespace is required; Validation rule check-default-namespace anyPattern[0] failed at path /metadata/namespace/.;Validation rule check-default-namespace anyPattern[1] failed at path /metadata/namespace/."}
msgs := []string{"Validation error: A namespace is required; Validation rule check-default-namespace anyPattern[0] failed at path /metadata/namespace/. Validation rule check-default-namespace anyPattern[1] failed at path /metadata/namespace/."}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
}
@ -1431,3 +1432,348 @@ func TestValidate_negationAnchor_pass(t *testing.T) {
}
assert.Assert(t, er.IsSuccesful())
}
func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "check-root-user"
},
"spec": {
"containers": [
{
"name": "check-root-user-a",
"image": "nginxinc/nginx-unprivileged",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"pattern": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name1}}*"
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
json.Unmarshal(policyraw, &policy)
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success, true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "test"
},
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "test-pod",
"image": "nginxinc/nginx-unprivileged"
}
]
}
}
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name1}}*"
}
]
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name}}*"
}
]
}
}
}
}
]
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success == true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent == false)
expectMsg := "Validation rule 'test-path-not-exist' anyPattern[1] succeeded."
assert.Assert(t, er.PolicyResponse.Rules[0].Message == expectMsg)
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "test"
},
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "test-pod",
"image": "nginxinc/nginx-unprivileged"
}
]
}
}
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name1}}*"
}
]
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name2}}*"
}
]
}
}
}
}
]
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success, true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatternSatisfy(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "test"
},
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "pod-test-pod",
"image": "nginxinc/nginx-unprivileged"
}
]
}
}
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "substitue-variable"
},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name}}*"
}
]
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "{{request.object.metadata.name}}*"
}
]
}
}
}
}
]
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
expectedMsg := "Validation error: ; Validation rule test-path-not-exist anyPattern[0] failed at path /spec/template/spec/containers/0/name/. Validation rule test-path-not-exist anyPattern[1] failed at path /spec/template/spec/containers/0/name/."
assert.Assert(t, er.PolicyResponse.Rules[0].Success == false)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent == false)
assert.Assert(t, er.PolicyResponse.Rules[0].Message == expectedMsg)
}

View file

@ -0,0 +1,82 @@
package variables
import (
"regexp"
"strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/context"
)
// ValidateVariables validates if referenced path is present
// return empty string if all paths are valid, otherwise return invalid path
func ValidateVariables(ctx context.EvalInterface, pattern interface{}) string {
var pathsNotPresent []string
variableList := extractVariables(pattern)
for i := 0; i < len(variableList)-1; i = i + 2 {
p := variableList[i+1]
glog.V(3).Infof("validating variables %s", p)
val, err := ctx.Query(p)
// reference path is not present
if err == nil && val == nil {
pathsNotPresent = append(pathsNotPresent, p)
}
}
if len(variableList) != 0 && len(pathsNotPresent) != 0 {
return strings.Join(pathsNotPresent, ";")
}
return ""
}
// extractVariables extracts variables in the pattern
func extractVariables(pattern interface{}) []string {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return extractMap(typedPattern)
case []interface{}:
return extractArray(typedPattern)
case string:
return extractValue(typedPattern)
default:
return nil
}
}
func extractMap(patternMap map[string]interface{}) []string {
var variableList []string
for _, patternElement := range patternMap {
if vars := extractVariables(patternElement); vars != nil {
variableList = append(variableList, vars...)
}
}
return variableList
}
func extractArray(patternList []interface{}) []string {
var variableList []string
for _, patternElement := range patternList {
if vars := extractVariables(patternElement); vars != nil {
variableList = append(variableList, vars...)
}
}
return variableList
}
func extractValue(valuePattern string) []string {
operatorVariable := getOperator(valuePattern)
variable := valuePattern[len(operatorVariable):]
return extractValueVariable(variable)
}
func extractValueVariable(valuePattern string) []string {
variableRegex := regexp.MustCompile(variableRegex)
groups := variableRegex.FindStringSubmatch(valuePattern)
if len(groups)%2 != 0 {
return nil
}
return groups
}

View file

@ -0,0 +1,158 @@
package variables
import (
"encoding/json"
"fmt"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
)
func Test_ExtractVariables(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner-{{request.userInfo.username}}",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name}}"
]
}
]
}
}
`)
var pattern interface{}
json.Unmarshal(patternRaw, &pattern)
vars := extractVariables(pattern)
result := []string{"{{request.userInfo.username}}", "request.userInfo.username", "{{request.object.metadata.name}}", "request.object.metadata.name"}
assert.Assert(t, len(vars) == len(result), fmt.Sprintf("result does not match, var: %s", vars))
}
func Test_ValidateVariables_NoVariable(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"Pod"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
// userInfo
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var pattern, resource interface{}
assert.NilError(t, json.Unmarshal(patternRaw, &pattern))
assert.NilError(t, json.Unmarshal(resourceRaw, &resource))
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
ctx.AddUserInfo(userReqInfo)
invalidPaths := ValidateVariables(ctx, pattern)
assert.Assert(t, len(invalidPaths) == 0)
}
func Test_ValidateVariables(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner-{{request.userInfo.username}}",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name1}}"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
// userInfo
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var pattern, resource interface{}
assert.NilError(t, json.Unmarshal(patternRaw, &pattern))
assert.NilError(t, json.Unmarshal(resourceRaw, &resource))
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
ctx.AddUserInfo(userReqInfo)
invalidPaths := ValidateVariables(ctx, pattern)
assert.Assert(t, len(invalidPaths) > 0)
}

View file

@ -9,6 +9,8 @@ import (
"github.com/nirmata/kyverno/pkg/engine/operator"
)
const variableRegex = `\{\{([^{}]*)\}\}`
//SubstituteVariables substitutes the JMESPATH with variable substitution
// supported substitutions
// - no operator + variable(string,object)
@ -73,7 +75,7 @@ func substituteValue(ctx context.EvalInterface, valuePattern string) interface{}
func getValueQuery(ctx context.EvalInterface, valuePattern string) interface{} {
var emptyInterface interface{}
// extract variable {{<variable>}}
validRegex := regexp.MustCompile(`\{\{([^{}]*)\}\}`)
validRegex := regexp.MustCompile(variableRegex)
groups := validRegex.FindAllStringSubmatch(valuePattern, -1)
// can have multiple variables in a single value pattern
// var Map <variable,value>

View file

@ -49,7 +49,7 @@ func checkValue(valuePattern string, variables []string, path string) error {
}
func checkValueVariable(valuePattern string, variables []string) bool {
variableRegex := regexp.MustCompile("^{{(.*)}}$")
variableRegex := regexp.MustCompile(variableRegex)
groups := variableRegex.FindStringSubmatch(valuePattern)
if len(groups) < 2 {
return false

View file

@ -1,7 +1,9 @@
package generate
import (
"errors"
"fmt"
"reflect"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -11,7 +13,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/validate"
"github.com/nirmata/kyverno/pkg/engine/variables"
"github.com/nirmata/kyverno/pkg/policyviolation"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
@ -83,6 +85,13 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern
return nil, fmt.Errorf("policy %s, dont not apply to resource %v", gr.Spec.Policy, gr.Spec.Resource)
}
if pv := buildPathNotPresentPV(engineResponse); pv != nil {
c.pvGenerator.Add(pv...)
// variable substitiution fails in ruleInfo (match,exclude,condition)
// the overall policy should not apply to resource
return nil, fmt.Errorf("referenced path not present in generate policy %s", policy.Name)
}
// Apply the generate rule on resource
return applyGeneratePolicy(c.client, policyContext, gr.Status.State)
}
@ -130,6 +139,10 @@ func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.
var err error
var noGenResource kyverno.ResourceSpec
if invalidPaths := variables.ValidateVariables(ctx, rule.Generation.ResourceSpec); len(invalidPaths) != 0 {
return noGenResource, NewViolation(rule.Name, fmt.Errorf("path not present in generate resource spec: %s", invalidPaths))
}
// variable substitution
// - name
// - namespace
@ -165,7 +178,7 @@ func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.
}
// CLONE
if gen.Clone != (kyverno.CloneFrom{}) {
if rdata, err = handleClone(gen, client, resource, ctx, state); err != nil {
if rdata, err = handleClone(rule.Name, gen, client, resource, ctx, state); err != nil {
glog.V(4).Info(err)
switch e := err.(type) {
case *NotFound:
@ -238,12 +251,16 @@ func variableSubsitutionForAttributes(gen kyverno.Generation, ctx context.EvalIn
}
func handleData(ruleName string, generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
if invalidPaths := variables.ValidateVariables(ctx, generateRule.Data); len(invalidPaths) != 0 {
return nil, NewViolation(ruleName, fmt.Errorf("path not present in generate data: %s", invalidPaths))
}
newData := variables.SubstituteVariables(ctx, generateRule.Data)
// check if resource exists
obj, err := client.GetResource(generateRule.Kind, generateRule.Namespace, generateRule.Name)
glog.V(4).Info(err)
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
glog.V(4).Info(string(state))
// Resource does not exist
if state == "" {
@ -279,21 +296,25 @@ func handleData(ruleName string, generateRule kyverno.Generation, client *dclien
return nil, nil
}
func handleClone(generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
func handleClone(ruleName string, generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
if invalidPaths := variables.ValidateVariables(ctx, generateRule.Clone); len(invalidPaths) != 0 {
return nil, NewViolation(ruleName, fmt.Errorf("path not present in generate clone: %s", invalidPaths))
}
// check if resource exists
_, err := client.GetResource(generateRule.Kind, generateRule.Namespace, generateRule.Name)
if err == nil {
// resource exists
return nil, nil
}
if !errors.IsNotFound(err) {
if !apierrors.IsNotFound(err) {
//something wrong while fetching resource
return nil, err
}
// get reference clone resource
obj, err := client.GetResource(generateRule.Kind, generateRule.Clone.Namespace, generateRule.Clone.Name)
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
return nil, NewNotFound(generateRule.Kind, generateRule.Clone.Namespace, generateRule.Clone.Name)
}
if err != nil {
@ -306,9 +327,9 @@ func handleClone(generateRule kyverno.Generation, client *dclient.Client, resour
func checkResource(ctx context.EvalInterface, newResourceSpec interface{}, resource *unstructured.Unstructured) (bool, error) {
// check if the resource spec if a subset of the resource
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, newResourceSpec)
if err != nil {
if !reflect.DeepEqual(err, validate.ValidationError{}) {
glog.V(4).Infof("config not a subset of resource. failed at path %s: %v", path, err)
return false, err
return false, errors.New(err.ErrorMsg)
}
return true, nil
}
@ -316,7 +337,6 @@ func checkResource(ctx context.EvalInterface, newResourceSpec interface{}, resou
func generatePV(gr kyverno.GenerateRequest, resource unstructured.Unstructured, err *Violation) policyviolation.Info {
info := policyviolation.Info{
Blocked: false,
PolicyName: gr.Spec.Policy,
Resource: resource,
Rules: []kyverno.ViolatedRule{kyverno.ViolatedRule{

View file

@ -5,7 +5,9 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -108,3 +110,13 @@ func successEvents(gr kyverno.GenerateRequest, resource unstructured.Unstructure
return events
}
// buildPathNotPresentPV build violation info when referenced path not found
func buildPathNotPresentPV(er response.EngineResponse) []policyviolation.Info {
for _, rr := range er.PolicyResponse.Rules {
if rr.PathNotPresent {
return policyviolation.GeneratePVsFromEngineResponse([]response.EngineResponse{er})
}
}
return nil
}

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
@ -15,55 +14,10 @@ func (nsc *NamespaceController) report(engineResponses []response.EngineResponse
eventInfos := generateEvents(engineResponses)
nsc.eventGen.Add(eventInfos...)
// generate policy violations
pvInfos := generatePVs(engineResponses)
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
nsc.pvGenerator.Add(pvInfos...)
}
func generatePVs(ers []response.EngineResponse) []policyviolation.Info {
var pvInfos []policyviolation.Info
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.PolicyResponse.Resource.Name == "" {
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er))
}
return pvInfos
}
func buildPVInfo(er response.EngineResponse) policyviolation.Info {
info := policyviolation.Info{
Blocked: false,
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er),
}
return info
}
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
Message: rule.Message,
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}
func generateEvents(ers []response.EngineResponse) []event.Info {
var eventInfos []event.Info
for _, er := range ers {

View file

@ -7,84 +7,52 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1"
dclient "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/policyviolation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
func (pc *PolicyController) cleanUpPolicyViolation(pResponse response.PolicyResponse) {
// 1- check if there is violation on resource (label:Selector)
// 2- check if there is violation on owner
// - recursively get owner by queries the api server for owner information of the resource
// there can be multiple violations as a resource can have multiple owners
// - check if there is violation on resource (label:Selector)
if pResponse.Resource.Namespace == "" {
pvs, err := getClusterPVs(pc.cpvLister, pc.client, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Name)
pv, err := getClusterPV(pc.cpvLister, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Name)
if err != nil {
glog.Errorf("failed to cleanUp violations: %v", err)
return
}
for _, pv := range pvs {
if reflect.DeepEqual(pv, kyverno.ClusterPolicyViolation{}) {
continue
}
glog.V(4).Infof("cleanup cluster violation %s on %s", pv.Name, pv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteClusterPolicyViolation(pv.Name); err != nil {
glog.Errorf("failed to delete cluster policy violation %s on %s: %v", pv.Name, pv.Spec.ResourceSpec.ToKey(), err)
continue
}
if reflect.DeepEqual(pv, kyverno.ClusterPolicyViolation{}) {
return
}
glog.V(4).Infof("cleanup cluster violation %s on %s", pv.Name, pv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteClusterPolicyViolation(pv.Name); err != nil {
glog.Errorf("failed to delete cluster policy violation %s on %s: %v", pv.Name, pv.Spec.ResourceSpec.ToKey(), err)
}
return
}
nspvs, err := getNamespacedPVs(pc.nspvLister, pc.client, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Namespace, pResponse.Resource.Name)
// namespace policy violation
nspv, err := getNamespacedPV(pc.nspvLister, pResponse.Policy, pResponse.Resource.Kind, pResponse.Resource.Namespace, pResponse.Resource.Name)
if err != nil {
glog.Error(err)
return
}
for _, pv := range nspvs {
if reflect.DeepEqual(pv, kyverno.PolicyViolation{}) {
continue
}
glog.V(4).Infof("cleanup namespaced violation %s on %s.%s", pv.Name, pResponse.Resource.Namespace, pv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteNamespacedPolicyViolation(pv.Namespace, pv.Name); err != nil {
glog.Errorf("failed to delete namespaced policy violation %s on %s: %v", pv.Name, pv.Spec.ResourceSpec.ToKey(), err)
continue
}
if reflect.DeepEqual(nspv, kyverno.PolicyViolation{}) {
return
}
glog.V(4).Infof("cleanup namespaced violation %s on %s.%s", nspv.Name, pResponse.Resource.Namespace, nspv.Spec.ResourceSpec.ToKey())
if err := pc.pvControl.DeleteNamespacedPolicyViolation(nspv.Namespace, nspv.Name); err != nil {
glog.Errorf("failed to delete namespaced policy violation %s on %s: %v", nspv.Name, nspv.Spec.ResourceSpec.ToKey(), err)
}
}
func getClusterPVs(pvLister kyvernolister.ClusterPolicyViolationLister, client *dclient.Client, policyName, kind, name string) ([]kyverno.ClusterPolicyViolation, error) {
var pvs []kyverno.ClusterPolicyViolation
// Wont do the claiming of objects, just lookup based on selectors
func getClusterPV(pvLister kyvernolister.ClusterPolicyViolationLister, policyName, rkind, rname string) (kyverno.ClusterPolicyViolation, error) {
var err error
// Check Violation on resource
pv, err := getClusterPVOnResource(pvLister, policyName, kind, name)
if err != nil {
glog.V(4).Infof("error while fetching violation on existing resource: %v", err)
return nil, err
}
if !reflect.DeepEqual(pv, kyverno.ClusterPolicyViolation{}) {
// found a violation on resource
pvs = append(pvs, pv)
return pvs, nil
}
// Check Violations on owner
pvs, err = getClusterPVonOwnerRef(pvLister, client, policyName, kind, name)
if err != nil {
glog.V(4).Infof("error while fetching pv: %v", err)
return nil, err
}
return pvs, nil
}
// Wont do the claiming of objects, just lookup based on selectors and owner references
func getClusterPVOnResource(pvLister kyvernolister.ClusterPolicyViolationLister, policyName, kind, name string) (kyverno.ClusterPolicyViolation, error) {
pvs, err := pvLister.List(labels.Everything())
if err != nil {
glog.V(2).Infof("unable to list policy violations : %v", err)
@ -94,65 +62,16 @@ func getClusterPVOnResource(pvLister kyvernolister.ClusterPolicyViolationLister,
for _, pv := range pvs {
// find a policy on same resource and policy combination
if pv.Spec.Policy == policyName &&
pv.Spec.ResourceSpec.Kind == kind &&
pv.Spec.ResourceSpec.Name == name {
pv.Spec.ResourceSpec.Kind == rkind &&
pv.Spec.ResourceSpec.Name == rname {
return *pv, nil
}
}
return kyverno.ClusterPolicyViolation{}, nil
}
func getClusterPVonOwnerRef(pvLister kyvernolister.ClusterPolicyViolationLister, dclient *dclient.Client, policyName, kind, name string) ([]kyverno.ClusterPolicyViolation, error) {
var pvs []kyverno.ClusterPolicyViolation
// get resource
resource, err := dclient.GetResource(kind, "", name)
if err != nil {
glog.V(4).Infof("error while fetching the resource: %v", err)
return pvs, fmt.Errorf("error while fetching the resource: %v", err)
}
// getOwners returns nil if there is any error
owners := map[kyverno.ResourceSpec]interface{}{}
policyviolation.GetOwner(dclient, owners, *resource)
// as we can have multiple top level owners to a resource
// check if pv exists on each one
for owner := range owners {
pv, err := getClusterPVOnResource(pvLister, policyName, owner.Kind, owner.Name)
if err != nil {
glog.Errorf("error while fetching resource owners: %v", err)
continue
}
pvs = append(pvs, pv)
}
return pvs, nil
}
func getNamespacedPVs(nspvLister kyvernolister.PolicyViolationLister, client *dclient.Client, policyName, kind, namespace, name string) ([]kyverno.PolicyViolation, error) {
var pvs []kyverno.PolicyViolation
var err error
pv, err := getNamespacedPVOnResource(nspvLister, policyName, kind, namespace, name)
if err != nil {
glog.V(4).Infof("error while fetching violation on existing resource: %v", err)
return nil, err
}
if !reflect.DeepEqual(pv, kyverno.PolicyViolation{}) {
// found a violation on resource
pvs = append(pvs, pv)
return pvs, nil
}
// Check Violations on owner
pvs, err = getNamespacedPVonOwnerRef(nspvLister, client, policyName, kind, namespace, name)
if err != nil {
glog.V(4).Infof("error while fetching pv: %v", err)
return nil, err
}
return pvs, nil
}
func getNamespacedPVOnResource(nspvLister kyvernolister.PolicyViolationLister, policyName, kind, namespace, name string) (kyverno.PolicyViolation, error) {
nspvs, err := nspvLister.PolicyViolations(namespace).List(labels.Everything())
func getNamespacedPV(nspvLister kyvernolister.PolicyViolationLister, policyName, rkind, rnamespace, rname string) (kyverno.PolicyViolation, error) {
nspvs, err := nspvLister.PolicyViolations(rnamespace).List(labels.Everything())
if err != nil {
glog.V(2).Infof("failed to list namespaced pv: %v", err)
return kyverno.PolicyViolation{}, fmt.Errorf("failed to list namespaced pv: %v", err)
@ -161,39 +80,15 @@ func getNamespacedPVOnResource(nspvLister kyvernolister.PolicyViolationLister, p
for _, nspv := range nspvs {
// find a policy on same resource and policy combination
if nspv.Spec.Policy == policyName &&
nspv.Spec.ResourceSpec.Kind == kind &&
nspv.Spec.ResourceSpec.Name == name {
nspv.Spec.ResourceSpec.Kind == rkind &&
nspv.Spec.ResourceSpec.Name == rname {
return *nspv, nil
}
}
return kyverno.PolicyViolation{}, nil
}
func getNamespacedPVonOwnerRef(nspvLister kyvernolister.PolicyViolationLister, dclient *dclient.Client, policyName, kind, namespace, name string) ([]kyverno.PolicyViolation, error) {
var pvs []kyverno.PolicyViolation
// get resource
resource, err := dclient.GetResource(kind, namespace, name)
if err != nil {
glog.V(4).Infof("error while fetching the resource: %v", err)
return pvs, err
}
// getOwners returns nil if there is any error
owners := map[kyverno.ResourceSpec]interface{}{}
policyviolation.GetOwner(dclient, owners, *resource)
// as we can have multiple top level owners to a resource
// check if pv exists on each one
for owner := range owners {
pv, err := getNamespacedPVOnResource(nspvLister, policyName, owner.Kind, namespace, owner.Name)
if err != nil {
glog.Errorf("error while fetching resource owners: %v", err)
continue
}
pvs = append(pvs, pv)
}
return pvs, nil
}
func converLabelToSelector(labelMap map[string]string) (labels.Selector, error) {
ls := &metav1.LabelSelector{}
err := metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&labelMap, ls, nil)

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
@ -12,13 +11,13 @@ import (
// for each policy-resource response
// - has violation -> report
// - no violation -> cleanup policy violations(resource or resource owner)
// - no violation -> cleanup policy violations
func (pc *PolicyController) cleanupAndReport(engineResponses []response.EngineResponse) {
// generate Events
eventInfos := generateEvents(engineResponses)
pc.eventGen.Add(eventInfos...)
// create policy violation
pvInfos := generatePVs(engineResponses)
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
pc.pvGenerator.Add(pvInfos...)
// cleanup existing violations if any
// if there is any error in clean up, we dont re-queue the resource
@ -39,51 +38,6 @@ func (pc *PolicyController) cleanUp(ers []response.EngineResponse) {
}
}
func generatePVs(ers []response.EngineResponse) []policyviolation.Info {
var pvInfos []policyviolation.Info
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.PolicyResponse.Resource.Name == "" {
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er))
}
return pvInfos
}
func buildPVInfo(er response.EngineResponse) policyviolation.Info {
info := policyviolation.Info{
Blocked: false,
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er),
}
return info
}
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
Message: rule.Message,
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}
func generateEvents(ers []response.EngineResponse) []event.Info {
var eventInfos []event.Info
for _, er := range ers {

View file

@ -3,55 +3,46 @@ package policyviolation
import (
"fmt"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine/response"
)
func GeneratePVsFromEngineResponse(ers []response.EngineResponse) (pvInfos []Info) {
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.PolicyResponse.Resource.Name == "" {
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
// skip when response succeed AND referenced paths exist
if er.IsSuccesful() && !er.IsPathNotPresent() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er))
}
return pvInfos
}
// Builder builds Policy Violation struct
// this is base type of namespaced and cluster policy violation
type Builder interface {
generate(info Info) []kyverno.PolicyViolationTemplate
generate(info Info) kyverno.PolicyViolationTemplate
build(policy, kind, namespace, name string, rules []kyverno.ViolatedRule) *kyverno.PolicyViolationTemplate
}
type pvBuilder struct {
// dynamic client
dclient *client.Client
type pvBuilder struct{}
func newPvBuilder() *pvBuilder {
return &pvBuilder{}
}
func newPvBuilder(dclient *client.Client) *pvBuilder {
pvb := pvBuilder{
dclient: dclient,
}
return &pvb
}
func (pvb *pvBuilder) generate(info Info) []kyverno.PolicyViolationTemplate {
var owners []kyverno.ResourceSpec
// get the owners if the resource is blocked or
// TODO: https://github.com/nirmata/kyverno/issues/535
if info.Blocked {
// get resource owners
owners = GetOwners(pvb.dclient, info.Resource)
}
pvs := pvb.buildPolicyViolations(owners, info)
return pvs
}
func (pvb *pvBuilder) buildPolicyViolations(owners []kyverno.ResourceSpec, info Info) []kyverno.PolicyViolationTemplate {
var pvs []kyverno.PolicyViolationTemplate
if len(owners) != 0 {
// there are resource owners
// generate PV on them
for _, resource := range owners {
pv := pvb.build(info.PolicyName, resource.Kind, resource.Namespace, resource.Name, info.Rules)
pvs = append(pvs, *pv)
}
} else {
// generate PV on resource
pv := pvb.build(info.PolicyName, info.Resource.GetKind(), info.Resource.GetNamespace(), info.Resource.GetName(), info.Rules)
pvs = append(pvs, *pv)
}
return pvs
func (pvb *pvBuilder) generate(info Info) kyverno.PolicyViolationTemplate {
pv := pvb.build(info.PolicyName, info.Resource.GetKind(), info.Resource.GetNamespace(), info.Resource.GetName(), info.Rules)
return *pv
}
func (pvb *pvBuilder) build(policy, kind, namespace, name string, rules []kyverno.ViolatedRule) *kyverno.PolicyViolationTemplate {
@ -77,3 +68,28 @@ func (pvb *pvBuilder) build(policy, kind, namespace, name string, rules []kyvern
pv.SetGenerateName(fmt.Sprintf("%s-", policy))
return pv
}
func buildPVInfo(er response.EngineResponse) Info {
info := Info{
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er),
}
return info
}
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success && !rule.PathNotPresent {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
Message: rule.Message,
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}

View file

@ -0,0 +1,60 @@
package policyviolation
import (
"testing"
"github.com/nirmata/kyverno/pkg/engine/response"
"gotest.tools/assert"
)
func Test_GeneratePVsFromEngineResponse_PathNotExist(t *testing.T) {
ers := []response.EngineResponse{
response.EngineResponse{
PolicyResponse: response.PolicyResponse{
Policy: "test-substitue-variable",
Resource: response.ResourceSpec{
Kind: "Pod",
Name: "test",
Namespace: "test",
},
Rules: []response.RuleResponse{
response.RuleResponse{
Name: "test-path-not-exist",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
PathNotPresent: true,
},
response.RuleResponse{
Name: "test-path-exist",
Type: "Mutation",
Success: true,
PathNotPresent: false,
},
},
},
},
response.EngineResponse{
PolicyResponse: response.PolicyResponse{
Policy: "test-substitue-variable2",
Resource: response.ResourceSpec{
Kind: "Pod",
Name: "test",
Namespace: "test",
},
Rules: []response.RuleResponse{
response.RuleResponse{
Name: "test-path-not-exist-accross-policy",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
PathNotPresent: true,
},
},
},
},
}
pvInfos := GeneratePVsFromEngineResponse(ers)
assert.Assert(t, len(pvInfos) == 2)
}

View file

@ -56,50 +56,6 @@ func retryGetResource(client *client.Client, rspec kyverno.ResourceSpec) (*unstr
return obj, nil
}
// GetOwners returns a list of owners
func GetOwners(dclient *client.Client, resource unstructured.Unstructured) []kyverno.ResourceSpec {
ownerMap := map[kyverno.ResourceSpec]interface{}{}
GetOwner(dclient, ownerMap, resource)
var owners []kyverno.ResourceSpec
for owner := range ownerMap {
owners = append(owners, owner)
}
return owners
}
// GetOwner of a resource by iterating over ownerReferences
func GetOwner(dclient *client.Client, ownerMap map[kyverno.ResourceSpec]interface{}, resource unstructured.Unstructured) {
var emptyInterface interface{}
resourceSpec := kyverno.ResourceSpec{
Kind: resource.GetKind(),
Namespace: resource.GetNamespace(),
Name: resource.GetName(),
}
if _, ok := ownerMap[resourceSpec]; ok {
// owner seen before
// breaking loop
return
}
rOwners := resource.GetOwnerReferences()
// if there are no resource owners then its top level resource
if len(rOwners) == 0 {
// add resource to map
ownerMap[resourceSpec] = emptyInterface
return
}
for _, rOwner := range rOwners {
// lookup resource via client
// owner has to be in same namespace
owner, err := dclient.GetResource(rOwner.Kind, resource.GetNamespace(), rOwner.Name)
if err != nil {
glog.Errorf("Failed to get resource owner for %s/%s/%s, err: %v", rOwner.Kind, resource.GetNamespace(), rOwner.Name, err)
// as we want to process other owners
continue
}
GetOwner(dclient, ownerMap, *owner)
}
}
func converLabelToSelector(labelMap map[string]string) (labels.Selector, error) {
ls := &metav1.LabelSelector{}
err := metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&labelMap, ls, nil)

View file

@ -76,7 +76,6 @@ func (ds *dataStore) delete(keyHash string) {
//Info is a request to create PV
type Info struct {
Blocked bool
PolicyName string
Resource unstructured.Unstructured
Rules []kyverno.ViolatedRule
@ -84,7 +83,6 @@ type Info struct {
func (i Info) toKey() string {
keys := []string{
strconv.FormatBool(i.Blocked),
i.PolicyName,
i.Resource.GetKind(),
i.Resource.GetNamespace(),
@ -219,7 +217,7 @@ func (gen *Generator) syncHandler(info Info) error {
glog.V(4).Infof("recieved info:%v", info)
var handler pvGenerator
var builder Builder
builder = newPvBuilder(gen.dclient)
builder = newPvBuilder()
if info.Resource.GetNamespace() == "" {
// cluster scope resource generate a clusterpolicy violation
handler = newClusterPV(gen.dclient, gen.cpvLister, gen.kyvernoInterface)
@ -229,20 +227,17 @@ func (gen *Generator) syncHandler(info Info) error {
}
failure := false
// Generate Policy Violations
// as there can be multiple owners we can have multiple violations
pvs := builder.generate(info)
for _, pv := range pvs {
// Create Policy Violations
glog.V(3).Infof("Creating policy violation: %s", info.toKey())
err := handler.create(pv)
if err != nil {
failure = true
glog.V(3).Infof("Failed to create policy violation: %v", err)
} else {
glog.V(3).Infof("Policy violation created: %s", info.toKey())
}
pv := builder.generate(info)
// Create Policy Violations
glog.V(3).Infof("Creating policy violation: %s", info.toKey())
if err := handler.create(pv); err != nil {
failure = true
glog.V(3).Infof("Failed to create policy violation: %v", err)
} else {
glog.V(3).Infof("Policy violation created: %s", info.toKey())
}
if failure {
// even if there is a single failure we requeue the request
return errors.New("Failed to process some policy violations, re-queuing")

View file

@ -26,7 +26,7 @@ func isResponseSuccesful(engineReponses []response.EngineResponse) bool {
// returns false -> if all the policies are meant to report only, we dont block resource request
func toBlockResource(engineReponses []response.EngineResponse) bool {
for _, er := range engineReponses {
if er.PolicyResponse.ValidationFailureAction == Enforce {
if !er.IsSuccesful() && er.PolicyResponse.ValidationFailureAction == Enforce {
glog.V(4).Infof("ValidationFailureAction set to enforce for policy %s , blocking resource request ", er.PolicyResponse.Policy)
return true
}
@ -43,7 +43,7 @@ func getErrorMsg(engineReponses []response.EngineResponse) string {
if !er.IsSuccesful() {
// resource in engineReponses is identical as this was called per admission request
resourceInfo = fmt.Sprintf("%s/%s/%s", er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name)
str = append(str, fmt.Sprintf("failed policy %s", er.PolicyResponse.Policy))
str = append(str, fmt.Sprintf("failed policy %s:", er.PolicyResponse.Policy))
for _, rule := range er.PolicyResponse.Rules {
if !rule.Success {
str = append(str, rule.ToString())
@ -51,7 +51,7 @@ func getErrorMsg(engineReponses []response.EngineResponse) string {
}
}
}
return fmt.Sprintf("Resource %s: %s", resourceInfo, strings.Join(str, "\n"))
return fmt.Sprintf("Resource %s %s", resourceInfo, strings.Join(str, ";"))
}
//ArrayFlags to store filterkinds

View file

@ -41,6 +41,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic
policyContext := engine.PolicyContext{
NewResource: *resource,
AdmissionInfo: userRequestInfo,
Context: ctx,
}
// engine.Generate returns a list of rules that are applicable on this resource

View file

@ -8,6 +8,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/response"
engineutils "github.com/nirmata/kyverno/pkg/engine/utils"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -70,6 +71,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou
policyContext := engine.PolicyContext{
NewResource: resource,
AdmissionInfo: userRequestInfo,
Context: ctx,
}
for _, policy := range policies {
@ -96,6 +98,10 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou
patches = append(patches, annPatches)
}
// generate violation when referenced path does not exist
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
ws.pvGenerator.Add(pvInfos...)
// ADD EVENTS
events := generateEvents(engineResponses, (request.Operation == v1beta1.Update))
ws.eventGen.Add(events...)

View file

@ -136,7 +136,7 @@ func defaultvalidationFailureAction(policy *kyverno.ClusterPolicy) ([]byte, stri
// scenario A: not exist, set default to "all", which generates on all pod controllers
// - if name / selector exist in resource description -> skip
// as these fields may not be applicable to pod controllers
// scenario B: "null", user explicitely disable this feature -> skip
// scenario B: "none", user explicitely disable this feature -> skip
// scenario C: some certain controllers that user set -> generate on defined controllers
// copy entrie match / exclude block, it's users' responsibility to
// make sure all fields are applicable to pod cotrollers
@ -158,7 +158,7 @@ func generatePodControllerRule(policy kyverno.ClusterPolicy) (patches [][]byte,
}
// scenario B
if controllers == "null" {
if controllers == "none" {
return nil, nil
}

View file

@ -66,6 +66,187 @@ func TestGeneratePodControllerRule_PredefinedAnnotation(t *testing.T) {
assert.Assert(t, len(patches) == 0)
}
func TestGeneratePodControllerRule_DisableFeature(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "none"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
patches, errs := generatePodControllerRule(policy)
assert.Assert(t, len(errs) == 0)
assert.Assert(t, len(patches) == 0)
}
func TestGeneratePodControllerRule_Mutate(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
patches, errs := generatePodControllerRule(policy)
assert.Assert(t, len(errs) == 0)
p, err := utils.ApplyPatches(policyRaw, patches)
assert.NilError(t, err)
expectedPolicy := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
},
{
"name": "autogen-annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"mutate": {
"overlay": {
"spec": {
"template": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {
}
}
]
}
}
}
}
}
}
]
}
}`)
compareJSONAsMap(t, p, expectedPolicy)
}
func TestGeneratePodControllerRule_ExistOtherAnnotation(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
@ -100,133 +281,55 @@ func TestGeneratePodControllerRule_ExistOtherAnnotation(t *testing.T) {
compareJSONAsMap(t, p, expectedPolicy)
}
func TestGeneratePodControllerRule(t *testing.T) {
func TestGeneratePodControllerRule_ValidateAnyPattern(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "add-safe-to-evict",
"annotations": {
"a": "b"
}
"annotations": {
"pod-policies.kyverno.io/autogen-controllers": "Deployment"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {}
}
]
}
}
}
"rules": [
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
{
"name": "annotate-host-path",
"match": {
"resources": {
"kinds": [
"Pod"
]
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
},
"spec": {
"volumes": [
{
"(hostPath)": {
"path": "*"
}
}
]
}
}
]
}
}
},
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
]
}
},
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
]
}
]
}
]
}
}`)
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
@ -236,278 +339,212 @@ func TestGeneratePodControllerRule(t *testing.T) {
p, err := utils.ApplyPatches(policyRaw, patches)
assert.NilError(t, err)
expectPolicy := []byte(`{
expectedPolicy := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"a": "b",
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
"annotations": {
"pod-policies.kyverno.io/autogen-controllers": "Deployment"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {}
}
]
}
}
}
"rules": [
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
{
"name": "annotate-host-path",
"match": {
"resources": {
"kinds": [
"Pod"
]
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
"mutate": {
"overlay": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
},
"spec": {
"volumes": [
{
"(hostPath)": {
"path": "*"
}
}
]
}
}
]
}
}
},
{
"name": "validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
{
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
]
}
},
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
},
{
"name": "autogen-annotate-empty-dir",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"mutate": {
"overlay": {
"spec": {
"template": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(emptyDir)": {}
}
]
}
}
}
}
}
},
{
"name": "autogen-annotate-host-path",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"mutate": {
"overlay": {
"spec": {
"template": {
"metadata": {
"annotations": {
"+(cluster-autoscaler.kubernetes.io/safe-to-evict)": "true"
}
},
"spec": {
"volumes": [
{
"(hostPath)": {
"path": "*"
}
}
]
}
}
}
}
}
},
{
"name": "autogen-validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
}
}
]
}
},
{
"name": "autogen-validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"template": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
}
}
]
}
]
},
{
"name": "autogen-validate-runAsNonRoot",
"match": {
"resources": {
"kinds": [
"Deployment"
]
}
},
"validate": {
"message": "Running as root user is not allowed. Set runAsNonRoot to true",
"anyPattern": [
{
"spec": {
"template": {
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
}
}
},
{
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "*",
"securityContext": {
"runAsNonRoot": true
}
}
]
}
}
}
}
]
}
}
]
}
}`)
t.Log(string(expectPolicy))
t.Log(string(p))
compareJSONAsMap(t, expectPolicy, p)
}`)
compareJSONAsMap(t, p, expectedPolicy)
}
func TestGeneratePodControllerRule_ValidatePattern(t *testing.T) {
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.Assert(t, json.Unmarshal(policyRaw, &policy))
patches, errs := generatePodControllerRule(policy)
assert.Assert(t, len(errs) == 0)
p, err := utils.ApplyPatches(policyRaw, patches)
assert.NilError(t, err)
expectedPolicy := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"annotations": {
"pod-policies.kyverno.io/autogen-controllers": "all"
},
"name": "add-safe-to-evict"
},
"spec": {
"rules": [
{
"name": "validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"Pod"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
},
{
"name": "autogen-validate-docker-sock-mount",
"match": {
"resources": {
"kinds": [
"DaemonSet",
"Deployment",
"Job",
"StatefulSet"
]
}
},
"validate": {
"message": "Use of the Docker Unix socket is not allowed",
"pattern": {
"spec": {
"template": {
"spec": {
"=(volumes)": [
{
"=(hostPath)": {
"path": "!/var/run/docker.sock"
}
}
]
}
}
}
}
}
}
]
}
}`)
compareJSONAsMap(t, p, expectedPolicy)
}

View file

@ -1,12 +1,10 @@
package webhooks
import (
"fmt"
"strings"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/event"
@ -105,57 +103,3 @@ func generateEvents(engineResponses []response.EngineResponse, onUpdate bool) []
}
return events
}
func generatePV(ers []response.EngineResponse, blocked bool) []policyviolation.Info {
var pvInfos []policyviolation.Info
// generate PV for each
for _, er := range ers {
// ignore creation of PV for resoruces that are yet to be assigned a name
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
// build policy violation info
pvInfos = append(pvInfos, buildPVInfo(er, blocked))
}
return pvInfos
}
func buildPVInfo(er response.EngineResponse, blocked bool) policyviolation.Info {
info := policyviolation.Info{
Blocked: blocked,
PolicyName: er.PolicyResponse.Policy,
Resource: er.PatchedResource,
Rules: buildViolatedRules(er, blocked),
}
return info
}
func buildViolatedRules(er response.EngineResponse, blocked bool) []kyverno.ViolatedRule {
blockMsg := fmt.Sprintf("Request Blocked for resource %s/%s; ", er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Kind)
var violatedRules []kyverno.ViolatedRule
// if resource was blocked we create dependent
dependant := kyverno.ManagedResourceSpec{
Kind: er.PolicyResponse.Resource.Kind,
CreationBlocked: true,
}
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{
Name: rule.Name,
Type: rule.Type,
}
if blocked {
vrule.Message = blockMsg + rule.Message
vrule.ManagedResource = dependant
} else {
vrule.Message = rule.Message
}
violatedRules = append(violatedRules, vrule)
}
return violatedRules
}

View file

@ -10,6 +10,7 @@ import (
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policyviolation"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
)
@ -100,25 +101,18 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol
reportTime := time.Now()
// If Validation fails then reject the request
// violations are created with resource owner(if exist) on "enforce"
// and if there are any then we dont block the resource creation
// Even if one the policy being applied
// no violations will be created on "enforce"
// the event will be reported on owner by k8s
blocked := toBlockResource(engineResponses)
if !isResponseSuccesful(engineResponses) && blocked {
if blocked {
glog.V(4).Infof("resource %s/%s/%s is blocked\n", newR.GetKind(), newR.GetNamespace(), newR.GetName())
pvInfos := generatePV(engineResponses, true)
ws.pvGenerator.Add(pvInfos...)
// ADD EVENTS
events := generateEvents(engineResponses, (request.Operation == v1beta1.Update))
ws.eventGen.Add(events...)
sendStat(true)
return false, getErrorMsg(engineResponses)
}
// ADD POLICY VIOLATIONS
// violations are created with resource on "audit"
pvInfos := generatePV(engineResponses, blocked)
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
ws.pvGenerator.Add(pvInfos...)
// ADD EVENTS
events := generateEvents(engineResponses, (request.Operation == v1beta1.Update))