diff --git a/cmd/kyverno/main.go b/cmd/kyverno/main.go index 62cefceeb9..5066591e1e 100644 --- a/cmd/kyverno/main.go +++ b/cmd/kyverno/main.go @@ -15,6 +15,7 @@ import ( "github.com/nirmata/kyverno/pkg/generate" generatecleanup "github.com/nirmata/kyverno/pkg/generate/cleanup" "github.com/nirmata/kyverno/pkg/policy" + "github.com/nirmata/kyverno/pkg/policystatus" "github.com/nirmata/kyverno/pkg/policystore" "github.com/nirmata/kyverno/pkg/policyviolation" "github.com/nirmata/kyverno/pkg/signal" @@ -135,12 +136,18 @@ func main() { client, pInformer.Kyverno().V1().ClusterPolicies()) + // Policy Status Handler - deals with all logic related to policy status + statusSync := policystatus.NewSync( + pclient, + policyMetaStore) + // POLICY VIOLATION GENERATOR // -- generate policy violation pvgen := policyviolation.NewPVGenerator(pclient, client, pInformer.Kyverno().V1().ClusterPolicyViolations(), - pInformer.Kyverno().V1().PolicyViolations()) + pInformer.Kyverno().V1().PolicyViolations(), + statusSync.Listener) // POLICY CONTROLLER // - reconciliation policy and policy violation @@ -174,6 +181,7 @@ func main() { egen, pvgen, kubedynamicInformer, + statusSync.Listener, ) // GENERATE REQUEST CLEANUP // -- cleans up the generate requests that have not been processed(i.e. state = [Pending, Failed]) for more than defined timeout @@ -215,7 +223,7 @@ func main() { kubeInformer.Rbac().V1().ClusterRoleBindings(), egen, webhookRegistrationClient, - pc.GetPolicyStatusAggregator(), + statusSync.Listener, configData, policyMetaStore, pvgen, @@ -238,6 +246,7 @@ func main() { go grc.Run(1, stopCh) go grcc.Run(1, stopCh) go pvgen.Run(1, stopCh) + go statusSync.Run(1, stopCh) // verifys if the admission control is enabled and active // resync: 60 seconds diff --git a/pkg/api/kyverno/v1/types.go b/pkg/api/kyverno/v1/types.go index 34a3273b18..29c44b4385 100644 --- a/pkg/api/kyverno/v1/types.go +++ b/pkg/api/kyverno/v1/types.go @@ -227,21 +227,24 @@ type CloneFrom struct { Name string `json:"name,omitempty"` } -//PolicyStatus provides status for violations +// PolicyStatus mostly contains statistics related to policy type PolicyStatus struct { - ViolationCount int `json:"violationCount"` + // average time required to process the policy rules on a resource + AvgExecutionTime string `json:"averageExecutionTime"` + // number of violations created by this policy + ViolationCount int `json:"violationCount,omitempty"` + // Count of rules that failed + RulesFailedCount int `json:"rulesFailedCount,omitempty"` // Count of rules that were applied - RulesAppliedCount int `json:"rulesAppliedCount"` - // Count of resources for whom update/create api requests were blocked as the resoruce did not satisfy the policy rules - ResourcesBlockedCount int `json:"resourcesBlockedCount"` - // average time required to process the policy Mutation rules on a resource - AvgExecutionTimeMutation string `json:"averageMutationRulesExecutionTime"` - // average time required to process the policy Validation rules on a resource - AvgExecutionTimeValidation string `json:"averageValidationRulesExecutionTime"` - // average time required to process the policy Validation rules on a resource - AvgExecutionTimeGeneration string `json:"averageGenerationRulesExecutionTime"` - // statistics per rule - Rules []RuleStats `json:"ruleStatus"` + RulesAppliedCount int `json:"rulesAppliedCount,omitempty"` + // Count of resources that were blocked for failing a validate, across all rules + ResourcesBlockedCount int `json:"resourcesBlockedCount,omitempty"` + // Count of resources that were successfully mutated, across all rules + ResourcesMutatedCount int `json:"resourcesMutatedCount,omitempty"` + // Count of resources that were successfully generated, across all rules + ResourcesGeneratedCount int `json:"resourcesGeneratedCount,omitempty"` + + Rules []RuleStats `json:"ruleStatus,omitempty"` } //RuleStats provides status per rule @@ -249,13 +252,19 @@ type RuleStats struct { // Rule name Name string `json:"ruleName"` // average time require to process the rule - ExecutionTime string `json:"averageExecutionTime"` - // Count of rules that were applied - AppliedCount int `json:"appliedCount"` + ExecutionTime string `json:"averageExecutionTime,omitempty"` + // number of violations created by this rule + ViolationCount int `json:"violationCount,omitempty"` // Count of rules that failed - ViolationCount int `json:"violationCount"` - // Count of mutations - MutationCount int `json:"mutationsCount"` + FailedCount int `json:"failedCount,omitempty"` + // Count of rules that were applied + AppliedCount int `json:"appliedCount,omitempty"` + // Count of resources for whom update/create api requests were blocked as the resource did not satisfy the policy rules + ResourcesBlockedCount int `json:"resourcesBlockedCount,omitempty"` + // Count of resources that were successfully mutated + ResourcesMutatedCount int `json:"resourcesMutatedCount,omitempty"` + // Count of resources that were successfully generated + ResourcesGeneratedCount int `json:"resourcesGeneratedCount,omitempty"` } // PolicyList is a list of Policy resources diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 25c7204851..0f8cea1937 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -1,6 +1,8 @@ package engine import ( + "time" + "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" "github.com/nirmata/kyverno/pkg/engine/context" @@ -25,6 +27,9 @@ func filterRule(rule kyverno.Rule, resource unstructured.Unstructured, admission if !rule.HasGenerate() { return nil } + + startTime := time.Now() + if err := MatchesResourceDescription(resource, rule, admissionInfo); err != nil { glog.V(4).Infof(err.Error()) return nil @@ -39,8 +44,12 @@ func filterRule(rule kyverno.Rule, resource unstructured.Unstructured, admission } // build rule Response return &response.RuleResponse{ - Name: rule.Name, - Type: "Generation", + Name: rule.Name, + Type: "Generation", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Since(startTime), + }, } } diff --git a/pkg/generate/controller.go b/pkg/generate/controller.go index 944b8b49cb..d40ed2af0d 100644 --- a/pkg/generate/controller.go +++ b/pkg/generate/controller.go @@ -11,6 +11,7 @@ import ( kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1" dclient "github.com/nirmata/kyverno/pkg/dclient" "github.com/nirmata/kyverno/pkg/event" + "github.com/nirmata/kyverno/pkg/policystatus" "github.com/nirmata/kyverno/pkg/policyviolation" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -57,6 +58,8 @@ type Controller struct { //TODO: list of generic informers // only support Namespaces for re-evalutation on resource updates nsInformer informers.GenericInformer + + policyStatusListener policystatus.Listener } //NewController returns an instance of the Generate-Request Controller @@ -68,6 +71,7 @@ func NewController( eventGen event.Interface, pvGenerator policyviolation.GeneratorInterface, dynamicInformer dynamicinformer.DynamicSharedInformerFactory, + policyStatus policystatus.Listener, ) *Controller { c := Controller{ client: client, @@ -76,8 +80,9 @@ func NewController( pvGenerator: pvGenerator, //TODO: do the math for worst case back off and make sure cleanup runs after that // as we dont want a deleted GR to be re-queue - queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(1, 30), "generate-request"), - dynamicInformer: dynamicInformer, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(1, 30), "generate-request"), + dynamicInformer: dynamicInformer, + policyStatusListener: policyStatus, } c.statusControl = StatusControl{client: kyvernoclient} diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index cd9ab1b2fd..bff1dbbca6 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -3,6 +3,7 @@ package generate import ( "encoding/json" "fmt" + "time" "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" @@ -80,7 +81,7 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern } // Apply the generate rule on resource - return applyGeneratePolicy(c.client, policyContext) + return c.applyGeneratePolicy(policyContext, gr) } func updateStatus(statusControl StatusControlInterface, gr kyverno.GenerateRequest, err error, genResources []kyverno.ResourceSpec) error { @@ -92,7 +93,7 @@ func updateStatus(statusControl StatusControlInterface, gr kyverno.GenerateReque return statusControl.Success(gr, genResources) } -func applyGeneratePolicy(client *dclient.Client, policyContext engine.PolicyContext) ([]kyverno.ResourceSpec, error) { +func (c *Controller) applyGeneratePolicy(policyContext engine.PolicyContext, gr kyverno.GenerateRequest) ([]kyverno.ResourceSpec, error) { // List of generatedResources var genResources []kyverno.ResourceSpec // Get the response as the actions to be performed on the resource @@ -107,20 +108,70 @@ func applyGeneratePolicy(client *dclient.Client, policyContext engine.PolicyCont return rcreationTime.Before(&pcreationTime) }() + ruleNameToProcessingTime := make(map[string]time.Duration) for _, rule := range policy.Spec.Rules { if !rule.HasGenerate() { continue } - genResource, err := applyRule(client, rule, resource, ctx, processExisting) + + startTime := time.Now() + genResource, err := applyRule(c.client, rule, resource, ctx, processExisting) if err != nil { return nil, err } + + ruleNameToProcessingTime[rule.Name] = time.Since(startTime) genResources = append(genResources, genResource) } + if gr.Status.State == "" { + c.policyStatusListener.Send(generateSyncStats{ + policyName: policy.Name, + ruleNameToProcessingTime: ruleNameToProcessingTime, + }) + } + return genResources, nil } +type generateSyncStats struct { + policyName string + ruleNameToProcessingTime map[string]time.Duration +} + +func (vc generateSyncStats) PolicyName() string { + return vc.policyName +} + +func (vc generateSyncStats) UpdateStatus(status kyverno.PolicyStatus) kyverno.PolicyStatus { + + for i := range status.Rules { + if executionTime, exist := vc.ruleNameToProcessingTime[status.Rules[i].Name]; exist { + status.ResourcesGeneratedCount += 1 + status.Rules[i].ResourcesGeneratedCount += 1 + averageOver := int64(status.Rules[i].AppliedCount + status.Rules[i].FailedCount) + status.Rules[i].ExecutionTime = updateGenerateExecutionTime( + executionTime, + status.Rules[i].ExecutionTime, + averageOver, + ).String() + } + } + + return status +} + +func updateGenerateExecutionTime(newTime time.Duration, oldAverageTimeString string, averageOver int64) time.Duration { + if averageOver == 0 { + return newTime + } + oldAverageExecutionTime, _ := time.ParseDuration(oldAverageTimeString) + numerator := (oldAverageExecutionTime.Nanoseconds() * averageOver) + newTime.Nanoseconds() + denominator := averageOver + newAverageTimeInNanoSeconds := numerator / denominator + return time.Duration(newAverageTimeInNanoSeconds) * time.Nanosecond +} + func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.Unstructured, ctx context.EvalInterface, processExisting bool) (kyverno.ResourceSpec, error) { var rdata map[string]interface{} var err error diff --git a/pkg/generate/policyStatus_test.go b/pkg/generate/policyStatus_test.go new file mode 100644 index 0000000000..b0fa04b591 --- /dev/null +++ b/pkg/generate/policyStatus_test.go @@ -0,0 +1,53 @@ +package generate + +import ( + "encoding/json" + "reflect" + "testing" + "time" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" +) + +func Test_Stats(t *testing.T) { + testCase := struct { + generatedSyncStats []generateSyncStats + expectedOutput []byte + existingStatus map[string]v1.PolicyStatus + }{ + expectedOutput: []byte(`{"policy1":{"averageExecutionTime":"","resourcesGeneratedCount":2,"ruleStatus":[{"ruleName":"rule1","averageExecutionTime":"23ns","resourcesGeneratedCount":1},{"ruleName":"rule2","averageExecutionTime":"44ns","resourcesGeneratedCount":1},{"ruleName":"rule3"}]}}`), + generatedSyncStats: []generateSyncStats{ + { + policyName: "policy1", + ruleNameToProcessingTime: map[string]time.Duration{ + "rule1": time.Nanosecond * 23, + "rule2": time.Nanosecond * 44, + }, + }, + }, + existingStatus: map[string]v1.PolicyStatus{ + "policy1": { + Rules: []v1.RuleStats{ + { + Name: "rule1", + }, + { + Name: "rule2", + }, + { + Name: "rule3", + }, + }, + }, + }, + } + + for _, generateSyncStat := range testCase.generatedSyncStats { + testCase.existingStatus[generateSyncStat.PolicyName()] = generateSyncStat.UpdateStatus(testCase.existingStatus[generateSyncStat.PolicyName()]) + } + + output, _ := json.Marshal(testCase.existingStatus) + if !reflect.DeepEqual(output, testCase.expectedOutput) { + t.Errorf("\n\nTestcase has failed\nExpected:\n%v\nGot:\n%v\n\n", string(testCase.expectedOutput), string(output)) + } +} diff --git a/pkg/policy/apply.go b/pkg/policy/apply.go index ed4b72ac65..32d654346c 100644 --- a/pkg/policy/apply.go +++ b/pkg/policy/apply.go @@ -19,45 +19,14 @@ import ( // applyPolicy applies policy on a resource //TODO: generation rules -func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, policyStatus PolicyStatusInterface) (responses []response.EngineResponse) { +func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (responses []response.EngineResponse) { startTime := time.Now() - var policyStats []PolicyStat + glog.V(4).Infof("Started apply policy %s on resource %s/%s/%s (%v)", policy.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), startTime) defer func() { glog.V(4).Infof("Finished applying %s on resource %s/%s/%s (%v)", policy.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), time.Since(startTime)) }() - // gather stats from the engine response - gatherStat := func(policyName string, policyResponse response.PolicyResponse) { - ps := PolicyStat{} - ps.PolicyName = policyName - ps.Stats.MutationExecutionTime = policyResponse.ProcessingTime - ps.Stats.RulesAppliedCount = policyResponse.RulesAppliedCount - // capture rule level stats - for _, rule := range policyResponse.Rules { - rs := RuleStatinfo{} - rs.RuleName = rule.Name - rs.ExecutionTime = rule.RuleStats.ProcessingTime - if rule.Success { - rs.RuleAppliedCount++ - } else { - rs.RulesFailedCount++ - } - if rule.Patches != nil { - rs.MutationCount++ - } - ps.Stats.Rules = append(ps.Stats.Rules, rs) - } - policyStats = append(policyStats, ps) - } - // send stats for aggregation - sendStat := func(blocked bool) { - for _, stat := range policyStats { - stat.Stats.ResourceBlocked = utils.Btoi(blocked) - //SEND - policyStatus.SendStat(stat) - } - } var engineResponses []response.EngineResponse var engineResponse response.EngineResponse var err error @@ -66,27 +35,20 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure ctx.AddResource(transformResource(resource)) //MUTATION - engineResponse, err = mutation(policy, resource, policyStatus, ctx) + engineResponse, err = mutation(policy, resource, ctx) engineResponses = append(engineResponses, engineResponse) if err != nil { glog.Errorf("unable to process mutation rules: %v", err) } - gatherStat(policy.Name, engineResponse.PolicyResponse) - //send stats - sendStat(false) //VALIDATION engineResponse = engine.Validate(engine.PolicyContext{Policy: policy, Context: ctx, NewResource: resource}) engineResponses = append(engineResponses, engineResponse) - // gather stats - gatherStat(policy.Name, engineResponse.PolicyResponse) - //send stats - sendStat(false) //TODO: GENERATION return engineResponses } -func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, policyStatus PolicyStatusInterface, ctx context.EvalInterface) (response.EngineResponse, error) { +func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, ctx context.EvalInterface) (response.EngineResponse, error) { engineResponse := engine.Mutate(engine.PolicyContext{Policy: policy, NewResource: resource, Context: ctx}) if !engineResponse.IsSuccesful() { diff --git a/pkg/policy/controller.go b/pkg/policy/controller.go index 04200cd5a5..d303bebf52 100644 --- a/pkg/policy/controller.go +++ b/pkg/policy/controller.go @@ -2,7 +2,6 @@ package policy import ( "fmt" - "reflect" "time" "github.com/golang/glog" @@ -67,8 +66,6 @@ type PolicyController struct { rm resourceManager // helpers to validate against current loaded configuration configHandler config.Interface - // receives stats and aggregates details - statusAggregator *PolicyStatusAggregator // store to hold policy meta data for faster lookup pMetaStore policystore.UpdateInterface // policy violation generator @@ -144,10 +141,6 @@ func NewPolicyController(kyvernoClient *kyvernoclient.Clientset, //TODO: pass the time in seconds instead of converting it internally pc.rm = NewResourceManager(30) - // aggregator - // pc.statusAggregator = NewPolicyStatAggregator(kyvernoClient, pInformer) - pc.statusAggregator = NewPolicyStatAggregator(kyvernoClient) - return &pc, nil } @@ -264,9 +257,6 @@ func (pc *PolicyController) Run(workers int, stopCh <-chan struct{}) { for i := 0; i < workers; i++ { go wait.Until(pc.worker, time.Second, stopCh) } - // policy status aggregator - //TODO: workers required for aggergation - pc.statusAggregator.Run(1, stopCh) <-stopCh } @@ -328,8 +318,6 @@ func (pc *PolicyController) syncPolicy(key string) error { if err := pc.deleteNamespacedPolicyViolations(key); err != nil { return err } - // remove the recorded stats for the policy - pc.statusAggregator.RemovePolicyStats(key) // remove webhook configurations if there are no policies if err := pc.removeResourceWebhookConfiguration(); err != nil { @@ -345,23 +333,12 @@ func (pc *PolicyController) syncPolicy(key string) error { pc.resourceWebhookWatcher.RegisterResourceWebhook() - // cluster policy violations - cpvList, err := pc.getClusterPolicyViolationForPolicy(policy.Name) - if err != nil { - return err - } - // namespaced policy violation - nspvList, err := pc.getNamespacedPolicyViolationForPolicy(policy.Name) - if err != nil { - return err - } - // process policies on existing resources engineResponses := pc.processExistingResources(*policy) // report errors pc.cleanupAndReport(engineResponses) - // sync active - return pc.syncStatusOnly(policy, cpvList, nspvList) + + return nil } func (pc *PolicyController) deleteClusterPolicyViolations(policy string) error { @@ -390,39 +367,6 @@ func (pc *PolicyController) deleteNamespacedPolicyViolations(policy string) erro return nil } -//syncStatusOnly updates the policy status subresource -func (pc *PolicyController) syncStatusOnly(p *kyverno.ClusterPolicy, pvList []*kyverno.ClusterPolicyViolation, nspvList []*kyverno.PolicyViolation) error { - newStatus := pc.calculateStatus(p.Name, pvList, nspvList) - if reflect.DeepEqual(newStatus, p.Status) { - // no update to status - return nil - } - // update status - newPolicy := p - newPolicy.Status = newStatus - _, err := pc.kyvernoClient.KyvernoV1().ClusterPolicies().UpdateStatus(newPolicy) - return err -} - -func (pc *PolicyController) calculateStatus(policyName string, pvList []*kyverno.ClusterPolicyViolation, nspvList []*kyverno.PolicyViolation) kyverno.PolicyStatus { - violationCount := len(pvList) + len(nspvList) - status := kyverno.PolicyStatus{ - ViolationCount: violationCount, - } - // get stats - stats := pc.statusAggregator.GetPolicyStats(policyName) - if !reflect.DeepEqual(stats, (PolicyStatInfo{})) { - status.RulesAppliedCount = stats.RulesAppliedCount - status.ResourcesBlockedCount = stats.ResourceBlocked - status.AvgExecutionTimeMutation = stats.MutationExecutionTime.String() - status.AvgExecutionTimeValidation = stats.ValidationExecutionTime.String() - status.AvgExecutionTimeGeneration = stats.GenerationExecutionTime.String() - // update rule stats - status.Rules = convertRules(stats.Rules) - } - return status -} - func (pc *PolicyController) getNamespacedPolicyViolationForPolicy(policy string) ([]*kyverno.PolicyViolation, error) { policySelector, err := buildPolicyLabel(policy) if err != nil { @@ -458,19 +402,3 @@ func (r RealPVControl) DeleteClusterPolicyViolation(name string) error { func (r RealPVControl) DeleteNamespacedPolicyViolation(ns, name string) error { return r.Client.KyvernoV1().PolicyViolations(ns).Delete(name, &metav1.DeleteOptions{}) } - -// convertRules converts the internal rule stats to one used in policy.stats struct -func convertRules(rules []RuleStatinfo) []kyverno.RuleStats { - var stats []kyverno.RuleStats - for _, r := range rules { - stat := kyverno.RuleStats{ - Name: r.RuleName, - ExecutionTime: r.ExecutionTime.String(), - AppliedCount: r.RuleAppliedCount, - ViolationCount: r.RulesFailedCount, - MutationCount: r.MutationCount, - } - stats = append(stats, stat) - } - return stats -} diff --git a/pkg/policy/existing.go b/pkg/policy/existing.go index 9165978ca4..97c09affac 100644 --- a/pkg/policy/existing.go +++ b/pkg/policy/existing.go @@ -39,7 +39,7 @@ func (pc *PolicyController) processExistingResources(policy kyverno.ClusterPolic // apply the policy on each glog.V(4).Infof("apply policy %s with resource version %s on resource %s/%s/%s with resource version %s", policy.Name, policy.ResourceVersion, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resource.GetResourceVersion()) - engineResponse := applyPolicy(policy, resource, pc.statusAggregator) + engineResponse := applyPolicy(policy, resource) // get engine response for mutation & validation independently engineResponses = append(engineResponses, engineResponse...) // post-processing, register the resource as processed diff --git a/pkg/policy/report.go b/pkg/policy/report.go index 206a6ebd61..3614da9b32 100644 --- a/pkg/policy/report.go +++ b/pkg/policy/report.go @@ -18,6 +18,10 @@ func (pc *PolicyController) cleanupAndReport(engineResponses []response.EngineRe pc.eventGen.Add(eventInfos...) // create policy violation pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses) + for i := range pvInfos { + pvInfos[i].FromSync = true + } + pc.pvGenerator.Add(pvInfos...) // cleanup existing violations if any // if there is any error in clean up, we dont re-queue the resource diff --git a/pkg/policy/status.go b/pkg/policy/status.go deleted file mode 100644 index 8d4beb4f02..0000000000 --- a/pkg/policy/status.go +++ /dev/null @@ -1,210 +0,0 @@ -package policy - -import ( - "sync" - "time" - - "github.com/golang/glog" - kyvernoclient "github.com/nirmata/kyverno/pkg/client/clientset/versioned" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/wait" -) - -//PolicyStatusAggregator stores information abt aggregation -type PolicyStatusAggregator struct { - // time since we start aggregating the stats - startTime time.Time - // channel to receive stats - ch chan PolicyStat - //TODO: lock based on key, possibly sync.Map ? - //sync RW for policyData - mux sync.RWMutex - // stores aggregated stats for policy - policyData map[string]PolicyStatInfo -} - -//NewPolicyStatAggregator returns a new policy status -func NewPolicyStatAggregator(client *kyvernoclient.Clientset) *PolicyStatusAggregator { - psa := PolicyStatusAggregator{ - startTime: time.Now(), - ch: make(chan PolicyStat), - policyData: map[string]PolicyStatInfo{}, - } - return &psa -} - -//Run begins aggregator -func (psa *PolicyStatusAggregator) Run(workers int, stopCh <-chan struct{}) { - defer utilruntime.HandleCrash() - glog.V(4).Info("Started aggregator for policy status stats") - defer func() { - glog.V(4).Info("Shutting down aggregator for policy status stats") - }() - for i := 0; i < workers; i++ { - go wait.Until(psa.process, time.Second, stopCh) - } - <-stopCh -} - -func (psa *PolicyStatusAggregator) process() { - // As mutation and validation are handled separately - // ideally we need to combine the execution time from both for a policy - // but its tricky to detect here the type of rules policy contains - // so we dont combine the results, but instead compute the execution time for - // mutation & validation rules separately - for r := range psa.ch { - glog.V(4).Infof("received policy stats %v", r) - psa.aggregate(r) - } -} - -func (psa *PolicyStatusAggregator) aggregate(ps PolicyStat) { - func() { - glog.V(4).Infof("write lock update policy %s", ps.PolicyName) - psa.mux.Lock() - }() - defer func() { - glog.V(4).Infof("write Unlock update policy %s", ps.PolicyName) - psa.mux.Unlock() - }() - - if len(ps.Stats.Rules) == 0 { - glog.V(4).Infof("ignoring stats, as no rule was applied") - return - } - - info, ok := psa.policyData[ps.PolicyName] - if !ok { - psa.policyData[ps.PolicyName] = ps.Stats - glog.V(4).Infof("added stats for policy %s", ps.PolicyName) - return - } - // aggregate policy information - info.RulesAppliedCount = info.RulesAppliedCount + ps.Stats.RulesAppliedCount - if ps.Stats.ResourceBlocked == 1 { - info.ResourceBlocked++ - } - var zeroDuration time.Duration - if info.MutationExecutionTime != zeroDuration { - info.MutationExecutionTime = (info.MutationExecutionTime + ps.Stats.MutationExecutionTime) / 2 - glog.V(4).Infof("updated avg mutation time %v", info.MutationExecutionTime) - } else { - info.MutationExecutionTime = ps.Stats.MutationExecutionTime - } - if info.ValidationExecutionTime != zeroDuration { - info.ValidationExecutionTime = (info.ValidationExecutionTime + ps.Stats.ValidationExecutionTime) / 2 - glog.V(4).Infof("updated avg validation time %v", info.ValidationExecutionTime) - } else { - info.ValidationExecutionTime = ps.Stats.ValidationExecutionTime - } - if info.GenerationExecutionTime != zeroDuration { - info.GenerationExecutionTime = (info.GenerationExecutionTime + ps.Stats.GenerationExecutionTime) / 2 - glog.V(4).Infof("updated avg generation time %v", info.GenerationExecutionTime) - } else { - info.GenerationExecutionTime = ps.Stats.GenerationExecutionTime - } - // aggregate rule details - info.Rules = aggregateRules(info.Rules, ps.Stats.Rules) - // update - psa.policyData[ps.PolicyName] = info - glog.V(4).Infof("updated stats for policy %s", ps.PolicyName) -} - -func aggregateRules(old []RuleStatinfo, update []RuleStatinfo) []RuleStatinfo { - var zeroDuration time.Duration - searchRule := func(list []RuleStatinfo, key string) *RuleStatinfo { - for _, v := range list { - if v.RuleName == key { - return &v - } - } - return nil - } - newRules := []RuleStatinfo{} - // search for new rules in old rules and update it - for _, updateR := range update { - if updateR.ExecutionTime != zeroDuration { - if rule := searchRule(old, updateR.RuleName); rule != nil { - rule.ExecutionTime = (rule.ExecutionTime + updateR.ExecutionTime) / 2 - rule.RuleAppliedCount = rule.RuleAppliedCount + updateR.RuleAppliedCount - rule.RulesFailedCount = rule.RulesFailedCount + updateR.RulesFailedCount - rule.MutationCount = rule.MutationCount + updateR.MutationCount - newRules = append(newRules, *rule) - } else { - newRules = append(newRules, updateR) - } - } - } - return newRules -} - -//GetPolicyStats returns the policy stats -func (psa *PolicyStatusAggregator) GetPolicyStats(policyName string) PolicyStatInfo { - func() { - glog.V(4).Infof("read lock update policy %s", policyName) - psa.mux.RLock() - }() - defer func() { - glog.V(4).Infof("read Unlock update policy %s", policyName) - psa.mux.RUnlock() - }() - glog.V(4).Infof("read stats for policy %s", policyName) - return psa.policyData[policyName] -} - -//RemovePolicyStats rmves policy stats records -func (psa *PolicyStatusAggregator) RemovePolicyStats(policyName string) { - func() { - glog.V(4).Infof("write lock update policy %s", policyName) - psa.mux.Lock() - }() - defer func() { - glog.V(4).Infof("write Unlock update policy %s", policyName) - psa.mux.Unlock() - }() - glog.V(4).Infof("removing stats for policy %s", policyName) - delete(psa.policyData, policyName) -} - -//PolicyStatusInterface provides methods to modify policyStatus -type PolicyStatusInterface interface { - SendStat(stat PolicyStat) - // UpdateViolationCount(policyName string, pvList []*kyverno.PolicyViolation) error -} - -//PolicyStat stored stats for policy -type PolicyStat struct { - PolicyName string - Stats PolicyStatInfo -} - -//PolicyStatInfo provides statistics for policy -type PolicyStatInfo struct { - MutationExecutionTime time.Duration - ValidationExecutionTime time.Duration - GenerationExecutionTime time.Duration - RulesAppliedCount int - ResourceBlocked int - Rules []RuleStatinfo -} - -//RuleStatinfo provides statistics for rule -type RuleStatinfo struct { - RuleName string - ExecutionTime time.Duration - RuleAppliedCount int - RulesFailedCount int - MutationCount int -} - -//SendStat sends the stat information for aggregation -func (psa *PolicyStatusAggregator) SendStat(stat PolicyStat) { - glog.V(4).Infof("sending policy stats: %v", stat) - // Send over channel - psa.ch <- stat -} - -//GetPolicyStatusAggregator returns interface to send policy status stats -func (pc *PolicyController) GetPolicyStatusAggregator() PolicyStatusInterface { - return pc.statusAggregator -} diff --git a/pkg/policystatus/keyToMutex.go b/pkg/policystatus/keyToMutex.go new file mode 100644 index 0000000000..801db2981a --- /dev/null +++ b/pkg/policystatus/keyToMutex.go @@ -0,0 +1,31 @@ +package policystatus + +import "sync" + +// keyToMutex allows status to be updated +//for different policies at the same time +//while ensuring the status for same policies +//are updated one at a time. +type keyToMutex struct { + mu sync.RWMutex + keyMu map[string]*sync.RWMutex +} + +func newKeyToMutex() *keyToMutex { + return &keyToMutex{ + mu: sync.RWMutex{}, + keyMu: make(map[string]*sync.RWMutex), + } +} + +func (k *keyToMutex) Get(key string) *sync.RWMutex { + k.mu.Lock() + defer k.mu.Unlock() + mutex := k.keyMu[key] + if mutex == nil { + mutex = &sync.RWMutex{} + k.keyMu[key] = mutex + } + + return mutex +} diff --git a/pkg/policystatus/main.go b/pkg/policystatus/main.go new file mode 100644 index 0000000000..3e633aaf82 --- /dev/null +++ b/pkg/policystatus/main.go @@ -0,0 +1,146 @@ +package policystatus + +import ( + "encoding/json" + "sync" + "time" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/nirmata/kyverno/pkg/client/clientset/versioned" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" +) + +// Policy status implementation works in the following way, +//Currently policy status maintains a cache of the status of +//each policy. +//Every x unit of time the status of policy is updated using +//the data from the cache. +//The sync exposes a listener which accepts a statusUpdater +//interface which dictates how the status should be updated. +//The status is updated by a worker that receives the interface +//on a channel. +//The worker then updates the current status using the methods +//exposed by the interface. +//Current implementation is designed to be threadsafe with optimised +//locking for each policy. + +// statusUpdater defines a type to have a method which +//updates the given status +type statusUpdater interface { + PolicyName() string + UpdateStatus(status v1.PolicyStatus) v1.PolicyStatus +} + +type policyStore interface { + Get(policyName string) (*v1.ClusterPolicy, error) +} + +type Listener chan statusUpdater + +func (l Listener) Send(s statusUpdater) { + l <- s +} + +// Sync is the object which is used to initialize +//the policyStatus sync, can be considered the parent object +//since it contains access to all the persistant data present +//in this package. +type Sync struct { + cache *cache + Listener Listener + client *versioned.Clientset + policyStore policyStore +} + +type cache struct { + dataMu sync.RWMutex + data map[string]v1.PolicyStatus + keyToMutex *keyToMutex +} + +func NewSync(c *versioned.Clientset, p policyStore) *Sync { + return &Sync{ + cache: &cache{ + dataMu: sync.RWMutex{}, + data: make(map[string]v1.PolicyStatus), + keyToMutex: newKeyToMutex(), + }, + client: c, + policyStore: p, + Listener: make(chan statusUpdater, 20), + } +} + +func (s *Sync) Run(workers int, stopCh <-chan struct{}) { + for i := 0; i < workers; i++ { + go s.updateStatusCache(stopCh) + } + + wait.Until(s.updatePolicyStatus, 2*time.Second, stopCh) + <-stopCh +} + +// updateStatusCache is a worker which updates the current status +//using the statusUpdater interface +func (s *Sync) updateStatusCache(stopCh <-chan struct{}) { + for { + select { + case statusUpdater := <-s.Listener: + s.cache.keyToMutex.Get(statusUpdater.PolicyName()).Lock() + + s.cache.dataMu.RLock() + status, exist := s.cache.data[statusUpdater.PolicyName()] + s.cache.dataMu.RUnlock() + if !exist { + policy, _ := s.policyStore.Get(statusUpdater.PolicyName()) + if policy != nil { + status = policy.Status + } + } + + updatedStatus := statusUpdater.UpdateStatus(status) + + s.cache.dataMu.Lock() + s.cache.data[statusUpdater.PolicyName()] = updatedStatus + s.cache.dataMu.Unlock() + + s.cache.keyToMutex.Get(statusUpdater.PolicyName()).Unlock() + oldStatus, _ := json.Marshal(status) + newStatus, _ := json.Marshal(updatedStatus) + + glog.V(4).Infof("\nupdated status of policy - %v\noldStatus:\n%v\nnewStatus:\n%v\n", statusUpdater.PolicyName(), string(oldStatus), string(newStatus)) + case <-stopCh: + return + } + } +} + +// updatePolicyStatus updates the status in the policy resource definition +//from the status cache, syncing them +func (s *Sync) updatePolicyStatus() { + s.cache.dataMu.Lock() + var nameToStatus = make(map[string]v1.PolicyStatus, len(s.cache.data)) + for k, v := range s.cache.data { + nameToStatus[k] = v + } + s.cache.dataMu.Unlock() + + for policyName, status := range nameToStatus { + policy, err := s.policyStore.Get(policyName) + if err != nil { + continue + } + policy.Status = status + _, err = s.client.KyvernoV1().ClusterPolicies().UpdateStatus(policy) + if err != nil { + s.cache.dataMu.Lock() + delete(s.cache.data, policyName) + s.cache.dataMu.Unlock() + glog.V(4).Info(err) + } + } +} diff --git a/pkg/policystatus/status_test.go b/pkg/policystatus/status_test.go new file mode 100644 index 0000000000..310852cf2f --- /dev/null +++ b/pkg/policystatus/status_test.go @@ -0,0 +1,50 @@ +package policystatus + +import ( + "encoding/json" + "testing" + "time" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" +) + +type dummyStore struct { +} + +func (d dummyStore) Get(policyName string) (*v1.ClusterPolicy, error) { + return &v1.ClusterPolicy{}, nil +} + +type dummyStatusUpdater struct { +} + +func (d dummyStatusUpdater) UpdateStatus(status v1.PolicyStatus) v1.PolicyStatus { + status.RulesAppliedCount++ + return status +} + +func (d dummyStatusUpdater) PolicyName() string { + return "policy1" +} + +func TestKeyToMutex(t *testing.T) { + expectedCache := `{"policy1":{"averageExecutionTime":"","rulesAppliedCount":100}}` + + stopCh := make(chan struct{}) + s := NewSync(nil, dummyStore{}) + for i := 0; i < 100; i++ { + go s.updateStatusCache(stopCh) + } + + for i := 0; i < 100; i++ { + go s.Listener.Send(dummyStatusUpdater{}) + } + + <-time.After(time.Second * 3) + stopCh <- struct{}{} + + cacheRaw, _ := json.Marshal(s.cache.data) + if string(cacheRaw) != expectedCache { + t.Errorf("\nTestcase Failed\nGot:\n%v\nExpected:\n%v\n", string(cacheRaw), expectedCache) + } +} diff --git a/pkg/policystore/policystore.go b/pkg/policystore/policystore.go index 3d23b106e4..f8a8e30874 100644 --- a/pkg/policystore/policystore.go +++ b/pkg/policystore/policystore.go @@ -96,6 +96,10 @@ func (ps *PolicyStore) ListAll() ([]kyverno.ClusterPolicy, error) { return policies, nil } +func (ps *PolicyStore) Get(policyName string) (*kyverno.ClusterPolicy, error) { + return ps.pLister.Get(policyName) +} + //UnRegister Remove policy information func (ps *PolicyStore) UnRegister(policy kyverno.ClusterPolicy) error { ps.mu.Lock() diff --git a/pkg/policyviolation/clusterpv.go b/pkg/policyviolation/clusterpv.go index 8b974594ae..c9336f8059 100644 --- a/pkg/policyviolation/clusterpv.go +++ b/pkg/policyviolation/clusterpv.go @@ -9,6 +9,7 @@ import ( kyvernov1 "github.com/nirmata/kyverno/pkg/client/clientset/versioned/typed/kyverno/v1" kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1" client "github.com/nirmata/kyverno/pkg/dclient" + "github.com/nirmata/kyverno/pkg/policystatus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,16 +21,20 @@ type clusterPV struct { cpvLister kyvernolister.ClusterPolicyViolationLister // policy violation interface kyvernoInterface kyvernov1.KyvernoV1Interface + // update policy stats with violationCount + policyStatusListener policystatus.Listener } func newClusterPV(dclient *client.Client, cpvLister kyvernolister.ClusterPolicyViolationLister, kyvernoInterface kyvernov1.KyvernoV1Interface, + policyStatus policystatus.Listener, ) *clusterPV { cpv := clusterPV{ - dclient: dclient, - cpvLister: cpvLister, - kyvernoInterface: kyvernoInterface, + dclient: dclient, + cpvLister: cpvLister, + kyvernoInterface: kyvernoInterface, + policyStatusListener: policyStatus, } return &cpv } @@ -93,6 +98,11 @@ func (cpv *clusterPV) createPV(newPv *kyverno.ClusterPolicyViolation) error { glog.V(4).Infof("failed to create Cluster Policy Violation: %v", err) return err } + + if newPv.Annotations["fromSync"] != "true" { + cpv.policyStatusListener.Send(violationCount{policyName: newPv.Spec.Policy, violatedRules: newPv.Spec.ViolatedRules}) + } + glog.Infof("policy violation created for resource %v", newPv.Spec.ResourceSpec) return nil } @@ -115,5 +125,8 @@ func (cpv *clusterPV) updatePV(newPv, oldPv *kyverno.ClusterPolicyViolation) err } glog.Infof("cluster policy violation updated for resource %v", newPv.Spec.ResourceSpec) + if newPv.Annotations["fromSync"] != "true" { + cpv.policyStatusListener.Send(violationCount{policyName: newPv.Spec.Policy, violatedRules: newPv.Spec.ViolatedRules}) + } return nil } diff --git a/pkg/policyviolation/common.go b/pkg/policyviolation/common.go index dfb4a704ac..6f077b25b8 100644 --- a/pkg/policyviolation/common.go +++ b/pkg/policyviolation/common.go @@ -7,6 +7,7 @@ import ( backoff "github.com/cenkalti/backoff" "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" client "github.com/nirmata/kyverno/pkg/dclient" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -70,3 +71,27 @@ func converLabelToSelector(labelMap map[string]string) (labels.Selector, error) return policyViolationSelector, nil } + +type violationCount struct { + policyName string + violatedRules []v1.ViolatedRule +} + +func (vc violationCount) PolicyName() string { + return vc.policyName +} + +func (vc violationCount) UpdateStatus(status kyverno.PolicyStatus) kyverno.PolicyStatus { + + var ruleNameToViolations = make(map[string]int) + for _, rule := range vc.violatedRules { + ruleNameToViolations[rule.Name]++ + } + + for i := range status.Rules { + status.ViolationCount += ruleNameToViolations[status.Rules[i].Name] + status.Rules[i].ViolationCount += ruleNameToViolations[status.Rules[i].Name] + } + + return status +} diff --git a/pkg/policyviolation/generator.go b/pkg/policyviolation/generator.go index 7d820f980b..db5ca63fcd 100644 --- a/pkg/policyviolation/generator.go +++ b/pkg/policyviolation/generator.go @@ -14,6 +14,7 @@ import ( kyvernov1 "github.com/nirmata/kyverno/pkg/client/clientset/versioned/typed/kyverno/v1" kyvernoinformer "github.com/nirmata/kyverno/pkg/client/informers/externalversions/kyverno/v1" kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1" + "github.com/nirmata/kyverno/pkg/policystatus" dclient "github.com/nirmata/kyverno/pkg/dclient" unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -37,9 +38,10 @@ type Generator struct { // returns true if the cluster policy store has been synced at least once pvSynced cache.InformerSynced // returns true if the namespaced cluster policy store has been synced at at least once - nspvSynced cache.InformerSynced - queue workqueue.RateLimitingInterface - dataStore *dataStore + nspvSynced cache.InformerSynced + queue workqueue.RateLimitingInterface + dataStore *dataStore + policyStatusListener policystatus.Listener } //NewDataStore returns an instance of data store @@ -79,6 +81,7 @@ type Info struct { PolicyName string Resource unstructured.Unstructured Rules []kyverno.ViolatedRule + FromSync bool } func (i Info) toKey() string { @@ -103,16 +106,18 @@ type GeneratorInterface interface { func NewPVGenerator(client *kyvernoclient.Clientset, dclient *dclient.Client, pvInformer kyvernoinformer.ClusterPolicyViolationInformer, - nspvInformer kyvernoinformer.PolicyViolationInformer) *Generator { + nspvInformer kyvernoinformer.PolicyViolationInformer, + policyStatus policystatus.Listener) *Generator { gen := Generator{ - kyvernoInterface: client.KyvernoV1(), - dclient: dclient, - cpvLister: pvInformer.Lister(), - pvSynced: pvInformer.Informer().HasSynced, - nspvLister: nspvInformer.Lister(), - nspvSynced: nspvInformer.Informer().HasSynced, - queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), workQueueName), - dataStore: newDataStore(), + kyvernoInterface: client.KyvernoV1(), + dclient: dclient, + cpvLister: pvInformer.Lister(), + pvSynced: pvInformer.Informer().HasSynced, + nspvLister: nspvInformer.Lister(), + nspvSynced: nspvInformer.Informer().HasSynced, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), workQueueName), + dataStore: newDataStore(), + policyStatusListener: policyStatus, } return &gen } @@ -219,15 +224,21 @@ func (gen *Generator) syncHandler(info Info) error { builder := newPvBuilder() if info.Resource.GetNamespace() == "" { // cluster scope resource generate a clusterpolicy violation - handler = newClusterPV(gen.dclient, gen.cpvLister, gen.kyvernoInterface) + handler = newClusterPV(gen.dclient, gen.cpvLister, gen.kyvernoInterface, gen.policyStatusListener) } else { // namespaced resources generated a namespaced policy violation in the namespace of the resource - handler = newNamespacedPV(gen.dclient, gen.nspvLister, gen.kyvernoInterface) + handler = newNamespacedPV(gen.dclient, gen.nspvLister, gen.kyvernoInterface, gen.policyStatusListener) } failure := false pv := builder.generate(info) + if info.FromSync { + pv.Annotations = map[string]string{ + "fromSync": "true", + } + } + // Create Policy Violations glog.V(3).Infof("Creating policy violation: %s", info.toKey()) if err := handler.create(pv); err != nil { diff --git a/pkg/policyviolation/namespacedpv.go b/pkg/policyviolation/namespacedpv.go index 1967a0ba3f..ff32748d0b 100644 --- a/pkg/policyviolation/namespacedpv.go +++ b/pkg/policyviolation/namespacedpv.go @@ -9,6 +9,7 @@ import ( kyvernov1 "github.com/nirmata/kyverno/pkg/client/clientset/versioned/typed/kyverno/v1" kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1" client "github.com/nirmata/kyverno/pkg/dclient" + "github.com/nirmata/kyverno/pkg/policystatus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,16 +21,20 @@ type namespacedPV struct { nspvLister kyvernolister.PolicyViolationLister // policy violation interface kyvernoInterface kyvernov1.KyvernoV1Interface + // update policy status with violationCount + policyStatusListener policystatus.Listener } func newNamespacedPV(dclient *client.Client, nspvLister kyvernolister.PolicyViolationLister, kyvernoInterface kyvernov1.KyvernoV1Interface, + policyStatus policystatus.Listener, ) *namespacedPV { nspv := namespacedPV{ - dclient: dclient, - nspvLister: nspvLister, - kyvernoInterface: kyvernoInterface, + dclient: dclient, + nspvLister: nspvLister, + kyvernoInterface: kyvernoInterface, + policyStatusListener: policyStatus, } return &nspv } @@ -92,6 +97,10 @@ func (nspv *namespacedPV) createPV(newPv *kyverno.PolicyViolation) error { glog.V(4).Infof("failed to create Cluster Policy Violation: %v", err) return err } + + if newPv.Annotations["fromSync"] != "true" { + nspv.policyStatusListener.Send(violationCount{policyName: newPv.Spec.Policy, violatedRules: newPv.Spec.ViolatedRules}) + } glog.Infof("policy violation created for resource %v", newPv.Spec.ResourceSpec) return nil } @@ -112,6 +121,9 @@ func (nspv *namespacedPV) updatePV(newPv, oldPv *kyverno.PolicyViolation) error return fmt.Errorf("failed to update namespaced policy violation: %v", err) } + if newPv.Annotations["fromSync"] != "true" { + nspv.policyStatusListener.Send(violationCount{policyName: newPv.Spec.Policy, violatedRules: newPv.Spec.ViolatedRules}) + } glog.Infof("namespaced policy violation updated for resource %v", newPv.Spec.ResourceSpec) return nil } diff --git a/pkg/policyviolation/policyStatus_test.go b/pkg/policyviolation/policyStatus_test.go new file mode 100644 index 0000000000..8db26ae4a6 --- /dev/null +++ b/pkg/policyviolation/policyStatus_test.go @@ -0,0 +1,74 @@ +package policyviolation + +import ( + "encoding/json" + "reflect" + "testing" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" +) + +func Test_Stats(t *testing.T) { + testCase := struct { + violationCountStats []struct { + policyName string + violatedRules []v1.ViolatedRule + } + expectedOutput []byte + existingCache map[string]v1.PolicyStatus + }{ + existingCache: map[string]v1.PolicyStatus{ + "policy1": { + Rules: []v1.RuleStats{ + { + Name: "rule4", + }, + }, + }, + "policy2": { + Rules: []v1.RuleStats{ + { + Name: "rule4", + }, + }, + }, + }, + expectedOutput: []byte(`{"policy1":{"averageExecutionTime":"","violationCount":1,"ruleStatus":[{"ruleName":"rule4","violationCount":1}]},"policy2":{"averageExecutionTime":"","violationCount":1,"ruleStatus":[{"ruleName":"rule4","violationCount":1}]}}`), + violationCountStats: []struct { + policyName string + violatedRules []v1.ViolatedRule + }{ + { + policyName: "policy1", + violatedRules: []v1.ViolatedRule{ + { + Name: "rule4", + }, + }, + }, + { + policyName: "policy2", + violatedRules: []v1.ViolatedRule{ + { + Name: "rule4", + }, + }, + }, + }, + } + + policyNameToStatus := testCase.existingCache + + for _, violationCountStat := range testCase.violationCountStats { + receiver := &violationCount{ + policyName: violationCountStat.policyName, + violatedRules: violationCountStat.violatedRules, + } + policyNameToStatus[receiver.PolicyName()] = receiver.UpdateStatus(policyNameToStatus[receiver.PolicyName()]) + } + + output, _ := json.Marshal(policyNameToStatus) + if !reflect.DeepEqual(output, testCase.expectedOutput) { + t.Errorf("\n\nTestcase has failed\nExpected:\n%v\nGot:\n%v\n\n", string(testCase.expectedOutput), string(output)) + } +} diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index 2dddf71e1c..956c568cac 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -1,8 +1,13 @@ package webhooks import ( + "reflect" + "sort" + "time" + "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" "github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/engine/context" "github.com/nirmata/kyverno/pkg/engine/response" @@ -61,6 +66,9 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic if len(engineResponse.PolicyResponse.Rules) > 0 { // some generate rules do apply to the resource engineResponses = append(engineResponses, engineResponse) + ws.statusListener.Send(generateStats{ + resp: engineResponse, + }) } } // Adds Generate Request to a channel(queue size 1000) to generators @@ -102,3 +110,73 @@ func transform(userRequestInfo kyverno.RequestInfo, er response.EngineResponse) } return gr } + +type generateStats struct { + resp response.EngineResponse +} + +func (gs generateStats) PolicyName() string { + return gs.resp.PolicyResponse.Policy +} + +func (gs generateStats) UpdateStatus(status kyverno.PolicyStatus) kyverno.PolicyStatus { + if reflect.DeepEqual(response.EngineResponse{}, gs.resp) { + return status + } + + var nameToRule = make(map[string]v1.RuleStats) + for _, rule := range status.Rules { + nameToRule[rule.Name] = rule + } + + for _, rule := range gs.resp.PolicyResponse.Rules { + ruleStat := nameToRule[rule.Name] + ruleStat.Name = rule.Name + + averageOver := int64(ruleStat.AppliedCount + ruleStat.FailedCount) + ruleStat.ExecutionTime = updateAverageTime( + rule.ProcessingTime, + ruleStat.ExecutionTime, + averageOver).String() + + if rule.Success { + status.RulesAppliedCount++ + ruleStat.AppliedCount++ + } else { + status.RulesFailedCount++ + ruleStat.FailedCount++ + } + + nameToRule[rule.Name] = ruleStat + } + + var policyAverageExecutionTime time.Duration + var ruleStats = make([]v1.RuleStats, 0, len(nameToRule)) + for _, ruleStat := range nameToRule { + executionTime, err := time.ParseDuration(ruleStat.ExecutionTime) + if err == nil { + policyAverageExecutionTime += executionTime + } + ruleStats = append(ruleStats, ruleStat) + } + + sort.Slice(ruleStats, func(i, j int) bool { + return ruleStats[i].Name < ruleStats[j].Name + }) + + status.AvgExecutionTime = policyAverageExecutionTime.String() + status.Rules = ruleStats + + return status +} + +func updateAverageTime(newTime time.Duration, oldAverageTimeString string, averageOver int64) time.Duration { + if averageOver == 0 { + return newTime + } + oldAverageExecutionTime, _ := time.ParseDuration(oldAverageTimeString) + numerator := (oldAverageExecutionTime.Nanoseconds() * averageOver) + newTime.Nanoseconds() + denominator := averageOver + 1 + newAverageTimeInNanoSeconds := numerator / denominator + return time.Duration(newAverageTimeInNanoSeconds) * time.Nanosecond +} diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index 650fce8374..2723b00110 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -1,17 +1,18 @@ package webhooks import ( + "reflect" + "sort" "time" "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" "github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/engine/context" "github.com/nirmata/kyverno/pkg/engine/response" engineutils "github.com/nirmata/kyverno/pkg/engine/utils" - policyctr "github.com/nirmata/kyverno/pkg/policy" "github.com/nirmata/kyverno/pkg/policyviolation" - "github.com/nirmata/kyverno/pkg/utils" v1beta1 "k8s.io/api/admission/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -23,40 +24,6 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) var patches [][]byte - var policyStats []policyctr.PolicyStat - - // gather stats from the engine response - gatherStat := func(policyName string, policyResponse response.PolicyResponse) { - ps := policyctr.PolicyStat{} - ps.PolicyName = policyName - ps.Stats.MutationExecutionTime = policyResponse.ProcessingTime - ps.Stats.RulesAppliedCount = policyResponse.RulesAppliedCount - // capture rule level stats - for _, rule := range policyResponse.Rules { - rs := policyctr.RuleStatinfo{} - rs.RuleName = rule.Name - rs.ExecutionTime = rule.RuleStats.ProcessingTime - if rule.Success { - rs.RuleAppliedCount++ - } else { - rs.RulesFailedCount++ - } - if rule.Patches != nil { - rs.MutationCount++ - } - ps.Stats.Rules = append(ps.Stats.Rules, rs) - } - policyStats = append(policyStats, ps) - } - // send stats for aggregation - sendStat := func(blocked bool) { - for _, stat := range policyStats { - stat.Stats.ResourceBlocked = utils.Btoi(blocked) - //SEND - ws.policyStatus.SendStat(stat) - } - } - var engineResponses []response.EngineResponse userRequestInfo := kyverno.RequestInfo{ @@ -91,12 +58,10 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou for _, policy := range policies { glog.V(2).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation) - policyContext.Policy = policy engineResponse := engine.Mutate(policyContext) engineResponses = append(engineResponses, engineResponse) - // Gather policy application statistics - gatherStat(policy.Name, engineResponse.PolicyResponse) + ws.statusListener.Send(mutateStats{resp: engineResponse}) if !engineResponse.IsSuccesful() { glog.V(4).Infof("Failed to apply policy %s on resource %s/%s\n", policy.Name, resource.GetNamespace(), resource.GetName()) continue @@ -131,8 +96,6 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou events := generateEvents(engineResponses, false, (request.Operation == v1beta1.Update)) ws.eventGen.Add(events...) - sendStat(false) - // debug info func() { if len(patches) != 0 { @@ -152,3 +115,64 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou // patches holds all the successful patches, if no patch is created, it returns nil return engineutils.JoinPatches(patches) } + +type mutateStats struct { + resp response.EngineResponse +} + +func (ms mutateStats) PolicyName() string { + return ms.resp.PolicyResponse.Policy +} + +func (ms mutateStats) UpdateStatus(status kyverno.PolicyStatus) kyverno.PolicyStatus { + if reflect.DeepEqual(response.EngineResponse{}, ms.resp) { + return status + } + + var nameToRule = make(map[string]v1.RuleStats) + for _, rule := range status.Rules { + nameToRule[rule.Name] = rule + } + + for _, rule := range ms.resp.PolicyResponse.Rules { + ruleStat := nameToRule[rule.Name] + ruleStat.Name = rule.Name + + averageOver := int64(ruleStat.AppliedCount + ruleStat.FailedCount) + ruleStat.ExecutionTime = updateAverageTime( + rule.ProcessingTime, + ruleStat.ExecutionTime, + averageOver).String() + + if rule.Success { + status.RulesAppliedCount++ + status.ResourcesMutatedCount++ + ruleStat.AppliedCount++ + ruleStat.ResourcesMutatedCount++ + } else { + status.RulesFailedCount++ + ruleStat.FailedCount++ + } + + nameToRule[rule.Name] = ruleStat + } + + var policyAverageExecutionTime time.Duration + var ruleStats = make([]v1.RuleStats, 0, len(nameToRule)) + for _, ruleStat := range nameToRule { + executionTime, err := time.ParseDuration(ruleStat.ExecutionTime) + if err == nil { + policyAverageExecutionTime += executionTime + } + ruleStats = append(ruleStats, ruleStat) + } + + sort.Slice(ruleStats, func(i, j int) bool { + return ruleStats[i].Name < ruleStats[j].Name + }) + + status.AvgExecutionTime = policyAverageExecutionTime.String() + status.Rules = ruleStats + + return status +} diff --git a/pkg/webhooks/policyStatus_test.go b/pkg/webhooks/policyStatus_test.go new file mode 100644 index 0000000000..6c71fc6222 --- /dev/null +++ b/pkg/webhooks/policyStatus_test.go @@ -0,0 +1,211 @@ +package webhooks + +import ( + "encoding/json" + "reflect" + "testing" + "time" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/nirmata/kyverno/pkg/engine/response" +) + +func Test_GenerateStats(t *testing.T) { + testCase := struct { + 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{ + { + PolicyResponse: response.PolicyResponse{ + Policy: "policy1", + Rules: []response.RuleResponse{ + { + Name: "rule5", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 243, + }, + }, + { + Name: "rule6", + Success: false, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 251, + }, + }, + }, + }, + }, + { + PolicyResponse: response.PolicyResponse{ + Policy: "policy2", + Rules: []response.RuleResponse{ + { + Name: "rule5", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 222, + }, + }, + { + Name: "rule6", + Success: false, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 211, + }, + }, + }, + }, + }, + }, + } + + policyNameToStatus := map[string]v1.PolicyStatus{} + + for _, generateStat := range testCase.generateStats { + receiver := generateStats{ + resp: generateStat, + } + policyNameToStatus[receiver.PolicyName()] = receiver.UpdateStatus(policyNameToStatus[receiver.PolicyName()]) + } + + output, _ := json.Marshal(policyNameToStatus) + if !reflect.DeepEqual(output, testCase.expectedOutput) { + t.Errorf("\n\nTestcase has failed\nExpected:\n%v\nGot:\n%v\n\n", string(testCase.expectedOutput), string(output)) + } +} + +func Test_MutateStats(t *testing.T) { + testCase := struct { + 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{ + { + PolicyResponse: response.PolicyResponse{ + Policy: "policy1", + Rules: []response.RuleResponse{ + { + Name: "rule1", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 243, + }, + }, + { + Name: "rule2", + Success: false, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 251, + }, + }, + }, + }, + }, + { + PolicyResponse: response.PolicyResponse{ + Policy: "policy2", + Rules: []response.RuleResponse{ + { + Name: "rule1", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 222, + }, + }, + { + Name: "rule2", + Success: false, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 211, + }, + }, + }, + }, + }, + }, + } + + policyNameToStatus := map[string]v1.PolicyStatus{} + for _, mutateStat := range testCase.mutateStats { + receiver := mutateStats{ + resp: mutateStat, + } + policyNameToStatus[receiver.PolicyName()] = receiver.UpdateStatus(policyNameToStatus[receiver.PolicyName()]) + } + + output, _ := json.Marshal(policyNameToStatus) + if !reflect.DeepEqual(output, testCase.expectedOutput) { + t.Errorf("\n\nTestcase has failed\nExpected:\n%v\nGot:\n%v\n\n", string(testCase.expectedOutput), string(output)) + } +} + +func Test_ValidateStats(t *testing.T) { + testCase := struct { + 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{ + { + PolicyResponse: response.PolicyResponse{ + Policy: "policy1", + ValidationFailureAction: "enforce", + Rules: []response.RuleResponse{ + { + Name: "rule3", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 243, + }, + }, + { + Name: "rule4", + Success: false, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 251, + }, + }, + }, + }, + }, + { + PolicyResponse: response.PolicyResponse{ + Policy: "policy2", + Rules: []response.RuleResponse{ + { + Name: "rule3", + Success: true, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 222, + }, + }, + { + Name: "rule4", + Success: false, + RuleStats: response.RuleStats{ + ProcessingTime: time.Nanosecond * 211, + }, + }, + }, + }, + }, + }, + } + + policyNameToStatus := map[string]v1.PolicyStatus{} + for _, validateStat := range testCase.validateStats { + receiver := validateStats{ + resp: validateStat, + } + policyNameToStatus[receiver.PolicyName()] = receiver.UpdateStatus(policyNameToStatus[receiver.PolicyName()]) + } + + output, _ := json.Marshal(policyNameToStatus) + if !reflect.DeepEqual(output, testCase.expectedOutput) { + t.Errorf("\n\nTestcase has failed\nExpected:\n%v\nGot:\n%v\n\n", string(testCase.expectedOutput), string(output)) + } +} diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index d401cdc449..b0ca988449 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -18,7 +18,7 @@ import ( "github.com/nirmata/kyverno/pkg/config" client "github.com/nirmata/kyverno/pkg/dclient" "github.com/nirmata/kyverno/pkg/event" - "github.com/nirmata/kyverno/pkg/policy" + "github.com/nirmata/kyverno/pkg/policystatus" "github.com/nirmata/kyverno/pkg/policystore" "github.com/nirmata/kyverno/pkg/policyviolation" tlsutils "github.com/nirmata/kyverno/pkg/tls" @@ -55,7 +55,7 @@ type WebhookServer struct { // webhook registration client webhookRegistrationClient *webhookconfig.WebhookRegistrationClient // API to send policy stats for aggregation - policyStatus policy.PolicyStatusInterface + statusListener policystatus.Listener // helpers to validate against current loaded configuration configHandler config.Interface // channel for cleanup notification @@ -82,7 +82,7 @@ func NewWebhookServer( crbInformer rbacinformer.ClusterRoleBindingInformer, eventGen event.Interface, webhookRegistrationClient *webhookconfig.WebhookRegistrationClient, - policyStatus policy.PolicyStatusInterface, + statusSync policystatus.Listener, configHandler config.Interface, pMetaStore policystore.LookupInterface, pvGenerator policyviolation.GeneratorInterface, @@ -112,7 +112,7 @@ func NewWebhookServer( crbSynced: crbInformer.Informer().HasSynced, eventGen: eventGen, webhookRegistrationClient: webhookRegistrationClient, - policyStatus: policyStatus, + statusListener: statusSync, configHandler: configHandler, cleanUp: cleanUp, lastReqTime: resourceWebhookWatcher.LastReqTime, diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index cf30c61f17..54a7fbdf3e 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -2,16 +2,16 @@ package webhooks import ( "reflect" + "sort" "time" "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" "github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/engine/context" "github.com/nirmata/kyverno/pkg/engine/response" - policyctr "github.com/nirmata/kyverno/pkg/policy" "github.com/nirmata/kyverno/pkg/policyviolation" - "github.com/nirmata/kyverno/pkg/utils" v1beta1 "k8s.io/api/admission/v1beta1" ) @@ -22,36 +22,7 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol 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 policyStats []policyctr.PolicyStat evalTime := time.Now() - // gather stats from the engine response - gatherStat := func(policyName string, policyResponse response.PolicyResponse) { - ps := policyctr.PolicyStat{} - ps.PolicyName = policyName - ps.Stats.ValidationExecutionTime = policyResponse.ProcessingTime - ps.Stats.RulesAppliedCount = policyResponse.RulesAppliedCount - // capture rule level stats - for _, rule := range policyResponse.Rules { - rs := policyctr.RuleStatinfo{} - rs.RuleName = rule.Name - rs.ExecutionTime = rule.RuleStats.ProcessingTime - if rule.Success { - rs.RuleAppliedCount++ - } else { - rs.RulesFailedCount++ - } - ps.Stats.Rules = append(ps.Stats.Rules, rs) - } - policyStats = append(policyStats, ps) - } - // send stats for aggregation - sendStat := func(blocked bool) { - for _, stat := range policyStats { - stat.Stats.ResourceBlocked = utils.Btoi(blocked) - //SEND - ws.policyStatus.SendStat(stat) - } - } // Get new and old resource newR, oldR, err := extractResources(patchedResource, request) @@ -100,8 +71,9 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol continue } engineResponses = append(engineResponses, engineResponse) - // Gather policy application statistics - gatherStat(policy.Name, engineResponse.PolicyResponse) + ws.statusListener.Send(validateStats{ + resp: engineResponse, + }) if !engineResponse.IsSuccesful() { glog.V(4).Infof("Failed to apply policy %s on resource %s/%s\n", policy.Name, newR.GetNamespace(), newR.GetName()) continue @@ -129,9 +101,6 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol ws.eventGen.Add(events...) if blocked { glog.V(4).Infof("resource %s/%s/%s is blocked\n", newR.GetKind(), newR.GetNamespace(), newR.GetName()) - sendStat(true) - // EVENTS - // - event on the Policy return false, getEnforceFailureErrorMsg(engineResponses) } @@ -139,8 +108,70 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol // violations are created with resource on "audit" pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses) ws.pvGenerator.Add(pvInfos...) - sendStat(false) // report time end glog.V(4).Infof("report: %v %s/%s/%s", time.Since(reportTime), request.Kind, request.Namespace, request.Name) return true, "" } + +type validateStats struct { + resp response.EngineResponse +} + +func (vs validateStats) PolicyName() string { + return vs.resp.PolicyResponse.Policy +} + +func (vs validateStats) UpdateStatus(status kyverno.PolicyStatus) kyverno.PolicyStatus { + if reflect.DeepEqual(response.EngineResponse{}, vs.resp) { + return status + } + + var nameToRule = make(map[string]v1.RuleStats) + for _, rule := range status.Rules { + nameToRule[rule.Name] = rule + } + + for _, rule := range vs.resp.PolicyResponse.Rules { + ruleStat := nameToRule[rule.Name] + ruleStat.Name = rule.Name + + averageOver := int64(ruleStat.AppliedCount + ruleStat.FailedCount) + ruleStat.ExecutionTime = updateAverageTime( + rule.ProcessingTime, + ruleStat.ExecutionTime, + averageOver).String() + + if rule.Success { + status.RulesAppliedCount++ + ruleStat.AppliedCount++ + } else { + status.RulesFailedCount++ + ruleStat.FailedCount++ + if vs.resp.PolicyResponse.ValidationFailureAction == "enforce" { + status.ResourcesBlockedCount++ + ruleStat.ResourcesBlockedCount++ + } + } + + nameToRule[rule.Name] = ruleStat + } + + var policyAverageExecutionTime time.Duration + var ruleStats = make([]v1.RuleStats, 0, len(nameToRule)) + for _, ruleStat := range nameToRule { + executionTime, err := time.ParseDuration(ruleStat.ExecutionTime) + if err == nil { + policyAverageExecutionTime += executionTime + } + ruleStats = append(ruleStats, ruleStat) + } + + sort.Slice(ruleStats, func(i, j int) bool { + return ruleStats[i].Name < ruleStats[j].Name + }) + + status.AvgExecutionTime = policyAverageExecutionTime.String() + status.Rules = ruleStats + + return status +}