From b062d70e298021f06e75f0e16722d573b9f600f0 Mon Sep 17 00:00:00 2001 From: shivkumar dudhani Date: Fri, 23 Aug 2019 18:34:23 -0700 Subject: [PATCH] initial redesign --- pkg/engine/mutation.go | 65 +++++++++++++++++++ pkg/engine/overlay.go | 57 +++++++++++++++++ pkg/engine/patches.go | 89 ++++++++++++++++++++++++++ pkg/engine/response.go | 96 +++++++++++++++++++++++++++++ pkg/engine/utils.go | 44 ++++++++++++- pkg/engine/validation.go | 120 +++++++++++++++++++++++++++--------- pkg/policy/utils.go | 17 +++++ pkg/webhooks/annotations.go | 66 ++++++++++++++++++++ pkg/webhooks/mutation.go | 89 ++++++++++++-------------- pkg/webhooks/server.go | 44 +++++++++---- pkg/webhooks/utils.go | 50 +++++++++++++++ pkg/webhooks/validation.go | 100 +++++++++++++----------------- 12 files changed, 687 insertions(+), 150 deletions(-) create mode 100644 pkg/engine/response.go create mode 100644 pkg/policy/utils.go diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index fd453f4d16..e76ead8dd5 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -59,6 +59,7 @@ func Mutate(policy kyverno.Policy, resource unstructured.Unstructured) (response // Process Overlay if rule.Mutation.Overlay != nil { + // ruleRespone := processOverlay(rule, res) rulePatches, err = processOverlay(rule, patchedDocument) if err == nil { if len(rulePatches) == 0 { @@ -123,3 +124,67 @@ func Mutate(policy kyverno.Policy, resource unstructured.Unstructured) (response response.RuleInfos = ris return response } + +//MutateNew ... +func MutateNew(policy kyverno.Policy, resource unstructured.Unstructured) (response EngineResponseNew) { + startTime := time.Now() + // policy information + func() { + // set policy information + response.PolicyResponse.Policy = policy.Name + // resource details + response.PolicyResponse.Resource.Name = resource.GetName() + response.PolicyResponse.Resource.Namespace = resource.GetNamespace() + response.PolicyResponse.Resource.Kind = resource.GetKind() + response.PolicyResponse.Resource.APIVersion = resource.GetAPIVersion() + }() + glog.V(4).Infof("started applying mutation rules of policy %q (%v)", policy.Name, startTime) + defer func() { + response.PolicyResponse.ProcessingTime = time.Since(startTime) + glog.V(4).Infof("finished applying mutation rules policy %v (%v)", policy.Name, response.PolicyResponse.ProcessingTime) + glog.V(4).Infof("Mutation Rules appplied succesfully count %v for policy %q", response.PolicyResponse.RulesAppliedCount, policy.Name) + }() + incrementAppliedRuleCount := func() { + // rules applied succesfully count + response.PolicyResponse.RulesAppliedCount++ + } + + var patchedResource unstructured.Unstructured + + for _, rule := range policy.Spec.Rules { + //TODO: to be checked before calling the resources as well + if reflect.DeepEqual(rule.Mutation, kyverno.Mutation{}) { + continue + } + // check if the resource satisfies the filter conditions defined in the rule + //TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that + // dont statisfy a policy rule resource description + ok := MatchesResourceDescription(resource, rule) + if !ok { + glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule ", resource.GetNamespace(), resource.GetName()) + continue + } + // Process Overlay + if rule.Mutation.Overlay != nil { + var ruleResponse RuleResponse + ruleResponse, patchedResource = processOverlayNew(rule, resource) + if reflect.DeepEqual(ruleResponse, (RuleResponse{})) { + // overlay pattern does not match the resource conditions + continue + } + response.PolicyResponse.Rules = append(response.PolicyResponse.Rules, ruleResponse) + incrementAppliedRuleCount() + } + + // Process Patches + if rule.Mutation.Patches != nil { + var ruleResponse RuleResponse + ruleResponse, patchedResource = processPatchesNew(rule, resource) + response.PolicyResponse.Rules = append(response.PolicyResponse.Rules, ruleResponse) + incrementAppliedRuleCount() + } + } + // send the patched resource + response.PatchedResource = patchedResource + return response +} diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index f71732e78c..0885ae9bfd 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -7,8 +7,10 @@ import ( "reflect" "strconv" "strings" + "time" "github.com/golang/glog" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" jsonpatch "github.com/evanphx/json-patch" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" @@ -38,6 +40,60 @@ func processOverlay(rule kyverno.Rule, rawResource []byte) ([][]byte, error) { return patches, err } +// rawResource handles validating admission request +// Checks the target resources for rules defined in the policy +// TODO: pass in the unstructured object in stead of raw byte? +func processOverlayNew(rule kyverno.Rule, resource unstructured.Unstructured) (response RuleResponse, patchedResource unstructured.Unstructured) { + startTime := time.Now() + glog.V(4).Infof("started applying overlay rule %q (%v)", rule.Name, startTime) + response.Name = rule.Name + response.Type = Mutation.String() + defer func() { + response.RuleStats.ProcessingTime = time.Since(startTime) + glog.V(4).Infof("finished applying overlay rule %q (%v)", response.Name, response.RuleStats.ProcessingTime) + }() + + patches, err := processOverlayPatches(resource.UnstructuredContent(), rule.Mutation.Overlay) + // resource does not satisfy the overlay pattern, we dont apply this rule + if err != nil && strings.Contains(err.Error(), "Conditions are not met") { + glog.V(4).Infof("Resource %s/%s/%s does not meet the conditions in the rule %s with overlay pattern %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Name, rule.Mutation.Overlay) + //TODO: send zero response and not consider this as applied? + return RuleResponse{}, resource + } + + if err != nil { + // rule application failed + response.Success = false + response.Message = fmt.Sprintf("failed to process overlay: %v", err) + return response, resource + } + // convert to RAW + resourceRaw, err := resource.MarshalJSON() + if err != nil { + response.Success = false + glog.Infof("unable to marshall resource: %v", err) + response.Message = fmt.Sprintf("failed to process JSON patches: %v", err) + return response, resource + } + + joinedPatches := JoinPatches(patches) + var patchResource []byte + patchResource, err = ApplyPatchNew(resourceRaw, joinedPatches) + err = patchedResource.UnmarshalJSON(patchResource) + if err != nil { + glog.Infof("failed to unmarshall resource to undstructured: %v", err) + response.Success = false + response.Message = fmt.Sprintf("failed to process JSON patches: %v", err) + return response, resource + } + + // rule application succesfuly + response.Success = true + response.Message = fmt.Sprintf("succesfully process overlay") + response.Patches = patches + // apply the patches to the resource + return response, patchedResource +} func processOverlayPatches(resource, overlay interface{}) ([][]byte, error) { if !meetConditions(resource, overlay) { @@ -65,6 +121,7 @@ func applyOverlay(resource, overlay interface{}, path string) ([][]byte, error) } appliedPatches = append(appliedPatches, patch) + //TODO : check if return is needed ? } return applyOverlayForSameTypes(resource, overlay, path) } diff --git a/pkg/engine/patches.go b/pkg/engine/patches.go index 557611e6e0..7d33ccb72e 100644 --- a/pkg/engine/patches.go +++ b/pkg/engine/patches.go @@ -3,9 +3,13 @@ package engine import ( "encoding/json" "errors" + "fmt" "reflect" + "strings" + "time" "github.com/golang/glog" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" jsonpatch "github.com/evanphx/json-patch" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" @@ -84,3 +88,88 @@ func ApplyPatches(resource []byte, patches [][]byte) ([]byte, error) { } return patchedDocument, err } + +//ApplyPatchNew ... +func ApplyPatchNew(resource, patch []byte) ([]byte, error) { + jsonpatch, err := jsonpatch.DecodePatch(patch) + if err != nil { + return nil, err + } + + patchedResource, err := jsonpatch.Apply(resource) + if err != nil { + return nil, err + } + return patchedResource, err + +} + +func processPatchesNew(rule kyverno.Rule, resource unstructured.Unstructured) (response RuleResponse, patchedResource unstructured.Unstructured) { + startTime := time.Now() + glog.V(4).Infof("started JSON patch rule %q (%v)", rule.Name, startTime) + response.Name = rule.Name + response.Type = Mutation.String() + defer func() { + response.RuleStats.ProcessingTime = time.Since(startTime) + glog.V(4).Infof("finished JSON patch rule %q (%v)", response.Name, response.RuleStats.ProcessingTime) + }() + + // convert to RAW + resourceRaw, err := resource.MarshalJSON() + if err != nil { + response.Success = false + glog.Infof("unable to marshall resource: %v", err) + response.Message = fmt.Sprintf("failed to process JSON patches: %v", err) + return response, resource + } + + var errs []error + var patches [][]byte + for _, patch := range rule.Mutation.Patches { + // JSON patch + patchRaw, err := json.Marshal(patch) + if err != nil { + glog.V(4).Infof("failed to marshall JSON patch %v: %v", patch, err) + errs = append(errs, err) + continue + } + patchResource, err := ApplyPatchNew(resourceRaw, patchRaw) + // TODO: continue on error if one of the patches fails, will add the failure event in such case + if err != nil && patch.Operation == "remove" { + glog.Info(err) + continue + } + if err != nil { + errs = append(errs, err) + continue + } + resourceRaw = patchResource + patches = append(patches, patchRaw) + } + + // error while processing JSON patches + if len(errs) > 0 { + response.Success = false + response.Message = fmt.Sprint("failed to process JSON patches: %v", func() string { + var str []string + for _, err := range errs { + str = append(str, err.Error()) + } + return strings.Join(str, ";") + }()) + return response, resource + } + err = patchedResource.UnmarshalJSON(resourceRaw) + if err != nil { + glog.Info("failed to unmarshall resource to undstructured: %v", err) + response.Success = false + response.Message = fmt.Sprintf("failed to process JSON patches: %v", err) + return response, resource + } + + // JSON patches processed succesfully + response.Success = true + response.Message = fmt.Sprintf("succesfully process JSON patches") + response.Patches = patches + return response, patchedResource +} diff --git a/pkg/engine/response.go b/pkg/engine/response.go new file mode 100644 index 0000000000..aabd228bfd --- /dev/null +++ b/pkg/engine/response.go @@ -0,0 +1,96 @@ +package engine + +import ( + "fmt" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +//EngineResponseNew engine response to the action +type EngineResponseNew struct { + // Resource patched with the engine action changes + PatchedResource unstructured.Unstructured + // Policy Response + PolicyResponse PolicyResponse +} + +//PolicyResponse policy application response +type PolicyResponse struct { + // policy name + Policy string + // resource details + Resource ResourceSpec + // policy statistics + PolicyStats + // rule response + Rules []RuleResponse + // ValidationFailureAction: audit,enforce(default) + ValidationFailureAction string +} + +//ResourceSpec resource action applied on +type ResourceSpec struct { + //TODO: support ApiVersion + Kind string + APIVersion string + Namespace string + Name string +} + +//PolicyStats stores statistics for the single policy application +type PolicyStats struct { + // time required to process the policy rules on a resource + ProcessingTime time.Duration + // Count of rules that were applied succesfully + RulesAppliedCount int +} + +//RuleResponse details for each rule applicatino +type RuleResponse struct { + // rule name specified in policy + Name string + // rule type (Mutation,Generation,Validation) for Kyverno Policy + Type string + // message response from the rule application + Message string + // JSON patches, for mutation rules + Patches [][]byte + // success/fail + Success bool + // statistics + RuleStats +} + +//ToString ... +func (rr RuleResponse) ToString() string { + return fmt.Sprintf("rule %s (%s): %v", rr.Name, rr.Type, rr.Message) +} + +//RuleStats stores the statisctis for the single rule application +type RuleStats struct { + // time required to appliy the rule on the resource + ProcessingTime time.Duration +} + +//IsSuccesful checks if any rule has failed or not +func (er EngineResponseNew) IsSuccesful() bool { + for _, r := range er.PolicyResponse.Rules { + if !r.Success { + return false + } + } + return true +} + +//GetPatches returns all the patches joined +func (er EngineResponseNew) GetPatches() []byte { + var patches [][]byte + for _, r := range er.PolicyResponse.Rules { + if r.Patches != nil { + patches = append(patches, r.Patches...) + } + } + // join patches + return JoinPatches(patches) +} diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index ff2f3f9745..a04e8c1868 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -21,12 +21,34 @@ import ( //EngineResponse provides the response to the application of a policy rule set on a resource type EngineResponse struct { - Patches [][]byte + // JSON patches for mutation rules + Patches [][]byte + // Resource patched with the policy changes PatchedResource unstructured.Unstructured - RuleInfos []info.RuleInfo + // Rule details + RuleInfos []info.RuleInfo + // PolicyS EngineStats } +// type EngineResponseNew struct { +// // error while processing engine action +// Err error +// // Resource patched with the engine action changes +// PatchedResource unstructured.Unstructured +// // Policy Response +// PolicyRespone PolicyResponse +// } + +// type PolicyResponse struct { +// // policy name +// Policy string +// // resource details +// Resource kyverno.ResourceSpec +// } + +// type PolicyStatus + //EngineStats stores in the statistics for a single application of resource type EngineStats struct { // average time required to process the policy rules on a resource @@ -518,3 +540,21 @@ func ConvertToUnstructured(data []byte) (*unstructured.Unstructured, error) { } return resource, nil } + +type RuleType int + +const ( + Mutation RuleType = iota + Validation + Generation + All +) + +func (ri RuleType) String() string { + return [...]string{ + "Mutation", + "Validation", + "Generation", + "All", + }[ri] +} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index a6385e38a1..c5f7d1ecbd 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -61,53 +61,67 @@ func Validate(policy kyverno.Policy, resource unstructured.Unstructured) (respon continue } - ruleInfo := validatePatterns(resource, rule.Validation, rule.Name) + // ruleInfo := validatePatterns(resource, rule) incrementAppliedRuleCount() - ruleInfos = append(ruleInfos, ruleInfo) + // ruleInfos = append(ruleInfos, ruleInfo) } response.RuleInfos = ruleInfos return response } // validatePatterns validate pattern and anyPattern -func validatePatterns(resource unstructured.Unstructured, validation kyverno.Validation, ruleName string) info.RuleInfo { - var errs []error - ruleInfo := info.NewRuleInfo(ruleName, info.Validation) +func validatePatterns(resource unstructured.Unstructured, rule kyverno.Rule) (response RuleResponse) { + startTime := time.Now() + glog.V(4).Infof("started applying validation rule %q (%v)", rule.Name, startTime) + response.Name = rule.Name + response.Type = Validation.String() + defer func() { + response.RuleStats.ProcessingTime = time.Since(startTime) + glog.V(4).Infof("finished applying validation rule %q (%v)", response.Name, response.RuleStats.ProcessingTime) + }() - if validation.Pattern != nil { - err := validateResourceWithPattern(resource.Object, validation.Pattern) + // either pattern or anyPattern can be specified in Validation rule + if rule.Validation.Pattern != nil { + err := validateResourceWithPattern(resource.Object, rule.Validation.Pattern) if err != nil { - ruleInfo.Fail() - ruleInfo.Addf("Failed to apply pattern: %v", err) - } else { - ruleInfo.Add("Pattern succesfully validated") - glog.V(4).Infof("pattern validated succesfully on resource %s/%s", resource.GetNamespace(), resource.GetName()) + // rule application failed + glog.V(4).Infof("failed to apply validation for rule %s on resource %s/%s/%s, pattern %v ", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Validation.Pattern) + response.Success = false + response.Message = fmt.Sprintf("failed to apply pattern: %v", err) + return response } - return ruleInfo + // rule application succesful + glog.V(4).Infof("rule %s pattern validated succesfully on resource %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName()) + response.Success = true + response.Message = fmt.Sprintf("validation pattern succesfully validated") + return response } - if validation.AnyPattern != nil { - for _, pattern := range validation.AnyPattern { + //TODO: add comments to explain the flow + if rule.Validation.AnyPattern != nil { + var errs []error + for _, pattern := range rule.Validation.AnyPattern { if err := validateResourceWithPattern(resource.Object, pattern); err != nil { errs = append(errs, err) } - } - - failedPattern := len(errs) - patterns := len(validation.AnyPattern) - - // all pattern fail - if failedPattern == patterns { - ruleInfo.Fail() - ruleInfo.Addf("None of anyPattern succeed: %v", errs) - } else { - ruleInfo.Addf("%d/%d patterns succesfully validated", patterns-failedPattern, patterns) + failedPattern := len(errs) + patterns := len(rule.Validation.AnyPattern) + // all patterns fail + if failedPattern == patterns { + // any Pattern application failed + glog.V(4).Infof("none of anyPattern were processed: %v", errs) + response.Success = false + response.Message = fmt.Sprintf("None of anyPattern succeed: %v", errs) + return response + } + // any Pattern application succesful glog.V(4).Infof("%d/%d patterns validated succesfully on resource %s/%s", patterns-failedPattern, patterns, resource.GetNamespace(), resource.GetName()) + response.Success = true + response.Message = fmt.Sprintf("%d/%d patterns succesfully validated", patterns-failedPattern, patterns) + return response } - return ruleInfo } - - return info.RuleInfo{} + return RuleResponse{} } // validateResourceWithPattern is a start of element-by-element validation process @@ -328,3 +342,51 @@ func validateArrayOfMaps(resourceMapArray []interface{}, patternMap map[string]i handler := CreateAnchorHandler(anchor, pattern, path) return handler.Handle(resourceMapArray, patternMap, originPattern) } + +//ValidateNew ... +func ValidateNew(policy kyverno.Policy, resource unstructured.Unstructured) (response EngineResponseNew) { + startTime := time.Now() + // policy information + func() { + // set policy information + response.PolicyResponse.Policy = policy.Name + // resource details + response.PolicyResponse.Resource.Name = resource.GetName() + response.PolicyResponse.Resource.Namespace = resource.GetNamespace() + response.PolicyResponse.Resource.Kind = resource.GetKind() + response.PolicyResponse.Resource.APIVersion = resource.GetAPIVersion() + response.PolicyResponse.ValidationFailureAction = policy.Spec.ValidationFailureAction + }() + + glog.V(4).Infof("started applying validation rules of policy %q (%v)", policy.Name, startTime) + defer func() { + response.PolicyResponse.ProcessingTime = time.Since(startTime) + glog.V(4).Infof("Finished applying validation rules policy %v (%v)", policy.Name, response.PolicyResponse.ProcessingTime) + glog.V(4).Infof("Validation Rules appplied succesfully count %v for policy %q", response.PolicyResponse.RulesAppliedCount, policy.Name) + }() + incrementAppliedRuleCount := func() { + // rules applied succesfully count + response.PolicyResponse.RulesAppliedCount++ + } + + for _, rule := range policy.Spec.Rules { + if reflect.DeepEqual(rule.Validation, kyverno.Validation{}) { + continue + } + + // check if the resource satisfies the filter conditions defined in the rule + // TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that + // dont statisfy a policy rule resource description + ok := MatchesResourceDescription(resource, rule) + if !ok { + glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule ", resource.GetNamespace(), resource.GetName()) + continue + } + if rule.Validation.Pattern != nil { + ruleResponse := validatePatterns(resource, rule) + incrementAppliedRuleCount() + response.PolicyResponse.Rules = append(response.PolicyResponse.Rules, ruleResponse) + } + } + return response +} diff --git a/pkg/policy/utils.go b/pkg/policy/utils.go new file mode 100644 index 0000000000..0110a5d6b1 --- /dev/null +++ b/pkg/policy/utils.go @@ -0,0 +1,17 @@ +package policy + +import kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" + +// reEvaulatePolicy checks if the policy needs to be re-evaulated +// during re-evaulation we remove all the old policy violations and re-create new ones +// - Rule count changes +// - Rule resource description changes +// - Rule operation changes +// - Rule name changed +func reEvaulatePolicy(curP, oldP *kyverno.Policy) bool { + // count of rules changed + if len(curP.Spec.Rules) != len(curP.Spec.Rules) { + + } + return true +} diff --git a/pkg/webhooks/annotations.go b/pkg/webhooks/annotations.go index 41a37633f7..b4b3f55a0e 100644 --- a/pkg/webhooks/annotations.go +++ b/pkg/webhooks/annotations.go @@ -3,6 +3,8 @@ package webhooks import ( "encoding/json" + "github.com/nirmata/kyverno/pkg/engine" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" jsonpatch "github.com/evanphx/json-patch" @@ -33,6 +35,42 @@ type response struct { Value interface{} `json:"value"` } +func generateAnnotationPatches(annotations map[string]string, policyResponse engine.PolicyResponse) []byte { + if annotations == nil { + annotations = map[string]string{} + } + var patchResponse response + value := generateAnnotationsFromPolicyInfo(policyResponse) + if value == nil { + // no patches or error while processing patches + return nil + } + if _, ok := annotations[policyAnnotation]; ok { + // create update patch string + patchResponse = response{ + Op: "replace", + Path: "/metadata/annotations/" + policyAnnotation, + Value: string(value), + } + } else { + patchResponse = response{ + Op: "add", + Path: "/metadata/annotations", + Value: map[string]string{policyAnnotation: string(value)}, + } + } + + patchByte, _ := json.Marshal(patchResponse) + + // check the patch + _, err := jsonpatch.DecodePatch([]byte("[" + string(patchByte) + "]")) + if err != nil { + glog.Errorf("Failed to make patch from annotation'%s', err: %v\n ", string(patchByte), err) + } + + return patchByte +} + func prepareAnnotationPatches(resource *unstructured.Unstructured, policyInfos []info.PolicyInfo) []byte { annots := resource.GetAnnotations() if annots == nil { @@ -82,6 +120,34 @@ func annotationFromPolicies(policyInfos []info.PolicyInfo) []byte { return result } +func generateAnnotationsFromPolicyInfo(policyResponse engine.PolicyResponse) []byte { + var rulePatches []rulePatch + // generate annotation for each mutation JSON patch to be applied on the resource + for _, rule := range policyResponse.Rules { + var patchmap map[string]string + patch := engine.JoinPatches(rule.Patches) + if err := json.Unmarshal(patch, &patchmap); err != nil { + glog.Errorf("Failed to parse patch bytes, err: %v\n", err) + continue + } + + rp := rulePatch{ + RuleName: rule.Name, + Op: patchmap["op"], + Path: patchmap["path"]} + + rulePatches = append(rulePatches, rp) + glog.V(4).Infof("Annotation value prepared: %v\n", rulePatches) + + } + patch, err := json.Marshal(rulePatches) + if err != nil { + glog.Info("failed to marshall: %v", err) + return nil + } + return patch +} + func annotationFromPolicy(policyInfo info.PolicyInfo) []rulePatch { if !policyInfo.IsSuccessful() { glog.V(2).Infof("Policy %s failed, skip preparing annotation\n", policyInfo.Name) diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index 620ab3242c..7236ace6d5 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -3,7 +3,6 @@ package webhooks import ( "github.com/golang/glog" engine "github.com/nirmata/kyverno/pkg/engine" - "github.com/nirmata/kyverno/pkg/info" policyctr "github.com/nirmata/kyverno/pkg/policy" "github.com/nirmata/kyverno/pkg/utils" v1beta1 "k8s.io/api/admission/v1beta1" @@ -12,16 +11,19 @@ import ( ) // HandleMutation handles mutating webhook admission request -func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) (bool, engine.EngineResponse) { +func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) (bool, []byte, string) { + glog.V(4).Infof("Receive request in mutating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + var patches [][]byte - var policyInfos []info.PolicyInfo var policyStats []policyctr.PolicyStat + // gather stats from the engine response - gatherStat := func(policyName string, er engine.EngineResponse) { + gatherStat := func(policyName string, policyResponse engine.PolicyResponse) { ps := policyctr.PolicyStat{} ps.PolicyName = policyName - ps.Stats.MutationExecutionTime = er.ExecutionTime - ps.Stats.RulesAppliedCount = er.RulesAppliedCount + ps.Stats.MutationExecutionTime = policyResponse.ProcessingTime + ps.Stats.RulesAppliedCount = policyResponse.RulesAppliedCount policyStats = append(policyStats, ps) } // send stats for aggregation @@ -32,85 +34,72 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) (bool ws.policyStatus.SendStat(stat) } } - - glog.V(5).Infof("Receive request in mutating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) - + // convert RAW to unstructured resource, err := engine.ConvertToUnstructured(request.Object.Raw) if err != nil { + //TODO: skip applying the amiddions control ? glog.Errorf("unable to convert raw resource to unstructured: %v", err) + return true, nil, "" } //TODO: check if resource gvk is available in raw resource, + //TODO: check if the name and namespace is also passed right in the resource? // if not then set it from the api request resource.SetGroupVersionKind(schema.GroupVersionKind{Group: request.Kind.Group, Version: request.Kind.Version, Kind: request.Kind.Kind}) - //TODO: check if the name and namespace is also passed right in the resource? - - engineResponse := engine.EngineResponse{PatchedResource: *resource} - policies, err := ws.pLister.List(labels.NewSelector()) if err != nil { //TODO check if the CRD is created ? // Unable to connect to policy Lister to access policies glog.Errorln("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied") glog.Warning(err) - return true, engineResponse + return true, nil, "" } + var engineResponses []engine.EngineResponseNew for _, policy := range policies { // check if policy has a rule for the admission request kind if !utils.Contains(getApplicableKindsForPolicy(policy), request.Kind.Kind) { continue } - - policyInfo := info.NewPolicyInfo(policy.Name, resource.GetKind(), resource.GetName(), resource.GetNamespace(), policy.Spec.ValidationFailureAction) + // policyInfo := info.NewPolicyInfo(policy.Name, resource.GetKind(), resource.GetName(), resource.GetNamespace(), policy.Spec.ValidationFailureAction) glog.V(4).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation) - glog.V(4).Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) - - engineResponse = engine.Mutate(*policy, *resource) - policyInfo.AddRuleInfos(engineResponse.RuleInfos) + // glog.V(4).Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) + // TODO: this can be + engineResponse := engine.MutateNew(*policy, *resource) + engineResponses = append(engineResponses, engineResponse) // Gather policy application statistics - gatherStat(policy.Name, engineResponse) - - // ps := policyctr.NewPolicyStat(policy.Name, engineResponse.ExecutionTime, nil, engineResponse.RulesAppliedCount) - - if !policyInfo.IsSuccessful() { + gatherStat(policy.Name, engineResponse.PolicyResponse) + if !engineResponse.IsSuccesful() { glog.V(4).Infof("Failed to apply policy %s on resource %s/%s\n", policy.Name, resource.GetNamespace(), resource.GetName()) - glog.V(4).Info("Failed rule details") - for _, r := range engineResponse.RuleInfos { - glog.V(4).Infof("%s: %s\n", r.Name, r.Msgs) - } continue } - - patches = append(patches, engineResponse.Patches...) - policyInfos = append(policyInfos, policyInfo) + // gather patches + patches = append(patches, engineResponse.GetPatches()) + // generate annotations + if annPatches := generateAnnotationPatches(resource.GetAnnotations(), engineResponse.PolicyResponse); annPatches != nil { + patches = append(patches, annPatches) + } glog.V(4).Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, resource.GetNamespace(), resource.GetName()) - + //TODO: check if there is an order to policy application on resource + // resource = &engineResponse.PatchedResource } + // combine rule patches & annotations - // ADD ANNOTATIONS // ADD EVENTS - if len(patches) > 0 { - eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Mutation) - ws.eventGen.Add(eventsInfo...) - - annotation := prepareAnnotationPatches(resource, policyInfos) - patches = append(patches, annotation) - } - - ok, msg := isAdmSuccesful(policyInfos) - // Send policy engine Stats - if ok { + // if len(patches) > 0 { + // eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Mutation) + // ws.eventGen.Add(eventsInfo...) + // } + if isResponseSuccesful(engineResponses) { sendStat(false) - engineResponse.Patches = patches - return true, engineResponse + patch := engine.JoinPatches(patches) + return true, patch, "" } sendStat(true) - glog.Errorf("Failed to mutate the resource: %s\n", msg) - return false, engineResponse + glog.Errorf("Failed to mutate the resource\n") + return false, nil, getErrorMsg(engineResponses) } diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 28483b47b2..ebee2e2bf2 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -10,8 +10,6 @@ import ( "net/http" "time" - "github.com/nirmata/kyverno/pkg/engine" - "github.com/golang/glog" kyvernoclient "github.com/nirmata/kyverno/pkg/client/clientset/versioned" kyvernoinformer "github.com/nirmata/kyverno/pkg/client/informers/externalversions/kyverno/v1alpha1" @@ -24,6 +22,7 @@ import ( "github.com/nirmata/kyverno/pkg/utils" "github.com/nirmata/kyverno/pkg/webhookconfig" v1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" ) @@ -135,24 +134,43 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { } func (ws *WebhookServer) HandleAdmissionRequest(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - var response *v1beta1.AdmissionResponse - - allowed, engineResponse := ws.HandleMutation(request) - if !allowed { - // TODO: add failure message to response + // MUTATION + ok, patches, msg := ws.HandleMutation(request) + if !ok { return &v1beta1.AdmissionResponse{ Allowed: false, + Result: &metav1.Status{ + Status: "Failure", + Message: msg, + }, } } - response = ws.HandleValidation(request, engineResponse.PatchedResource) - if response.Allowed && len(engineResponse.Patches) > 0 { - patchType := v1beta1.PatchTypeJSONPatch - response.Patch = engine.JoinPatches(engineResponse.Patches) - response.PatchType = &patchType + // patch the resource with patches before handling validation rules + patchedResource := processResourceWithPatches(patches, request.Object.Raw) + + // VALIDATION + ok, msg = ws.HandleValidation(request, patchedResource) + if !ok { + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Status: "Failure", + Message: msg, + }, + } } - return response + // Succesfful processing of mutation & validation rules in policy + patchType := v1beta1.PatchTypeJSONPatch + return &v1beta1.AdmissionResponse{ + Allowed: true, + Result: &metav1.Status{ + Status: "Success", + }, + Patch: patches, + PatchType: &patchType, + } } // RunAsync TLS server in separate thread and returns control immediately diff --git a/pkg/webhooks/utils.go b/pkg/webhooks/utils.go index f522a135f2..6921574ffd 100644 --- a/pkg/webhooks/utils.go +++ b/pkg/webhooks/utils.go @@ -6,6 +6,7 @@ import ( "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" + "github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/info" ) @@ -26,6 +27,43 @@ func isAdmSuccesful(policyInfos []info.PolicyInfo) (bool, string) { return admSuccess, strings.Join(errMsgs, ";") } +func isResponseSuccesful(engineReponses []engine.EngineResponseNew) bool { + for _, er := range engineReponses { + if !er.IsSuccesful() { + return false + } + } + return true +} + +// returns true -> if there is even one policy that blocks resource requst +// returns false -> if all the policies are meant to report only, we dont block resource request +func toBlockResource(engineReponses []engine.EngineResponseNew) bool { + for _, er := range engineReponses { + if er.PolicyResponse.ValidationFailureAction != ReportViolation { + glog.V(4).Infof("ValidationFailureAction set to enforce for policy %s , blocking resource ceation", er.PolicyResponse.Policy) + return true + } + } + glog.V(4).Infoln("ValidationFailureAction set to audit, allowing resource creation, reporting with violation") + return false +} + +func getErrorMsg(engineReponses []engine.EngineResponseNew) string { + var str []string + for _, er := range engineReponses { + if !er.IsSuccesful() { + str = append(str, fmt.Sprintf("failed policy %s"), er.PolicyResponse.Policy) + for _, rule := range er.PolicyResponse.Rules { + if !rule.Success { + str = append(str, rule.ToString()) + } + } + } + } + return strings.Join(str, "\n") +} + //ArrayFlags to store filterkinds type ArrayFlags []string @@ -87,3 +125,15 @@ func toBlock(pis []info.PolicyInfo) bool { glog.V(3).Infoln("ValidationFailureAction set to audit, allowing resource creation, reporting with violation") return false } + +func processResourceWithPatches(patch []byte, resource []byte) []byte { + if patch == nil { + return nil + } + resource, err := engine.ApplyPatchNew(resource, patch) + if err != nil { + glog.Error("failed to patch resource: %v", err) + return nil + } + return resource +} diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index 14105ed0f3..ecd44f8daf 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -8,23 +8,26 @@ import ( "github.com/nirmata/kyverno/pkg/policyviolation" "github.com/nirmata/kyverno/pkg/utils" v1beta1 "k8s.io/api/admission/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" ) // HandleValidation handles validating webhook admission request // If there are no errors in validating rule we apply generation rules -func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, resource unstructured.Unstructured) *v1beta1.AdmissionResponse { +// patchedResource is the (resource + patches) after applying mutation rules +func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, patchedResource []byte) (bool, string) { + glog.V(4).Infof("Receive request in validating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + var policyInfos []info.PolicyInfo var policyStats []policyctr.PolicyStat + // gather stats from the engine response - gatherStat := func(policyName string, er engine.EngineResponse) { + gatherStat := func(policyName string, policyResponse engine.PolicyResponse) { ps := policyctr.PolicyStat{} ps.PolicyName = policyName - ps.Stats.ValidationExecutionTime = er.ExecutionTime - ps.Stats.RulesAppliedCount = er.RulesAppliedCount + ps.Stats.ValidationExecutionTime = policyResponse.ProcessingTime + ps.Stats.RulesAppliedCount = policyResponse.RulesAppliedCount policyStats = append(policyStats, ps) } // send stats for aggregation @@ -36,8 +39,23 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, res } } - glog.V(5).Infof("Receive request in validating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + resourceRaw := request.Object.Raw + if patchedResource != nil { + glog.V(4).Info("using patched resource from mutation to process validation rules") + resourceRaw = patchedResource + } + // convert RAW to unstructured + resource, err := engine.ConvertToUnstructured(resourceRaw) + if err != nil { + //TODO: skip applying the amiddions control ? + glog.Errorf("unable to convert raw resource to unstructured: %v", err) + return true, "" + } + //TODO: check if resource gvk is available in raw resource, + // if not then set it from the api request + resource.SetGroupVersionKind(schema.GroupVersionKind{Group: request.Kind.Group, Version: request.Kind.Version, Kind: request.Kind.Kind}) + //TODO: check if the name and namespace is also passed right in the resource? + // all the patches to be applied on the resource policies, err := ws.pLister.List(labels.NewSelector()) if err != nil { @@ -45,81 +63,51 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, res // Unable to connect to policy Lister to access policies glog.Error("Unable to connect to policy controller to access policies. Validation Rules are NOT being applied") glog.Warning(err) - return &v1beta1.AdmissionResponse{ - Allowed: true, - } + return true, "" } - //TODO: check if resource gvk is available in raw resource, - // if not then set it from the api request - resource.SetGroupVersionKind(schema.GroupVersionKind{Group: request.Kind.Group, Version: request.Kind.Version, Kind: request.Kind.Kind}) - //TODO: check if the name and namespace is also passed right in the resource? - // all the patches to be applied on the resource - + var engineResponses []engine.EngineResponseNew for _, policy := range policies { if !utils.Contains(getApplicableKindsForPolicy(policy), request.Kind.Kind) { continue } - policyInfo := info.NewPolicyInfo(policy.Name, resource.GetKind(), resource.GetName(), resource.GetNamespace(), policy.Spec.ValidationFailureAction) - glog.V(4).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation) - glog.V(4).Infof("Validating resource %s/%s/%s with policy %s with %d rules\n", resource.GetKind(), resource.GetNamespace(), resource.GetName(), policy.ObjectMeta.Name, len(policy.Spec.Rules)) + // glog.V(4).Infof("Validating resource %s/%s/%s with policy %s with %d rules\n", resource.GetKind(), resource.GetNamespace(), resource.GetName(), policy.ObjectMeta.Name, len(policy.Spec.Rules)) - engineResponse := engine.Validate(*policy, resource) - if len(engineResponse.RuleInfos) == 0 { + engineResponse := engine.ValidateNew(*policy, *resource) + engineResponses = append(engineResponses, engineResponse) + // Gather policy application statistics + gatherStat(policy.Name, engineResponse.PolicyResponse) + if !engineResponse.IsSuccesful() { + glog.V(4).Infof("Failed to apply policy %s on resource %s/%s\n", policy.Name, resource.GetNamespace(), resource.GetName()) continue } - gatherStat(policy.Name, engineResponse) - - if len(engineResponse.RuleInfos) > 0 { - glog.V(4).Infof("Validation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, resource.GetNamespace(), resource.GetName()) - } - - policyInfo.AddRuleInfos(engineResponse.RuleInfos) - - if !policyInfo.IsSuccessful() { - glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, resource.GetNamespace(), resource.GetName()) - for _, r := range engineResponse.RuleInfos { - glog.Warningf("%s: %s\n", r.Name, r.Msgs) - } - } - - policyInfos = append(policyInfos, policyInfo) - } // ADD EVENTS - if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 { - eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Validation) - // If the validationFailureAction flag is set "audit", - // then we dont block the request and report the violations - ws.eventGen.Add(eventsInfo...) - } + // if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 { + // eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Validation) + // // If the validationFailureAction flag is set "audit", + // // then we dont block the request and report the violations + // ws.eventGen.Add(eventsInfo...) + // } // If Validation fails then reject the request // violations are created if "audit" flag is set // and if there are any then we dont block the resource creation // Even if one the policy being applied - ok, msg := isAdmSuccesful(policyInfos) - if !ok && toBlock(policyInfos) { + if !isResponseSuccesful(engineResponses) && toBlockResource(engineResponses) { sendStat(true) - return &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: msg, - }, - } + return false, getErrorMsg(engineResponses) } // ADD POLICY VIOLATIONS policyviolation.GeneratePolicyViolations(ws.pvListerSynced, ws.pvLister, ws.kyvernoClient, policyInfos) sendStat(false) - return &v1beta1.AdmissionResponse{ - Allowed: true, - } + return true, "" }