diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 8f1112a28c..1de615622c 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -5,16 +5,19 @@ import ( kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" "github.com/nirmata/kyverno/pkg/info" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - // "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Mutate performs mutation. Overlay first and then mutation patches +//TODO: check if gvk needs to be passed or can be set in resource func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) { - // - - var allPatches [][]byte - patchedDocument := rawResource - ris := []*info.RuleInfo{} + //TODO: convert rawResource to unstructured to avoid unmarhalling all the time for get some resource information + //TODO: pass unstructured instead of rawResource ? + resource, err := convertToUnstructured(rawResource) + if err != nil { + glog.Errorf("unable to convert raw resource to unstructured: %v", err) + } + var patches [][]byte + var ruleInfos []*info.RuleInfo for _, rule := range policy.Spec.Rules { if rule.Mutation == nil { @@ -24,56 +27,57 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio // check if the resource satisfies the filter conditions defined in the rule //TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that // dont statisfy a policy rule resource description - ok := ResourceMeetsDescription(rawResource, rule.MatchResources.ResourceDescription, rule.ExcludeResources.ResourceDescription, gvk) + ok := MatchesResourceDescription(resource, rule, gvk) if !ok { - name := ParseNameFromObject(rawResource) - namespace := ParseNamespaceFromObject(rawResource) - glog.V(3).Infof("resource %s/%s does not satisfy the resource description for the rule ", namespace, name) + glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule ", resource.GetNamespace(), resource.GetName()) continue } - ri := info.NewRuleInfo(rule.Name, info.Mutation) + + ruleInfo := info.NewRuleInfo(rule.Name, info.Mutation) // Process Overlay if rule.Mutation.Overlay != nil { - overlayPatches, err := processOverlay(rule, rawResource, gvk) + oPatches, err := processOverlay(resource, rule, rawResource) if err == nil { - if len(overlayPatches) == 0 { + if len(oPatches) == 0 { // if array elements dont match then we skip(nil patch, no error) // or if acnohor is defined and doenst match // policy is not applicable + glog.V(4).Info("overlay does not match, so skipping applying rule") continue } - glog.V(4).Infof("overlay applied succesfully on resource") - ri.Add("Overlay succesfully applied") - patch := JoinPatches(overlayPatches) - allPatches = append(allPatches, overlayPatches...) + + glog.V(4).Infof("overlay applied succesfully on resource %s/%s", resource.GetNamespace(), resource.GetName()) + ruleInfo.Add("Overlay succesfully applied") + // update rule information // strip slashes from string - ri.Changes = string(patch) + patch := JoinPatches(oPatches) + ruleInfo.Changes = string(patch) + patches = append(patches, oPatches...) } else { glog.V(4).Infof("failed to apply overlay: %v", err) - ri.Fail() - ri.Addf("failed to apply overlay: %v", err) + ruleInfo.Fail() + ruleInfo.Addf("failed to apply overlay: %v", err) } } // Process Patches if len(rule.Mutation.Patches) != 0 { - rulePatches, errs := processPatches(rule, patchedDocument) + jsonPatches, errs := processPatches(rule, rawResource) if len(errs) > 0 { - ri.Fail() + ruleInfo.Fail() for _, err := range errs { glog.V(4).Infof("failed to apply patches: %v", err) - ri.Addf("patches application has failed, err %v.", err) + ruleInfo.Addf("patches application has failed, err %v.", err) } } else { - glog.V(4).Infof("patches applied succesfully on resource") - ri.Addf("Patches succesfully applied.") - allPatches = append(allPatches, rulePatches...) + glog.V(4).Infof("patches applied succesfully on resource %s/%s", resource.GetNamespace(), resource.GetName()) + ruleInfo.Addf("Patches succesfully applied.") + patches = append(patches, jsonPatches...) } } - ris = append(ris, ri) + ruleInfos = append(ruleInfos, ruleInfo) } - - return allPatches, ris + return patches, ruleInfos } diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index fb4c556471..62af73e867 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -12,22 +12,30 @@ import ( jsonpatch "github.com/evanphx/json-patch" kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // ProcessOverlay handles validating admission request // Checks the target resources for rules defined in the policy -func processOverlay(rule kubepolicy.Rule, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, error) { +func processOverlay(resourceUnstr *unstructured.Unstructured, rule kubepolicy.Rule, rawResource []byte) ([][]byte, error) { + //TODO check if there is better solution + resourceRaw, err := resourceUnstr.MarshalJSON() + if err != nil { + glog.V(4).Infof("unable to marshal resource : %v", err) + return nil, err + } var resource interface{} - if err := json.Unmarshal(rawResource, &resource); err != nil { + if err := json.Unmarshal(resourceRaw, &resource); err != nil { + glog.V(4).Infof("unable to unmarshal resource : %v", err) return nil, err } - resourceInfo := ParseResourceInfoFromObject(rawResource) + // resourceInfo := ParseResourceInfoFromObject(rawResource) patches, err := processOverlayPatches(resource, *rule.Mutation.Overlay) if err != nil && strings.Contains(err.Error(), "Conditions are not met") { - glog.Infof("Resource does not meet conditions in overlay pattern, resource=%s, rule=%s\n", resourceInfo, rule.Name) + glog.V(4).Infof("overlay pattern %s does not match resource %s/%s", *rule.Mutation.Overlay, resourceUnstr.GetNamespace(), resourceUnstr.GetName()) + // glog.Infof("Resource does not meet conditions in overlay pattern, resource=%s, rule=%s\n", resourceInfo, rule.Name) return nil, nil } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index ecb03f2eac..ba9c372a61 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -150,6 +150,66 @@ func excludeNamespaces(namespaces []string, excludeNs *string) []string { return filteredNamespaces } +//MatchesResourceDescription checks if the resource matches resource desription of the rule or not +func MatchesResourceDescription(resource *unstructured.Unstructured, rule v1alpha1.Rule, gvk metav1.GroupVersionKind) bool { + matches := rule.MatchResources.ResourceDescription + exclude := rule.ExcludeResources.ResourceDescription + + if !findKind(matches.Kinds, gvk.Kind) { + return false + } + + // meta := parseMetadataFromObject(resourceRaw) + name := resource.GetName() + namespace := resource.GetNamespace() + + if matches.Name != nil { + // Matches + if !wildcard.Match(*matches.Name, name) { + return false + } + } + // Exclude + // the resource name matches the exclude resource name then reject + if exclude.Name != nil { + if wildcard.Match(*exclude.Name, name) { + return false + } + } + // Matches + if matches.Namespace != nil && *matches.Namespace != namespace { + return false + } + // Exclude + if exclude.Namespace != nil && *exclude.Namespace == namespace { + return false + } + // Matches + if matches.Selector != nil { + selector, err := metav1.LabelSelectorAsSelector(matches.Selector) + if err != nil { + glog.Error(err) + return false + } + if !selector.Matches(labels.Set(resource.GetLabels())) { + return false + } + } + // Exclude + if exclude.Selector != nil { + selector, err := metav1.LabelSelectorAsSelector(exclude.Selector) + // if the label selector is incorrect, should be fail or + if err != nil { + glog.Error(err) + return false + } + if selector.Matches(labels.Set(resource.GetLabels())) { + return false + } + } + return true +} + // ResourceMeetsDescription checks requests kind, name and labels to fit the policy rule func ResourceMeetsDescription(resourceRaw []byte, matches v1alpha1.ResourceDescription, exclude v1alpha1.ResourceDescription, gvk metav1.GroupVersionKind) bool { if !findKind(matches.Kinds, gvk.Kind) { @@ -458,3 +518,13 @@ type resourceInfo struct { Resource unstructured.Unstructured Gvk *metav1.GroupVersionKind } + +func convertToUnstructured(data []byte) (*unstructured.Unstructured, error) { + resource := &unstructured.Unstructured{} + err := resource.UnmarshalJSON(data) + if err != nil { + glog.V(4).Infof("failed to unmarshall resource: %v", err) + return nil, err + } + return resource, nil +} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 92a98e2451..aa49d68f7f 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -18,38 +18,53 @@ import ( // Validate handles validating admission request // Checks the target resources for rules defined in the policy func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([]*info.RuleInfo, error) { - var resource interface{} - ris := []*info.RuleInfo{} - - err := json.Unmarshal(rawResource, &resource) + //TODO: convert rawResource to unstructured to avoid unmarhalling all the time for get some resource information + //TODO: pass unstructured instead of rawResource ? + resourceUnstr, err := convertToUnstructured(rawResource) if err != nil { + glog.Errorf("unable to convert raw resource to unstructured: %v", err) + } + resourceRaw, err := resourceUnstr.MarshalJSON() + if err != nil { + glog.V(4).Infof("unable to marshal resource : %v", err) return nil, err } + var resource interface{} + if err := json.Unmarshal(resourceRaw, &resource); err != nil { + glog.V(4).Infof("unable to unmarshal resource : %v", err) + return nil, err + } + + var ruleInfos []*info.RuleInfo for _, rule := range policy.Spec.Rules { if rule.Validation == nil { continue } - ri := info.NewRuleInfo(rule.Name, info.Validation) - ok := ResourceMeetsDescription(rawResource, rule.MatchResources.ResourceDescription, rule.ExcludeResources.ResourceDescription, gvk) + // check if the resource satisfies the filter conditions defined in the rule + //TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that + // dont statisfy a policy rule resource description + ok := MatchesResourceDescription(resourceUnstr, rule, gvk) if !ok { - glog.V(3).Infof("Not applicable on specified resource kind%s", gvk.Kind) + glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule ", resourceUnstr.GetNamespace(), resourceUnstr.GetName()) continue } + ruleInfo := info.NewRuleInfo(rule.Name, info.Validation) + err := validateResourceWithPattern(resource, rule.Validation.Pattern) if err != nil { - ri.Fail() - ri.Addf("validation has failed, err %v.", err) + ruleInfo.Fail() + ruleInfo.Addf("Failed to apply pattern: %v.", err) } else { - ri.Addf("Rule %s: Validation succesfully.", rule.Name) - + ruleInfo.Add("Pattern succesfully validated") + glog.V(4).Infof("pattern validated succesfully on resource %s/%s", resourceUnstr.GetNamespace(), resourceUnstr.GetName()) } - ris = append(ris, ri) + ruleInfos = append(ruleInfos, ruleInfo) } - return ris, nil + return ruleInfos, nil } // validateResourceWithPattern is a start of element-by-element validation process diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index 82c5f01aac..db94cab820 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -7,16 +7,19 @@ import ( v1beta1 "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" ) // HandleMutation handles mutating webhook admission request func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - + var patches [][]byte + var policyInfos []*info.PolicyInfo glog.V(4).Infof("Receive request in mutating webhook: Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) policies, err := ws.policyLister.List(labels.NewSelector()) if err != nil { + //TODO check if the CRD is created ? // 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) @@ -24,74 +27,58 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be Allowed: true, } } - rname := engine.ParseNameFromObject(request.Object.Raw) - rns := engine.ParseNamespaceFromObject(request.Object.Raw) - rkind := request.Kind.Kind - if rkind == "" { - glog.Errorf("failed to parse KIND from request: Namespace=%s Name=%s UID=%s patchOperation=%s\n", request.Namespace, request.Name, request.UID, request.Operation) - } - var allPatches [][]byte - policyInfos := []*info.PolicyInfo{} + resource, err := convertToUnstructured(request.Object.Raw) + if err != nil { + glog.Errorf("unable to convert raw resource to unstructured: %v", err) + } + //TODO: check if resource gvk is available in raw resource, + // if not then set it from the api request + resource.SetGroupVersionKind(schema.GroupVersionKind{Group: request.Kind.Group, Version: request.Kind.Version, Kind: request.Kind.Kind}) + //TODO: check if the name and namespace is also passed right in the resource? + // all the patches to be applied on the resource + 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, + resource.GetKind(), + resource.GetName(), + resource.GetNamespace(), 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)) + glog.V(4).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + resource.GetKind(), resource.GetNamespace(), resource.GetName(), request.UID, request.Operation) + glog.V(4).Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) + //TODO: passing policy value as we dont wont to modify the policy 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 { - // CleanUp Violations if exists - err := ws.violationBuilder.RemoveInactiveViolation(policy.Name, request.Kind.Kind, rns, rname, info.Mutation) - 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 { - // add annotations - ws.annotationsController.Add(rkind, rns, rname, annPatch) + if !policyInfo.IsSuccessful() { + glog.V(4).Infof("Failed to apply policy %s on resource %s/%s", policy.Name, resource.GetNamespace(), resource.GetName()) + glog.V(4).Info("Failed rule details") + for _, r := range ruleInfos { + glog.V(4).Infof("%s: %s\n", r.Name, r.Msgs) + } + continue } + patches = append(patches, policyPatches...) + glog.V(4).Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, resource.GetNamespace(), resource.GetName()) } - if len(allPatches) > 0 { - eventsInfo, _ := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Mutation) - ws.eventController.Add(eventsInfo...) - } + // ADD ANNOTATIONS + // ADD EVENTS + ok, msg := isAdmSuccesful(policyInfos) if ok { patchType := v1beta1.PatchTypeJSONPatch return &v1beta1.AdmissionResponse{ Allowed: true, - Patch: engine.JoinPatches(allPatches), + Patch: engine.JoinPatches(patches), PatchType: &patchType, } } diff --git a/pkg/webhooks/utils.go b/pkg/webhooks/utils.go index 83ca8944d7..e22737bd2d 100644 --- a/pkg/webhooks/utils.go +++ b/pkg/webhooks/utils.go @@ -210,3 +210,13 @@ func setKindForObject(bytes []byte, kind string) []byte { } return data } + +func convertToUnstructured(data []byte) (*unstructured.Unstructured, error) { + resource := &unstructured.Unstructured{} + err := resource.UnmarshalJSON(data) + if err != nil { + glog.V(4).Infof("failed to unmarshall resource: %v", err) + return nil, err + } + return resource, nil +}