diff --git a/definitions/install.yaml b/definitions/install.yaml index 8ed3b6cf67..f076a730c7 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -22,6 +22,12 @@ spec: required: - rules properties: + # default values to be handled by user + validationFailureAction: + type: string + enum: + - block + - report rules: type: array items: diff --git a/definitions/install_debug.yaml b/definitions/install_debug.yaml index 313c348b65..732b3cd5f7 100644 --- a/definitions/install_debug.yaml +++ b/definitions/install_debug.yaml @@ -22,6 +22,12 @@ spec: required: - rules properties: + # default values to be handled by user + validationFailureAction: + type: string + enum: + - block + - report rules: type: array items: diff --git a/examples/cli/nginx.yaml b/examples/cli/nginx.yaml index db1db6b186..cc6d30eead 100644 --- a/examples/cli/nginx.yaml +++ b/examples/cli/nginx.yaml @@ -20,4 +20,4 @@ spec: image: nginx:1.7.9 imagePullPolicy: Always ports: - - containerPort: 80 + - containerPort: 80 \ No newline at end of file diff --git a/examples/cli/policy_deployment.yaml b/examples/cli/policy_deployment.yaml index 5957912c55..064b68bb4d 100644 --- a/examples/cli/policy_deployment.yaml +++ b/examples/cli/policy_deployment.yaml @@ -35,4 +35,3 @@ spec : containers: - (image): "nginx*" imagePullPolicy: Always - diff --git a/examples/demo/6_qos/policy_qos.yaml b/examples/demo/6_qos/policy_qos.yaml index ca005e05d2..42348f1baf 100644 --- a/examples/demo/6_qos/policy_qos.yaml +++ b/examples/demo/6_qos/policy_qos.yaml @@ -5,37 +5,37 @@ metadata: spec: validationFailureAction: "report" rules: - - name: check-cpu-memory-limits - resource: - kinds: - - Deployment - validate: - message: "Resource limits are required for CPU and memory" - pattern: - spec: - template: - spec: - containers: - # match all contianers - - (name): "*" - resources: - limits: - # cpu and memory are required - memory: "?*" - cpu: "?*" - # - name: add-memory-limit - # resource: - # kinds: - # - Deployment - # mutate: - # overlay: - # spec: - # template: - # spec: - # containers: - # # the wildcard * will match all containers in the list - # - (name): "*" - # resources: - # limits: - # # add memory limit if it is not exist - # "+(memory)": "300Mi" \ No newline at end of file + - name: add-memory-limit + resource: + kinds: + - Deployment + mutate: + overlay: + spec: + template: + spec: + containers: + # the wildcard * will match all containers in the list + - (name): "*" + resources: + limits: + # add memory limit if it is not exist + "+(memory)": "300Mi" + - name: check-cpu-memory-limits + resource: + kinds: + - Deployment + validate: + message: "Resource limits are required for CPU and memory" + pattern: + spec: + template: + spec: + containers: + # match all contianers + - (name): "*" + resources: + limits: + # cpu and memory are required + memory: "?*" + cpu: "?*" \ No newline at end of file diff --git a/examples/mutate/overlay/nginx.yaml b/examples/mutate/overlay/nginx.yaml index bdf22b13cb..107f97e446 100644 --- a/examples/mutate/overlay/nginx.yaml +++ b/examples/mutate/overlay/nginx.yaml @@ -22,4 +22,4 @@ spec: ports: - containerPort: 80 - name: ghost - image: ghost:latest + image: ghost:latest \ No newline at end of file diff --git a/examples/mutate/overlay/policy_imagePullPolicy.yaml b/examples/mutate/overlay/policy_imagePullPolicy.yaml index cb6ac8025f..46537f9cd7 100644 --- a/examples/mutate/overlay/policy_imagePullPolicy.yaml +++ b/examples/mutate/overlay/policy_imagePullPolicy.yaml @@ -16,4 +16,4 @@ spec: containers: # if the image tag is latest, set the imagePullPolicy to Always - (image): "*:latest" - imagePullPolicy: "IfNotPresent" + imagePullPolicy: "IfNotPresent" \ No newline at end of file diff --git a/examples/mutate/patches/endpoints.yaml b/examples/mutate/patches/endpoints.yaml index 958d931482..792a83da96 100644 --- a/examples/mutate/patches/endpoints.yaml +++ b/examples/mutate/patches/endpoints.yaml @@ -10,4 +10,4 @@ subsets: ports: - name: secure-connection port: 443 - protocol: TCP + protocol: TCP \ No newline at end of file diff --git a/main.go b/main.go index b27405c47e..d3fb1a6a8f 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "flag" "github.com/golang/glog" + "github.com/nirmata/kyverno/pkg/annotations" "github.com/nirmata/kyverno/pkg/config" controller "github.com/nirmata/kyverno/pkg/controller" client "github.com/nirmata/kyverno/pkg/dclient" @@ -24,7 +25,6 @@ var ( func main() { defer glog.Flush() - printVersionInfo() clientConfig, err := createClientConfig(kubeconfig) if err != nil { @@ -43,19 +43,20 @@ func main() { kubeInformer := utils.NewKubeInformerFactory(clientConfig) eventController := event.NewEventController(client, policyInformerFactory) violationBuilder := violation.NewPolicyViolationBuilder(client, policyInformerFactory, eventController) - + annotationsController := annotations.NewAnnotationControler(client) policyController := controller.NewPolicyController( client, policyInformerFactory, violationBuilder, - eventController) + eventController, + annotationsController) genControler := gencontroller.NewGenController(client, eventController, policyInformerFactory, violationBuilder, kubeInformer.Core().V1().Namespaces()) tlsPair, err := initTLSPemPair(clientConfig, client) if err != nil { glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err) } - server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, eventController, filterK8Kinds) + server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, eventController, violationBuilder, annotationsController, filterK8Kinds) if err != nil { glog.Fatalf("Unable to create webhook server: %v\n", err) } @@ -67,23 +68,25 @@ func main() { stopCh := signals.SetupSignalHandler() + if err = webhookRegistrationClient.Register(); err != nil { + glog.Fatalf("Failed registering Admission Webhooks: %v\n", err) + } + policyInformerFactory.Run(stopCh) kubeInformer.Start(stopCh) eventController.Run(stopCh) genControler.Run(stopCh) + annotationsController.Run(stopCh) if err = policyController.Run(stopCh); err != nil { glog.Fatalf("Error running PolicyController: %v\n", err) } - if err = webhookRegistrationClient.Register(); err != nil { - glog.Fatalf("Failed registering Admission Webhooks: %v\n", err) - } - server.RunAsync() <-stopCh server.Stop() genControler.Stop() eventController.Stop() + annotationsController.Stop() policyController.Stop() } diff --git a/pkg/annotations/annotations.go b/pkg/annotations/annotations.go new file mode 100644 index 0000000000..34bbfe1177 --- /dev/null +++ b/pkg/annotations/annotations.go @@ -0,0 +1,302 @@ +package annotations + +import ( + "encoding/json" + "reflect" + + "github.com/golang/glog" + pinfo "github.com/nirmata/kyverno/pkg/info" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +//Policy information for annotations +type Policy struct { + Status string `json:"status"` + // Key Type/Name + MutationRules map[string]Rule `json:"mutationrules,omitempty"` + ValidationRules map[string]Rule `json:"validationrules,omitempty"` + GenerationRules map[string]Rule `json:"generationrules,omitempty"` +} + +//Rule information for annotations +type Rule struct { + Status string `json:"status"` + Changes string `json:"changes,omitempty"` // TODO for mutation changes + Error string `json:"error,omitempty"` +} + +func (p *Policy) getOverAllStatus() string { + // mutation + for _, v := range p.MutationRules { + if v.Status == "Failure" { + return "Failure" + } + } + // validation + for _, v := range p.ValidationRules { + if v.Status == "Failure" { + return "Failure" + } + } + // generation + for _, v := range p.GenerationRules { + if v.Status == "Failure" { + return "Failure" + } + } + return "Success" +} + +func getRules(rules []*pinfo.RuleInfo, ruleType pinfo.RuleType) map[string]Rule { + if len(rules) == 0 { + return nil + } + annrules := make(map[string]Rule, 0) + // var annrules map[string]Rule + for _, r := range rules { + if r.RuleType != ruleType { + continue + } + + rule := Rule{Status: getStatus(r.IsSuccessful())} + if !r.IsSuccessful() { + rule.Error = r.GetErrorString() + } + annrules[r.Name] = rule + } + return annrules +} + +func (p *Policy) updatePolicy(obj *Policy, ruleType pinfo.RuleType) bool { + updates := false + // Check Mutation rules + switch ruleType { + case pinfo.Mutation: + if p.compareMutationRules(obj.MutationRules) { + updates = true + } + case pinfo.Validation: + if p.compareValidationRules(obj.ValidationRules) { + updates = true + } + case pinfo.Generation: + if p.compareGenerationRules(obj.GenerationRules) { + updates = true + } + if p.Status != obj.Status { + updates = true + } + } + // check if any rules failed + p.Status = p.getOverAllStatus() + // If there are any updates then the annotation can be updated, can skip + return updates +} + +func (p *Policy) compareMutationRules(rules map[string]Rule) bool { + // check if the rules have changed + if !reflect.DeepEqual(p.MutationRules, rules) { + p.MutationRules = rules + return true + } + return false +} + +func (p *Policy) compareValidationRules(rules map[string]Rule) bool { + // check if the rules have changed + if !reflect.DeepEqual(p.ValidationRules, rules) { + p.ValidationRules = rules + return true + } + return false +} + +func (p *Policy) compareGenerationRules(rules map[string]Rule) bool { + // check if the rules have changed + if !reflect.DeepEqual(p.GenerationRules, rules) { + p.GenerationRules = rules + return true + } + return false +} + +func newAnnotationForPolicy(pi *pinfo.PolicyInfo) *Policy { + return &Policy{Status: getStatus(pi.IsSuccessful()), + MutationRules: getRules(pi.Rules, pinfo.Mutation), + ValidationRules: getRules(pi.Rules, pinfo.Validation), + GenerationRules: getRules(pi.Rules, pinfo.Generation), + } +} + +//AddPolicy will add policy annotation if not present or update if present +// modifies obj +// returns true, if there is any update -> caller need to update the obj +// returns false, if there is no change -> caller can skip the update +func AddPolicy(obj *unstructured.Unstructured, pi *pinfo.PolicyInfo, ruleType pinfo.RuleType) bool { + PolicyObj := newAnnotationForPolicy(pi) + // get annotation + ann := obj.GetAnnotations() + // check if policy already has annotation + cPolicy, ok := ann[BuildKey(pi.Name)] + if !ok { + PolicyByte, err := json.Marshal(PolicyObj) + if err != nil { + glog.Error(err) + return false + } + // insert policy information + ann[BuildKey(pi.Name)] = string(PolicyByte) + // set annotation back to unstr + obj.SetAnnotations(ann) + return true + } + cPolicyObj := Policy{} + err := json.Unmarshal([]byte(cPolicy), &cPolicyObj) + if err != nil { + return false + } + // update policy information inside the annotation + // 1> policy status + // 2> Mutation, Validation, Generation + if cPolicyObj.updatePolicy(PolicyObj, ruleType) { + + cPolicyByte, err := json.Marshal(cPolicyObj) + if err != nil { + return false + } + // update policy information + ann[BuildKey(pi.Name)] = string(cPolicyByte) + // set annotation back to unstr + obj.SetAnnotations(ann) + return true + } + return false +} + +//RemovePolicy to remove annotations +// return true -> if there was an entry and we deleted it +// return false -> if there is no entry, caller need not update +func RemovePolicy(obj *unstructured.Unstructured, policy string) bool { + // get annotations + ann := obj.GetAnnotations() + if ann == nil { + return false + } + if _, ok := ann[BuildKey(policy)]; !ok { + return false + } + delete(ann, BuildKey(policy)) + // set annotation back to unstr + obj.SetAnnotations(ann) + return true +} + +//ParseAnnotationsFromObject extracts annotations from the JSON obj +func ParseAnnotationsFromObject(bytes []byte) map[string]string { + var objectJSON map[string]interface{} + json.Unmarshal(bytes, &objectJSON) + meta, ok := objectJSON["metadata"].(map[string]interface{}) + if !ok { + glog.Error("unable to parse") + return nil + } + ann, ok, err := unstructured.NestedStringMap(meta, "annotations") + if err != nil || !ok { + return nil + } + return ann +} + +//AddPolicyJSONPatch generate JSON Patch to add policy informatino JSON patch +func AddPolicyJSONPatch(ann map[string]string, pi *pinfo.PolicyInfo, ruleType pinfo.RuleType) (map[string]string, []byte, error) { + if ann == nil { + ann = make(map[string]string, 0) + } + PolicyObj := newAnnotationForPolicy(pi) + cPolicy, ok := ann[BuildKey(pi.Name)] + if !ok { + PolicyByte, err := json.Marshal(PolicyObj) + if err != nil { + return nil, nil, err + } + // insert policy information + ann[BuildKey(pi.Name)] = string(PolicyByte) + // create add JSON patch + jsonPatch, err := createAddJSONPatch(ann) + + return ann, jsonPatch, err + } + cPolicyObj := Policy{} + err := json.Unmarshal([]byte(cPolicy), &cPolicyObj) + // update policy information inside the annotation + // 1> policy status + // 2> rule (name, status,changes,type) + update := cPolicyObj.updatePolicy(PolicyObj, ruleType) + if !update { + return nil, nil, err + } + + cPolicyByte, err := json.Marshal(cPolicyObj) + if err != nil { + return nil, nil, err + } + // update policy information + ann[BuildKey(pi.Name)] = string(cPolicyByte) + // create update JSON patch + jsonPatch, err := createReplaceJSONPatch(ann) + return ann, jsonPatch, err +} + +//RemovePolicyJSONPatch remove JSON patch +func RemovePolicyJSONPatch(ann map[string]string, policy string) (map[string]string, []byte, error) { + if ann == nil { + return nil, nil, nil + } + delete(ann, policy) + if len(ann) == 0 { + jsonPatch, err := createRemoveJSONPatch(ann) + return nil, jsonPatch, err + } + jsonPatch, err := createReplaceJSONPatch(ann) + return ann, jsonPatch, err +} + +type patchMapValue struct { + Op string `json:"op"` + Path string `json:"path"` + Value map[string]string `json:"value"` +} + +func createRemoveJSONPatch(ann map[string]string) ([]byte, error) { + payload := []patchMapValue{{ + Op: "remove", + Path: "/metadata/annotations", + }} + return json.Marshal(payload) + +} + +func createAddJSONPatch(ann map[string]string) ([]byte, error) { + if ann == nil { + ann = make(map[string]string, 0) + } + payload := []patchMapValue{{ + Op: "add", + Path: "/metadata/annotations", + Value: ann, + }} + return json.Marshal(payload) +} + +func createReplaceJSONPatch(ann map[string]string) ([]byte, error) { + if ann == nil { + ann = make(map[string]string, 0) + } + payload := []patchMapValue{{ + Op: "replace", + Path: "/metadata/annotations", + Value: ann, + }} + return json.Marshal(payload) +} diff --git a/pkg/annotations/annotations_test.go b/pkg/annotations/annotations_test.go new file mode 100644 index 0000000000..558746a936 --- /dev/null +++ b/pkg/annotations/annotations_test.go @@ -0,0 +1,36 @@ +package annotations + +import ( + "encoding/json" + "testing" + + pinfo "github.com/nirmata/kyverno/pkg/info" +) + +func TestAddPatch(t *testing.T) { + // Create + objRaw := []byte(`{"kind":"Deployment","apiVersion":"apps/v1","metadata":{"name":"nginx-deployment","namespace":"default","creationTimestamp":null,"labels":{"app":"nginx"}},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"nginx"}},"spec":{"containers":[{"name":"nginx","image":"nginx:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"},{"name":"ghost","image":"ghost:latest","resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","securityContext":{},"schedulerName":"default-scheduler"}},"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxUnavailable":"25%","maxSurge":"25%"}},"revisionHistoryLimit":10,"progressDeadlineSeconds":600},"status":{}}`) + piRaw := []byte(`{"Name":"set-image-pull-policy","RKind":"Deployment","RName":"nginx-deployment","RNamespace":"default","ValidationFailureAction":"","Rules":[{"Name":"nginx-deployment","Msgs":["Rule nginx-deployment: Overlay succesfully applied."],"RuleType":0}]}`) + ann := ParseAnnotationsFromObject(objRaw) + pi := pinfo.PolicyInfo{} + err := json.Unmarshal(piRaw, &pi) + if err != nil { + panic(err) + } + ann, _, err = AddPolicyJSONPatch(ann, &pi, pinfo.Mutation) + if err != nil { + panic(err) + } + // Update + piRaw = []byte(`{"Name":"set-image-pull-policy","RKind":"Deployment","RName":"nginx-deployment","RNamespace":"default","ValidationFailureAction":"","Rules":[{"Name":"nginx-deployment","Msgs":["Rule nginx-deployment1: Overlay succesfully applied."],"RuleType":0}]}`) + // ann = ParseAnnotationsFromObject(objRaw) + pi = pinfo.PolicyInfo{} + err = json.Unmarshal(piRaw, &pi) + if err != nil { + panic(err) + } + ann, _, err = AddPolicyJSONPatch(ann, &pi, pinfo.Mutation) + if err != nil { + panic(err) + } +} diff --git a/pkg/annotations/controller.go b/pkg/annotations/controller.go new file mode 100644 index 0000000000..b57077989f --- /dev/null +++ b/pkg/annotations/controller.go @@ -0,0 +1,115 @@ +package annotations + +import ( + "fmt" + "time" + + "github.com/golang/glog" + client "github.com/nirmata/kyverno/pkg/dclient" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/workqueue" +) + +type controller struct { + client *client.Client + queue workqueue.RateLimitingInterface +} + +type Interface interface { + Add(rkind, rns, rname string, patch []byte) +} + +type Controller interface { + Interface + Run(stopCh <-chan struct{}) + Stop() +} + +func NewAnnotationControler(client *client.Client) Controller { + return &controller{ + client: client, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), annotationQueueName), + } +} + +func (c *controller) Add(rkind, rns, rname string, patch []byte) { + c.queue.Add(newInfo(rkind, rns, rname, patch)) +} + +func (c *controller) Run(stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + for i := 0; i < workerThreadCount; i++ { + go wait.Until(c.runWorker, time.Second, stopCh) + } + glog.Info("Started annotation controller workers") +} + +func (c *controller) Stop() { + defer c.queue.ShutDown() + glog.Info("Shutting down annotation controller workers") +} + +func (c *controller) runWorker() { + for c.processNextWorkItem() { + } +} + +func (pc *controller) processNextWorkItem() bool { + obj, shutdown := pc.queue.Get() + if shutdown { + return false + } + + err := func(obj interface{}) error { + defer pc.queue.Done(obj) + err := pc.syncHandler(obj) + pc.handleErr(err, obj) + return nil + }(obj) + if err != nil { + glog.Error(err) + return true + } + return true +} +func (pc *controller) handleErr(err error, key interface{}) { + if err == nil { + pc.queue.Forget(key) + return + } + // This controller retries if something goes wrong. After that, it stops trying. + if pc.queue.NumRequeues(key) < workQueueRetryLimit { + glog.Warningf("Error syncing events %v: %v", key, err) + // Re-enqueue the key rate limited. Based on the rate limiter on the + // queue and the re-enqueue history, the key will be processed later again. + pc.queue.AddRateLimited(key) + return + } + pc.queue.Forget(key) + glog.Error(err) + glog.Warningf("Dropping the key out of the queue: %v", err) +} + +func (c *controller) syncHandler(obj interface{}) error { + var key info + var ok bool + if key, ok = obj.(info); !ok { + return fmt.Errorf("expected string in workqueue but got %#v", obj) + } + + var err error + // check if the resource is created + _, err = c.client.GetResource(key.RKind, key.RNs, key.RName) + if err != nil { + glog.Errorf("Error creating annotation: unable to get resource %s/%s/%s, will retry: %s ", key.RKind, key.RNs, key.RName, err) + return err + } + // if it is patch the resource + _, err = c.client.PatchResource(key.RKind, key.RNs, key.RName, *key.Patch) + if err != nil { + glog.Errorf("Error creating annotation: unable to get resource %s/%s/%s, will retry: %s", key.RKind, key.RNs, key.RName, err) + return err + } + return nil +} diff --git a/pkg/annotations/info.go b/pkg/annotations/info.go new file mode 100644 index 0000000000..dbf38f4226 --- /dev/null +++ b/pkg/annotations/info.go @@ -0,0 +1,18 @@ +package annotations + +type info struct { + RKind string + RNs string + RName string + //TODO:Hack as slice makes the struct unhasable + Patch *[]byte +} + +func newInfo(rkind, rns, rname string, patch []byte) info { + return info{ + RKind: rkind, + RNs: rns, + RName: rname, + Patch: &patch, + } +} diff --git a/pkg/annotations/utils.go b/pkg/annotations/utils.go new file mode 100644 index 0000000000..2c30938d7c --- /dev/null +++ b/pkg/annotations/utils.go @@ -0,0 +1,16 @@ +package annotations + +const annotationQueueName = "annotation-queue" +const workerThreadCount = 1 +const workQueueRetryLimit = 3 + +func getStatus(status bool) string { + if status { + return "Success" + } + return "Failure" +} + +func BuildKey(policyName string) string { + return "policies.kyverno.io." + policyName +} diff --git a/pkg/apis/policy/v1alpha1/types.go b/pkg/apis/policy/v1alpha1/types.go index 889d1bf775..9842d2c929 100644 --- a/pkg/apis/policy/v1alpha1/types.go +++ b/pkg/apis/policy/v1alpha1/types.go @@ -18,7 +18,8 @@ type Policy struct { // Spec describes policy behavior by its rules type Spec struct { - Rules []Rule `json:"rules"` + Rules []Rule `json:"rules"` + ValidationFailureAction string `json:"validationFailureAction"` } // Rule is set of mutation, validation and generation actions @@ -77,16 +78,24 @@ type CloneFrom struct { // Status contains violations for existing resources type Status struct { - Violations []Violation `json:"violations,omitempty"` + // Violations map[kind/namespace/resource]Violation + Violations map[string]Violation `json:"violations,omitempty"` } // Violation for the policy type Violation struct { - Kind string `json:"kind,omitempty"` - Name string `json:"name,omitempty"` - Namespace string `json:"namespace,omitempty"` - Reason string `json:"reason,omitempty"` - Message string `json:"message,omitempty"` + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Rules []FailedRule `json:"rules"` + Reason string `json:"reason,omitempty"` +} + +// FailedRule stored info and type of failed rules +type FailedRule struct { + Name string `json:"name"` + Type string `json:"type"` //Mutation, Validation, Genertaion + Error string `json:"error"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/policy/v1alpha1/utils.go b/pkg/apis/policy/v1alpha1/utils.go index 59adc88a5d..0c2c8fa00e 100644 --- a/pkg/apis/policy/v1alpha1/utils.go +++ b/pkg/apis/policy/v1alpha1/utils.go @@ -104,3 +104,44 @@ func (in *Generation) DeepCopyInto(out *Generation) { *out = *in } } + +// return true -> if there were any removals +// return false -> if it looks the same +func (v *Violation) RemoveRulesOfType(ruleType string) bool { + removed := false + updatedRules := []FailedRule{} + for _, r := range v.Rules { + if r.Type == ruleType { + removed = true + continue + } + updatedRules = append(updatedRules, r) + } + + if removed { + v.Rules = updatedRules + return true + } + return false +} + +//IsEqual Check if violatiosn are equal +func (v *Violation) IsEqual(nv Violation) bool { + // We do not need to compare resource info as it will be same + // Reason + if v.Reason != nv.Reason { + return false + } + // Rule + if len(v.Rules) != len(nv.Rules) { + return false + } + // assumes the rules will be in order, as the rule are proceeed in order + // if the rule order changes, it means the policy has changed.. as it will afffect the order in which mutation rules are applied + for i, r := range v.Rules { + if r != nv.Rules[i] { + return false + } + } + return true +} diff --git a/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go index 903ae1f40c..627535f830 100644 --- a/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/policy/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,22 @@ func (in *CloneFrom) DeepCopy() *CloneFrom { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FailedRule) DeepCopyInto(out *FailedRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FailedRule. +func (in *FailedRule) DeepCopy() *FailedRule { + if in == nil { + return nil + } + out := new(FailedRule) + in.DeepCopyInto(out) + return out +} + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generation. func (in *Generation) DeepCopy() *Generation { if in == nil { @@ -135,6 +151,11 @@ func (in *ResourceDescription) DeepCopyInto(out *ResourceDescription) { *out = new(string) **out = **in } + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = new(v1.LabelSelector) @@ -210,8 +231,10 @@ func (in *Status) DeepCopyInto(out *Status) { *out = *in if in.Violations != nil { in, out := &in.Violations, &out.Violations - *out = make([]Violation, len(*in)) - copy(*out, *in) + *out = make(map[string]Violation, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } } return } @@ -239,6 +262,11 @@ func (in *Validation) DeepCopy() *Validation { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Violation) DeepCopyInto(out *Violation) { *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]FailedRule, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/controller/cleanup.go b/pkg/controller/cleanup.go new file mode 100644 index 0000000000..9d42fbc136 --- /dev/null +++ b/pkg/controller/cleanup.go @@ -0,0 +1,77 @@ +package controller + +import ( + "github.com/golang/glog" + "github.com/minio/minio/pkg/wildcard" + "github.com/nirmata/kyverno/pkg/annotations" + v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + client "github.com/nirmata/kyverno/pkg/dclient" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func cleanAnnotations(client *client.Client, obj interface{}) { + // get the policy struct from interface + unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + glog.Error(err) + return + } + policy := v1alpha1.Policy{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr, &policy); err != nil { + glog.Error(err) + return + } + // Get the resources that apply to the policy + // key uid + resourceMap := map[string]unstructured.Unstructured{} + for _, rule := range policy.Spec.Rules { + for _, k := range rule.Kinds { + if k == "Namespace" { + continue + } + // kind -> resource + gvr := client.DiscoveryClient.GetGVRFromKind(k) + // label selectors + // namespace ? should it be default or allow policy to specify it + namespace := "default" + if rule.ResourceDescription.Namespace != nil { + namespace = *rule.ResourceDescription.Namespace + } + list, err := client.ListResource(k, namespace, rule.ResourceDescription.Selector) + if err != nil { + glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String()) + glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err) + continue + } + for _, res := range list.Items { + name := rule.ResourceDescription.Name + if name != nil { + // wild card matching + if !wildcard.Match(*name, res.GetName()) { + continue + } + } + resourceMap[string(res.GetUID())] = res + } + } + } + + // remove annotations for the resources + for _, obj := range resourceMap { + // get annotations + ann := obj.GetAnnotations() + + _, patch, err := annotations.RemovePolicyJSONPatch(ann, annotations.BuildKey(policy.Name)) + if err != nil { + glog.Error(err) + continue + } + // patch the resource + _, err = client.PatchResource(obj.GetKind(), obj.GetNamespace(), obj.GetName(), patch) + if err != nil { + glog.Error(err) + continue + } + } +} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 01347ee09a..d66469c946 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -3,15 +3,17 @@ package controller import ( "fmt" "reflect" - "strings" "time" + jsonpatch "github.com/evanphx/json-patch" + + "github.com/nirmata/kyverno/pkg/annotations" "github.com/nirmata/kyverno/pkg/info" "github.com/nirmata/kyverno/pkg/engine" "github.com/golang/glog" - types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1" client "github.com/nirmata/kyverno/pkg/dclient" "github.com/nirmata/kyverno/pkg/event" @@ -27,27 +29,30 @@ import ( //PolicyController to manage Policy CRD type PolicyController struct { - client *client.Client - policyLister lister.PolicyLister - policySynced cache.InformerSynced - violationBuilder violation.Generator - eventController event.Generator - queue workqueue.RateLimitingInterface + client *client.Client + policyLister lister.PolicyLister + policySynced cache.InformerSynced + violationBuilder violation.Generator + eventController event.Generator + annotationsController annotations.Controller + queue workqueue.RateLimitingInterface } // NewPolicyController from cmd args func NewPolicyController(client *client.Client, policyInformer sharedinformer.PolicyInformer, violationBuilder violation.Generator, - eventController event.Generator) *PolicyController { + eventController event.Generator, + annotationsController annotations.Controller) *PolicyController { controller := &PolicyController{ - client: client, - policyLister: policyInformer.GetLister(), - policySynced: policyInformer.GetInfomer().HasSynced, - violationBuilder: violationBuilder, - eventController: eventController, - queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName), + client: client, + policyLister: policyInformer.GetLister(), + policySynced: policyInformer.GetInfomer().HasSynced, + violationBuilder: violationBuilder, + eventController: eventController, + annotationsController: annotationsController, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName), } policyInformer.GetInfomer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -63,13 +68,13 @@ func (pc *PolicyController) createPolicyHandler(resource interface{}) { } func (pc *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) { - newPolicy := newResource.(*types.Policy) - oldPolicy := oldResource.(*types.Policy) - newPolicy.Status = types.Status{} - oldPolicy.Status = types.Status{} + newPolicy := newResource.(*v1alpha1.Policy) + oldPolicy := oldResource.(*v1alpha1.Policy) + newPolicy.Status = v1alpha1.Status{} + oldPolicy.Status = v1alpha1.Status{} newPolicy.ResourceVersion = "" oldPolicy.ResourceVersion = "" - if reflect.DeepEqual(newPolicy.ResourceVersion, oldPolicy.ResourceVersion) { + if reflect.DeepEqual(newPolicy, oldPolicy) { return } pc.enqueuePolicy(newResource) @@ -82,6 +87,7 @@ func (pc *PolicyController) deletePolicyHandler(resource interface{}) { glog.Error("error decoding object, invalid type") return } + cleanAnnotations(pc.client, resource) glog.Infof("policy deleted: %s", object.GetName()) } @@ -155,7 +161,7 @@ func (pc *PolicyController) handleErr(err error, key interface{}) { } pc.queue.Forget(key) glog.Error(err) - glog.Warningf("Dropping the key %q out of the queue: %v", key, err) + glog.Warningf("Dropping the key out of the queue: %v", err) } func (pc *PolicyController) syncHandler(obj interface{}) error { @@ -169,8 +175,7 @@ func (pc *PolicyController) syncHandler(obj interface{}) error { glog.Errorf("invalid policy key: %s", key) return nil } - - // Get Policy resource with namespace/name + // Get Policy policy, err := pc.policyLister.Get(name) if err != nil { if errors.IsNotFound(err) { @@ -179,40 +184,103 @@ func (pc *PolicyController) syncHandler(obj interface{}) error { } return err } - // process policy on existing resource - // get the violations and pass to violation Builder - // get the events and pass to event Builder - //TODO: processPolicy + glog.Infof("process policy %s on existing resources", policy.GetName()) + // Process policy on existing resources policyInfos := engine.ProcessExisting(pc.client, policy) - events, violations := createEventsAndViolations(pc.eventController, policyInfos) + + events, violations := pc.createEventsAndViolations(policyInfos) + // Events, Violations pc.eventController.Add(events...) err = pc.violationBuilder.Add(violations...) if err != nil { glog.Error(err) } + + // Annotations + pc.createAnnotations(policyInfos) + return nil } -func createEventsAndViolations(eventController event.Generator, policyInfos []*info.PolicyInfo) ([]*event.Info, []*violation.Info) { +func (pc *PolicyController) createAnnotations(policyInfos []*info.PolicyInfo) { + for _, pi := range policyInfos { + var patch []byte + //get resource + obj, err := pc.client.GetResource(pi.RKind, pi.RNamespace, pi.RName) + if err != nil { + glog.Error(err) + continue + } + // add annotation for policy application + ann := obj.GetAnnotations() + // Mutation rules + ann, mpatch, err := annotations.AddPolicyJSONPatch(ann, pi, info.Mutation) + if err != nil { + glog.Error(err) + continue + } + // Validation rules + ann, vpatch, err := annotations.AddPolicyJSONPatch(ann, pi, info.Validation) + if err != nil { + glog.Error(err) + } + if mpatch == nil && vpatch == nil { + //nothing to patch + continue + } + // merge the patches + if mpatch != nil && vpatch != nil { + patch, err = jsonpatch.MergePatch(mpatch, vpatch) + if err != nil { + glog.Error(err) + continue + } + } + if mpatch == nil { + patch = vpatch + } else { + patch = mpatch + } + // add the anotation to the resource + _, err = pc.client.PatchResource(pi.RKind, pi.RNamespace, pi.RName, patch) + if err != nil { + glog.Error(err) + continue + } + } +} + +func (pc *PolicyController) createEventsAndViolations(policyInfos []*info.PolicyInfo) ([]*event.Info, []*violation.Info) { events := []*event.Info{} violations := []*violation.Info{} // Create events from the policyInfo for _, policyInfo := range policyInfos { - fruleNames := []string{} + frules := []v1alpha1.FailedRule{} sruleNames := []string{} for _, rule := range policyInfo.Rules { if !rule.IsSuccessful() { e := &event.Info{} - fruleNames = append(fruleNames, rule.Name) + frule := v1alpha1.FailedRule{Name: rule.Name} switch rule.RuleType { case info.Mutation, info.Validation, info.Generation: // Events e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation, event.FProcessRule, rule.Name, policyInfo.Name) + switch rule.RuleType { + case info.Mutation: + frule.Type = info.Mutation.String() + case info.Validation: + frule.Type = info.Validation.String() + case info.Generation: + frule.Type = info.Generation.String() + } + frule.Error = rule.GetErrorString() default: glog.Info("Unsupported Rule type") } + frule.Error = rule.GetErrorString() + frules = append(frules, frule) events = append(events, e) } else { sruleNames = append(sruleNames, rule.Name) @@ -220,22 +288,16 @@ func createEventsAndViolations(eventController event.Generator, policyInfos []*i } if !policyInfo.IsSuccessful() { - // Event - // list of failed rules : ruleNames - e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, strings.Join(fruleNames, ";")) + e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, concatFailedRules(frules)) events = append(events, e) // Violation - v := violation.NewViolationFromEvent(e, policyInfo.Name, policyInfo.RKind, policyInfo.RName, policyInfo.RNamespace) + v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules()) violations = append(violations, v) + } else { + // clean up violations + pc.violationBuilder.RemoveInactiveViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, info.Mutation) + pc.violationBuilder.RemoveInactiveViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, info.Validation) } - // else { - // // Policy was processed succesfully - // e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyApplied, event.SPolicyApply, policyInfo.Name) - // events = append(events, e) - // // Policy applied succesfully on resource - // e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyApplied, event.SRuleApply, strings.Join(sruleNames, ";"), policyInfo.RName) - // events = append(events, e) - // } } return events, violations } diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 5df64bd924..34448ffb20 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -41,7 +41,8 @@ func (f *fixture) runControler(policyName string) { f.Client, policyInformerFactory, violationBuilder, - eventController) + eventController, + nil) stopCh := signals.SetupSignalHandler() // start informer & controller diff --git a/pkg/controller/utils.go b/pkg/controller/utils.go index b5eb6de4d6..2501addb51 100644 --- a/pkg/controller/utils.go +++ b/pkg/controller/utils.go @@ -1,7 +1,21 @@ package controller +import ( + "bytes" + + v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" +) + const policyWorkQueueName = "policyworkqueue" -const policyWorkQueueRetryLimit = 5 +const policyWorkQueueRetryLimit = 3 const policyControllerWorkerCount = 2 + +func concatFailedRules(frules []v1alpha1.FailedRule) string { + var buffer bytes.Buffer + for _, frule := range frules { + buffer.WriteString(frule.Name + ";") + } + return buffer.String() +} diff --git a/pkg/dclient/client.go b/pkg/dclient/client.go index c473221c59..fb591b95b2 100644 --- a/pkg/dclient/client.go +++ b/pkg/dclient/client.go @@ -16,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + patchTypes "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/dynamic" @@ -40,7 +41,6 @@ func NewClient(config *rest.Config) (*Client, error) { if err != nil { return nil, err } - kclient, err := kubernetes.NewForConfig(config) if err != nil { return nil, err @@ -110,6 +110,11 @@ func (c *Client) GetResource(kind string, namespace string, name string, subreso return c.getResourceInterface(kind, namespace).Get(name, meta.GetOptions{}, subresources...) } +//Patch +func (c *Client) PatchResource(kind string, namespace string, name string, patch []byte) (*unstructured.Unstructured, error) { + return c.getResourceInterface(kind, namespace).Patch(name, patchTypes.JSONPatchType, patch, meta.PatchOptions{}) +} + // ListResource returns the list of resources in unstructured/json format // Access items using []Items func (c *Client) ListResource(kind string, namespace string, lselector *meta.LabelSelector) (*unstructured.UnstructuredList, error) { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index d61545fb8f..5dfed4c162 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -14,11 +14,11 @@ import ( // Instead we expose them as standalone functions passing the required atrributes // The each function returns the changes that need to be applied on the resource // the caller is responsible to apply the changes to the resource - // ProcessExisting checks for mutation and validation violations of existing resources func ProcessExisting(client *client.Client, policy *types.Policy) []*info.PolicyInfo { glog.Infof("Applying policy %s on existing resources", policy.Name) - resources := []*resourceInfo{} + // key uid + resourceMap := map[string]*resourceInfo{} for _, rule := range policy.Spec.Rules { for _, k := range rule.Kinds { @@ -34,7 +34,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy if rule.ResourceDescription.Namespace != nil { namespace = *rule.ResourceDescription.Namespace } - list, err := client.ListResource(gvr.Resource, namespace, rule.ResourceDescription.Selector) + list, err := client.ListResource(k, namespace, rule.ResourceDescription.Selector) if err != nil { glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String()) glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err) @@ -52,18 +52,19 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy ri := &resourceInfo{resource: &res, gvk: &metav1.GroupVersionKind{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind}} - resources = append(resources, ri) + + resourceMap[string(res.GetUID())] = ri } } } policyInfos := []*info.PolicyInfo{} // for the filtered resource apply policy - for _, r := range resources { + for _, v := range resourceMap { - policyInfo, err := applyPolicy(client, policy, r) + policyInfo, err := applyPolicy(client, policy, v) if err != nil { - glog.Errorf("unable to apply policy %s on resource %s/%s", policy.Name, r.resource.GetName(), r.resource.GetNamespace()) + glog.Errorf("unable to apply policy %s on resource %s/%s", policy.Name, v.resource.GetName(), v.resource.GetNamespace()) glog.Error(err) continue } @@ -74,7 +75,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy } func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo) (*info.PolicyInfo, error) { - policyInfo := info.NewPolicyInfo(policy.Name, res.gvk.Kind, res.resource.GetName(), res.resource.GetNamespace()) + policyInfo := info.NewPolicyInfo(policy.Name, res.gvk.Kind, res.resource.GetName(), res.resource.GetNamespace(), policy.Spec.ValidationFailureAction) glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) rawResource, err := res.resource.MarshalJSON() if err != nil { @@ -99,6 +100,10 @@ func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo) func mutation(p *types.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]*info.RuleInfo, error) { patches, ruleInfos := Mutate(*p, rawResource, *gvk) + if len(ruleInfos) == 0 { + // no rules were processed + return nil, nil + } // option 2: (original Resource + patch) compare with (original resource) mergePatches := JoinPatches(patches) // merge the patches diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 6d5b2b0b32..2721a4746e 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -10,7 +10,7 @@ import ( // Mutate performs mutation. Overlay first and then mutation patches func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) { var allPatches [][]byte - var err error + patchedDocument := rawResource ris := []*info.RuleInfo{} for _, rule := range policy.Spec.Rules { @@ -29,38 +29,24 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio overlayPatches, err := ProcessOverlay(rule, rawResource, gvk) if err != nil { ri.Fail() - ri.Addf("Rule %s: Overlay application has failed, err %s.", rule.Name, err) + ri.Addf("overlay application has failed, err %v.", err) } else { - // Apply the JSON patches from the rule to the resource - rawResource, err = ApplyPatches(rawResource, overlayPatches) - if err != nil { - ri.Fail() - ri.Addf("Unable to apply JSON patch to resource, err %s.", err) - } else { - ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name) - allPatches = append(allPatches, overlayPatches...) - } + ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name) + allPatches = append(allPatches, overlayPatches...) } } // Process Patches if len(rule.Mutation.Patches) != 0 { - rulePatches, errs := ProcessPatches(rule, rawResource) + rulePatches, errs := ProcessPatches(rule, patchedDocument) if len(errs) > 0 { ri.Fail() for _, err := range errs { - ri.Addf("Rule %s: Patches application has failed, err %s.", rule.Name, err) + ri.Addf("patches application has failed, err %v.", err) } } else { - // Apply the JSON patches from the rule to the resource - rawResource, err = ApplyPatches(rawResource, rulePatches) - if err != nil { - ri.Fail() - ri.Addf("Unable to apply JSON patch to resource, err %s.", err) - } else { - ri.Addf("Rule %s: Patches succesfully applied.", rule.Name) - allPatches = append(allPatches, rulePatches...) - } + ri.Addf("Rule %s: Patches succesfully applied.", rule.Name) + allPatches = append(allPatches, rulePatches...) } } ris = append(ris, ri) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 8210f6e609..832feffb35 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -41,7 +41,7 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers err := validateResourceWithPattern(resource, rule.Validation.Pattern) if err != nil { ri.Fail() - ri.Addf("Rule %s: Validation has failed, err %s.", rule.Name, err) + ri.Addf("validation has failed, err %v.", err) } else { ri.Addf("Rule %s: Validation succesfully.", rule.Name) diff --git a/pkg/event/controller.go b/pkg/event/controller.go index e519238f67..5c5e9fca0b 100644 --- a/pkg/event/controller.go +++ b/pkg/event/controller.go @@ -1,7 +1,6 @@ package event import ( - "fmt" "time" "github.com/golang/glog" @@ -42,13 +41,12 @@ type Controller interface { func NewEventController(client *client.Client, shareInformer sharedinformer.PolicyInformer) Controller { - controller := &controller{ + return &controller{ client: client, policyLister: shareInformer.GetLister(), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), eventWorkQueueName), recorder: initRecorder(client), } - return controller } func initRecorder(client *client.Client) record.EventRecorder { @@ -93,40 +91,58 @@ func (c *controller) Stop() { defer c.queue.ShutDown() glog.Info("Shutting down eventbuilder controller workers") } + func (c *controller) runWorker() { for c.processNextWorkItem() { } } +func (c *controller) handleErr(err error, key interface{}) { + if err == nil { + c.queue.Forget(key) + return + } + // This controller retries if something goes wrong. After that, it stops trying. + if c.queue.NumRequeues(key) < workQueueRetryLimit { + glog.Warningf("Error syncing events %v: %v", key, err) + // Re-enqueue the key rate limited. Based on the rate limiter on the + // queue and the re-enqueue history, the key will be processed later again. + c.queue.AddRateLimited(key) + return + } + c.queue.Forget(key) + glog.Error(err) + glog.Warningf("Dropping the key out of the queue: %v", err) +} + func (c *controller) processNextWorkItem() bool { obj, shutdown := c.queue.Get() if shutdown { return false } + err := func(obj interface{}) error { defer c.queue.Done(obj) var key Info var ok bool + if key, ok = obj.(Info); !ok { c.queue.Forget(obj) glog.Warningf("Expecting type info by got %v\n", obj) return nil } - // Run the syncHandler, passing the resource and the policy - if err := c.SyncHandler(key); err != nil { - c.queue.AddRateLimited(key) - return fmt.Errorf("error syncing '%s' : %s, requeuing event creation request", key.Namespace+"/"+key.Name, err.Error()) - } + err := c.syncHandler(key) + c.handleErr(err, obj) return nil }(obj) - if err != nil { - glog.Warning(err) + glog.Error(err) + return true } return true } -func (c *controller) SyncHandler(key Info) error { +func (c *controller) syncHandler(key Info) error { var robj runtime.Object var err error diff --git a/pkg/event/util.go b/pkg/event/util.go index f3601d05fd..34daa0a9b1 100644 --- a/pkg/event/util.go +++ b/pkg/event/util.go @@ -6,6 +6,8 @@ const eventWorkQueueName = "policy-controller-events" const eventWorkerThreadCount = 1 +const workQueueRetryLimit = 1 + //Info defines the event details type Info struct { Kind string diff --git a/pkg/gencontroller/generation.go b/pkg/gencontroller/generation.go index b8cc3cea2e..c86b408294 100644 --- a/pkg/gencontroller/generation.go +++ b/pkg/gencontroller/generation.go @@ -59,7 +59,8 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) { policyInfo := info.NewPolicyInfo(p.Name, "Namespace", ns.Name, - "") // Namespace has no namespace..WOW + "", + p.Spec.ValidationFailureAction) // Namespace has no namespace..WOW ruleInfos := engine.GenerateNew(c.client, p, ns) policyInfo.AddRuleInfos(ruleInfos) @@ -77,9 +78,7 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) { if onViolation { glog.Infof("Adding violation for generation rule of policy %s\n", policyInfo.Name) - - v := violation.NewViolation(event.PolicyViolation, policyInfo.Name, policyInfo.RKind, policyInfo.RName, - policyInfo.RNamespace, msg) + v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules()) c.violationBuilder.Add(v) } else { eventInfo = event.NewEvent(policyKind, "", policyInfo.Name, event.RequestBlocked, diff --git a/pkg/info/info.go b/pkg/info/info.go index 3f839bd098..a74458d1d2 100644 --- a/pkg/info/info.go +++ b/pkg/info/info.go @@ -3,6 +3,8 @@ package info import ( "fmt" "strings" + + v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" ) //PolicyInfo defines policy information @@ -16,25 +18,68 @@ type PolicyInfo struct { // Namespace is the ns of resource // empty on non-namespaced resources RNamespace string - Rules []*RuleInfo - success bool + //TODO: add check/enum for types + ValidationFailureAction string // BlockChanges, ReportViolation + Rules []*RuleInfo + success bool } //NewPolicyInfo returns a new policy info -func NewPolicyInfo(policyName string, rKind string, rName string, rNamespace string) *PolicyInfo { +func NewPolicyInfo(policyName, rKind, rName, rNamespace, validationFailureAction string) *PolicyInfo { return &PolicyInfo{ - Name: policyName, - RKind: rKind, - RName: rName, - RNamespace: rNamespace, - success: true, // fail to be set explicity + Name: policyName, + RKind: rKind, + RName: rName, + RNamespace: rNamespace, + success: true, // fail to be set explicity + ValidationFailureAction: validationFailureAction, } } //IsSuccessful checks if policy is succesful // the policy is set to fail, if any of the rules have failed func (pi *PolicyInfo) IsSuccessful() bool { - return pi.success + for _, r := range pi.Rules { + if !r.success { + pi.success = false + return false + } + } + pi.success = true + return true +} + +// SuccessfulRules returns list of successful rule names +func (pi *PolicyInfo) SuccessfulRules() []string { + var rules []string + for _, r := range pi.Rules { + if r.IsSuccessful() { + rules = append(rules, r.Name) + } + } + return rules +} + +// FailedRules returns list of failed rule names +func (pi *PolicyInfo) FailedRules() []string { + var rules []string + for _, r := range pi.Rules { + if !r.IsSuccessful() { + rules = append(rules, r.Name) + } + } + return rules +} + +//GetFailedRules returns the failed rules with rule type +func (pi *PolicyInfo) GetFailedRules() []v1alpha1.FailedRule { + var rules []v1alpha1.FailedRule + for _, r := range pi.Rules { + if !r.IsSuccessful() { + rules = append(rules, v1alpha1.FailedRule{Name: r.Name, Type: r.RuleType.String(), Error: r.GetErrorString()}) + } + } + return rules } //ErrorRules returns error msgs from all rule @@ -73,12 +118,18 @@ type RuleInfo struct { } //ToString reule information +//TODO: check if this is needed func (ri *RuleInfo) ToString() string { str := "rulename: " + ri.Name msgs := strings.Join(ri.Msgs, ";") return strings.Join([]string{str, msgs}, ";") } +//GetErrorString returns the error message for a rule +func (ri *RuleInfo) GetErrorString() string { + return strings.Join(ri.Msgs, ";") +} + //NewRuleInfo creates a new RuleInfo func NewRuleInfo(ruleName string, ruleType RuleType) *RuleInfo { return &RuleInfo{ @@ -121,6 +172,9 @@ func RulesSuccesfuly(rules []*RuleInfo) bool { //AddRuleInfos sets the rule information func (pi *PolicyInfo) AddRuleInfos(rules []*RuleInfo) { + if rules == nil { + return + } if !RulesSuccesfuly(rules) { pi.success = false } diff --git a/pkg/kyverno/apply/apply.go b/pkg/kyverno/apply/apply.go index baa645609f..eb30c5dc26 100644 --- a/pkg/kyverno/apply/apply.go +++ b/pkg/kyverno/apply/apply.go @@ -103,7 +103,8 @@ func applyPolicyOnRaw(policy *kubepolicy.Policy, rawResource []byte, gvk *metav1 policyInfo := info.NewPolicyInfo(policy.Name, gvk.Kind, rname, - rns) + rns, + policy.Spec.ValidationFailureAction) // Process Mutation patches, ruleInfos := engine.Mutate(*policy, rawResource, *gvk) diff --git a/pkg/kyverno/apply/util.go b/pkg/kyverno/apply/util.go index f46a364961..bb71715ac3 100644 --- a/pkg/kyverno/apply/util.go +++ b/pkg/kyverno/apply/util.go @@ -80,7 +80,7 @@ func scanDir(dir string) ([]string, error) { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { - return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v\n", dir, err) + return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v", dir, err) } res = append(res, path) diff --git a/pkg/testrunner/test.go b/pkg/testrunner/test.go index f76ba343b1..bc73f795b1 100644 --- a/pkg/testrunner/test.go +++ b/pkg/testrunner/test.go @@ -173,7 +173,8 @@ func (t *test) applyPolicy(policy *pt.Policy, policyInfo := info.NewPolicyInfo(policy.Name, rkind, rname, - rns) + rns, + policy.Spec.ValidationFailureAction) // Apply Mutation Rules patches, ruleInfos := engine.Mutate(*policy, rawResource, *tresource.gvk) policyInfo.AddRuleInfos(ruleInfos) diff --git a/pkg/violation/builder.go b/pkg/violation/builder.go index 7fe9645274..6f8f68aa7a 100644 --- a/pkg/violation/builder.go +++ b/pkg/violation/builder.go @@ -2,24 +2,26 @@ package violation import ( "errors" - "reflect" - "github.com/golang/glog" - types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" - v1alpha1 "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1" + v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1" client "github.com/nirmata/kyverno/pkg/dclient" event "github.com/nirmata/kyverno/pkg/event" + "github.com/nirmata/kyverno/pkg/info" "github.com/nirmata/kyverno/pkg/sharedinformer" + "k8s.io/apimachinery/pkg/runtime" ) //Generator to generate policy violation type Generator interface { Add(infos ...*Info) error + RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error + ResourceRemoval(policy, rKind, rNs, rName string) error } type builder struct { client *client.Client - policyLister v1alpha1.PolicyLister + policyLister lister.PolicyLister eventBuilder event.Generator } @@ -27,7 +29,6 @@ type builder struct { type Builder interface { Generator processViolation(info *Info) error - isActive(kind string, rname string, rnamespace string) (bool, error) } //NewPolicyViolationBuilder returns new violation builder @@ -43,7 +44,24 @@ func NewPolicyViolationBuilder(client *client.Client, return builder } +//BuldNewViolation returns a new violation +func BuldNewViolation(pName string, rKind string, rNs string, rName string, reason string, frules []v1alpha1.FailedRule) *Info { + return &Info{ + Policy: pName, + Violation: v1alpha1.Violation{ + Kind: rKind, + Namespace: rNs, + Name: rName, + Reason: reason, + Rules: frules, + }, + } +} + func (b *builder) Add(infos ...*Info) error { + if infos == nil { + return nil + } for _, info := range infos { return b.processViolation(info) } @@ -51,96 +69,203 @@ func (b *builder) Add(infos ...*Info) error { } func (b *builder) processViolation(info *Info) error { - currentViolations := []interface{}{} statusMap := map[string]interface{}{} - var ok bool - //TODO: hack get from client - p1, err := b.client.GetResource("Policy", "", info.Policy, "status") + violationsMap := map[string]interface{}{} + violationMap := map[string]interface{}{} + var violations interface{} + var violation interface{} + // Get Policy + obj, err := b.client.GetResource("Policy", "", info.Policy, "status") if err != nil { return err } - unstr := p1.UnstructuredContent() - // check if "status" field exists + unstr := obj.UnstructuredContent() + // get "status" subresource status, ok := unstr["status"] if ok { + // status exists // status is already present then we append violations if statusMap, ok = status.(map[string]interface{}); !ok { return errors.New("Unable to parse status subresource") } - violations, ok := statusMap["violations"] + // get policy violations + violations, ok = statusMap["violations"] if !ok { - glog.Info("violation not present") + return nil } - glog.Info(reflect.TypeOf(violations)) - if currentViolations, ok = violations.([]interface{}); !ok { - return errors.New("Unable to parse violations") + violationsMap, ok = violations.(map[string]interface{}) + if !ok { + return errors.New("Unable to get status.violations subresource") } - } - newViolation := info.Violation - for _, violation := range currentViolations { - glog.Info(reflect.TypeOf(violation)) - if v, ok := violation.(map[string]interface{}); ok { - if name, ok := v["name"].(string); ok { - if namespace, ok := v["namespace"].(string); ok { - ok, err := b.isActive(info.Kind, name, namespace) - if err != nil { - glog.Error(err) - continue - } - if !ok { - //TODO remove the violation as it corresponds to resource that does not exist - glog.Info("removed violation") - } - } + // check if the resource has a violation + violation, ok = violationsMap[info.getKey()] + if !ok { + // add resource violation + violationsMap[info.getKey()] = info.Violation + statusMap["violations"] = violationsMap + unstr["status"] = statusMap + } else { + violationMap, ok = violation.(map[string]interface{}) + if !ok { + return errors.New("Unable to get status.violations.violation subresource") } + // we check if the new violation updates are different from stored violation info + v := v1alpha1.Violation{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(violationMap, &v) + if err != nil { + return err + } + // compare v & info.Violation + if v.IsEqual(info.Violation) { + // no updates to violation + // do nothing + return nil + } + // update the violation + violationsMap[info.getKey()] = info.Violation + statusMap["violations"] = violationsMap + unstr["status"] = statusMap } + } else { + violationsMap[info.getKey()] = info.Violation + statusMap["violations"] = violationsMap + unstr["status"] = statusMap } - currentViolations = append(currentViolations, newViolation) - // update violations - // set the updated status - statusMap["violations"] = currentViolations - unstr["status"] = statusMap - p1.SetUnstructuredContent(unstr) - _, err = b.client.UpdateStatusResource("policies", "", p1, false) + + obj.SetUnstructuredContent(unstr) + // update the status sub-resource for policy + _, err = b.client.UpdateStatusResource("Policy", "", obj, false) if err != nil { return err } return nil } -func (b *builder) isActive(kind, rname, rnamespace string) (bool, error) { - // Generate Merge Patch - _, err := b.client.GetResource(b.client.DiscoveryClient.GetGVRFromKind(kind).Resource, rnamespace, rname) +//RemoveInactiveViolation +func (b *builder) RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error { + statusMap := map[string]interface{}{} + violationsMap := map[string]interface{}{} + violationMap := map[string]interface{}{} + var violations interface{} + var violation interface{} + // Get Policy + obj, err := b.client.GetResource("Policy", "", policy, "status") if err != nil { - glog.Errorf("unable to get resource %s/%s ", rnamespace, rname) - return false, err + return err } - return true, nil + unstr := obj.UnstructuredContent() + // get "status" subresource + status, ok := unstr["status"] + if !ok { + return nil + } + // status exists + // status is already present then we append violations + if statusMap, ok = status.(map[string]interface{}); !ok { + return errors.New("Unable to parse status subresource") + } + // get policy violations + violations, ok = statusMap["violations"] + if !ok { + return nil + } + violationsMap, ok = violations.(map[string]interface{}) + if !ok { + return errors.New("Unable to get status.violations subresource") + } + // check if the resource has a violation + violation, ok = violationsMap[BuildKey(rKind, rNs, rName)] + if !ok { + // no violation for this resource + return nil + } + violationMap, ok = violation.(map[string]interface{}) + if !ok { + return errors.New("Unable to get status.violations.violation subresource") + } + // check remove the rules of the given type + // this is called when the policy is applied succesfully, so we can remove the previous failed rules + // if all rules are to be removed, the deleted the violation + v := v1alpha1.Violation{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(violationMap, &v) + if err != nil { + return err + } + if !v.RemoveRulesOfType(ruleType.String()) { + // no rule of given type found, + // no need to remove rule + return nil + } + // if there are no faile rules remove the violation + if len(v.Rules) == 0 { + delete(violationsMap, BuildKey(rKind, rNs, rName)) + } else { + // update the rules + violationsMap[BuildKey(rKind, rNs, rName)] = v + } + statusMap["violations"] = violationsMap + unstr["status"] = statusMap + + obj.SetUnstructuredContent(unstr) + // update the status sub-resource for policy + _, err = b.client.UpdateStatusResource("Policy", "", obj, false) + if err != nil { + return err + } + return nil } -//NewViolation return new policy violation -func NewViolation(reason event.Reason, policyName, kind, rname, rnamespace, msg string) *Info { - return &Info{Policy: policyName, - Violation: types.Violation{ - Kind: kind, - Name: rname, - Namespace: rnamespace, - Reason: reason.String(), - Message: msg, - }, +// ResourceRemoval on resources reoval we remove the policy violation in the policy +func (b *builder) ResourceRemoval(policy, rKind, rNs, rName string) error { + statusMap := map[string]interface{}{} + violationsMap := map[string]interface{}{} + var violations interface{} + // Get Policy + obj, err := b.client.GetResource("Policy", "", policy, "status") + if err != nil { + return err + } + unstr := obj.UnstructuredContent() + // get "status" subresource + status, ok := unstr["status"] + if !ok { + return nil + } + // status exists + // status is already present then we append violations + if statusMap, ok = status.(map[string]interface{}); !ok { + return errors.New("Unable to parse status subresource") + } + // get policy violations + violations, ok = statusMap["violations"] + if !ok { + return nil + } + violationsMap, ok = violations.(map[string]interface{}) + if !ok { + return errors.New("Unable to get status.violations subresource") } -} -//NewViolationFromEvent returns violation info from event -func NewViolationFromEvent(e *event.Info, pName, rKind, rName, rnamespace string) *Info { - return &Info{ - Policy: pName, - Violation: types.Violation{ - Kind: rKind, - Name: rName, - Namespace: rnamespace, - Reason: e.Reason, - Message: e.Message, - }, + // check if the resource has a violation + _, ok = violationsMap[BuildKey(rKind, rNs, rName)] + if !ok { + // no violation for this resource + return nil } + // remove the pair from the map + delete(violationsMap, BuildKey(rKind, rNs, rName)) + if len(violationsMap) == 0 { + delete(statusMap, "violations") + } else { + statusMap["violations"] = violationsMap + } + unstr["status"] = statusMap + + obj.SetUnstructuredContent(unstr) + // update the status sub-resource for policy + _, err = b.client.UpdateStatusResource("Policy", "", obj, false) + if err != nil { + return err + } + return nil } diff --git a/pkg/violation/util.go b/pkg/violation/util.go index e83b9916ab..3f525b53d2 100644 --- a/pkg/violation/util.go +++ b/pkg/violation/util.go @@ -11,8 +11,17 @@ const workqueueViolationName = "Policy-Violations" // Event Reason const violationEventResrouce = "Violation" -//ViolationInfo describes the policyviolation details +//Info describes the policyviolation details type Info struct { Policy string policytype.Violation } + +func (i Info) getKey() string { + return i.Kind + "/" + i.Namespace + "/" + i.Name +} + +//BuildKey returns the key format +func BuildKey(rKind, rNs, rName string) string { + return rKind + "/" + rNs + "/" + rName +} diff --git a/pkg/webhooks/deleteresource.go b/pkg/webhooks/deleteresource.go new file mode 100644 index 0000000000..eb39e6df36 --- /dev/null +++ b/pkg/webhooks/deleteresource.go @@ -0,0 +1,40 @@ +package webhooks + +import ( + "errors" + + engine "github.com/nirmata/kyverno/pkg/engine" + v1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/labels" +) + +func (ws *WebhookServer) removePolicyViolation(request *v1beta1.AdmissionRequest) error { + // Get the list of policies that apply on the resource + policies, err := ws.policyLister.List(labels.NewSelector()) + if err != nil { + // Unable to connect to policy Lister to access policies + return errors.New("Unable to connect to policy controller to access policies. Clean Up of Policy Violations is not being done") + } + for _, policy := range policies { + // check if policy has a rule for the admission request kind + if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { + continue + } + // get the details from the request + rname := request.Name + rns := request.Namespace + rkind := request.Kind.Kind + // check if the resource meets the policy Resource description + for _, rule := range policy.Spec.Rules { + ok := engine.ResourceMeetsDescription(request.Object.Raw, rule.ResourceDescription, request.Kind) + if ok { + // Check if the policy has a violation for this resource + err := ws.violationBuilder.ResourceRemoval(policy.Name, rkind, rns, rname) + if err != nil { + return err + } + } + } + } + return nil +} diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go new file mode 100644 index 0000000000..7df5f9a8c8 --- /dev/null +++ b/pkg/webhooks/mutation.go @@ -0,0 +1,113 @@ +package webhooks + +import ( + jsonpatch "github.com/evanphx/json-patch" + "github.com/golang/glog" + engine "github.com/nirmata/kyverno/pkg/engine" + "github.com/nirmata/kyverno/pkg/info" + v1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// HandleMutation handles mutating webhook admission request +func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { + + policies, err := ws.policyLister.List(labels.NewSelector()) + if err != nil { + // Unable to connect to policy Lister to access policies + glog.Error("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied") + glog.Warning(err) + return &v1beta1.AdmissionResponse{ + Allowed: true, + } + } + rname := engine.ParseNameFromObject(request.Object.Raw) + rns := engine.ParseNamespaceFromObject(request.Object.Raw) + rkind := engine.ParseKindFromObject(request.Object.Raw) + + var allPatches [][]byte + var annPatches []byte + policyInfos := []*info.PolicyInfo{} + for _, policy := range policies { + // check if policy has a rule for the admission request kind + if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { + continue + } + //TODO: HACK Check if an update of annotations + if checkIfOnlyAnnotationsUpdate(request) { + return &v1beta1.AdmissionResponse{ + Allowed: true, + } + } + policyInfo := info.NewPolicyInfo(policy.Name, + rkind, + rname, + rns, + policy.Spec.ValidationFailureAction) + + glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, rns, rname, request.UID, request.Operation) + + glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) + + policyPatches, ruleInfos := engine.Mutate(*policy, request.Object.Raw, request.Kind) + + policyInfo.AddRuleInfos(ruleInfos) + + if !policyInfo.IsSuccessful() { + glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns) + for _, r := range ruleInfos { + glog.Warningf("%s: %s\n", r.Name, r.Msgs) + } + } else { + // TODO + // // CleanUp Violations if exists + // err := ws.violationBuilder.RemoveInactiveViolation(policy.Name, request.Kind.Kind, rns, rname, info.Validation) + // if err != nil { + // glog.Info(err) + // } + allPatches = append(allPatches, policyPatches...) + glog.Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns) + } + policyInfos = append(policyInfos, policyInfo) + + annPatch := addAnnotationsToResource(request.Object.Raw, policyInfo, info.Mutation) + if annPatch != nil { + if annPatches == nil { + annPatches = annPatch + } else { + annPatches, err = jsonpatch.MergePatch(annPatches, annPatch) + if err != nil { + glog.Error(err) + } + } + } + } + + if len(allPatches) > 0 { + eventsInfo, _ := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Mutation) + ws.eventController.Add(eventsInfo...) + } + // add annotations + if annPatches != nil { + // fmt.Println(string(annPatches)) + ws.annotationsController.Add(rkind, rns, rname, annPatches) + } + + ok, msg := isAdmSuccesful(policyInfos) + if ok { + patchType := v1beta1.PatchTypeJSONPatch + return &v1beta1.AdmissionResponse{ + Allowed: true, + Patch: engine.JoinPatches(allPatches), + PatchType: &patchType, + } + } + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: msg, + }, + } +} diff --git a/pkg/webhooks/policyvalidation.go b/pkg/webhooks/policyvalidation.go new file mode 100644 index 0000000000..873e7e5bfa --- /dev/null +++ b/pkg/webhooks/policyvalidation.go @@ -0,0 +1,45 @@ +package webhooks + +import ( + "encoding/json" + "fmt" + + "github.com/golang/glog" + policyv1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kyverno/pkg/utils" + v1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//HandlePolicyValidation performs the validation check on policy resource +func (ws *WebhookServer) HandlePolicyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { + return ws.validateUniqueRuleName(request.Object.Raw) +} + +// Verify if the Rule names are unique within a policy +func (ws *WebhookServer) validateUniqueRuleName(rawPolicy []byte) *v1beta1.AdmissionResponse { + var policy *policyv1.Policy + var ruleNames []string + + json.Unmarshal(rawPolicy, &policy) + + for _, rule := range policy.Spec.Rules { + if utils.Contains(ruleNames, rule.Name) { + msg := fmt.Sprintf(`The policy "%s" is invalid: duplicate rule name: "%s"`, policy.Name, rule.Name) + glog.Errorln(msg) + + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: msg, + }, + } + } + ruleNames = append(ruleNames, rule.Name) + } + + glog.V(3).Infof("Policy validation passed") + return &v1beta1.AdmissionResponse{ + Allowed: true, + } +} diff --git a/pkg/webhooks/registration.go b/pkg/webhooks/registration.go index be5d79a882..b6e63124ad 100644 --- a/pkg/webhooks/registration.go +++ b/pkg/webhooks/registration.go @@ -139,7 +139,8 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati constructWebhook( config.MutatingWebhookName, config.MutatingWebhookServicePath, - caData), + caData, + false), }, }, nil } @@ -157,7 +158,8 @@ func (wrc *WebhookRegistrationClient) contructDebugMutatingWebhookConfig(caData constructDebugWebhook( config.MutatingWebhookName, url, - caData), + caData, + false), }, } } @@ -190,7 +192,8 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura constructWebhook( config.ValidatingWebhookName, config.ValidatingWebhookServicePath, - caData), + caData, + true), }, }, nil } @@ -208,7 +211,8 @@ func (wrc *WebhookRegistrationClient) contructDebugValidatingWebhookConfig(caDat constructDebugWebhook( config.ValidatingWebhookName, url, - caData), + caData, + true), }, } } @@ -241,7 +245,8 @@ func (wrc *WebhookRegistrationClient) contructPolicyValidatingWebhookConfig() (* constructWebhook( config.PolicyValidatingWebhookName, config.PolicyValidatingWebhookServicePath, - caData), + caData, + true), }, }, nil } @@ -259,12 +264,13 @@ func (wrc *WebhookRegistrationClient) contructDebugPolicyValidatingWebhookConfig constructDebugWebhook( config.PolicyValidatingWebhookName, url, - caData), + caData, + true), }, } } -func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook { +func constructWebhook(name, servicePath string, caData []byte, validation bool) admregapi.Webhook { resource := "*/*" apiGroups := "*" apiversions := "*" @@ -273,6 +279,15 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook apiGroups = "kyverno.io" apiversions = "v1alpha1" } + operationtypes := []admregapi.OperationType{ + admregapi.Create, + admregapi.Update, + } + // Add operation DELETE for validation + if validation { + operationtypes = append(operationtypes, admregapi.Delete) + + } return admregapi.Webhook{ Name: name, @@ -286,10 +301,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook }, Rules: []admregapi.RuleWithOperations{ admregapi.RuleWithOperations{ - Operations: []admregapi.OperationType{ - admregapi.Create, - admregapi.Update, - }, + Operations: operationtypes, Rule: admregapi.Rule{ APIGroups: []string{ apiGroups, @@ -306,7 +318,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook } } -func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook { +func constructDebugWebhook(name, url string, caData []byte, validation bool) admregapi.Webhook { resource := "*/*" apiGroups := "*" apiversions := "*" @@ -316,6 +328,14 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook { apiGroups = "kyverno.io" apiversions = "v1alpha1" } + operationtypes := []admregapi.OperationType{ + admregapi.Create, + admregapi.Update, + } + // Add operation DELETE for validation + if validation { + operationtypes = append(operationtypes, admregapi.Delete) + } return admregapi.Webhook{ Name: name, @@ -325,10 +345,7 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook { }, Rules: []admregapi.RuleWithOperations{ admregapi.RuleWithOperations{ - Operations: []admregapi.OperationType{ - admregapi.Create, - admregapi.Update, - }, + Operations: operationtypes, Rule: admregapi.Rule{ APIGroups: []string{ apiGroups, diff --git a/pkg/webhooks/report.go b/pkg/webhooks/report.go new file mode 100644 index 0000000000..7f9b4697af --- /dev/null +++ b/pkg/webhooks/report.go @@ -0,0 +1,77 @@ +package webhooks + +import ( + "strings" + + "github.com/nirmata/kyverno/pkg/annotations" + "github.com/nirmata/kyverno/pkg/violation" + + "github.com/golang/glog" + "github.com/nirmata/kyverno/pkg/event" + "github.com/nirmata/kyverno/pkg/info" +) + +//TODO: change validation from bool -> enum(validation, mutation) +func newEventInfoFromPolicyInfo(policyInfoList []*info.PolicyInfo, onUpdate bool, ruleType info.RuleType) ([]*event.Info, []*violation.Info) { + var eventsInfo []*event.Info + var violations []*violation.Info + ok, msg := isAdmSuccesful(policyInfoList) + // Some policies failed to apply succesfully + if !ok { + for _, pi := range policyInfoList { + if pi.IsSuccessful() { + continue + } + rules := pi.FailedRules() + ruleNames := strings.Join(rules, ";") + if !onUpdate { + // CREATE + eventsInfo = append(eventsInfo, + event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyApplyBlockCreate, pi.RName, ruleNames)) + + glog.V(3).Infof("Rule(s) %s of policy %s blocked resource creation, error: %s\n", ruleNames, pi.Name, msg) + } else { + // UPDATE + eventsInfo = append(eventsInfo, + event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.RequestBlocked, event.FPolicyApplyBlockUpdate, ruleNames, pi.Name)) + eventsInfo = append(eventsInfo, + event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyBlockResourceUpdate, pi.RName, ruleNames)) + glog.V(3).Infof("Request blocked events info has prepared for %s/%s and %s/%s\n", policyKind, pi.Name, pi.RKind, pi.RName) + } + // if report flag is set + if pi.ValidationFailureAction == ReportViolation && ruleType == info.Validation { + // Create Violations + v := violation.BuldNewViolation(pi.Name, pi.RKind, pi.RNamespace, pi.RName, event.PolicyViolation.String(), pi.GetFailedRules()) + violations = append(violations, v) + } + } + } else { + if !onUpdate { + // All policies were applied succesfully + // CREATE + for _, pi := range policyInfoList { + rules := pi.SuccessfulRules() + ruleNames := strings.Join(rules, ";") + eventsInfo = append(eventsInfo, + event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.PolicyApplied, event.SRulesApply, ruleNames, pi.Name)) + + glog.V(3).Infof("Success event info has prepared for %s/%s\n", pi.RKind, pi.RName) + } + } + } + return eventsInfo, violations +} + +func addAnnotationsToResource(rawResource []byte, pi *info.PolicyInfo, ruleType info.RuleType) []byte { + if len(pi.Rules) == 0 { + return nil + } + // get annotations + ann := annotations.ParseAnnotationsFromObject(rawResource) + ann, patch, err := annotations.AddPolicyJSONPatch(ann, pi, ruleType) + if err != nil { + glog.Error(err) + return nil + } + return patch +} diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index b38e3f9796..515b1fb4d7 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -8,35 +8,30 @@ import ( "fmt" "io/ioutil" "net/http" - "strings" "time" "github.com/golang/glog" - policyv1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kyverno/pkg/annotations" "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1" "github.com/nirmata/kyverno/pkg/config" client "github.com/nirmata/kyverno/pkg/dclient" - engine "github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/event" - "github.com/nirmata/kyverno/pkg/info" "github.com/nirmata/kyverno/pkg/sharedinformer" tlsutils "github.com/nirmata/kyverno/pkg/tls" - "github.com/nirmata/kyverno/pkg/utils" + "github.com/nirmata/kyverno/pkg/violation" v1beta1 "k8s.io/api/admission/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" ) -const policyKind = "Policy" - // WebhookServer contains configured TLS server with MutationWebhook. // MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient. type WebhookServer struct { - server http.Server - client *client.Client - policyLister v1alpha1.PolicyLister - eventController event.Generator - filterKinds []string + server http.Server + client *client.Client + policyLister v1alpha1.PolicyLister + eventController event.Generator + violationBuilder violation.Generator + annotationsController annotations.Controller + filterKinds []string } // NewWebhookServer creates new instance of WebhookServer accordingly to given configuration @@ -46,6 +41,8 @@ func NewWebhookServer( tlsPair *tlsutils.TlsPemPair, shareInformer sharedinformer.PolicyInformer, eventController event.Generator, + violationBuilder violation.Generator, + annotationsController annotations.Controller, filterKinds []string) (*WebhookServer, error) { if tlsPair == nil { @@ -60,10 +57,12 @@ func NewWebhookServer( tlsConfig.Certificates = []tls.Certificate{pair} ws := &WebhookServer{ - client: client, - policyLister: shareInformer.GetLister(), - eventController: eventController, - filterKinds: parseKinds(filterKinds), + client: client, + policyLister: shareInformer.GetLister(), + eventController: eventController, + violationBuilder: violationBuilder, + annotationsController: annotationsController, + filterKinds: parseKinds(filterKinds), } mux := http.NewServeMux() mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve) @@ -94,14 +93,30 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { // Do not process the admission requests for kinds that are in filterKinds for filtering if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) { + // if the resource is being deleted we need to clear any existing Policy Violations + // TODO: can report to the user that we clear the violation corresponding to this resource + if admissionReview.Request.Operation == v1beta1.Delete { + // Resource DELETE + err := ws.removePolicyViolation(admissionReview.Request) + if err != nil { + glog.Info(err) + } + admissionReview.Response = &v1beta1.AdmissionResponse{ + Allowed: true, + } + admissionReview.Response.UID = admissionReview.Request.UID + } else { + // Resource CREATE + // Resource UPDATE + switch r.URL.Path { + case config.MutatingWebhookServicePath: + admissionReview.Response = ws.HandleMutation(admissionReview.Request) + case config.ValidatingWebhookServicePath: + admissionReview.Response = ws.HandleValidation(admissionReview.Request) + case config.PolicyValidatingWebhookServicePath: + admissionReview.Response = ws.HandlePolicyValidation(admissionReview.Request) + } - switch r.URL.Path { - case config.MutatingWebhookServicePath: - admissionReview.Response = ws.HandleMutation(admissionReview.Request) - case config.ValidatingWebhookServicePath: - admissionReview.Response = ws.HandleValidation(admissionReview.Request) - case config.PolicyValidatingWebhookServicePath: - admissionReview.Response = ws.HandlePolicyValidation(admissionReview.Request) } } @@ -140,169 +155,6 @@ func (ws *WebhookServer) Stop() { } } -// HandleMutation handles mutating webhook admission request -func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - - policies, err := ws.policyLister.List(labels.NewSelector()) - if err != nil { - // Unable to connect to policy Lister to access policies - glog.Error("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied") - glog.Warning(err) - return &v1beta1.AdmissionResponse{ - Allowed: true, - } - } - - var allPatches [][]byte - policyInfos := []*info.PolicyInfo{} - for _, policy := range policies { - // check if policy has a rule for the admission request kind - if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { - continue - } - rname := engine.ParseNameFromObject(request.Object.Raw) - rns := engine.ParseNamespaceFromObject(request.Object.Raw) - rkind := engine.ParseKindFromObject(request.Object.Raw) - policyInfo := info.NewPolicyInfo(policy.Name, - rkind, - rname, - rns) - - glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, rns, rname, request.UID, request.Operation) - - glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) - - policyPatches, ruleInfos := engine.Mutate(*policy, request.Object.Raw, request.Kind) - - policyInfo.AddRuleInfos(ruleInfos) - - if !policyInfo.IsSuccessful() { - glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns) - for _, r := range ruleInfos { - glog.Warning(r.Msgs) - } - } else if len(policyPatches) > 0 { - allPatches = append(allPatches, policyPatches...) - glog.Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns) - } - policyInfos = append(policyInfos, policyInfo) - } - - if len(allPatches) > 0 { - eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update)) - ws.eventController.Add(eventsInfo...) - } - - ok, msg := isAdmSuccesful(policyInfos) - if ok { - patchType := v1beta1.PatchTypeJSONPatch - return &v1beta1.AdmissionResponse{ - Allowed: true, - Patch: engine.JoinPatches(allPatches), - PatchType: &patchType, - } - } - return &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: msg, - }, - } -} - -func isAdmSuccesful(policyInfos []*info.PolicyInfo) (bool, string) { - var admSuccess = true - var errMsgs []string - for _, pi := range policyInfos { - if !pi.IsSuccessful() { - admSuccess = false - errMsgs = append(errMsgs, fmt.Sprintf("\nPolicy %s failed with following rules", pi.Name)) - // Get the error rules - errorRules := pi.ErrorRules() - errMsgs = append(errMsgs, errorRules) - } - } - return admSuccess, strings.Join(errMsgs, ";") -} - -// 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) *v1beta1.AdmissionResponse { - policyInfos := []*info.PolicyInfo{} - - policies, err := ws.policyLister.List(labels.NewSelector()) - if err != nil { - // 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, - } - } - - for _, policy := range policies { - - if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { - continue - } - rname := engine.ParseNameFromObject(request.Object.Raw) - rns := engine.ParseNamespaceFromObject(request.Object.Raw) - rkind := engine.ParseKindFromObject(request.Object.Raw) - - policyInfo := info.NewPolicyInfo(policy.Name, - rkind, - rname, - rns) - - glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, rns, rname, request.UID, request.Operation) - - glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) - ruleInfos, err := engine.Validate(*policy, request.Object.Raw, request.Kind) - if err != nil { - // This is not policy error - // but if unable to parse request raw resource - // TODO : create event ? dont think so - glog.Error(err) - continue - } - policyInfo.AddRuleInfos(ruleInfos) - - if !policyInfo.IsSuccessful() { - glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns) - for _, r := range ruleInfos { - glog.Warning(r.Msgs) - } - } else if len(ruleInfos) > 0 { - glog.Infof("Validation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns) - } - policyInfos = append(policyInfos, policyInfo) - } - - if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 { - eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update)) - ws.eventController.Add(eventsInfo...) - - } - - // If Validation fails then reject the request - ok, msg := isAdmSuccesful(policyInfos) - if !ok { - return &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: msg, - }, - } - } - - return &v1beta1.AdmissionResponse{ - Allowed: true, - } - // Generation rules applied via generation controller -} - // bodyToAdmissionReview creates AdmissionReview object from request body // Answers to the http.ResponseWriter if request is not valid func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview { @@ -334,96 +186,3 @@ func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer htt return admissionReview } - -//HandlePolicyValidation performs the validation check on policy resource -func (ws *WebhookServer) HandlePolicyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - return ws.validateUniqueRuleName(request.Object.Raw) -} - -func (ws *WebhookServer) validateUniqueRuleName(rawPolicy []byte) *v1beta1.AdmissionResponse { - var policy *policyv1.Policy - var ruleNames []string - - json.Unmarshal(rawPolicy, &policy) - - for _, rule := range policy.Spec.Rules { - if utils.Contains(ruleNames, rule.Name) { - msg := fmt.Sprintf(`The policy "%s" is invalid: duplicate rule name: "%s"`, policy.Name, rule.Name) - glog.Errorln(msg) - - return &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: msg, - }, - } - } - ruleNames = append(ruleNames, rule.Name) - } - - glog.V(3).Infof("Policy validation passed") - return &v1beta1.AdmissionResponse{ - Allowed: true, - } -} - -func newEventInfoFromPolicyInfo(policyInfoList []*info.PolicyInfo, onUpdate bool) []*event.Info { - var eventsInfo []*event.Info - - ok, msg := isAdmSuccesful(policyInfoList) - // create events on operation UPDATE - if onUpdate { - if !ok { - for _, pi := range policyInfoList { - ruleNames := getRuleNames(*pi, false) - eventsInfo = append(eventsInfo, - event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.RequestBlocked, event.FPolicyApplyBlockUpdate, ruleNames, pi.Name)) - - eventsInfo = append(eventsInfo, - event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyBlockResourceUpdate, pi.RName, ruleNames)) - - glog.V(3).Infof("Request blocked events info has prepared for %s/%s and %s/%s\n", policyKind, pi.Name, pi.RKind, pi.RName) - } - } - return eventsInfo - } - - // create events on operation CREATE - if ok { - for _, pi := range policyInfoList { - ruleNames := getRuleNames(*pi, true) - - eventsInfo = append(eventsInfo, - event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.PolicyApplied, event.SRulesApply, ruleNames, pi.Name)) - - glog.V(3).Infof("Success event info has prepared for %s/%s\n", pi.RKind, pi.RName) - } - return eventsInfo - } - - for _, pi := range policyInfoList { - ruleNames := getRuleNames(*pi, false) - - eventsInfo = append(eventsInfo, - event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyApplyBlockCreate, pi.RName, ruleNames)) - - glog.V(3).Infof("Rule(s) %s of policy %s blocked resource creation, error: %s\n", ruleNames, pi.Name, msg) - } - return eventsInfo -} - -func getRuleNames(policyInfo info.PolicyInfo, onSuccess bool) string { - var ruleNames []string - for _, rule := range policyInfo.Rules { - if onSuccess { - if rule.IsSuccessful() { - ruleNames = append(ruleNames, rule.Name) - } - } else { - if !rule.IsSuccessful() { - ruleNames = append(ruleNames, rule.Name) - } - } - } - return strings.Join(ruleNames, ",") -} diff --git a/pkg/webhooks/utils.go b/pkg/webhooks/utils.go index 116c06053a..238222216d 100644 --- a/pkg/webhooks/utils.go +++ b/pkg/webhooks/utils.go @@ -1,11 +1,34 @@ package webhooks import ( + "fmt" + "reflect" "strings" + "github.com/golang/glog" "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kyverno/pkg/info" + v1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +const policyKind = "Policy" + +func isAdmSuccesful(policyInfos []*info.PolicyInfo) (bool, string) { + var admSuccess = true + var errMsgs []string + for _, pi := range policyInfos { + if !pi.IsSuccessful() { + admSuccess = false + errMsgs = append(errMsgs, fmt.Sprintf("\nPolicy %s failed with following rules", pi.Name)) + // Get the error rules + errorRules := pi.ErrorRules() + errMsgs = append(errMsgs, errorRules) + } + } + return admSuccess, strings.Join(errMsgs, ";") +} + //StringInSlice checks if string is present in slice of strings func StringInSlice(kind string, list []string) bool { for _, b := range list { @@ -63,3 +86,50 @@ func getApplicableKindsForPolicy(p *v1alpha1.Policy) []string { } return kinds } + +// Policy Reporting Modes +const ( + BlockChanges = "block" + ReportViolation = "report" +) + +// 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 toBlock(pis []*info.PolicyInfo) bool { + for _, pi := range pis { + if pi.ValidationFailureAction != ReportViolation { + return true + } + } + return false +} + +func checkIfOnlyAnnotationsUpdate(request *v1beta1.AdmissionRequest) bool { + // process only if its for existing resources + if request.Operation != v1beta1.Update { + return false + } + // updated resoruce + obj := request.Object + objUnstr := unstructured.Unstructured{} + err := objUnstr.UnmarshalJSON(obj.Raw) + if err != nil { + glog.Error(err) + return false + } + objUnstr.SetAnnotations(nil) + objUnstr.SetGeneration(0) + oldobj := request.OldObject + oldobjUnstr := unstructured.Unstructured{} + err = oldobjUnstr.UnmarshalJSON(oldobj.Raw) + if err != nil { + glog.Error(err) + return false + } + oldobjUnstr.SetAnnotations(nil) + oldobjUnstr.SetGeneration(0) + if reflect.DeepEqual(objUnstr, oldobjUnstr) { + return true + } + return false +} diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go new file mode 100644 index 0000000000..5c90c92c12 --- /dev/null +++ b/pkg/webhooks/validation.go @@ -0,0 +1,126 @@ +package webhooks + +import ( + jsonpatch "github.com/evanphx/json-patch" + "github.com/golang/glog" + engine "github.com/nirmata/kyverno/pkg/engine" + "github.com/nirmata/kyverno/pkg/info" + v1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// 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) *v1beta1.AdmissionResponse { + policyInfos := []*info.PolicyInfo{} + policies, err := ws.policyLister.List(labels.NewSelector()) + if err != nil { + // 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, + } + } + + rname := engine.ParseNameFromObject(request.Object.Raw) + rns := engine.ParseNamespaceFromObject(request.Object.Raw) + rkind := engine.ParseKindFromObject(request.Object.Raw) + + var annPatches []byte + for _, policy := range policies { + + if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { + continue + } + //TODO: HACK Check if an update of annotations + if checkIfOnlyAnnotationsUpdate(request) { + // allow the update of resource to add annotations + return &v1beta1.AdmissionResponse{ + Allowed: true, + } + } + + policyInfo := info.NewPolicyInfo(policy.Name, + rkind, + rname, + rns, + policy.Spec.ValidationFailureAction) + + glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, rns, rname, request.UID, request.Operation) + + glog.Infof("Validating resource %s/%s/%s with policy %s with %d rules", rkind, rns, rname, policy.ObjectMeta.Name, len(policy.Spec.Rules)) + ruleInfos, err := engine.Validate(*policy, request.Object.Raw, request.Kind) + if err != nil { + // This is not policy error + // but if unable to parse request raw resource + // TODO : create event ? dont think so + glog.Error(err) + continue + } + policyInfo.AddRuleInfos(ruleInfos) + + if !policyInfo.IsSuccessful() { + glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns) + for _, r := range ruleInfos { + glog.Warningf("%s: %s\n", r.Name, r.Msgs) + } + } else { + // CleanUp Violations if exists + err := ws.violationBuilder.RemoveInactiveViolation(policy.Name, request.Kind.Kind, rns, rname, info.Validation) + if err != nil { + glog.Info(err) + } + + if len(ruleInfos) > 0 { + glog.Infof("Validation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns) + } + } + policyInfos = append(policyInfos, policyInfo) + // annotations + annPatch := addAnnotationsToResource(request.Object.Raw, policyInfo, info.Validation) + if annPatch != nil { + if annPatches == nil { + annPatches = annPatch + } else { + annPatches, err = jsonpatch.MergePatch(annPatches, annPatch) + if err != nil { + glog.Error(err) + } + } + } + } + + if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 { + eventsInfo, violations := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Validation) + // If the validationFailureAction flag is set "report", + // then we dont block the request and report the violations + ws.violationBuilder.Add(violations...) + ws.eventController.Add(eventsInfo...) + } + // add annotations + if annPatches != nil { + ws.annotationsController.Add(rkind, rns, rname, annPatches) + } + // If Validation fails then reject the request + ok, msg := isAdmSuccesful(policyInfos) + // violations are created if "report" flag is set + // and if there are any then we dont bock the resource creation + // Even if one the policy being applied + + if !ok && toBlock(policyInfos) { + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: msg, + }, + } + } + + return &v1beta1.AdmissionResponse{ + Allowed: true, + } + // Generation rules applied via generation controller +} diff --git a/test/StatefulSet/policy-StatefulSet.yaml b/test/StatefulSet/policy-StatefulSet.yaml index 2460a1235c..00b31b3225 100644 --- a/test/StatefulSet/policy-StatefulSet.yaml +++ b/test/StatefulSet/policy-StatefulSet.yaml @@ -28,7 +28,7 @@ spec: message: "This SS is broken" pattern: spec: - replicas: ">20" + replicas: ">2" volumeClaimTemplates: - metadata: name: www