From bd9e8585c7379c20cce6f13326437c348b3fb6f4 Mon Sep 17 00:00:00 2001 From: shivkumar dudhani Date: Wed, 17 Jul 2019 15:04:02 -0700 Subject: [PATCH] annotations v1 --- pkg/annotations/annotations.go | 220 ++++++++++++++++++++++++++++ pkg/annotations/annotations_test.go | 25 ++++ pkg/dclient/client.go | 7 +- pkg/dclient/utils.go | 215 +++++++++++++++++++++++++++ pkg/kyverno/apply/apply.go | 3 +- pkg/kyverno/apply/util.go | 2 +- pkg/webhooks/mutation.go | 38 ++++- 7 files changed, 506 insertions(+), 4 deletions(-) create mode 100644 pkg/annotations/annotations.go create mode 100644 pkg/annotations/annotations_test.go diff --git a/pkg/annotations/annotations.go b/pkg/annotations/annotations.go new file mode 100644 index 0000000000..dd68f1c91e --- /dev/null +++ b/pkg/annotations/annotations.go @@ -0,0 +1,220 @@ +package annotations + +import ( + "encoding/json" + + "github.com/golang/glog" + + "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"` + Rules []Rule `json:"rules,omitempty"` +} + +//Rule information for annotations +type Rule struct { + Name string `json:"name"` + Status string `json:"status"` + Type string `json:"type"` + Changes string `json:"changes"` +} + +func getStatus(status bool) string { + if status { + return "Success" + } + return "Failure" +} +func getRules(rules []*info.RuleInfo) []Rule { + var annrules []Rule + for _, r := range rules { + annrule := Rule{Name: r.Name, + Status: getStatus(r.IsSuccessful()), + Type: r.RuleType.String()} + //TODO: add mutation changes in policyInfo and in annotation + annrules = append(annrules, annrule) + } + return annrules +} + +func (p *Policy) updatePolicy(obj *Policy, ruleType info.RuleType) { + p.Status = obj.Status + p.updatePolicyRules(obj.Rules, ruleType) +} + +// Update rules of a given type +func (p *Policy) updatePolicyRules(rules []Rule, ruleType info.RuleType) { + var updatedRules []Rule + //TODO: check the selecting update add advantage + // filter rules for different type + for _, r := range rules { + if r.Type != ruleType.String() { + updatedRules = append(updatedRules, r) + } + } + // Add rules for current type + updatedRules = append(updatedRules, rules...) + // set the rule + p.Rules = updatedRules +} + +// func (p *Policy) containsPolicyRules(rules []Rule, ruleType info.RuleType) { +// for _, r := range rules { +// } +// } +func newAnnotationForPolicy(pi *info.PolicyInfo) *Policy { + return &Policy{Status: getStatus(pi.IsSuccessful()), + Rules: getRules(pi.Rules)} +} + +//AddPolicy will add policy annotation if not present or update if present +func AddPolicy(obj *unstructured.Unstructured, pi *info.PolicyInfo, ruleType info.RuleType) error { + PolicyObj := newAnnotationForPolicy(pi) + // get annotation + ann := obj.GetAnnotations() + // check if policy already has annotation + cPolicy, ok := ann[pi.Name] + if !ok { + PolicyByte, err := json.Marshal(PolicyObj) + if err != nil { + return err + } + // insert policy information + ann[pi.Name] = string(PolicyByte) + // set annotation back to unstr + obj.SetAnnotations(ann) + return nil + } + cPolicyObj := Policy{} + err := json.Unmarshal([]byte(cPolicy), &cPolicyObj) + // update policy information inside the annotation + // 1> policy status + // 2> rule (name, status,changes,type) + cPolicyObj.updatePolicy(PolicyObj, ruleType) + if err != nil { + return err + } + cPolicyByte, err := json.Marshal(cPolicyObj) + if err != nil { + return err + } + // update policy information + ann[pi.Name] = string(cPolicyByte) + // set annotation back to unstr + obj.SetAnnotations(ann) + return nil +} + +//RemovePolicy to remove annotations fro +func RemovePolicy(obj *unstructured.Unstructured, policy string) { + // get annotations + ann := obj.GetAnnotations() + delete(ann, policy) + // set annotation back to unstr + obj.SetAnnotations(ann) +} + +//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 + } + if annotations, ok := meta["annotations"].(map[string]string); ok { + return annotations + } + return nil +} + +//AddPolicyJSONPatch generate JSON Patch to add policy informatino JSON patch +func AddPolicyJSONPatch(ann map[string]string, pi *info.PolicyInfo, ruleType info.RuleType) ([]byte, error) { + if ann == nil { + ann = make(map[string]string, 0) + } + PolicyObj := newAnnotationForPolicy(pi) + cPolicy, ok := ann[pi.Name] + if !ok { + PolicyByte, err := json.Marshal(PolicyObj) + if err != nil { + return nil, err + } + // insert policy information + ann[pi.Name] = string(PolicyByte) + // create add JSON patch + return createAddJSONPatch(ann) + } + cPolicyObj := Policy{} + err := json.Unmarshal([]byte(cPolicy), &cPolicyObj) + // update policy information inside the annotation + // 1> policy status + // 2> rule (name, status,changes,type) + cPolicyObj.updatePolicy(PolicyObj, ruleType) + if err != nil { + return nil, err + } + cPolicyByte, err := json.Marshal(cPolicyObj) + if err != nil { + return nil, err + } + // update policy information + ann[pi.Name] = string(cPolicyByte) + // create update JSON patch + return createReplaceJSONPatch(ann) +} + +//RemovePolicyJSONPatch remove JSON patch +func RemovePolicyJSONPatch(ann map[string]string, policy string) ([]byte, error) { + if ann == nil { + return nil, nil + } + delete(ann, policy) + if len(ann) == 0 { + return createRemoveJSONPatch(ann) + } + return createReplaceJSONPatch(ann) +} + +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..64dd25c652 --- /dev/null +++ b/pkg/annotations/annotations_test.go @@ -0,0 +1,25 @@ +package annotations + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/nirmata/kyverno/pkg/info" +) + +func TestAddPatch(t *testing.T) { + 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 := info.PolicyInfo{} + err := json.Unmarshal(piRaw, &pi) + if err != nil { + panic(err) + } + patch, err := AddPolicyJSONPatch(ann, &pi, info.Mutation) + if err != nil { + panic(err) + } + fmt.Println(string(patch)) +} 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/dclient/utils.go b/pkg/dclient/utils.go index 6492187038..c982c24574 100644 --- a/pkg/dclient/utils.go +++ b/pkg/dclient/utils.go @@ -1,9 +1,12 @@ package client import ( + "errors" "strings" "time" + "github.com/golang/glog" + "github.com/nirmata/kyverno/pkg/info" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -110,3 +113,215 @@ func retry(attempts int, sleep time.Duration, fn func() error) error { type stop struct { error } + +func GetAnnotations(obj *unstructured.Unstructured) map[string]interface{} { + var annotationsMaps map[string]interface{} + unstr := obj.UnstructuredContent() + metadata, ok := unstr["metadata"] + if ok { + metadataMap, ok := metadata.(map[string]interface{}) + if !ok { + glog.Info("type mismatch") + return nil + } + annotations, ok := metadataMap["annotations"] + if !ok { + glog.Info("annotations not present") + return nil + } + annotationsMaps, ok = annotations.(map[string]interface{}) + if !ok { + glog.Info("type mismatch") + return nil + } + } + return annotationsMaps +} + +func SetAnnotations(obj *unstructured.Unstructured, annotations map[string]interface{}) error { + unstr := obj.UnstructuredContent() + metadata, ok := unstr["metadata"] + if ok { + metadataMap, ok := metadata.(map[string]interface{}) + if !ok { + return errors.New("type mismatch") + } + metadataMap["annotations"] = annotations + unstr["metadata"] = metadataMap + obj.SetUnstructuredContent(unstr) + } + return nil +} + +type AnnotationPolicies struct { + // map[policy_name] + Policies map[string]AnnotationPolicy `json:"policies"` +} + +type AnnotationPolicy struct { + Status string `json:"status"` + Rules []AnnotationRule `json:"rules,omitempty"` +} + +type AnnotationRule struct { + Name string `json:"name"` + Status string `json:"status"` + Type string `json:"type"` + Changes string `json:"changes"` +} + +func getStatus(status bool) string { + if status { + return "Success" + } + return "Failure" +} + +func getRules(rules []*info.RuleInfo) []AnnotationRule { + var annrules []AnnotationRule + for _, r := range rules { + annrule := AnnotationRule{Name: r.Name, + Status: getStatus(r.IsSuccessful())} + //TODO: add mutation changes in policyInfo and in annotation + annrules = append(annrules, annrule) + } + return annrules +} + +// input rules can be mutation or validation +func (ap AnnotationPolicy) updateRules(rules interface{}, validation bool) (error, interface{}) { + ruleList, ok := rules.([]interface{}) + updated := false + if !ok { + return errors.New("type mismatch"), false + } + + // for mutation rule check if the rules are same + // var mode string + // if validation { + // mode = "Validation" + // } else { + // mode = "Mutation" + // } + // // if lengths are differrent then update + // if len(ruleList) != len(ap.Rules) { + // return nil, ap.updateRules + // } + // check if there is any update in the rules + // order of rules is assumed same while comparison + for i, r := range ruleList { + rule, ok := r.(map[string]interface{}) + if !ok { + return errors.New("type mismatch"), nil + } + // Name + name, ok := rule["name"].(string) + if !ok { + return errors.New("type mismatch"), nil + } + if name != ap.Rules[i].Name { + updated = true + break + } + // Status + status, ok := rule["status"].(string) + if !ok { + return errors.New("type mismatch"), nil + } + if status != ap.Rules[i].Status { + updated = true + break + } + } + if updated { + return nil, ap.Rules + } + return nil, nil +} + +func newAnnotationPolicy(pi *info.PolicyInfo) AnnotationPolicy { + status := getStatus(pi.IsSuccessful()) + rules := getRules(pi.Rules) + return AnnotationPolicy{Status: status, + Rules: rules} +} + +//func GetPolicies(policies interface{}) map[string] +func AddPolicy(pi *info.PolicyInfo, ann map[string]interface{}, validation bool) (error, map[string]interface{}) { + // Lets build the policy annotation struct from policyInfo + annpolicy := newAnnotationPolicy(pi) + // Add policy to annotations + // If policy does not exist -> Add + // If already exists then update the status and rules + policies, ok := ann["policies"] + if ok { + policiesMap, ok := policies.(map[string]interface{}) + if !ok { + glog.Info("type mismatch") + return errors.New("type mismatch"), nil + } + // check if policy record is present + policy, ok := policiesMap[pi.Name] + if !ok { + // not present then we add + policiesMap[pi.Name] = annpolicy + ann["policies"] = policiesMap + return nil, ann + } + policyMap, ok := policy.(map[string]interface{}) + if !ok { + return errors.New("type mismatch"), nil + } + // We just update the annotations + // status + status := policyMap["status"] + statusStr, ok := status.(string) + if !ok { + return errors.New("type mismatch"), nil + } + if statusStr != annpolicy.Status { + policyMap["status"] = annpolicy.Status + } + // check rules + rules, ok := policyMap["rules"] + if !ok { + return errors.New("no rules"), nil + } + err, newRules := annpolicy.updateRules(rules, validation) + if err != nil { + return err, nil + } + if newRules == nil { + //nothing to update + return nil, nil + } + // update the new rule + policyMap["rules"] = newRules + // update policies map + policiesMap[pi.Name] = policyMap + ann["policies"] = policiesMap + return nil, ann + } + return nil, nil +} + +// RemovePolicy +func RemovePolicy(pi *info.PolicyInfo, ann map[string]interface{}) (error, map[string]interface{}) { + policies, ok := ann["policies"] + if ok { + policiesMap, ok := policies.(map[string]interface{}) + if !ok { + glog.Info("type mismatch") + return errors.New("type mismatch"), nil + } + // check if policy record is present + _, ok = policiesMap[pi.Name] + if ok { + // delete the pair + delete(policiesMap, pi.Name) + ann["policies"] = policiesMap + return nil, ann + } + } + return nil, nil +} 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/webhooks/mutation.go b/pkg/webhooks/mutation.go index 213ae501d6..d57d18528f 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -1,7 +1,12 @@ package webhooks import ( + "fmt" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/golang/glog" + "github.com/nirmata/kyverno/pkg/annotations" engine "github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/info" v1beta1 "k8s.io/api/admission/v1beta1" @@ -23,6 +28,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be } var allPatches [][]byte + var annPatches []byte policyInfos := []*info.PolicyInfo{} for _, policy := range policies { // check if policy has a rule for the admission request kind @@ -65,6 +71,18 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be } } 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 { + fmt.Println("Mergining docs") + fmt.Println(err) + } + } + } } if len(allPatches) > 0 { @@ -74,10 +92,17 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be ok, msg := isAdmSuccesful(policyInfos) if ok { + patches := engine.JoinPatches(allPatches) + if len(annPatches) > 0 { + patches, err = jsonpatch.MergePatch(patches, annPatches) + if err != nil { + fmt.Println(err) + } + } patchType := v1beta1.PatchTypeJSONPatch return &v1beta1.AdmissionResponse{ Allowed: true, - Patch: engine.JoinPatches(allPatches), + Patch: patches, PatchType: &patchType, } } @@ -88,3 +113,14 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be }, } } + +func addAnnotationsToResource(rawResource []byte, pi *info.PolicyInfo, ruleType info.RuleType) []byte { + // get annotations + ann := annotations.ParseAnnotationsFromObject(rawResource) + patch, err := annotations.AddPolicyJSONPatch(ann, pi, ruleType) + if err != nil { + fmt.Println(err) + return nil + } + return patch +}