diff --git a/pkg/apis/policy/v1alpha1/types.go b/pkg/apis/policy/v1alpha1/types.go index a3db0ce9b2..84d6918e8a 100644 --- a/pkg/apis/policy/v1alpha1/types.go +++ b/pkg/apis/policy/v1alpha1/types.go @@ -1,8 +1,7 @@ - package v1alpha1 import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient @@ -10,62 +9,63 @@ import ( // Policy is a specification for a Policy resource type Policy struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec PolicySpec `json:"spec"` - Status PolicyStatus `json:"status"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec PolicySpec `json:"spec"` + Status PolicyStatus `json:"status"` } // PolicySpec is the spec for a Policy resource type PolicySpec struct { - FailurePolicy *string `json:"failurePolicy"` - Rules []PolicyRule `json:"rules"` + FailurePolicy *string `json:"failurePolicy"` + Rules []PolicyRule `json:"rules"` } // PolicyRule is policy rule that will be applied to resource type PolicyRule struct { - Resource PolicyResource `json:"resource"` - Patches []PolicyPatch `json:"patches"` - Generators []PolicyConfigGenerator `json:"generator"` + Resource PolicyResource `json:"resource"` + Patches *[]PolicyPatch `json:"patches"` + ConfigMapGenerator *PolicyConfigGenerator `json:"configMapGenerator"` + SecretGenerator *PolicyConfigGenerator `json:"secretGenerator"` } // PolicyResource describes the resource rule applied to type PolicyResource struct { - Kind string `json:"kind"` - Name *string `json:"name"` - Selector *metav1.LabelSelector `json:"selector"` + Kind string `json:"kind"` + Name *string `json:"name"` + Selector *metav1.LabelSelector `json:"selector"` } // PolicyPatch is TODO type PolicyPatch struct { - Path string `json:"path"` - Operation string `json:"operation"` - Value int `json:"value"` + Path string `json:"path"` + Operation string `json:"op"` + Value string `json:"value"` } // PolicyConfigGenerator is TODO type PolicyConfigGenerator struct { - Name string `json:"name"` - CopyFrom *PolicyCopyFrom `json:"copyFrom"` - Data map[string]string `json:"data"` + Name string `json:"name"` + CopyFrom *PolicyCopyFrom `json:"copyFrom"` + Data map[string]string `json:"data"` } // PolicyCopyFrom is TODO type PolicyCopyFrom struct { - Namespace string `json:"namespace"` - Name string `json:"name"` + Namespace string `json:"namespace"` + Name string `json:"name"` } // PolicyStatus is the status for a Policy resource type PolicyStatus struct { - Logs []string `json:"log"` + Logs []string `json:"log"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // PolicyList is a list of Policy resources type PolicyList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata"` - Items []Policy `json:"items"` + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []Policy `json:"items"` } diff --git a/server/server.go b/server/server.go index 392c5b5871..3eb371994c 100644 --- a/server/server.go +++ b/server/server.go @@ -25,16 +25,6 @@ type WebhookServer struct { mutationWebhook *webhooks.MutationWebhook } -type patchOperations struct { - patches []patchOperation -} - -type patchOperation struct { - Op string `json:"op"` - Path string `json:"path"` - Value interface{} `json:"value,omitempty"` -} - // NewWebhookServer creates new instance of WebhookServer and configures it func NewWebhookServer(certFile string, keyFile string, controller *controller.PolicyController, logger *log.Logger) *WebhookServer { if logger == nil { diff --git a/webhooks/admission.go b/webhooks/admission.go index 5e69a99fd0..e354f85027 100644 --- a/webhooks/admission.go +++ b/webhooks/admission.go @@ -1,6 +1,9 @@ package webhooks -import "k8s.io/api/admission/v1beta1" +import ( + types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + "k8s.io/api/admission/v1beta1" +) var supportedKinds = [...]string{ "ConfigMap", @@ -36,3 +39,17 @@ func AdmissionIsRequired(request *v1beta1.AdmissionRequest) bool { // Here you can make additional hardcoded checks return kindIsSupported(request.Kind.Kind) } + +func IsRuleApplicableToRequest(rule types.PolicyRule, request *v1beta1.AdmissionRequest) bool { + return IsRuleResourceFitsRequest(rule.Resource, request) +} + +func IsRuleResourceFitsRequest(resource types.PolicyResource, request *v1beta1.AdmissionRequest) bool { + if resource.Kind != request.Kind.Kind { + return false + } + if resource.Name != nil && *resource.Name != request.Name { + return false + } + return true +} diff --git a/webhooks/admission_test.go b/webhooks/admission_test.go new file mode 100644 index 0000000000..dddc9eaad6 --- /dev/null +++ b/webhooks/admission_test.go @@ -0,0 +1,87 @@ +package webhooks_test + +import ( + "testing" + + types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kube-policy/webhooks" + v1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAdmissionIsRequired(t *testing.T) { + var request v1beta1.AdmissionRequest + request.Kind.Kind = "ConfigMap" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "CronJob" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "DaemonSet" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Deployment" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Endpoint" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "HorizontalPodAutoscaler" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Ingress" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Job" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "LimitRange" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Namespace" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "NetworkPolicy" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "PersistentVolumeClaim" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "PodDisruptionBudget" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "PodTemplate" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "ResourceQuota" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Secret" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "Service" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) + request.Kind.Kind = "StatefulSet" + assertEq(t, true, webhooks.AdmissionIsRequired(&request)) +} + +func TestIsRuleResourceFitsRequest_Kind(t *testing.T) { + resource := types.PolicyResource{ + Kind: "ConfigMap", + } + request := v1beta1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: "ConfigMap"}, + } + + assertEq(t, true, webhooks.IsRuleResourceFitsRequest(resource, &request)) + resource.Kind = "Deployment" + assertEq(t, false, webhooks.IsRuleResourceFitsRequest(resource, &request)) +} + +func TestIsRuleResourceFitsRequest_Name(t *testing.T) { + resourceName := "test-config-map" + resource := types.PolicyResource{ + Kind: "ConfigMap", + Name: &resourceName, + } + request := v1beta1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: "ConfigMap"}, + Name: "test-config-map", + } + + assertEq(t, true, webhooks.IsRuleResourceFitsRequest(resource, &request)) + resourceName = "test-config-map-new" + assertEq(t, false, webhooks.IsRuleResourceFitsRequest(resource, &request)) + request.Name = "test-config-map-new" + assertEq(t, true, webhooks.IsRuleResourceFitsRequest(resource, &request)) + request.Name = "" + assertEq(t, false, webhooks.IsRuleResourceFitsRequest(resource, &request)) +} + +func TestIsRuleApplicableToRequest(t *testing.T) { + // TODO +} diff --git a/webhooks/mutation.go b/webhooks/mutation.go index 4b85e19a45..b8226278b2 100644 --- a/webhooks/mutation.go +++ b/webhooks/mutation.go @@ -8,7 +8,6 @@ import ( types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" v1beta1 "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - coreTypes "k8s.io/kubernetes/pkg/apis/core" ) type MutationWebhook struct { @@ -30,23 +29,44 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies [] return nil } - var configMap coreTypes.ConfigMap - if err := json.Unmarshal(request.Object.Raw, &configMap); err != nil { - mw.logger.Printf("Could not unmarshal raw object: %v", err) - return errorToResponse(err) + var allPatches []types.PolicyPatch + for _, policy := range policies { + var stopOnError bool = true + if policy.Spec.FailurePolicy != nil && *policy.Spec.FailurePolicy == "continueOnError" { + stopOnError = false + } + + for ruleIdx, rule := range policy.Spec.Rules { + if IsRuleApplicableToRequest(rule, request) { + mw.logger.Printf("Applying policy %v, rule index = %v", policy.ObjectMeta.Name, ruleIdx) + rulePatches, err := mw.applyPolicyRule(request, rule) + /* + * If at least one error is detected in the rule, the entire rule will not be applied. + * This may be changed in the future by varying the policy.Spec.FailurePolicy values. + */ + if err != nil { + mw.logger.Printf("Error occurred while applying the policy: %v", err) + if stopOnError { + mw.logger.Printf("/!\\ Denying the request according to FailurePolicy spec /!\\") + return errorToResponse(err, false) + } + } else { + mw.logger.Printf("Prepared %v patches", len(rulePatches)) + allPatches = append(allPatches, rulePatches...) + } + } + } + } + + patchesBytes, err := SerializePatches(allPatches) + if err != nil { + mw.logger.Printf("Error occerred while serializing JSONPathch: %v", err) + return errorToResponse(err, true) } - /*patch := patchOperation{ - Path: "/labels", - Op: "add", - Value: map[string]string{ - "is-mutated": "true", - }, - }*/ - patch := `[ {"op":"add","path":"/metadata/labels","value":{"is-mutated":"true"}} ]` return &v1beta1.AdmissionResponse{ Allowed: true, - Patch: []byte(patch), + Patch: patchesBytes, PatchType: func() *v1beta1.PatchType { pt := v1beta1.PatchTypeJSONPatch return &pt @@ -54,10 +74,57 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies [] } } -func errorToResponse(err error) *v1beta1.AdmissionResponse { +// Applies all possible patches in a rule +func (mw *MutationWebhook) applyPolicyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule) ([]types.PolicyPatch, error) { + var allPatches []types.PolicyPatch + if rule.Patches == nil && rule.ConfigMapGenerator == nil && rule.SecretGenerator == nil { + return nil, errors.New("The rule is empty!") + } + + if rule.Patches != nil { + for _, patch := range *rule.Patches { + allPatches = append(allPatches, patch) + } + } + + if rule.ConfigMapGenerator != nil { + // TODO: Make patches from configMapGenerator and add them to returned array + } + + if rule.SecretGenerator != nil { + // TODO: Make patches from secretGenerator and add them to returned array + } + + return allPatches, nil +} + +func SerializePatches(patches []types.PolicyPatch) ([]byte, error) { + var result []byte + result = append(result, []byte("[\n")...) + for index, patch := range patches { + if patch.Operation == "" || patch.Path == "" { + return nil, errors.New("JSONPatch doesn't contain mandatory fields 'path' or 'op'") + } + + patchBytes, err := json.Marshal(patch) + if err != nil { + return nil, err + } + + result = append(result, patchBytes...) + if index != (len(patches) - 1) { + result = append(result, []byte(",\n")...) + } + } + result = append(result, []byte("\n]")...) + return result, nil +} + +func errorToResponse(err error, allowed bool) *v1beta1.AdmissionResponse { return &v1beta1.AdmissionResponse{ Result: &metav1.Status{ Message: err.Error(), }, + Allowed: allowed, } } diff --git a/webhooks/mutation_test.go b/webhooks/mutation_test.go new file mode 100644 index 0000000000..06ddb54787 --- /dev/null +++ b/webhooks/mutation_test.go @@ -0,0 +1,48 @@ +package webhooks_test + +import ( + "testing" + + "github.com/nirmata/kube-policy/webhooks" + + //v1beta1 "k8s.io/api/admission/v1beta1" + //metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" +) + +func TestSerializePatches_Empty(t *testing.T) { + var patches []types.PolicyPatch + bytes, err := webhooks.SerializePatches(patches) + assertEq(t, nil, err) + assertEqStringAndData(t, "[\n\n]", bytes) +} + +func TestSerializePatches_SingleValid(t *testing.T) { + patch := types.PolicyPatch{ + Path: "/metadata/labels/is-mutated", + Operation: "add", + Value: "true", + } + patches := []types.PolicyPatch{patch} + bytes, err := webhooks.SerializePatches(patches) + assertEq(t, nil, err) + assertEqStringAndData(t, `[ +{"path":"/metadata/labels/is-mutated","op":"add","value":"true"} +]`, bytes) +} + +func TestSerializePatches_SingleInvalid(t *testing.T) { + patch := types.PolicyPatch{ + Path: "/metadata/labels/is-mutated", + Value: "true", + } + patches := []types.PolicyPatch{patch} + _, err := webhooks.SerializePatches(patches) + assertNe(t, nil, err) + patches[0].Path = "" + patches[0].Operation = "delete" + _, err = webhooks.SerializePatches(patches) + assertNe(t, nil, err) +} + +// patch := `[ {"op":"add","path":"/metadata/labels","value":{"is-mutated":"true"}} ]` diff --git a/webhooks/utils_test.go b/webhooks/utils_test.go new file mode 100644 index 0000000000..4b03350431 --- /dev/null +++ b/webhooks/utils_test.go @@ -0,0 +1,38 @@ +package webhooks_test + +import ( + "testing" +) + +func assertEq(t *testing.T, expected interface{}, actual interface{}) { + if expected != actual { + t.Errorf("%s != %s", expected, actual) + } +} + +func assertNe(t *testing.T, expected interface{}, actual interface{}) { + if expected == actual { + t.Errorf("%s != %s", expected, actual) + } +} + +func assertEqDataImpl(t *testing.T, expected, actual []byte, formatModifier string) { + if len(expected) != len(actual) { + t.Errorf("len(expected) != len(actual): %d != %d\n1:"+formatModifier+"\n2:"+formatModifier, len(expected), len(actual), expected, actual) + return + } + + for idx, val := range actual { + if val != expected[idx] { + t.Errorf("Slices not equal at index %d:\n1:"+formatModifier+"\n2:"+formatModifier, idx, expected, actual) + } + } +} + +func assertEqData(t *testing.T, expected, actual []byte) { + assertEqDataImpl(t, expected, actual, "%x") +} + +func assertEqStringAndData(t *testing.T, str string, data []byte) { + assertEqDataImpl(t, []byte(str), data, "%s") +}