diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go index 80632acd7c..cb72aeba22 100644 --- a/pkg/engine/context/context.go +++ b/pkg/engine/context/context.go @@ -15,14 +15,19 @@ import ( //Interface to manage context operations type Interface interface { - //AddJSON merges the json with context + + // AddJSON merges the json with context AddJSON(dataRaw []byte) error - //AddResource merges resource json under request.object + + // AddResource merges resource json under request.object AddResource(dataRaw []byte) error - //AddUserInfo merges userInfo json under kyverno.userInfo + + // AddUserInfo merges userInfo json under kyverno.userInfo AddUserInfo(userInfo kyverno.UserInfo) error - //AddSA merges serrviceaccount - AddSA(userName string) error + + // AddServiceAccount merges ServiceAccount types + AddServiceAccount(userName string) error + EvalInterface } @@ -126,8 +131,8 @@ func (ctx *Context) AddUserInfo(userRequestInfo kyverno.RequestInfo) error { return ctx.AddJSON(objRaw) } -//AddSA removes prefix 'system:serviceaccount:' and namespace, then loads only SA name and SA namespace -func (ctx *Context) AddSA(userName string) error { +//AddServiceAccount removes prefix 'system:serviceaccount:' and namespace, then loads only SA name and SA namespace +func (ctx *Context) AddServiceAccount(userName string) error { saPrefix := "system:serviceaccount:" var sa string saName := "" diff --git a/pkg/engine/context/context_test.go b/pkg/engine/context/context_test.go index 6997aa5c09..bc79a836e3 100644 --- a/pkg/engine/context/context_test.go +++ b/pkg/engine/context/context_test.go @@ -93,7 +93,7 @@ func Test_addResourceAndUserContext(t *testing.T) { t.Error("exected result does not match") } // Add service account Name - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + err = ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username) if err != nil { t.Error(err) } diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 316cad6092..5e619d0682 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -12,16 +12,16 @@ import ( // 1. validate variables to be substitute 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 Generate(policyContext PolicyContext) (resp response.EngineResponse) { +func Generate(policyContext PolicyContext) (resp *response.EngineResponse) { return filterRules(policyContext) } -func filterRules(policyContext PolicyContext) response.EngineResponse { +func filterRules(policyContext PolicyContext) *response.EngineResponse { kind := policyContext.NewResource.GetKind() name := policyContext.NewResource.GetName() namespace := policyContext.NewResource.GetNamespace() - resp := response.EngineResponse{ + resp := &response.EngineResponse{ PolicyResponse: response.PolicyResponse{ Policy: policyContext.Policy.Name, Resource: response.ResourceSpec{ @@ -57,7 +57,7 @@ func filterRule(rule kyverno.Rule, policyContext PolicyContext) *response.RuleRe newResource := policyContext.NewResource oldResource := policyContext.OldResource admissionInfo := policyContext.AdmissionInfo - ctx := policyContext.Context + ctx := policyContext.JSONContext resCache := policyContext.ResourceCache jsonContext := policyContext.JSONContext excludeGroupRole := policyContext.ExcludeGroupRole diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index f60bbbbb9e..2a421705eb 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -22,11 +22,12 @@ const ( ) // Mutate performs mutation. Overlay first and then mutation patches -func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { +func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) { + resp = &response.EngineResponse{} startTime := time.Now() policy := policyContext.Policy patchedResource := policyContext.NewResource - ctx := policyContext.Context + ctx := policyContext.JSONContext resCache := policyContext.ResourceCache jsonContext := policyContext.JSONContext @@ -35,10 +36,10 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { logger.V(4).Info("start policy processing", "startTime", startTime) - startMutateResultResponse(&resp, policy, patchedResource) - defer endMutateResultResponse(logger, &resp, startTime) + startMutateResultResponse(resp, policy, patchedResource) + defer endMutateResultResponse(logger, resp, startTime) - if SkipPolicyApplication(policy, patchedResource) { + if ManagedPodResource(policy, patchedResource) { logger.V(5).Info("skip applying policy as direct changes to pods managed by workload controllers are not allowed", "policy", policy.GetName()) resp.PatchedResource = patchedResource return @@ -58,6 +59,7 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { if len(policyContext.ExcludeGroupRole) > 0 { excludeResource = policyContext.ExcludeGroupRole } + if err := MatchesResourceDescription(patchedResource, rule, policyContext.AdmissionInfo, excludeResource); err != nil { logger.V(3).Info("resource not matched", "reason", err.Error()) continue @@ -79,7 +81,6 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { } mutation := rule.Mutation.DeepCopy() - mutateHandler := mutate.CreateMutateHandler(rule.Name, mutation, patchedResource, ctx, logger) ruleResponse, patchedResource = mutateHandler.Handle() if ruleResponse.Success { @@ -87,11 +88,12 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { if ruleResponse.Patches == nil { continue } + logger.V(4).Info("mutate rule applied successfully", "ruleName", rule.Name) } resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse) - incrementAppliedRuleCount(&resp) + incrementAppliedRuleCount(resp) } resp.PatchedResource = patchedResource @@ -103,9 +105,11 @@ func incrementAppliedRuleCount(resp *response.EngineResponse) { } func startMutateResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, resource unstructured.Unstructured) { - // set policy information + if resp == nil { + return + } + resp.PolicyResponse.Policy = policy.Name - // resource details resp.PolicyResponse.Resource.Name = resource.GetName() resp.PolicyResponse.Resource.Namespace = resource.GetNamespace() resp.PolicyResponse.Resource.Kind = resource.GetKind() @@ -113,6 +117,10 @@ func startMutateResultResponse(resp *response.EngineResponse, policy kyverno.Clu } func endMutateResultResponse(logger logr.Logger, resp *response.EngineResponse, startTime time.Time) { + if resp == nil { + return + } + resp.PolicyResponse.ProcessingTime = time.Since(startTime) logger.V(4).Info("finished processing policy", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "mutationRulesApplied", resp.PolicyResponse.RulesAppliedCount) } diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go index 2edddf3177..d03163d65c 100644 --- a/pkg/engine/mutation_test.go +++ b/pkg/engine/mutation_test.go @@ -84,9 +84,9 @@ func Test_VariableSubstitutionOverlay(t *testing.T) { if err != nil { t.Error(err) } - policyContext := PolicyContext{ + policyContext := &PolicyContext{ Policy: policy, - Context: ctx, + JSONContext: ctx, NewResource: *resourceUnstructured} er := Mutate(policyContext) t.Log(string(expectedPatch)) @@ -155,9 +155,9 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) { err = ctx.AddResource(resourceRaw) assert.NilError(t, err) - policyContext := PolicyContext{ + policyContext := &PolicyContext{ Policy: policy, - Context: ctx, + JSONContext: ctx, NewResource: *resourceUnstructured} er := Mutate(policyContext) expectedErrorStr := "variable request.object.metadata.name1 not resolved at path /spec/name" diff --git a/pkg/engine/policyContext.go b/pkg/engine/policyContext.go index 1b457230bf..2d8bb3cce1 100644 --- a/pkg/engine/policyContext.go +++ b/pkg/engine/policyContext.go @@ -26,9 +26,6 @@ type PolicyContext struct { // Dynamic client - used by generate Client *client.Client - // Contexts to store resources - Context context.EvalInterface - // Config handler ExcludeGroupRole []string diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 00bb616b5a..26924be635 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -269,10 +269,15 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k return nil } func copyConditions(original []kyverno.Condition) []kyverno.Condition { + if original == nil || len(original) == 0 { + return []kyverno.Condition{} + } + var copy []kyverno.Condition for _, condition := range original { copy = append(copy, *condition.DeepCopy()) } + return copy } @@ -288,10 +293,10 @@ func excludeResource(resource unstructured.Unstructured) bool { return false } -// SkipPolicyApplication returns true: +// ManagedPodResource returns true: // - if the policy has auto-gen annotation && resource == Pod // - if the auto-gen contains cronJob && resource == Job -func SkipPolicyApplication(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) bool { +func ManagedPodResource(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) bool { if policy.HasAutoGenAnnotation() && excludeResource(resource) { return true } diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 545a69ab81..92a589e4f4 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -14,238 +14,192 @@ import ( "github.com/kyverno/kyverno/pkg/engine/utils" "github.com/kyverno/kyverno/pkg/engine/validate" "github.com/kyverno/kyverno/pkg/engine/variables" - "github.com/kyverno/kyverno/pkg/resourcecache" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/log" ) //Validate applies validation rules from policy on the resource -func Validate(policyContext PolicyContext) (resp response.EngineResponse) { +func Validate(policyContext *PolicyContext) (resp *response.EngineResponse) { + resp = &response.EngineResponse{} startTime := time.Now() - policy := policyContext.Policy - newR := policyContext.NewResource - oldR := policyContext.OldResource - ctx := policyContext.Context - admissionInfo := policyContext.AdmissionInfo - - resCache := policyContext.ResourceCache - jsonContext := policyContext.JSONContext - logger := log.Log.WithName("EngineValidate").WithValues("policy", policy.Name) - - if reflect.DeepEqual(newR, unstructured.Unstructured{}) { - logger = logger.WithValues("kind", oldR.GetKind(), "namespace", oldR.GetNamespace(), "name", oldR.GetName()) - } else { - logger = logger.WithValues("kind", newR.GetKind(), "namespace", newR.GetNamespace(), "name", newR.GetName()) - } + logger := buildLogger(policyContext) logger.V(4).Info("start processing", "startTime", startTime) defer func() { - if reflect.DeepEqual(resp, response.EngineResponse{}) { - return - } - var resource unstructured.Unstructured - if reflect.DeepEqual(resp.PatchedResource, unstructured.Unstructured{}) { - // for delete requests patched resource will be oldR since newR is empty - if reflect.DeepEqual(newR, unstructured.Unstructured{}) { - resource = oldR - } else { - resource = newR - } - } - for i := range resp.PolicyResponse.Rules { - messageInterface, err := variables.SubstituteVars(logger, ctx, resp.PolicyResponse.Rules[i].Message) - if err != nil { - logger.V(4).Info("failed to substitute variables", "error", err.Error()) - continue - } - resp.PolicyResponse.Rules[i].Message, _ = messageInterface.(string) - } - resp.PatchedResource = resource - startResultResponse(&resp, policy, resource) - endResultResponse(logger, &resp, startTime) + buildResponse(logger, policyContext, resp, startTime) + logger.V(4).Info("finished processing", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "validationRulesApplied", resp.PolicyResponse.RulesAppliedCount) }() - // If request is delete, newR will be empty - if reflect.DeepEqual(newR, unstructured.Unstructured{}) { - return *isRequestDenied(logger, ctx, policy, oldR, admissionInfo, policyContext.ExcludeGroupRole, resCache, jsonContext) - } - - if denyResp := isRequestDenied(logger, ctx, policy, newR, admissionInfo, policyContext.ExcludeGroupRole, resCache, jsonContext); !denyResp.IsSuccessful() { - return *denyResp - } - if reflect.DeepEqual(oldR, unstructured.Unstructured{}) { - return *validateResource(logger, ctx, policy, newR, admissionInfo, policyContext.ExcludeGroupRole, resCache, jsonContext) - } - - oldResponse := validateResource(logger, ctx, policy, oldR, admissionInfo, policyContext.ExcludeGroupRole, resCache, jsonContext) - newResponse := validateResource(logger, ctx, policy, newR, admissionInfo, policyContext.ExcludeGroupRole, resCache, jsonContext) - if !isSameResponse(oldResponse, newResponse) { - return *newResponse - } - return response.EngineResponse{} + return validateResource(logger, policyContext) } -func startResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, newR unstructured.Unstructured) { - // set policy information +func buildLogger(ctx *PolicyContext) logr.Logger { + logger := log.Log.WithName("EngineValidate").WithValues("policy", ctx.Policy.Name) + if reflect.DeepEqual(ctx.NewResource, unstructured.Unstructured{}) { + logger = logger.WithValues("kind", ctx.OldResource.GetKind(), "namespace", ctx.OldResource.GetNamespace(), "name", ctx.OldResource.GetName()) + } else { + logger = logger.WithValues("kind", ctx.NewResource.GetKind(), "namespace", ctx.NewResource.GetNamespace(), "name", ctx.NewResource.GetName()) + } + + return logger +} + +func buildResponse(logger logr.Logger, ctx *PolicyContext, resp *response.EngineResponse, startTime time.Time) { + if reflect.DeepEqual(resp, response.EngineResponse{}) { + return + } + + var resource unstructured.Unstructured + if reflect.DeepEqual(resp.PatchedResource, unstructured.Unstructured{}) { + // for delete requests patched resource will be oldResource since newResource is empty + if reflect.DeepEqual(ctx.NewResource, unstructured.Unstructured{}) { + resource = ctx.OldResource + } else { + resource = ctx.NewResource + } + } + + for i := range resp.PolicyResponse.Rules { + messageInterface, err := variables.SubstituteVars(logger, ctx.JSONContext, resp.PolicyResponse.Rules[i].Message) + if err != nil { + logger.V(4).Info("failed to substitute variables", "error", err.Error()) + continue + } + + resp.PolicyResponse.Rules[i].Message, _ = messageInterface.(string) + } + + resp.PatchedResource = resource + setResponse(resp, ctx.Policy, resource, startTime) +} + +func setResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, startTime time.Time) { 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.Resource.Name = resource.GetName() + resp.PolicyResponse.Resource.Namespace = resource.GetNamespace() + resp.PolicyResponse.Resource.Kind = resource.GetKind() + resp.PolicyResponse.Resource.APIVersion = resource.GetAPIVersion() resp.PolicyResponse.ValidationFailureAction = policy.Spec.ValidationFailureAction -} - -func endResultResponse(log logr.Logger, resp *response.EngineResponse, startTime time.Time) { resp.PolicyResponse.ProcessingTime = time.Since(startTime) - log.V(4).Info("finished processing", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "validationRulesApplied", resp.PolicyResponse.RulesAppliedCount) } func incrementAppliedCount(resp *response.EngineResponse) { - // rules applied successfully count resp.PolicyResponse.RulesAppliedCount++ } -func isRequestDenied(log logr.Logger, ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, admissionInfo kyverno.RequestInfo, excludeGroupRole []string, resCache resourcecache.ResourceCacheIface, jsonContext *context.Context) *response.EngineResponse { +func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineResponse { resp := &response.EngineResponse{} - if SkipPolicyApplication(policy, resource) { - log.V(5).Info("skip applying policy as direct changes to pods managed by workload controllers are not allowed", "policy", policy.GetName()) + if ManagedPodResource(ctx.Policy, ctx.NewResource) { + log.V(5).Info("skip applying policy as direct changes to pods managed by workload controllers are not allowed", "policy", ctx.Policy.GetName()) return resp } - excludeResource := []string{} - if len(excludeGroupRole) > 0 { - excludeResource = excludeGroupRole - } - for _, rule := range policy.Spec.Rules { + + for _, rule := range ctx.Policy.Spec.Rules { if !rule.HasValidate() { continue } // add configmap json data to context - if err := AddResourceToContext(log, rule.Context, resCache, jsonContext); err != nil { + if err := AddResourceToContext(log, rule.Context, ctx.ResourceCache, ctx.JSONContext); err != nil { log.V(4).Info("cannot add configmaps to context", "reason", err.Error()) continue } - if err := MatchesResourceDescription(resource, rule, admissionInfo, excludeResource); err != nil { - log.V(4).Info("resource fails the match description", "reason", err.Error()) - continue - } - - preconditionsCopy := copyConditions(rule.Conditions) - - if !variables.EvaluateConditions(log, ctx, preconditionsCopy) { - log.V(4).Info("resource fails the preconditions") - continue - } - - if rule.Validation.Deny != nil { - denyConditionsCopy := copyConditions(rule.Validation.Deny.Conditions) - if len(rule.Validation.Deny.Conditions) == 0 || variables.EvaluateConditions(log, ctx, denyConditionsCopy) { - ruleResp := response.RuleResponse{ - Name: rule.Name, - Type: utils.Validation.String(), - Message: rule.Validation.Message, - Success: false, - } - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResp) - } - continue - } - - } - return resp -} - -func validateResource(log logr.Logger, ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, admissionInfo kyverno.RequestInfo, excludeGroupRole []string, resCache resourcecache.ResourceCacheIface, jsonContext *context.Context) *response.EngineResponse { - resp := &response.EngineResponse{} - - if SkipPolicyApplication(policy, resource) { - log.V(5).Info("skip applying policy as direct changes to pods managed by workload controllers are not allowed", "policy", policy.GetName()) - return resp - } - - excludeResource := []string{} - if len(excludeGroupRole) > 0 { - excludeResource = excludeGroupRole - } - - for _, rule := range policy.Spec.Rules { - if !rule.HasValidate() { - continue - } - - // check if the resource satisfies the filter conditions defined in the rule - if err := MatchesResourceDescription(resource, rule, admissionInfo, excludeResource); err != nil { - log.V(4).Info("resource fails the match description", "reason", err.Error()) - continue - } - - // add configmap json data to context - if err := AddResourceToContext(log, rule.Context, resCache, jsonContext); err != nil { - log.V(4).Info("cannot add configmaps to context", "reason", err.Error()) + if !matches(log, rule, ctx) { continue } // operate on the copy of the conditions, as we perform variable substitution preconditionsCopy := copyConditions(rule.Conditions) + // evaluate pre-conditions // - handle variable substitutions - if !variables.EvaluateConditions(log, ctx, preconditionsCopy) { + if !variables.EvaluateConditions(log, ctx.JSONContext, preconditionsCopy) { log.V(4).Info("resource fails the preconditions") continue } if rule.Validation.Pattern != nil || rule.Validation.AnyPattern != nil { - ruleResponse := validatePatterns(log, ctx, resource, rule) - if common.IsConditionalAnchorError(ruleResponse.Message) { - continue + ruleResponse := validateResourceWithRule(log, ctx, rule) + if !common.IsConditionalAnchorError(ruleResponse.Message) { + incrementAppliedCount(resp) + resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse) + } + + } else if rule.Validation.Deny != nil { + + // validate new resource if available - otherwise old resource + resource := ctx.NewResource + if reflect.DeepEqual(resource, unstructured.Unstructured{}) { + resource = ctx.OldResource + } + + denyConditionsCopy := copyConditions(rule.Validation.Deny.Conditions) + deny := variables.EvaluateConditions(log, ctx.JSONContext, denyConditionsCopy) + ruleResp := response.RuleResponse{ + Name: rule.Name, + Type: utils.Validation.String(), + Message: rule.Validation.Message, + Success: !deny, } incrementAppliedCount(resp) - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse) + resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResp) } - } + return resp } -func isSameResponse(oldResponse, newResponse *response.EngineResponse) bool { - // if the response are same then return true - return isSamePolicyResponse(oldResponse.PolicyResponse, newResponse.PolicyResponse) +func validateResourceWithRule(log logr.Logger, ctx *PolicyContext, rule kyverno.Rule) (resp response.RuleResponse) { + if reflect.DeepEqual(ctx.OldResource, unstructured.Unstructured{}) { + return validatePatterns(log, ctx.JSONContext, ctx.NewResource, rule) + } + oldResp := validatePatterns(log, ctx.JSONContext, ctx.OldResource, rule) + newResp := validatePatterns(log, ctx.JSONContext, ctx.NewResource, rule) + if !isSameRuleResponse(oldResp, newResp) { + return newResp + } + + return response.RuleResponse{} } -func isSamePolicyResponse(oldPolicyRespone, newPolicyResponse response.PolicyResponse) bool { - // can skip policy and resource checks as they will be same - // compare rules - return isSameRules(oldPolicyRespone.Rules, newPolicyResponse.Rules) +// matches checks if either the new or old resource satisfies the filter conditions defined in the rule +func matches(logger logr.Logger, rule kyverno.Rule, ctx *PolicyContext) bool { + err := MatchesResourceDescription(ctx.NewResource, rule, ctx.AdmissionInfo, ctx.ExcludeGroupRole) + if err == nil { + return true + } + + if !reflect.DeepEqual(ctx.OldResource, unstructured.Unstructured{}) { + err := MatchesResourceDescription(ctx.OldResource, rule, ctx.AdmissionInfo, ctx.ExcludeGroupRole) + if err == nil { + return true + } + } + + logger.V(4).Info("resource fails the match description", "reason", err.Error()) + return false } -func isSameRules(oldRules []response.RuleResponse, newRules []response.RuleResponse) bool { - if len(oldRules) != len(newRules) { +func isSameRuleResponse(r1 response.RuleResponse, r2 response.RuleResponse) bool { + if r1.Name != r2.Name { return false } - // as the rules are always processed in order the indices wil be same - for idx, oldrule := range oldRules { - newrule := newRules[idx] - // Name - if oldrule.Name != newrule.Name { - return false - } - // Type - if oldrule.Type != newrule.Type { - return false - } - // Message - if oldrule.Message != newrule.Message { - return false - } - // skip patches - if oldrule.Success != newrule.Success { - return false - } + + if r1.Type != r2.Type { + return false } + + if r1.Message != r2.Message { + return false + } + + if r1.Success != r2.Success { + return false + } + return true } diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 76bdb21a79..fdcb47d082 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -127,7 +127,7 @@ func TestValidate_image_tag_fail(t *testing.T) { "validation rule 'validate-tag' passed.", "validation error: imagePullPolicy 'Always' required with tag 'latest'. Rule validate-latest failed at path /spec/containers/0/imagePullPolicy/", } - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) for index, r := range er.PolicyResponse.Rules { assert.Equal(t, r.Message, msgs[index]) } @@ -226,7 +226,7 @@ func TestValidate_image_tag_pass(t *testing.T) { "validation rule 'validate-tag' passed.", "validation rule 'validate-latest' passed.", } - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) for index, r := range er.PolicyResponse.Rules { assert.Equal(t, r.Message, msgs[index]) } @@ -300,12 +300,13 @@ func TestValidate_Fail_anyPattern(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + assert.Assert(t, !er.IsSuccessful()) + msgs := []string{"validation error: A namespace is required. Rule check-default-namespace[0] failed at path /metadata/namespace/. Rule check-default-namespace[1] failed at path /metadata/namespace/."} for index, r := range er.PolicyResponse.Rules { assert.Equal(t, r.Message, msgs[index]) } - assert.Assert(t, !er.IsSuccessful()) } func TestValidate_host_network_port(t *testing.T) { @@ -382,7 +383,7 @@ func TestValidate_host_network_port(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation error: Host network and port are not allowed. Rule validate-host-network-port failed at path /spec/containers/0/ports/0/hostPort/"} for index, r := range er.PolicyResponse.Rules { @@ -472,7 +473,7 @@ func TestValidate_anchor_arraymap_pass(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation rule 'validate-host-path' passed."} for index, r := range er.PolicyResponse.Rules { @@ -560,7 +561,7 @@ func TestValidate_anchor_arraymap_fail(t *testing.T) { assert.NilError(t, err) resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation error: Host path '/var/lib/' is not allowed. Rule validate-host-path failed at path /spec/volumes/0/hostPath/path/"} for index, r := range er.PolicyResponse.Rules { @@ -630,7 +631,7 @@ func TestValidate_anchor_map_notfound(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation rule 'pod rule 2' passed."} for index, r := range er.PolicyResponse.Rules { @@ -703,12 +704,13 @@ func TestValidate_anchor_map_found_valid(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation rule 'pod rule 2' passed."} for index, r := range er.PolicyResponse.Rules { assert.Equal(t, r.Message, msgs[index]) } + assert.Assert(t, er.IsSuccessful()) } @@ -776,7 +778,7 @@ func TestValidate_anchor_map_found_invalid(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation error: pod: validate run as non root user. Rule pod rule 2 failed at path /spec/securityContext/runAsNonRoot/"} for index, r := range er.PolicyResponse.Rules { @@ -851,7 +853,7 @@ func TestValidate_AnchorList_pass(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation rule 'pod image rule' passed."} for index, r := range er.PolicyResponse.Rules { @@ -926,7 +928,7 @@ func TestValidate_AnchorList_fail(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) assert.Assert(t, !er.IsSuccessful()) } @@ -996,7 +998,7 @@ func TestValidate_existenceAnchor_fail(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) assert.Assert(t, !er.IsSuccessful()) } @@ -1066,7 +1068,7 @@ func TestValidate_existenceAnchor_pass(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation rule 'pod image rule' passed."} for index, r := range er.PolicyResponse.Rules { @@ -1154,7 +1156,7 @@ func TestValidate_negationAnchor_deny(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation error: Host path is not allowed. Rule validate-host-path failed at path /spec/volumes/0/hostPath/"} for index, r := range er.PolicyResponse.Rules { @@ -1241,7 +1243,7 @@ func TestValidate_negationAnchor_pass(t *testing.T) { resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) assert.NilError(t, err) - er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) + er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured}) msgs := []string{"validation rule 'validate-host-path' passed."} for index, r := range er.PolicyResponse.Rules { @@ -1313,9 +1315,9 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) { err = ctx.AddResource(resourceRaw) assert.NilError(t, err) - policyContext := PolicyContext{ + policyContext := &PolicyContext{ Policy: policy, - Context: ctx, + JSONContext: ctx, NewResource: *resourceUnstructured} er := Validate(policyContext) assert.Assert(t, !er.PolicyResponse.Rules[0].Success) @@ -1405,9 +1407,9 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *t err = ctx.AddResource(resourceRaw) assert.NilError(t, err) - policyContext := PolicyContext{ + policyContext := &PolicyContext{ Policy: policy, - Context: ctx, + JSONContext: ctx, NewResource: *resourceUnstructured} er := Validate(policyContext) assert.Assert(t, er.PolicyResponse.Rules[0].Success) @@ -1496,9 +1498,9 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *test err = ctx.AddResource(resourceRaw) assert.NilError(t, err) - policyContext := PolicyContext{ + policyContext := &PolicyContext{ Policy: policy, - Context: ctx, + JSONContext: ctx, NewResource: *resourceUnstructured} er := Validate(policyContext) assert.Assert(t, !er.PolicyResponse.Rules[0].Success) @@ -1587,9 +1589,9 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatter err = ctx.AddResource(resourceRaw) assert.NilError(t, err) - policyContext := PolicyContext{ + policyContext := &PolicyContext{ Policy: policy, - Context: ctx, + JSONContext: ctx, NewResource: *resourceUnstructured} er := Validate(policyContext) @@ -1598,28 +1600,16 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatter "validation error: Rule test-path-not-exist[0] failed at path /spec/template/spec/containers/0/name/. Rule test-path-not-exist[1] failed at path /spec/template/spec/containers/0/name/.") } -func Test_denyFeatureIssue744(t *testing.T) { - testcases := []struct { - description string - policy []byte - request []byte - userInfo []byte - requestDenied bool - }{ - { - description: "Blocks delete requests for resources with label allow-deletes(success case)", - policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-deletes-success"},"spec":{"validationFailureAction":"enforce","background":false,"rules":[{"name":"check-allow-deletes","match":{"resources":{"selector":{"matchLabels":{"allow-deletes":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Deleting {{request.oldObject.kind}} / {{request.oldObject.metadata.name}} is not allowed","deny":{"conditions":[{"key":"{{request.operation}}","operator":"Equal","value":"DELETE"}]}}}]}}`), - request: []byte(`{"uid":"b553344a-172a-4257-8ec4-a8f379f8b844","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"hello-world","namespace":"default","operation":"DELETE","userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"object":null,"oldObject":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"f093e3da-f13a-474f-87e8-43e98fe363bf","resourceVersion":"1983","creationTimestamp":"2020-05-06T20:28:43Z","labels":{"allow-deletes":"false"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-deletes\":\"false\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:28:43Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"dryRun":false,"options":{"kind":"DeleteOptions","apiVersion":"meta.k8s.io/v1","gracePeriodSeconds":30,"propagationPolicy":"Background"}}`), - userInfo: []byte(`{"roles":null,"clusterRoles":["cluster-admin","system:basic-user","system:discovery","system:public-info-viewer"],"userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}}`), - requestDenied: true, - }, - { - description: "Blocks delete requests for resources with label allow-deletes(failure case)", - policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-deletes-failure"},"spec":{"validationFailureAction":"enforce","background":false,"rules":[{"name":"check-allow-deletes","match":{"resources":{"selector":{"matchLabels":{"allow-deletes":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Deleting {{request.oldObject.kind}} / {{request.oldObject.metadata.name}} is not allowed","deny":{"conditions":[{"key":"{{request.operation}}","operator":"Equal","value":"DELETE"}]}}}]}}`), - request: []byte(`{"uid":"9a83234d-95d1-4105-b6bf-7d72fd0183ce","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"subResource":"status","requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"requestSubResource":"status","name":"hello-world","namespace":"default","operation":"UPDATE","userInfo":{"username":"system:node:kind-control-plane","groups":["system:nodes","system:authenticated"]},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"10fb7e1f-3710-43fa-9b7d-fc532b5ff70e","resourceVersion":"2829","creationTimestamp":"2020-05-06T20:36:51Z","labels":{"allow-deletes":"false"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-deletes\":\"false\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:36:51Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"oldObject":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"10fb7e1f-3710-43fa-9b7d-fc532b5ff70e","resourceVersion":"2829","creationTimestamp":"2020-05-06T20:36:51Z","labels":{"allow-deletes":"false"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-deletes\":\"false\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z"}],"qosClass":"Burstable"}},"dryRun":false,"options":{"kind":"UpdateOptions","apiVersion":"meta.k8s.io/v1"}}`), - userInfo: []byte(`{"roles":["kube-system:kubeadm:nodes-kubeadm-config","kube-system:kubeadm:kubelet-config-1.17"],"clusterRoles":["system:discovery","system:certificates.k8s.io:certificatesigningrequests:selfnodeclient","system:public-info-viewer","system:basic-user"],"userInfo":{"username":"kubernetes-admin","groups":["system:nodes","system:authenticated"]}}`), - requestDenied: false, - }, +type testCase struct { + description string + policy []byte + request []byte + userInfo []byte + requestDenied bool +} + +func Test_denyFeatureIssue744_BlockUpdate(t *testing.T) { + testcases := []testCase{ { description: "Blocks update requests for resources with label allow-updates(success case)", policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-updates-success"},"spec":{"validationFailureAction":"enforce","background":false,"rules":[{"name":"check-allow-updates","match":{"resources":{"selector":{"matchLabels":{"allow-updates":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Updating {{request.object.kind}} / {{request.object.metadata.name}} is not allowed","deny":{"conditions":[{"key":"{{request.operation}}","operator":"Equals","value":"UPDATE"}]}}}]}}`), @@ -1634,6 +1624,33 @@ func Test_denyFeatureIssue744(t *testing.T) { userInfo: []byte(`{"roles":null,"clusterRoles":["system:public-info-viewer","cluster-admin","system:discovery","system:basic-user"],"userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}}`), requestDenied: false, }, + } + + var err error + for _, testcase := range testcases { + executeTest(t, err, testcase) + } +} + +func Test_denyFeatureIssue744_DenyAll(t *testing.T) { + testcases := []testCase{ + { + description: "Deny all requests on a namespace", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-request"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"block-request","match":{"resources":{"namespaces":["kube-system"]}},"validate":{"deny":{}}}]}}`), + request: []byte(`{"uid":"2cf2b192-2c25-4f14-ac3a-315408d398f2","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"hello-world","namespace":"default","operation":"UPDATE","userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"f5c33eaf-79d8-4bc0-8819-749b3606012c","resourceVersion":"5470","creationTimestamp":"2020-05-06T20:57:15Z","labels":{"allow-updates":"false","something":"existes","something2":"feeereeeeeeeee"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-updates\":\"false\",\"something\":\"existes\",\"something2\":\"feeereeeeeeeee\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:57:15Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"oldObject":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"f5c33eaf-79d8-4bc0-8819-749b3606012c","resourceVersion":"5470","creationTimestamp":"2020-05-06T20:57:15Z","labels":{"allow-updates":"false","something":"existes","something2":"feeereeeeeeeee"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-updates\":\"false\",\"something\":\"existes\",\"something2\":\"feeereeeeeeeee\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:57:15Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"dryRun":false,"options":{"kind":"UpdateOptions","apiVersion":"meta.k8s.io/v1"}}`), + userInfo: []byte(`{"roles":null,"clusterRoles":null,"userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}}`), + requestDenied: false, + }, + } + + var err error + for _, testcase := range testcases { + executeTest(t, err, testcase) + } +} + +func Test_denyFeatureIssue744_BlockFields(t *testing.T) { + testcases := []testCase{ { description: "Blocks certain fields(success case)", policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"prevent-field-update-success"},"spec":{"validationFailureAction":"enforce","background":false,"rules":[{"name":"prevent-field-update","match":{"resources":{"selector":{"matchLabels":{"allow-updates":"false"}}}},"validate":{"message":"Updating field label 'something' is not allowed","deny":{"conditions":[{"key":"{{request.object.metadata.labels.something}}","operator":"NotEqual","value":""},{"key":"{{request.object.metadata.labels.something}}","operator":"NotEquals","value":"{{request.oldObject.metadata.labels.something}}"}]}}}]}}`), @@ -1648,66 +1665,88 @@ func Test_denyFeatureIssue744(t *testing.T) { userInfo: []byte(`{"roles":null,"clusterRoles":null,"userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}}`), requestDenied: false, }, + } + + var err error + for _, testcase := range testcases { + executeTest(t, err, testcase) + } +} + +func Test_denyFeatureIssue744_BlockDelete(t *testing.T) { + testcases := []testCase{ { - description: "Deny all requests on a namespace", - policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-request"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"block-request","match":{"resources":{"namespaces":["kube-system"]}},"validate":{"deny":{}}}]}}`), - request: []byte(`{"uid":"2cf2b192-2c25-4f14-ac3a-315408d398f2","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"hello-world","namespace":"default","operation":"UPDATE","userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"f5c33eaf-79d8-4bc0-8819-749b3606012c","resourceVersion":"5470","creationTimestamp":"2020-05-06T20:57:15Z","labels":{"allow-updates":"false","something":"existes","something2":"feeereeeeeeeee"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-updates\":\"false\",\"something\":\"existes\",\"something2\":\"feeereeeeeeeee\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:57:15Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"oldObject":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"f5c33eaf-79d8-4bc0-8819-749b3606012c","resourceVersion":"5470","creationTimestamp":"2020-05-06T20:57:15Z","labels":{"allow-updates":"false","something":"existes","something2":"feeereeeeeeeee"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-updates\":\"false\",\"something\":\"existes\",\"something2\":\"feeereeeeeeeee\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:57:15Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:57:15Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"dryRun":false,"options":{"kind":"UpdateOptions","apiVersion":"meta.k8s.io/v1"}}`), - userInfo: []byte(`{"roles":null,"clusterRoles":null,"userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}}`), + description: "Blocks delete requests for resources with label allow-deletes(success case)", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-deletes-success"},"spec":{"validationFailureAction":"enforce","background":false,"rules":[{"name":"check-allow-deletes","match":{"resources":{"selector":{"matchLabels":{"allow-deletes":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Deleting {{request.oldObject.kind}} / {{request.oldObject.metadata.name}} is not allowed","deny":{"conditions":[{"key":"{{request.operation}}","operator":"Equal","value":"DELETE"}]}}}]}}`), + request: []byte(`{"uid":"b553344a-172a-4257-8ec4-a8f379f8b844","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"hello-world","namespace":"default","operation":"DELETE","userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"object":null,"oldObject":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"f093e3da-f13a-474f-87e8-43e98fe363bf","resourceVersion":"1983","creationTimestamp":"2020-05-06T20:28:43Z","labels":{"allow-deletes":"false"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-deletes\":\"false\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:28:43Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:28:43Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"dryRun":false,"options":{"kind":"DeleteOptions","apiVersion":"meta.k8s.io/v1","gracePeriodSeconds":30,"propagationPolicy":"Background"}}`), + userInfo: []byte(`{"roles":null,"clusterRoles":["cluster-admin","system:basic-user","system:discovery","system:public-info-viewer"],"userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}}`), + requestDenied: true, + }, + { + description: "Blocks delete requests for resources with label allow-deletes(failure case)", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"block-deletes-failure"},"spec":{"validationFailureAction":"enforce","background":false,"rules":[{"name":"check-allow-deletes","match":{"resources":{"selector":{"matchLabels":{"allow-deletes":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Deleting {{request.oldObject.kind}} / {{request.oldObject.metadata.name}} is not allowed","deny":{"conditions":[{"key":"{{request.operation}}","operator":"Equal","value":"DELETE"}]}}}]}}`), + request: []byte(`{"uid":"9a83234d-95d1-4105-b6bf-7d72fd0183ce","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"subResource":"status","requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"requestSubResource":"status","name":"hello-world","namespace":"default","operation":"UPDATE","userInfo":{"username":"system:node:kind-control-plane","groups":["system:nodes","system:authenticated"]},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"10fb7e1f-3710-43fa-9b7d-fc532b5ff70e","resourceVersion":"2829","creationTimestamp":"2020-05-06T20:36:51Z","labels":{"allow-deletes":"false"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-deletes\":\"false\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"Initialized","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z"},{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"ContainersReady","status":"False","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z","reason":"ContainersNotReady","message":"containers with unready status: [hello-world]"},{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z"}],"hostIP":"172.17.0.2","startTime":"2020-05-06T20:36:51Z","containerStatuses":[{"name":"hello-world","state":{"waiting":{"reason":"ContainerCreating"}},"lastState":{},"ready":false,"restartCount":0,"image":"hello-world:latest","imageID":"","started":false}],"qosClass":"Burstable"}},"oldObject":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"hello-world","namespace":"default","uid":"10fb7e1f-3710-43fa-9b7d-fc532b5ff70e","resourceVersion":"2829","creationTimestamp":"2020-05-06T20:36:51Z","labels":{"allow-deletes":"false"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"allow-deletes\":\"false\"},\"name\":\"hello-world\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"hello-world:latest\",\"name\":\"hello-world\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"0.2\",\"memory\":\"30Mi\"},\"requests\":{\"cpu\":\"0.1\",\"memory\":\"20Mi\"}}}]}}\n"}},"spec":{"volumes":[{"name":"default-token-4q2mj","secret":{"secretName":"default-token-4q2mj","defaultMode":420}}],"containers":[{"name":"hello-world","image":"hello-world:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{"limits":{"cpu":"200m","memory":"30Mi"},"requests":{"cpu":"100m","memory":"20Mi"}},"volumeMounts":[{"name":"default-token-4q2mj","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"kind-control-plane","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{"phase":"Pending","conditions":[{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2020-05-06T20:36:51Z"}],"qosClass":"Burstable"}},"dryRun":false,"options":{"kind":"UpdateOptions","apiVersion":"meta.k8s.io/v1"}}`), + userInfo: []byte(`{"roles":["kube-system:kubeadm:nodes-kubeadm-config","kube-system:kubeadm:kubelet-config-1.17"],"clusterRoles":["system:discovery","system:certificates.k8s.io:certificatesigningrequests:selfnodeclient","system:public-info-viewer","system:basic-user"],"userInfo":{"username":"kubernetes-admin","groups":["system:nodes","system:authenticated"]}}`), requestDenied: false, }, } var err error for _, testcase := range testcases { - var policy kyverno.ClusterPolicy - err = json.Unmarshal(testcase.policy, &policy) - if err != nil { - t.Fatal(err) - } + executeTest(t, err, testcase) + } +} - var request *v1beta1.AdmissionRequest - err = json.Unmarshal(testcase.request, &request) - if err != nil { - t.Fatal(err) - } +func executeTest(t *testing.T, err error, test testCase) { + var policy kyverno.ClusterPolicy + err = json.Unmarshal(test.policy, &policy) + if err != nil { + t.Fatal(err) + } - var userInfo kyverno.RequestInfo - err = json.Unmarshal(testcase.userInfo, &userInfo) - if err != nil { - t.Fatal(err) - } + var request *v1beta1.AdmissionRequest + err = json.Unmarshal(test.request, &request) + if err != nil { + t.Fatal(err) + } - ctx := context.NewContext() - err = ctx.AddRequest(request) - if err != nil { - t.Fatal(err) - } - err = ctx.AddUserInfo(userInfo) - if err != nil { - t.Fatal(err) - } - err = ctx.AddSA(userInfo.AdmissionUserInfo.Username) - if err != nil { - t.Fatal(err) - } + var userInfo kyverno.RequestInfo + err = json.Unmarshal(test.userInfo, &userInfo) + if err != nil { + t.Fatal(err) + } - newR, oldR, err := utils2.ExtractResources(nil, request) - if err != nil { - t.Fatal(err) - } + ctx := context.NewContext() + err = ctx.AddRequest(request) + if err != nil { + t.Fatal(err) + } - pc := PolicyContext{ - Policy: policy, - NewResource: newR, - OldResource: oldR, - AdmissionInfo: userInfo, - Context: ctx, - } - resp := Validate(pc) - if resp.IsSuccessful() == !testcase.requestDenied { - continue - } + err = ctx.AddUserInfo(userInfo) + if err != nil { + t.Fatal(err) + } + err = ctx.AddServiceAccount(userInfo.AdmissionUserInfo.Username) + if err != nil { + t.Fatal(err) + } + + newR, oldR, err := utils2.ExtractResources(nil, request) + if err != nil { + t.Fatal(err) + } + + pc := &PolicyContext{ + Policy: policy, + NewResource: newR, + OldResource: oldR, + AdmissionInfo: userInfo, + JSONContext: ctx, + } + + resp := Validate(pc) + if resp.IsSuccessful() && test.requestDenied { t.Errorf("Testcase has failed, policy: %v", policy.Name) } } diff --git a/pkg/engine/variables/evaluate.go b/pkg/engine/variables/evaluate.go index 34ab58a316..11a2931cac 100644 --- a/pkg/engine/variables/evaluate.go +++ b/pkg/engine/variables/evaluate.go @@ -17,13 +17,13 @@ func Evaluate(log logr.Logger, ctx context.EvalInterface, condition kyverno.Cond return handle.Evaluate(condition.Key, condition.Value) } -//EvaluateConditions evaluates multiple conditions +//EvaluateConditions evaluates multiple conditions as a logical AND operation func EvaluateConditions(log logr.Logger, ctx context.EvalInterface, conditions []kyverno.Condition) bool { - // AND the conditions for _, condition := range conditions { if !Evaluate(log, ctx, condition) { return false } } + return true } diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index aa02594db0..9540c1bb43 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -115,7 +115,7 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern return nil, err } - err = ctx.AddSA(gr.Spec.Context.UserRequestInfo.AdmissionUserInfo.Username) + err = ctx.AddServiceAccount(gr.Spec.Context.UserRequestInfo.AdmissionUserInfo.Username) if err != nil { logger.Error(err, "failed to load UserInfo in context") return nil, err @@ -124,7 +124,6 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern policyContext := engine.PolicyContext{ NewResource: resource, Policy: *policyObj, - Context: ctx, AdmissionInfo: gr.Spec.Context.UserRequestInfo, ExcludeGroupRole: c.Config.GetExcludeGroupRole(), ExcludeResourceFunc: c.Config.ToFilter, @@ -187,7 +186,6 @@ func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext engine.P // - - substitute values policy := policyContext.Policy resource := policyContext.NewResource - ctx := policyContext.Context resCache := policyContext.ResourceCache jsonContext := policyContext.JSONContext @@ -222,7 +220,7 @@ func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext engine.P return nil, err } - genResource, err := applyRule(log, c.client, rule, resource, ctx, policy.Name, gr, processExisting) + genResource, err := applyRule(log, c.client, rule, resource, jsonContext, policy.Name, gr, processExisting) if err != nil { log.Error(err, "failed to apply generate rule", "policy", policy.Name, "rule", rule.Name, "resource", resource.GetName()) diff --git a/pkg/kyverno/apply/command.go b/pkg/kyverno/apply/command.go index aeda110bea..3b3defa8f2 100644 --- a/pkg/kyverno/apply/command.go +++ b/pkg/kyverno/apply/command.go @@ -144,7 +144,9 @@ func Command() *cobra.Command { return cmd } -func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool, mutateLogPath string, variablesString string, valuesFile string, namespace string, policyPaths []string) (validateEngineResponses []response.EngineResponse, rc *resultCounts, resources []*unstructured.Unstructured, skippedPolicies []SkippedPolicy, err error) { +func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool, mutateLogPath string, + variablesString string, valuesFile string, namespace string, policyPaths []string) (validateEngineResponses []*response.EngineResponse, rc *resultCounts, resources []*unstructured.Unstructured, skippedPolicies []SkippedPolicy, err error) { + kubernetesConfig := genericclioptions.NewConfigFlags(true) if valuesFile != "" && variablesString != "" { @@ -230,8 +232,8 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool, } rc = &resultCounts{} - engineResponses := make([]response.EngineResponse, 0) - validateEngineResponses = make([]response.EngineResponse, 0) + engineResponses := make([]*response.EngineResponse, 0) + validateEngineResponses = make([]*response.EngineResponse, 0) skippedPolicies = make([]SkippedPolicy, 0) for _, policy := range mutatedPolicies { @@ -276,6 +278,7 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool, if err != nil { return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.Name, resource.GetName()).Error(), err) } + engineResponses = append(engineResponses, ers...) validateEngineResponses = append(validateEngineResponses, validateErs) } @@ -407,7 +410,7 @@ func getResourceAccordingToResourcePath(resourcePaths []string, cluster bool, po } // printReportOrViolation - printing policy report/violations -func printReportOrViolation(policyReport bool, validateEngineResponses []response.EngineResponse, rc *resultCounts, resourcePaths []string, resourcesLen int, skippedPolicies []SkippedPolicy) { +func printReportOrViolation(policyReport bool, validateEngineResponses []*response.EngineResponse, rc *resultCounts, resourcePaths []string, resourcesLen int, skippedPolicies []SkippedPolicy) { if policyReport { os.Setenv("POLICY-TYPE", pkgCommon.PolicyReport) resps := buildPolicyReports(validateEngineResponses, skippedPolicies) @@ -435,9 +438,12 @@ func printReportOrViolation(policyReport bool, validateEngineResponses []respons } // applyPolicyOnResource - function to apply policy on resource -func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unstructured, mutateLogPath string, mutateLogPathIsDir bool, variables map[string]string, rc *resultCounts, policyReport bool) ([]response.EngineResponse, response.EngineResponse, error) { +func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unstructured, + mutateLogPath string, mutateLogPathIsDir bool, variables map[string]string, + rc *resultCounts, policyReport bool) ([]*response.EngineResponse, *response.EngineResponse, error) { + responseError := false - engineResponses := make([]response.EngineResponse, 0) + engineResponses := make([]*response.EngineResponse, 0) resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName()) log.Log.V(3).Info("applying policy on resource", "policy", policy.Name, "resource", resPath) @@ -457,7 +463,7 @@ func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unst ctx.AddJSON(jsonData) } - mutateResponse := engine.Mutate(engine.PolicyContext{Policy: *policy, NewResource: *resource, Context: ctx}) + mutateResponse := engine.Mutate(&engine.PolicyContext{Policy: *policy, NewResource: *resource, JSONContext: ctx}) engineResponses = append(engineResponses, mutateResponse) if !mutateResponse.IsSuccessful() { @@ -483,7 +489,7 @@ func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unst } else { err := printMutatedOutput(mutateLogPath, mutateLogPathIsDir, string(yamlEncodedResource), resource.GetName()+"-mutated") if err != nil { - return engineResponses, response.EngineResponse{}, sanitizederror.NewWithError("failed to print mutated result", err) + return engineResponses, &response.EngineResponse{}, sanitizederror.NewWithError("failed to print mutated result", err) } fmt.Printf("\n\nMutation:\nMutation has been applied successfully. Check the files.") } @@ -499,7 +505,8 @@ func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unst } } - validateResponse := engine.Validate(engine.PolicyContext{Policy: *policy, NewResource: mutateResponse.PatchedResource, Context: ctx}) + policyCtx := &engine.PolicyContext{Policy: *policy, NewResource: mutateResponse.PatchedResource, JSONContext: ctx} + validateResponse := engine.Validate(policyCtx) if !policyReport { if !validateResponse.IsSuccessful() { fmt.Printf("\npolicy %s -> resource %s failed: \n", policy.Name, resPath) diff --git a/pkg/kyverno/apply/report.go b/pkg/kyverno/apply/report.go index 75c3415e5c..23b04ff35f 100644 --- a/pkg/kyverno/apply/report.go +++ b/pkg/kyverno/apply/report.go @@ -20,7 +20,7 @@ import ( const clusterpolicyreport = "clusterpolicyreport" // resps is the engine responses generated for a single policy -func buildPolicyReports(resps []response.EngineResponse, skippedPolicies []SkippedPolicy) (res []*unstructured.Unstructured) { +func buildPolicyReports(resps []*response.EngineResponse, skippedPolicies []SkippedPolicy) (res []*unstructured.Unstructured) { var raw []byte var err error @@ -107,7 +107,7 @@ func buildPolicyReports(resps []response.EngineResponse, skippedPolicies []Skipp // buildPolicyResults returns a string-PolicyReportResult map // the key of the map is one of "clusterpolicyreport", "policyreport-ns-" -func buildPolicyResults(resps []response.EngineResponse) map[string][]*report.PolicyReportResult { +func buildPolicyResults(resps []*response.EngineResponse) map[string][]*report.PolicyReportResult { results := make(map[string][]*report.PolicyReportResult) infos := policyreport.GeneratePRsFromEngineResponse(resps, log.Log) diff --git a/pkg/kyverno/apply/report_test.go b/pkg/kyverno/apply/report_test.go index 5e357adfc1..75054fc903 100644 --- a/pkg/kyverno/apply/report_test.go +++ b/pkg/kyverno/apply/report_test.go @@ -14,7 +14,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -var engineResponses = []response.EngineResponse{ +var engineResponses = []*response.EngineResponse{ { PatchedResource: unstructured.Unstructured{ Object: map[string]interface{}{ diff --git a/pkg/policy/apply.go b/pkg/policy/apply.go index 5281b2295a..f102b96869 100644 --- a/pkg/policy/apply.go +++ b/pkg/policy/apply.go @@ -19,7 +19,8 @@ import ( ) // applyPolicy applies policy on a resource -func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, logger logr.Logger, excludeGroupRole []string, resCache resourcecache.ResourceCacheIface) (responses []response.EngineResponse) { +func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, + logger logr.Logger, excludeGroupRole []string, resCache resourcecache.ResourceCacheIface) (responses []*response.EngineResponse) { startTime := time.Now() defer func() { name := resource.GetKind() + "/" + resource.GetName() @@ -31,8 +32,8 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure logger.V(3).Info("applyPolicy", "resource", name, "processingTime", time.Since(startTime).String()) }() - var engineResponses []response.EngineResponse - var engineResponseMutation, engineResponseValidation response.EngineResponse + var engineResponses []*response.EngineResponse + var engineResponseMutation, engineResponseValidation *response.EngineResponse var err error ctx := context.NewContext() @@ -41,20 +42,27 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure logger.Error(err, "enable to add transform resource to ctx") } - engineResponseMutation, err = mutation(policy, resource, ctx, logger, resCache, ctx) + engineResponseMutation, err = mutation(policy, resource, logger, resCache, ctx) if err != nil { logger.Error(err, "failed to process mutation rule") } - engineResponseValidation = engine.Validate(engine.PolicyContext{Policy: policy, Context: ctx, NewResource: resource, ExcludeGroupRole: excludeGroupRole, ResourceCache: resCache, JSONContext: ctx}) + engineResponseValidation = engine.Validate(&engine.PolicyContext{Policy: policy, NewResource: resource, ExcludeGroupRole: excludeGroupRole, ResourceCache: resCache, JSONContext: ctx}) engineResponses = append(engineResponses, mergeRuleRespose(engineResponseMutation, engineResponseValidation)) return engineResponses } -func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, ctx context.EvalInterface, log logr.Logger, resCache resourcecache.ResourceCacheIface, jsonContext *context.Context) (response.EngineResponse, error) { +func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, log logr.Logger, resCache resourcecache.ResourceCacheIface, jsonContext *context.Context) (*response.EngineResponse, error) { - engineResponse := engine.Mutate(engine.PolicyContext{Policy: policy, NewResource: resource, Context: ctx, ResourceCache: resCache, JSONContext: jsonContext}) + policyContext := &engine.PolicyContext{ + Policy: policy, + NewResource: resource, + ResourceCache: resCache, + JSONContext: jsonContext, + } + + engineResponse := engine.Mutate(policyContext) if !engineResponse.IsSuccessful() { log.V(4).Info("failed to apply mutation rules; reporting them") return engineResponse, nil @@ -69,11 +77,11 @@ func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, } // getFailedOverallRuleInfo gets detailed info for over-all mutation failure -func getFailedOverallRuleInfo(resource unstructured.Unstructured, engineResponse response.EngineResponse, log logr.Logger) (response.EngineResponse, error) { +func getFailedOverallRuleInfo(resource unstructured.Unstructured, engineResponse *response.EngineResponse, log logr.Logger) (*response.EngineResponse, error) { rawResource, err := resource.MarshalJSON() if err != nil { log.Error(err, "failed to marshall resource") - return response.EngineResponse{}, err + return &response.EngineResponse{}, err } // resource does not match so there was a mutation rule violated @@ -85,14 +93,14 @@ func getFailedOverallRuleInfo(resource unstructured.Unstructured, engineResponse patch, err := jsonpatch.DecodePatch(utils.JoinPatches(patches)) if err != nil { log.Error(err, "failed to decode JSON patch", "patches", patches) - return response.EngineResponse{}, err + return &response.EngineResponse{}, err } // apply the patches returned by mutate to the original resource patchedResource, err := patch.Apply(rawResource) if err != nil { log.Error(err, "failed to apply JSON patch", "patches", patches) - return response.EngineResponse{}, err + return &response.EngineResponse{}, err } if !jsonpatch.Equal(patchedResource, rawResource) { @@ -126,7 +134,7 @@ func extractPatchPath(patches [][]byte, log logr.Logger) string { return strings.Join(resultPath, ";") } -func mergeRuleRespose(mutation, validation response.EngineResponse) response.EngineResponse { +func mergeRuleRespose(mutation, validation *response.EngineResponse) *response.EngineResponse { mutation.PolicyResponse.Rules = append(mutation.PolicyResponse.Rules, validation.PolicyResponse.Rules...) return mutation } diff --git a/pkg/policy/existing.go b/pkg/policy/existing.go index 4437208025..ccf657ebb9 100644 --- a/pkg/policy/existing.go +++ b/pkg/policy/existing.go @@ -59,7 +59,7 @@ func (pc *PolicyController) applyAndReportPerNamespace(policy *kyverno.ClusterPo return } - var engineResponses []response.EngineResponse + var engineResponses []*response.EngineResponse for _, resource := range rMap { responses := pc.applyPolicy(policy, resource, logger) engineResponses = append(engineResponses, responses...) @@ -68,7 +68,7 @@ func (pc *PolicyController) applyAndReportPerNamespace(policy *kyverno.ClusterPo pc.report(policy.Name, engineResponses, logger) } -func (pc *PolicyController) applyPolicy(policy *kyverno.ClusterPolicy, resource unstructured.Unstructured, logger logr.Logger) (engineResponses []response.EngineResponse) { +func (pc *PolicyController) applyPolicy(policy *kyverno.ClusterPolicy, resource unstructured.Unstructured, logger logr.Logger) (engineResponses []*response.EngineResponse) { // pre-processing, check if the policy and resource version has been processed before if !pc.rm.ProcessResource(policy.Name, policy.ResourceVersion, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resource.GetResourceVersion()) { logger.V(4).Info("policy and resource already processed", "policyResourceVersion", policy.ResourceVersion, "resourceResourceVersion", resource.GetResourceVersion(), "kind", resource.GetKind(), "namespace", resource.GetNamespace(), "name", resource.GetName()) @@ -86,7 +86,7 @@ func (pc *PolicyController) applyPolicy(policy *kyverno.ClusterPolicy, resource // excludeAutoGenResources filter out the pods / jobs with ownerReference func excludeAutoGenResources(policy kyverno.ClusterPolicy, resourceMap map[string]unstructured.Unstructured, log logr.Logger) { for uid, r := range resourceMap { - if engine.SkipPolicyApplication(policy, r) { + if engine.ManagedPodResource(policy, r) { log.V(4).Info("exclude resource", "namespace", r.GetNamespace(), "kind", r.GetKind(), "name", r.GetName()) delete(resourceMap, uid) } diff --git a/pkg/policy/report.go b/pkg/policy/report.go index 1680511506..6fb2e13b64 100644 --- a/pkg/policy/report.go +++ b/pkg/policy/report.go @@ -9,7 +9,7 @@ import ( "github.com/kyverno/kyverno/pkg/policyreport" ) -func (pc *PolicyController) report(policy string, engineResponses []response.EngineResponse, logger logr.Logger) { +func (pc *PolicyController) report(policy string, engineResponses []*response.EngineResponse, logger logr.Logger) { eventInfos := generateEvents(logger, engineResponses) pc.eventGen.Add(eventInfos...) @@ -22,7 +22,7 @@ func (pc *PolicyController) report(policy string, engineResponses []response.Eng logger.V(4).Info("added a request to RCR generator", "key", info.ToKey()) } -func generateEvents(log logr.Logger, ers []response.EngineResponse) []event.Info { +func generateEvents(log logr.Logger, ers []*response.EngineResponse) []event.Info { var eventInfos []event.Info for _, er := range ers { if er.IsSuccessful() { @@ -33,7 +33,7 @@ func generateEvents(log logr.Logger, ers []response.EngineResponse) []event.Info return eventInfos } -func generateEventsPerEr(log logr.Logger, er response.EngineResponse) []event.Info { +func generateEventsPerEr(log logr.Logger, er *response.EngineResponse) []event.Info { var eventInfos []event.Info logger := log.WithValues("policy", er.PolicyResponse.Policy, "kind", er.PolicyResponse.Resource.Kind, "namespace", er.PolicyResponse.Resource.Namespace, "name", er.PolicyResponse.Resource.Name) diff --git a/pkg/policyreport/builder.go b/pkg/policyreport/builder.go index 062e57ac4f..c939c028d9 100755 --- a/pkg/policyreport/builder.go +++ b/pkg/policyreport/builder.go @@ -41,7 +41,7 @@ func generatePolicyReportName(ns string) string { } //GeneratePRsFromEngineResponse generate Violations from engine responses -func GeneratePRsFromEngineResponse(ers []response.EngineResponse, log logr.Logger) (pvInfos []Info) { +func GeneratePRsFromEngineResponse(ers []*response.EngineResponse, log logr.Logger) (pvInfos []Info) { for _, er := range ers { // ignore creation of PV for resources that are yet to be assigned a name if er.PolicyResponse.Resource.Name == "" { @@ -210,7 +210,7 @@ func calculateSummary(results []*report.PolicyReportResult) (summary report.Poli return } -func buildPVInfo(er response.EngineResponse) Info { +func buildPVInfo(er *response.EngineResponse) Info { info := Info{ PolicyName: er.PolicyResponse.Policy, Namespace: er.PatchedResource.GetNamespace(), @@ -224,7 +224,7 @@ func buildPVInfo(er response.EngineResponse) Info { return info } -func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule { +func buildViolatedRules(er *response.EngineResponse) []kyverno.ViolatedRule { var violatedRules []kyverno.ViolatedRule for _, rule := range er.PolicyResponse.Rules { vrule := kyverno.ViolatedRule{ diff --git a/pkg/testrunner/scenario.go b/pkg/testrunner/scenario.go index 572c96a474..569df78a6d 100644 --- a/pkg/testrunner/scenario.go +++ b/pkg/testrunner/scenario.go @@ -126,9 +126,7 @@ func runTestCase(t *testing.T, tc scaseT) bool { t.FailNow() } - var er response.EngineResponse - - er = engine.Mutate(engine.PolicyContext{Policy: *policy, NewResource: *resource, ExcludeGroupRole: []string{}}) + er := engine.Mutate(&engine.PolicyContext{Policy: *policy, NewResource: *resource, ExcludeGroupRole: []string{}}) t.Log("---Mutation---") validateResource(t, er.PatchedResource, tc.Expected.Mutation.PatchedResource) validateResponse(t, er.PolicyResponse, tc.Expected.Mutation.PolicyResponse) @@ -138,7 +136,7 @@ func runTestCase(t *testing.T, tc scaseT) bool { resource = &er.PatchedResource } - er = engine.Validate(engine.PolicyContext{Policy: *policy, NewResource: *resource, ExcludeGroupRole: []string{}}) + er = engine.Validate(&engine.PolicyContext{Policy: *policy, NewResource: *resource, ExcludeGroupRole: []string{}}) t.Log("---Validation---") validateResponse(t, er.PolicyResponse, tc.Expected.Validation.PolicyResponse) @@ -235,7 +233,7 @@ func validateResponse(t *testing.T, er response.PolicyResponse, expected respons // rules if len(er.Rules) != len(expected.Rules) { - t.Errorf("rule count error, er.Rules=%d, expected.Rules=%d", len(er.Rules), len(expected.Rules)) + t.Errorf("rule count error, er.Rules=%v, expected.Rules=%v", er.Rules, expected.Rules) return } if len(er.Rules) == len(expected.Rules) { diff --git a/pkg/webhooks/annotations.go b/pkg/webhooks/annotations.go index 736df71451..3910fd9269 100644 --- a/pkg/webhooks/annotations.go +++ b/pkg/webhooks/annotations.go @@ -36,7 +36,7 @@ var operationToPastTense = map[string]string{ "test": "tested", } -func generateAnnotationPatches(engineResponses []response.EngineResponse, log logr.Logger) []byte { +func generateAnnotationPatches(engineResponses []*response.EngineResponse, log logr.Logger) []byte { var annotations map[string]string for _, er := range engineResponses { @@ -94,7 +94,7 @@ func generateAnnotationPatches(engineResponses []response.EngineResponse, log lo return patchByte } -func annotationFromEngineResponses(engineResponses []response.EngineResponse, log logr.Logger) []byte { +func annotationFromEngineResponses(engineResponses []*response.EngineResponse, log logr.Logger) []byte { var annotationContent = make(map[string]string) for _, engineResponse := range engineResponses { if !engineResponse.IsSuccessful() { diff --git a/pkg/webhooks/annotations_test.go b/pkg/webhooks/annotations_test.go index da80adb5d8..a341df5e38 100644 --- a/pkg/webhooks/annotations_test.go +++ b/pkg/webhooks/annotations_test.go @@ -26,8 +26,8 @@ func newPolicyResponse(policy, rule string, patchesStr []string, success bool) r } } -func newEngineResponse(policy, rule string, patchesStr []string, success bool, annotation map[string]string) response.EngineResponse { - return response.EngineResponse{ +func newEngineResponse(policy, rule string, patchesStr []string, success bool, annotation map[string]string) *response.EngineResponse { + return &response.EngineResponse{ PatchedResource: unstructured.Unstructured{ Object: map[string]interface{}{ "metadata": map[string]interface{}{ @@ -43,7 +43,7 @@ func Test_empty_annotation(t *testing.T) { patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }` engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, nil) - annPatches := generateAnnotationPatches([]response.EngineResponse{engineResponse}, log.Log) + annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log) expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/patches":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}}` assert.Assert(t, string(annPatches) == expectedPatches) } @@ -55,7 +55,7 @@ func Test_exist_annotation(t *testing.T) { patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }` engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation) - annPatches := generateAnnotationPatches([]response.EngineResponse{engineResponse}, log.Log) + annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log) expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/patches":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}}` assert.Assert(t, string(annPatches) == expectedPatches) @@ -68,7 +68,7 @@ func Test_exist_kyverno_annotation(t *testing.T) { patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }` engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation) - annPatches := generateAnnotationPatches([]response.EngineResponse{engineResponse}, log.Log) + annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log) expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/patches":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}}` assert.Assert(t, string(annPatches) == expectedPatches) @@ -80,11 +80,11 @@ func Test_annotation_nil_patch(t *testing.T) { } engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, true, annotation) - annPatches := generateAnnotationPatches([]response.EngineResponse{engineResponse}, log.Log) + annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log) assert.Assert(t, annPatches == nil) engineResponseNew := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{""}, true, annotation) - annPatchesNew := generateAnnotationPatches([]response.EngineResponse{engineResponseNew}, log.Log) + annPatchesNew := generateAnnotationPatches([]*response.EngineResponse{engineResponseNew}, log.Log) assert.Assert(t, annPatchesNew == nil) } @@ -94,7 +94,7 @@ func Test_annotation_failed_Patch(t *testing.T) { } engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, false, annotation) - annPatches := generateAnnotationPatches([]response.EngineResponse{engineResponse}, log.Log) + annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log) assert.Assert(t, annPatches == nil) } diff --git a/pkg/webhooks/common.go b/pkg/webhooks/common.go index 510dcae8b0..01c253e39c 100644 --- a/pkg/webhooks/common.go +++ b/pkg/webhooks/common.go @@ -16,7 +16,7 @@ import ( ) // isResponseSuccessful return true if all responses are successful -func isResponseSuccessful(engineReponses []response.EngineResponse) bool { +func isResponseSuccessful(engineReponses []*response.EngineResponse) bool { for _, er := range engineReponses { if !er.IsSuccessful() { return false @@ -27,7 +27,7 @@ func isResponseSuccessful(engineReponses []response.EngineResponse) bool { // returns true -> if there is even one policy that blocks resource request // returns false -> if all the policies are meant to report only, we dont block resource request -func toBlockResource(engineReponses []response.EngineResponse, log logr.Logger) bool { +func toBlockResource(engineReponses []*response.EngineResponse, log logr.Logger) bool { for _, er := range engineReponses { if !er.IsSuccessful() && er.PolicyResponse.ValidationFailureAction == common.Enforce { log.Info("spec.ValidationFailureAction set to enforce blocking resource request", "policy", er.PolicyResponse.Policy) @@ -39,7 +39,7 @@ func toBlockResource(engineReponses []response.EngineResponse, log logr.Logger) } // getEnforceFailureErrorMsg gets the error messages for failed enforce policy -func getEnforceFailureErrorMsg(engineResponses []response.EngineResponse) string { +func getEnforceFailureErrorMsg(engineResponses []*response.EngineResponse) string { policyToRule := make(map[string]interface{}) var resourceName string for _, er := range engineResponses { @@ -61,7 +61,7 @@ func getEnforceFailureErrorMsg(engineResponses []response.EngineResponse) string } // getErrorMsg gets all failed engine response message -func getErrorMsg(engineReponses []response.EngineResponse) string { +func getErrorMsg(engineReponses []*response.EngineResponse) string { var str []string var resourceInfo string diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index b9294c49b6..03eef0bdc3 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -29,7 +29,7 @@ import ( func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, policies []*kyverno.ClusterPolicy, ctx *context.Context, userRequestInfo kyverno.RequestInfo, dynamicConfig config.Interface) { logger := ws.log.WithValues("action", "generation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) logger.V(4).Info("incoming request") - var engineResponses []response.EngineResponse + var engineResponses []*response.EngineResponse if len(policies) == 0 { return @@ -44,7 +44,6 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic NewResource: new, OldResource: old, AdmissionInfo: userRequestInfo, - Context: ctx, ExcludeGroupRole: dynamicConfig.GetExcludeGroupRole(), ExcludeResourceFunc: ws.configHandler.ToFilter, ResourceCache: ws.resCache, @@ -86,7 +85,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic return } -func (ws *WebhookServer) deleteGR(logger logr.Logger, engineResponse response.EngineResponse) { +func (ws *WebhookServer) deleteGR(logger logr.Logger, engineResponse *response.EngineResponse) { logger.V(4).Info("querying all generate requests") selector := labels.SelectorFromSet(labels.Set(map[string]string{ "policyName": engineResponse.PolicyResponse.Policy, @@ -110,7 +109,7 @@ func (ws *WebhookServer) deleteGR(logger logr.Logger, engineResponse response.En } func applyGenerateRequest(gnGenerator generate.GenerateRequests, userRequestInfo kyverno.RequestInfo, - action v1beta1.Operation, engineResponses ...response.EngineResponse) (failedGenerateRequest []generateRequestResponse) { + action v1beta1.Operation, engineResponses ...*response.EngineResponse) (failedGenerateRequest []generateRequestResponse) { for _, er := range engineResponses { gr := transform(userRequestInfo, er) @@ -122,8 +121,7 @@ func applyGenerateRequest(gnGenerator generate.GenerateRequests, userRequestInfo return } -func transform(userRequestInfo kyverno.RequestInfo, er response.EngineResponse) kyverno.GenerateRequestSpec { - +func transform(userRequestInfo kyverno.RequestInfo, er *response.EngineResponse) kyverno.GenerateRequestSpec { gr := kyverno.GenerateRequestSpec{ Policy: er.PolicyResponse.Policy, Resource: kyverno.ResourceSpec{ @@ -135,11 +133,12 @@ func transform(userRequestInfo kyverno.RequestInfo, er response.EngineResponse) UserRequestInfo: userRequestInfo, }, } + return gr } type generateStats struct { - resp response.EngineResponse + resp *response.EngineResponse } func (gs generateStats) PolicyName() string { diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index 21d5c9fd74..851313cb5b 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -36,11 +36,10 @@ func (ws *WebhookServer) HandleMutation( logger := ws.log.WithValues("action", "mutate", "resource", resourceName, "operation", request.Operation) var patches [][]byte - var engineResponses []response.EngineResponse - policyContext := engine.PolicyContext{ + var engineResponses []*response.EngineResponse + policyContext := &engine.PolicyContext{ NewResource: resource, AdmissionInfo: userRequestInfo, - Context: ctx, ExcludeGroupRole: ws.configHandler.GetExcludeGroupRole(), ExcludeResourceFunc: ws.configHandler.ToFilter, ResourceCache: ws.resCache, @@ -117,7 +116,7 @@ func (ws *WebhookServer) HandleMutation( } type mutateStats struct { - resp response.EngineResponse + resp *response.EngineResponse namespace string } diff --git a/pkg/webhooks/policyStatus_test.go b/pkg/webhooks/policyStatus_test.go index 2b80850d7d..aba56b2c42 100644 --- a/pkg/webhooks/policyStatus_test.go +++ b/pkg/webhooks/policyStatus_test.go @@ -12,11 +12,11 @@ import ( func Test_GenerateStats(t *testing.T) { testCase := struct { - generateStats []response.EngineResponse + generateStats []*response.EngineResponse expectedOutput []byte }{ expectedOutput: []byte(`{"policy1":{"averageExecutionTime":"494ns","rulesFailedCount":1,"rulesAppliedCount":1,"ruleStatus":[{"ruleName":"rule5","averageExecutionTime":"243ns","appliedCount":1},{"ruleName":"rule6","averageExecutionTime":"251ns","failedCount":1}]},"policy2":{"averageExecutionTime":"433ns","rulesFailedCount":1,"rulesAppliedCount":1,"ruleStatus":[{"ruleName":"rule5","averageExecutionTime":"222ns","appliedCount":1},{"ruleName":"rule6","averageExecutionTime":"211ns","failedCount":1}]}}`), - generateStats: []response.EngineResponse{ + generateStats: []*response.EngineResponse{ { PolicyResponse: response.PolicyResponse{ Policy: "policy1", @@ -79,11 +79,11 @@ func Test_GenerateStats(t *testing.T) { func Test_MutateStats(t *testing.T) { testCase := struct { - mutateStats []response.EngineResponse + mutateStats []*response.EngineResponse expectedOutput []byte }{ expectedOutput: []byte(`{"policy1":{"averageExecutionTime":"494ns","rulesFailedCount":1,"rulesAppliedCount":1,"resourcesMutatedCount":1,"ruleStatus":[{"ruleName":"rule1","averageExecutionTime":"243ns","appliedCount":1,"resourcesMutatedCount":1},{"ruleName":"rule2","averageExecutionTime":"251ns","failedCount":1}]},"policy2":{"averageExecutionTime":"433ns","rulesFailedCount":1,"rulesAppliedCount":1,"resourcesMutatedCount":1,"ruleStatus":[{"ruleName":"rule1","averageExecutionTime":"222ns","appliedCount":1,"resourcesMutatedCount":1},{"ruleName":"rule2","averageExecutionTime":"211ns","failedCount":1}]}}`), - mutateStats: []response.EngineResponse{ + mutateStats: []*response.EngineResponse{ { PolicyResponse: response.PolicyResponse{ Policy: "policy1", @@ -145,11 +145,11 @@ func Test_MutateStats(t *testing.T) { func Test_ValidateStats(t *testing.T) { testCase := struct { - validateStats []response.EngineResponse + validateStats []*response.EngineResponse expectedOutput []byte }{ expectedOutput: []byte(`{"policy1":{"averageExecutionTime":"494ns","rulesFailedCount":1,"rulesAppliedCount":1,"resourcesBlockedCount":1,"ruleStatus":[{"ruleName":"rule3","averageExecutionTime":"243ns","appliedCount":1},{"ruleName":"rule4","averageExecutionTime":"251ns","failedCount":1,"resourcesBlockedCount":1}]},"policy2":{"averageExecutionTime":"433ns","rulesFailedCount":1,"rulesAppliedCount":1,"ruleStatus":[{"ruleName":"rule3","averageExecutionTime":"222ns","appliedCount":1},{"ruleName":"rule4","averageExecutionTime":"211ns","failedCount":1}]}}`), - validateStats: []response.EngineResponse{ + validateStats: []*response.EngineResponse{ { PolicyResponse: response.PolicyResponse{ Policy: "policy1", diff --git a/pkg/webhooks/report.go b/pkg/webhooks/report.go index 33522b4653..fd82f118ee 100644 --- a/pkg/webhooks/report.go +++ b/pkg/webhooks/report.go @@ -10,7 +10,7 @@ import ( ) //generateEvents generates event info for the engine responses -func generateEvents(engineResponses []response.EngineResponse, blocked, onUpdate bool, log logr.Logger) []event.Info { +func generateEvents(engineResponses []*response.EngineResponse, blocked, onUpdate bool, log logr.Logger) []event.Info { var events []event.Info // - Admission-Response is SUCCESS diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 99baab4698..00585f5bb3 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -330,7 +330,7 @@ func (ws *WebhookServer) ResourceMutation(request *v1beta1.AdmissionRequest) *v1 if err != nil { logger.Error(err, "failed to load userInfo in context") } - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + err = ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username) if err != nil { logger.Error(err, "failed to load service account in context") } @@ -441,7 +441,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * if err != nil { logger.Error(err, "failed to load userInfo in context") } - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + err = ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username) if err != nil { logger.Error(err, "failed to load service account in context") } diff --git a/pkg/webhooks/validate_audit.go b/pkg/webhooks/validate_audit.go index bb6c78e7a1..472e919072 100644 --- a/pkg/webhooks/validate_audit.go +++ b/pkg/webhooks/validate_audit.go @@ -166,7 +166,7 @@ func (h *auditHandler) process(request *v1beta1.AdmissionRequest) error { if err != nil { return errors.Wrap(err, "failed to load userInfo in context") } - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + err = ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username) if err != nil { return errors.Wrap(err, "failed to load service account in context") } diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index e91780d686..e8dbc9130a 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -68,10 +68,9 @@ func HandleValidation( return true, "" } - policyContext := engine.PolicyContext{ + policyContext := &engine.PolicyContext{ NewResource: newR, OldResource: oldR, - Context: ctx, AdmissionInfo: userRequestInfo, ExcludeGroupRole: dynamicConfig.GetExcludeGroupRole(), ExcludeResourceFunc: dynamicConfig.ToFilter, @@ -79,7 +78,7 @@ func HandleValidation( JSONContext: ctx, } - var engineResponses []response.EngineResponse + var engineResponses []*response.EngineResponse for _, policy := range policies { logger.V(3).Info("evaluating policy", "policy", policy.Name) @@ -152,7 +151,7 @@ func buildDeletionPrInfo(oldR unstructured.Unstructured) policyreport.Info { } type validateStats struct { - resp response.EngineResponse + resp *response.EngineResponse namespace string } diff --git a/test/scenarios/samples/best_practices/disallow_default_namespace.yaml b/test/scenarios/samples/best_practices/disallow_default_namespace.yaml index fe44d3389d..365379f35f 100644 --- a/test/scenarios/samples/best_practices/disallow_default_namespace.yaml +++ b/test/scenarios/samples/best_practices/disallow_default_namespace.yaml @@ -10,7 +10,7 @@ expected: kind: Pod apiVersion: v1 # this is set to pass resource NS check - # actual valiation is defined through rule success=false + # actual validation is defined through rule success=false namespace: 'default' name: myapp-pod rules: