From e8de9a111ae89e3eaad72bef802a23395fdff59c Mon Sep 17 00:00:00 2001 From: shuting Date: Thu, 16 May 2019 14:09:02 -0700 Subject: [PATCH 01/11] Finished Generate() logic to actual generating the resource --- .../policy-namespace-patch-cmgCG-sgCG.yaml | 51 ++++++++++--------- kubeclient/kubeclient.go | 20 +++++--- main.go | 2 +- pkg/apis/policy/v1alpha1/types.go | 6 +-- pkg/apis/policy/v1alpha1/utils.go | 5 +- pkg/engine/generation.go | 39 ++++++++------ pkg/webhooks/server.go | 8 +++ 7 files changed, 78 insertions(+), 53 deletions(-) diff --git a/examples/ConfigMapGenerator-SecretGenerator/policy-namespace-patch-cmgCG-sgCG.yaml b/examples/ConfigMapGenerator-SecretGenerator/policy-namespace-patch-cmgCG-sgCG.yaml index 922eeee05a..a400677700 100644 --- a/examples/ConfigMapGenerator-SecretGenerator/policy-namespace-patch-cmgCG-sgCG.yaml +++ b/examples/ConfigMapGenerator-SecretGenerator/policy-namespace-patch-cmgCG-sgCG.yaml @@ -3,42 +3,47 @@ # To apply this policy you need to create secret and configMap in "default" namespace # and then create a namespace -apiVersion : policy.nirmata.io/v1alpha1 +apiVersion : kubepolicy.nirmata.io/v1alpha1 kind : Policy metadata : name : "policy-ns-patch-cmg-sg" spec : - failurePolicy: stopOnError rules: - - resource : + - name: "patchNamespace2" + resource : kind : Namespace selector: matchLabels: LabelForSelector : "namespace2" - patch: - - path: "/metadata/labels/isMutatedByPolicy" - op: add - value: "true" + mutate: + patches: + - path: "/metadata/labels/isMutatedByPolicy" + op: add + value: "true" - - resource : + - name: "copyCM" + resource : kind : Namespace selector: matchLabels: LabelForSelector : "namespace2" - configMapGenerator : + generate : + kind: ConfigMap name : copied-cm copyFrom : namespace : default name : game-config data : secretData: "data from cmg" - - - resource : + + - name: "generateCM" + resource : kind : Namespace selector: matchLabels: LabelForSelector : "namespace2" - configMapGenerator : + generate : + kind: ConfigMap name : generated-cm data : secretData: "very sensitive data from cmg" @@ -49,13 +54,12 @@ spec : image.public.key=771 rsa.public.key=42 - - resource : + - name: "generateSecret" + resource : kind : Namespace - selector: - matchLabels: - LabelForSelector : "namespace2" - - secretGenerator : + name: ns2 + generate : + kind: Secret name : generated-secrets data : foo : bar @@ -66,13 +70,12 @@ spec : foo1=bar1 foo2=bar2 - - resource : + - name: "copySecret" + resource : kind : Namespace - selector: - matchLabels: - LabelForSelector : "namespace2" - - secretGenerator : + name: ns2 + generate : + kind: Secret name : copied-secrets copyFrom : namespace : default diff --git a/kubeclient/kubeclient.go b/kubeclient/kubeclient.go index f32d6950b6..98797907bc 100644 --- a/kubeclient/kubeclient.go +++ b/kubeclient/kubeclient.go @@ -67,10 +67,12 @@ func (kc *KubeClient) GenerateConfigMap(generator types.Generation, namespace st var err error - kc.logger.Printf("Copying data from configmap %s/%s", generator.CopyFrom.Namespace, generator.CopyFrom.Name) - configMap, err = kc.client.CoreV1().ConfigMaps(generator.CopyFrom.Namespace).Get(generator.CopyFrom.Name, metav1.GetOptions{}) - if err != nil { - return err + if generator.CopyFrom != nil { + kc.logger.Printf("Copying data from configmap %s/%s", generator.CopyFrom.Namespace, generator.CopyFrom.Name) + configMap, err = kc.client.CoreV1().ConfigMaps(generator.CopyFrom.Namespace).Get(generator.CopyFrom.Name, metav1.GetOptions{}) + if err != nil { + return err + } } configMap.ObjectMeta = metav1.ObjectMeta{ @@ -101,10 +103,12 @@ func (kc *KubeClient) GenerateSecret(generator types.Generation, namespace strin var err error - kc.logger.Printf("Copying data from secret %s/%s", generator.CopyFrom.Namespace, generator.CopyFrom.Name) - secret, err = kc.client.CoreV1().Secrets(generator.CopyFrom.Namespace).Get(generator.CopyFrom.Name, metav1.GetOptions{}) - if err != nil { - return err + if generator.CopyFrom != nil { + kc.logger.Printf("Copying data from secret %s/%s", generator.CopyFrom.Namespace, generator.CopyFrom.Name) + secret, err = kc.client.CoreV1().Secrets(generator.CopyFrom.Namespace).Get(generator.CopyFrom.Name, metav1.GetOptions{}) + if err != nil { + return err + } } secret.ObjectMeta = metav1.ObjectMeta{ diff --git a/main.go b/main.go index abf9b47711..9bd39c21a5 100644 --- a/main.go +++ b/main.go @@ -59,7 +59,7 @@ func main() { log.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err) } - server, err := webhooks.NewWebhookServer(tlsPair, policyInformer.Lister(), nil) + server, err := webhooks.NewWebhookServer(tlsPair, policyInformer.Lister(), kubeclient, nil) if err != nil { log.Fatalf("Unable to create webhook server: %v\n", err) } diff --git a/pkg/apis/policy/v1alpha1/types.go b/pkg/apis/policy/v1alpha1/types.go index dd7d8cd43d..0bc0699eef 100644 --- a/pkg/apis/policy/v1alpha1/types.go +++ b/pkg/apis/policy/v1alpha1/types.go @@ -60,9 +60,9 @@ type Validation struct { // Generation describes which resources will be created when other resource is created type Generation struct { - Kind string `json:"kind"` - Name string `json:"name"` - CopyFrom `json:"copyFrom"` + Kind string `json:"kind"` + Name string `json:"name"` + CopyFrom *CopyFrom `json:"copyFrom,omitempty"` Data map[string]string `json:"data"` Labels map[string]string `json:"labels"` } diff --git a/pkg/apis/policy/v1alpha1/utils.go b/pkg/apis/policy/v1alpha1/utils.go index 653513a232..dd0374bb26 100644 --- a/pkg/apis/policy/v1alpha1/utils.go +++ b/pkg/apis/policy/v1alpha1/utils.go @@ -78,7 +78,10 @@ func (pcg *Generation) Validate() error { return errors.New("Name or/and Kind of generator is not specified") } - return pcg.CopyFrom.Validate() + if pcg.CopyFrom != nil { + return pcg.CopyFrom.Validate() + } + return nil } // DeepCopyInto is declared because k8s:deepcopy-gen is diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index e5cb259942..11f70ae346 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -4,6 +4,7 @@ import ( "fmt" "log" + kubeClient "github.com/nirmata/kube-policy/kubeclient" kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" "github.com/nirmata/kube-policy/pkg/engine/mutation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,14 +17,12 @@ type GenerationResponse struct { // Generate should be called to process generate rules on the resource // TODO: extend kubeclient(will change to dynamic client) to create resources -func Generate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) []GenerationResponse { +func Generate(policy kubepolicy.Policy, rawResource []byte, kubeClient *kubeClient.KubeClient, gvk metav1.GroupVersionKind) { // configMapGenerator and secretGenerator can be applied only to namespaces if gvk.Kind != "Namespace" { - return nil + return } - var generateResps []GenerationResponse - for i, rule := range policy.Spec.Rules { // Checks for preconditions @@ -48,31 +47,39 @@ func Generate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers continue } - generateResps, err = applyRuleGenerator(rawResource, rule.Generation) + err = applyRuleGenerator(rawResource, rule.Generation, kubeClient) if err != nil { log.Printf("Failed to apply rule generator: %v", err) - } else { - generateResps = append(generateResps, generateResps...) } } - - return generateResps } // Applies "configMapGenerator" and "secretGenerator" described in PolicyRule // TODO: plan to support all kinds of generator -func applyRuleGenerator(rawResource []byte, generator *kubepolicy.Generation) ([]GenerationResponse, error) { - var generateResps []GenerationResponse +func applyRuleGenerator(rawResource []byte, generator *kubepolicy.Generation, kubeClient *kubeClient.KubeClient) error { if generator == nil { - return nil, nil + return nil } err := generator.Validate() if err != nil { - return nil, fmt.Errorf("Generator for '%s' is invalid: %s", generator.Kind, err) + return fmt.Errorf("Generator for '%s/%s' is invalid: %s", generator.Kind, generator.Name, err) } - namespaceName := mutation.ParseNameFromObject(rawResource) - generateResps = append(generateResps, GenerationResponse{generator, namespaceName}) - return generateResps, nil + namespace := mutation.ParseNameFromObject(rawResource) + switch generator.Kind { + case "ConfigMap": + err = kubeClient.GenerateConfigMap(*generator, namespace) + case "Secret": + err = kubeClient.GenerateSecret(*generator, namespace) + default: + err = fmt.Errorf("Unsupported config Kind '%s'", generator.Kind) + } + + if err != nil { + return fmt.Errorf("Unable to apply generator for %s '%s/%s' : %v", generator.Kind, namespace, generator.Name, err) + } + + log.Printf("Successfully applied generator %s/%s", generator.Kind, generator.Name) + return nil } diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index b74a6a36e8..807f6bc23b 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -13,6 +13,7 @@ import ( "time" "github.com/nirmata/kube-policy/config" + kubeClient "github.com/nirmata/kube-policy/kubeclient" policylister "github.com/nirmata/kube-policy/pkg/client/listers/policy/v1alpha1" engine "github.com/nirmata/kube-policy/pkg/engine" "github.com/nirmata/kube-policy/pkg/engine/mutation" @@ -27,6 +28,7 @@ import ( type WebhookServer struct { server http.Server policyLister policylister.PolicyLister + kubeClient *kubeClient.KubeClient logger *log.Logger } @@ -35,6 +37,7 @@ type WebhookServer struct { func NewWebhookServer( tlsPair *tlsutils.TlsPemPair, policyLister policylister.PolicyLister, + kubeClient *kubeClient.KubeClient, logger *log.Logger) (*WebhookServer, error) { if logger == nil { logger = log.New(os.Stdout, "Webhook Server: ", log.LstdFlags) @@ -53,6 +56,7 @@ func NewWebhookServer( ws := &WebhookServer{ policyLister: policyLister, + kubeClient: kubeClient, logger: logger, } @@ -174,6 +178,7 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 allowed := true for _, policy := range policies { + // validation ws.logger.Printf("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) if ok := engine.Validate(*policy, request.Object.Raw, request.Kind); !ok { @@ -183,6 +188,9 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 } else { ws.logger.Println("Validation is successful") } + + // generation + engine.Generate(*policy, request.Object.Raw, ws.kubeClient, request.Kind) } return &v1beta1.AdmissionResponse{ From 36f76a0f2fe8f701ea24cc4beea8ba5118bed394 Mon Sep 17 00:00:00 2001 From: shuting Date: Thu, 16 May 2019 17:19:38 -0700 Subject: [PATCH 02/11] - Correct crd yaml, since we only allow 1 generation per rule. - update example for generator --- definitions/install.yaml | 57 +++++++++---------- .../policy-cm-test.yaml | 20 ++++--- pkg/apis/policy/v1alpha1/types.go | 2 +- pkg/apis/policy/v1alpha1/utils.go | 16 +----- 4 files changed, 41 insertions(+), 54 deletions(-) diff --git a/definitions/install.yaml b/definitions/install.yaml index 5884555bbc..f23e12fad3 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -29,14 +29,14 @@ spec: required: - name - resource - parameters: + properties: name: type: string resource: type: object required: - kind - parameters: + properties: kind: type: string enum: @@ -115,36 +115,33 @@ spec: pattern: AnyValue: {} generate: - type: array - items: - type: object - required: - - kind - - name - - copyFrom - properties: - kind: - type: string - name: - type: string - copyFrom: - type: object - required: - - namespace - - name - properties: - namespace: - type: string - name: - type: string - data: - type: object - additionalProperties: + type: object + required: + - kind + - name + properties: + kind: + type: string + name: + type: string + copyFrom: + type: object + required: + - namespace + - name + properties: + namespace: type: string - labels: - type: object - additionalProperties: + name: type: string + data: + type: object + additionalProperties: + type: string + labels: + type: object + additionalProperties: + type: string --- apiVersion: v1 kind: Service diff --git a/examples/ConfigMapGenerator-SecretGenerator/policy-cm-test.yaml b/examples/ConfigMapGenerator-SecretGenerator/policy-cm-test.yaml index 9e1056611b..3a0ce26477 100644 --- a/examples/ConfigMapGenerator-SecretGenerator/policy-cm-test.yaml +++ b/examples/ConfigMapGenerator-SecretGenerator/policy-cm-test.yaml @@ -4,15 +4,17 @@ metadata : name: "policy-configmapgenerator-test" spec: rules: - - name: "Policy ConfigMap sample rule" - resource: + - name: "copyCM" + resource : kind : Namespace - name: "ns2" - generate: + selector: + matchLabels: + LabelForSelector : "namespace2" + generate : kind: ConfigMap - name: copied-cm - copyFrom: - namespace: default - name: game-config - data: + name : copied-cm + copyFrom : + namespace : default + name : game-config + data : secretData: "data from cmg" \ No newline at end of file diff --git a/pkg/apis/policy/v1alpha1/types.go b/pkg/apis/policy/v1alpha1/types.go index 0bc0699eef..f167a8a09a 100644 --- a/pkg/apis/policy/v1alpha1/types.go +++ b/pkg/apis/policy/v1alpha1/types.go @@ -62,7 +62,7 @@ type Validation struct { type Generation struct { Kind string `json:"kind"` Name string `json:"name"` - CopyFrom *CopyFrom `json:"copyFrom,omitempty"` + CopyFrom *CopyFrom `json:"copyFrom"` Data map[string]string `json:"data"` Labels map[string]string `json:"labels"` } diff --git a/pkg/apis/policy/v1alpha1/utils.go b/pkg/apis/policy/v1alpha1/utils.go index dd0374bb26..65719ba360 100644 --- a/pkg/apis/policy/v1alpha1/utils.go +++ b/pkg/apis/policy/v1alpha1/utils.go @@ -64,22 +64,10 @@ func (pp *Patch) Validate() error { return fmt.Errorf("Unsupported JSONPatch operation '%s'", pp.Operation) } -// Validate returns error if Name or namespace is not cpecified -func (pcf *CopyFrom) Validate() error { - if pcf.Name == "" || pcf.Namespace == "" { - return errors.New("Name or/and Namespace is not specified") - } - return nil -} - // Validate returns error if generator is configured incompletely func (pcg *Generation) Validate() error { - if pcg.Name == "" || pcg.Kind == "" { - return errors.New("Name or/and Kind of generator is not specified") - } - - if pcg.CopyFrom != nil { - return pcg.CopyFrom.Validate() + if len(pcg.Data) == 0 && pcg.CopyFrom == nil { + return fmt.Errorf("Neither Data nor CopyFrom (source) of %s/%s is specified", pcg.Kind, pcg.Name) } return nil } From e878c8bc1e595c2fef76f52a385637e5b4a197b5 Mon Sep 17 00:00:00 2001 From: shuting Date: Fri, 17 May 2019 11:15:30 -0700 Subject: [PATCH 03/11] move config to pkg/config --- init.go | 2 +- kubeclient/kubeclient.go | 2 +- {config => pkg/config}/config.go | 0 pkg/webhooks/registration.go | 2 +- pkg/webhooks/server.go | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename {config => pkg/config}/config.go (100%) diff --git a/init.go b/init.go index edb1f64f53..28f747e863 100644 --- a/init.go +++ b/init.go @@ -5,8 +5,8 @@ import ( "log" "net/url" - "github.com/nirmata/kube-policy/config" "github.com/nirmata/kube-policy/kubeclient" + "github.com/nirmata/kube-policy/pkg/config" "github.com/nirmata/kube-policy/pkg/tls" rest "k8s.io/client-go/rest" diff --git a/kubeclient/kubeclient.go b/kubeclient/kubeclient.go index 98797907bc..b259f911b1 100644 --- a/kubeclient/kubeclient.go +++ b/kubeclient/kubeclient.go @@ -6,8 +6,8 @@ import ( "os" "time" - "github.com/nirmata/kube-policy/config" types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kube-policy/pkg/config" apps "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/config/config.go b/pkg/config/config.go similarity index 100% rename from config/config.go rename to pkg/config/config.go diff --git a/pkg/webhooks/registration.go b/pkg/webhooks/registration.go index 6fe8a64f2b..d917c36a8c 100644 --- a/pkg/webhooks/registration.go +++ b/pkg/webhooks/registration.go @@ -4,8 +4,8 @@ import ( "errors" "io/ioutil" - "github.com/nirmata/kube-policy/config" kubeclient "github.com/nirmata/kube-policy/kubeclient" + "github.com/nirmata/kube-policy/pkg/config" admregapi "k8s.io/api/admissionregistration/v1beta1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 4cd34aa1c0..59735f1c2e 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -12,9 +12,9 @@ import ( "os" "time" - "github.com/nirmata/kube-policy/config" kubeClient "github.com/nirmata/kube-policy/kubeclient" policylister "github.com/nirmata/kube-policy/pkg/client/listers/policy/v1alpha1" + "github.com/nirmata/kube-policy/pkg/config" engine "github.com/nirmata/kube-policy/pkg/engine" "github.com/nirmata/kube-policy/pkg/engine/mutation" tlsutils "github.com/nirmata/kube-policy/pkg/tls" From 47916acc63a0a117cd4bb67571078975665e4906 Mon Sep 17 00:00:00 2001 From: shuting Date: Fri, 17 May 2019 11:36:58 -0700 Subject: [PATCH 04/11] move test utils to patches_test.go --- pkg/engine/mutation/patches_test.go | 21 +++++++++++++++++++++ pkg/engine/mutation/utils_test.go | 26 -------------------------- 2 files changed, 21 insertions(+), 26 deletions(-) delete mode 100644 pkg/engine/mutation/utils_test.go diff --git a/pkg/engine/mutation/patches_test.go b/pkg/engine/mutation/patches_test.go index 60ee4d2835..825a5723cb 100644 --- a/pkg/engine/mutation/patches_test.go +++ b/pkg/engine/mutation/patches_test.go @@ -116,3 +116,24 @@ func TestProcessPatches_RemovePathDoesntExist_NotEmptyResult(t *testing.T) { assert.Assert(t, len(patchesBytes) == 1) assertEqStringAndData(t, `{"path":"/metadata/labels/label2","op":"add","value":"label2Value"}`, patchesBytes[0]) } + +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") +} diff --git a/pkg/engine/mutation/utils_test.go b/pkg/engine/mutation/utils_test.go deleted file mode 100644 index f8473ae287..0000000000 --- a/pkg/engine/mutation/utils_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package mutation - -import ( - "testing" -) - -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") -} From be13b041b68de5e8daa7b7c0df8360568b4058f9 Mon Sep 17 00:00:00 2001 From: Maxim Goncharenko Date: Mon, 20 May 2019 14:48:38 +0300 Subject: [PATCH 05/11] Fixed issue with validation error messages --- pkg/engine/generation.go | 22 +---- pkg/engine/mutation.go | 35 ++------ pkg/engine/utils.go | 14 ++-- pkg/engine/validation.go | 149 ++++++++++++++-------------------- pkg/engine/validation_test.go | 104 ++++++++++++++---------- pkg/webhooks/server.go | 2 +- 6 files changed, 140 insertions(+), 186 deletions(-) diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 9785487347..b87ab71ff3 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -22,31 +22,15 @@ func Generate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers var generateResps []GenerationResponse - for i, rule := range policy.Spec.Rules { - - // Checks for preconditions - // TODO: Rework PolicyEngine interface that it receives not a policy, but mutation object for - // Mutate, validation for Validate and so on. It will allow to bring this checks outside of PolicyEngine - // to common part as far as they present for all: mutation, validation, generation - - err := rule.Validate() - if err != nil { - log.Printf("Rule has invalid structure: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - - ok, err := ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) - if err != nil { - log.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } + for _, rule := range policy.Spec.Rules { + ok := ResourceMeetsDescription(rawResource, rule.ResourceDescription, gvk) if !ok { log.Printf("Rule is not applicable to the request: rule name = %s in policy %s \n", rule.Name, policy.ObjectMeta.Name) continue } - generateResps, err = applyRuleGenerator(rawResource, rule.Generation) + generateResps, err := applyRuleGenerator(rawResource, rule.Generation) if err != nil { log.Printf("Failed to apply rule generator: %v", err) } else { diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index ec946f10db..a0cde8b9d0 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -13,40 +13,23 @@ import ( func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) []mutation.PatchBytes { var policyPatches []mutation.PatchBytes - for i, rule := range policy.Spec.Rules { - - // Checks for preconditions - // TODO: Rework PolicyEngine interface that it receives not a policy, but mutation object for - // Mutate, validation for Validate and so on. It will allow to bring this checks outside of PolicyEngine - // to common part as far as they present for all: mutation, validation, generation - - err := rule.Validate() - if err != nil { - log.Printf("Rule has invalid structure: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - - ok, err := ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) - if err != nil { - log.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - - if !ok { - log.Printf("Rule is not applicable to the request: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - + for _, rule := range policy.Spec.Rules { if rule.Mutation == nil { continue } + ok := ResourceMeetsDescription(rawResource, rule.ResourceDescription, gvk) + if !ok { + log.Printf("Rule \"%s\" is not applicable to resource\n", rule.Name) + continue + } + // Process Overlay if rule.Mutation.Overlay != nil { overlayPatches, err := mutation.ProcessOverlay(rule.Mutation.Overlay, rawResource) if err != nil { - log.Printf("Overlay application failed: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) + log.Printf("Overlay application has failed for rule %s in policy %s, err: %v\n", rule.Name, policy.ObjectMeta.Name, err) } else { policyPatches = append(policyPatches, overlayPatches...) } @@ -57,7 +40,7 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio if rule.Mutation.Patches != nil { processedPatches, err := mutation.ProcessPatches(rule.Mutation.Patches, rawResource) if err != nil { - log.Printf("Patches application failed: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) + log.Printf("Patches application has failed for rule %s in policy %s, err: %v\n", rule.Name, policy.ObjectMeta.Name, err) } else { policyPatches = append(policyPatches, processedPatches...) } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index f4ce55a8cb..0646bf33e3 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -11,10 +11,10 @@ import ( "k8s.io/apimachinery/pkg/labels" ) -// ResourceMeetsRules checks requests kind, name and labels to fit the policy -func ResourceMeetsRules(resourceRaw []byte, description kubepolicy.ResourceDescription, gvk metav1.GroupVersionKind) (bool, error) { +// ResourceMeetsDescription checks requests kind, name and labels to fit the policy rule +func ResourceMeetsDescription(resourceRaw []byte, description kubepolicy.ResourceDescription, gvk metav1.GroupVersionKind) bool { if description.Kind != gvk.Kind { - return false, nil + return false } if resourceRaw != nil { @@ -24,7 +24,7 @@ func ResourceMeetsRules(resourceRaw []byte, description kubepolicy.ResourceDescr if description.Name != nil { if !wildcard.Match(*description.Name, name) { - return false, nil + return false } } @@ -32,18 +32,18 @@ func ResourceMeetsRules(resourceRaw []byte, description kubepolicy.ResourceDescr selector, err := metav1.LabelSelectorAsSelector(description.Selector) if err != nil { - return false, err + return false } labelMap := ParseLabelsFromMetadata(meta) if !selector.Matches(labelMap) { - return false, nil + return false } } } - return true, nil + return true } func ParseMetadataFromObject(bytes []byte) map[string]interface{} { diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 2927fe0c3a..7cc1ec9e49 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -13,7 +13,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// TODO: This operators are already implemented in kubernetes +// Operator is string alias that represents selection operators enum type Operator string const ( @@ -25,66 +25,43 @@ const ( ) // TODO: Refactor using State pattern +// TODO: Return Events and pass all checks to get all validation errors (not ) // Validate handles validating admission request // Checks the target resourse for rules defined in the policy -func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) bool { +func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) error { var resource interface{} json.Unmarshal(rawResource, &resource) - allowed := true - for i, rule := range policy.Spec.Rules { - - // Checks for preconditions - // TODO: Rework PolicyEngine interface that it receives not a policy, but mutation object for - // Mutate, validation for Validate and so on. It will allow to bring this checks outside of PolicyEngine - // to common part as far as they present for all: mutation, validation, generation - - err := rule.Validate() - if err != nil { - log.Printf("Rule has invalid structure: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - - ok, err := ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) - if err != nil { - log.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - - if !ok { - log.Printf("Rule is not applicable to the request: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) - continue - } - + for _, rule := range policy.Spec.Rules { if rule.Validation == nil { continue } - if !validateMap(resource, rule.Validation.Pattern) { - log.Printf("Validation with the rule %s has failed: %s\n", rule.Name, *rule.Validation.Message) - allowed = false - } else { - log.Printf("Validation rule %s is successful\n", rule.Name) + ok := ResourceMeetsDescription(rawResource, rule.ResourceDescription, gvk) + if !ok { + log.Printf("Rule \"%s\" is not applicable to resource\n", rule.Name) + continue + } + + if err := validateMap(resource, rule.Validation.Pattern); err != nil { + return fmt.Errorf("%s: %s", *rule.Validation.Message, err.Error()) } } - return allowed + log.Println("Validation is successful") + return nil } -func validateMap(resourcePart, patternPart interface{}) bool { +func validateMap(resourcePart, patternPart interface{}) error { pattern, ok := patternPart.(map[string]interface{}) - if !ok { - fmt.Printf("Validating error: expected Map, found %T\n", patternPart) - return false + return fmt.Errorf("Expected map, found %T", patternPart) } resource, ok := resourcePart.(map[string]interface{}) - if !ok { - fmt.Printf("Validating error: expected Map, found %T\n", resourcePart) - return false + return fmt.Errorf("Expected map, found %T", resourcePart) } for key, value := range pattern { @@ -92,82 +69,70 @@ func validateMap(resourcePart, patternPart interface{}) bool { key = key[1 : len(key)-1] } - if !validateMapElement(resource[key], value) { - return false + if err := validateMapElement(resource[key], value); err != nil { + return err } } - return true + return nil } -func validateArray(resourcePart, patternPart interface{}) bool { +func validateArray(resourcePart, patternPart interface{}) error { patternArray, ok := patternPart.([]interface{}) - if !ok { - fmt.Printf("Validating error: expected array, found %T\n", patternPart) - return false + return fmt.Errorf("Expected array, found %T", patternPart) } resourceArray, ok := resourcePart.([]interface{}) - if !ok { - fmt.Printf("Validating error: expected array, found %T\n", resourcePart) - return false + return fmt.Errorf("Expected array, found %T", resourcePart) } switch pattern := patternArray[0].(type) { case map[string]interface{}: anchors, err := getAnchorsFromMap(pattern) if err != nil { - fmt.Printf("Validating error: %v\n", err) - return false + return err } for _, value := range resourceArray { resource, ok := value.(map[string]interface{}) if !ok { - fmt.Printf("Validating error: expected Map, found %T\n", resourcePart) - return false + return fmt.Errorf("Expected array, found %T", resourcePart) } if skipArrayObject(resource, anchors) { continue } - if !validateMap(resource, pattern) { - return false + if err := validateMap(resource, pattern); err != nil { + return err } } - - return true default: for _, value := range resourceArray { - if !checkSingleValue(value, patternArray[0]) { - return false + if err := checkSingleValue(value, patternArray[0]); err != nil { + return err } } } - return true + return nil } -func validateMapElement(resourcePart, patternPart interface{}) bool { +func validateMapElement(resourcePart, patternPart interface{}) error { switch pattern := patternPart.(type) { case map[string]interface{}: dictionary, ok := resourcePart.(map[string]interface{}) - if !ok { - fmt.Printf("Validating error: expected %T, found %T\n", patternPart, resourcePart) - return false + return fmt.Errorf("Expected %T, found %T", patternPart, resourcePart) } return validateMap(dictionary, pattern) case []interface{}: array, ok := resourcePart.([]interface{}) - if !ok { - fmt.Printf("Validating error: expected %T, found %T\n", patternPart, resourcePart) - return false + return fmt.Errorf("Expected %T, found %T", patternPart, resourcePart) } return validateArray(array, pattern) @@ -175,14 +140,12 @@ func validateMapElement(resourcePart, patternPart interface{}) bool { str, ok := resourcePart.(string) if !ok { - fmt.Printf("Validating error: expected %T, found %T\n", patternPart, resourcePart) - return false + return fmt.Errorf("Expected %T, found %T", patternPart, resourcePart) } return checkSingleValue(str, pattern) default: - fmt.Printf("Validating error: unknown type in map: %T\n", patternPart) - return false + return fmt.Errorf("Validating error: unknown type in map: %T", patternPart) } } @@ -207,7 +170,7 @@ func skipArrayObject(object, anchors map[string]interface{}) bool { return true } - if !checkSingleValue(value, pattern) { + if err := checkSingleValue(value, pattern); err != nil { return true } } @@ -215,7 +178,7 @@ func skipArrayObject(object, anchors map[string]interface{}) bool { return false } -func checkSingleValue(value, pattern interface{}) bool { +func checkSingleValue(value, pattern interface{}) error { switch typedPattern := pattern.(type) { case string: switch typedValue := value.(type) { @@ -226,46 +189,54 @@ func checkSingleValue(value, pattern interface{}) bool { case int: return checkForOperator(float64(typedValue), typedPattern) default: - fmt.Printf("Validating error: expected string or numerical type, found %T, pattern: %s\n", value, typedPattern) - return false + return fmt.Errorf("Expected string or numerical type, found %T, pattern: %s", value, typedPattern) } case float64: num, ok := value.(float64) if !ok { - fmt.Printf("Validating error: expected float, found %T\n", value) - return false + return fmt.Errorf("Expected float, found %T", value) } - return typedPattern == num + if typedPattern != num { + return fmt.Errorf("Value %f is not equal to pattern %f", value, typedPattern) + } case int: num, ok := value.(int) if !ok { - fmt.Printf("Validating error: expected int, found %T\n", value) - return false + return fmt.Errorf("Expected int, found %T", value) } - return typedPattern == num + if typedPattern != num { + return fmt.Errorf("Value %d is not equal to pattern %d", num, typedPattern) + } default: - fmt.Printf("Validating error: expected pattern (string or numerical type), found %T\n", pattern) - return false + return fmt.Errorf("Expected pattern (string or numerical type), found %T", pattern) } + + return nil } -func checkForWildcard(value, pattern string) bool { - return wildcard.Match(pattern, value) +func checkForWildcard(value, pattern string) error { + if !wildcard.Match(pattern, value) { + return fmt.Errorf("Wildcard check has failed. Pattern: \"%s\". Value: \"%s\"", pattern, value) + } + + return nil } -func checkForOperator(value float64, pattern string) bool { +func checkForOperator(value float64, pattern string) error { operators := strings.Split(pattern, "|") for _, operator := range operators { operator = strings.Replace(operator, " ", "", -1) + + // At least one success - return nil if checkSingleOperator(value, operator) { - return true + return nil } } - return false + return fmt.Errorf("Operator check has failed. Pattern: \"%s\". Value: \"%f\"", pattern, value) } func checkSingleOperator(value float64, pattern string) bool { diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 68d07ca73e..93f5ca7360 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -4,7 +4,9 @@ import ( "encoding/json" "testing" + kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" "gotest.tools/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { @@ -62,190 +64,190 @@ func TestCheckForWildcard_LeftAsteriskTest(t *testing.T) { value = "leftmiddle" middle := "middle" - assert.Assert(t, !checkForWildcard(value, pattern)) - assert.Assert(t, !checkForWildcard(middle, pattern)) + assert.Assert(t, checkForWildcard(value, pattern) != nil) + assert.Assert(t, checkForWildcard(middle, pattern) != nil) } func TestCheckForWildcard_MiddleAsteriskTest(t *testing.T) { pattern := "ab*ba" - value := "abbba" - assert.Assert(t, checkForWildcard(value, pattern)) + value := "abbeba" + assert.NilError(t, checkForWildcard(value, pattern)) value = "abbca" - assert.Assert(t, !checkForWildcard(value, pattern)) + assert.Assert(t, checkForWildcard(value, pattern) != nil) } func TestCheckForWildcard_QuestionMark(t *testing.T) { pattern := "ab?ba" value := "abbba" - assert.Assert(t, checkForWildcard(value, pattern)) + assert.NilError(t, checkForWildcard(value, pattern)) value = "abbbba" - assert.Assert(t, !checkForWildcard(value, pattern)) + assert.Assert(t, checkForWildcard(value, pattern) != nil) } func TestCheckSingleValue_CheckInt(t *testing.T) { pattern := 89 value := 89 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) value = 202 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) } func TestCheckSingleValue_CheckFloat(t *testing.T) { pattern := 89.9091 value := 89.9091 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) value = 89.9092 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) } func TestCheckSingleValue_CheckOperatorMoreEqual(t *testing.T) { pattern := " >= 89 " value := 89 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = ">=10.0001" floatValue := 89.901 - assert.Assert(t, checkSingleValue(floatValue, pattern)) + assert.NilError(t, checkSingleValue(floatValue, pattern)) } func TestCheckSingleValue_CheckOperatorMoreEqualFail(t *testing.T) { pattern := " >= 90 " value := 89 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = ">=910.0001" floatValue := 89.901 - assert.Assert(t, !checkSingleValue(floatValue, pattern)) + assert.Assert(t, checkSingleValue(floatValue, pattern) != nil) } func TestCheckSingleValue_CheckOperatorLessEqual(t *testing.T) { pattern := " <= 1 " value := 1 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = "<=10.0001" floatValue := 1.901 - assert.Assert(t, checkSingleValue(floatValue, pattern)) + assert.NilError(t, checkSingleValue(floatValue, pattern)) } func TestCheckSingleValue_CheckOperatorLessEqualFail(t *testing.T) { pattern := " <= 0.1558 " value := 1 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = "<=10.0001" floatValue := 12.901 - assert.Assert(t, !checkSingleValue(floatValue, pattern)) + assert.Assert(t, checkSingleValue(floatValue, pattern) != nil) } func TestCheckSingleValue_CheckOperatorMore(t *testing.T) { pattern := " > 10 " value := 89 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = ">10.0001" floatValue := 89.901 - assert.Assert(t, checkSingleValue(floatValue, pattern)) + assert.NilError(t, checkSingleValue(floatValue, pattern)) } func TestCheckSingleValue_CheckOperatorMoreFail(t *testing.T) { pattern := " > 89 " value := 89 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = ">910.0001" floatValue := 89.901 - assert.Assert(t, !checkSingleValue(floatValue, pattern)) + assert.Assert(t, checkSingleValue(floatValue, pattern) != nil) } func TestCheckSingleValue_CheckOperatorLess(t *testing.T) { pattern := " < 10 " value := 9 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = "<10.0001" floatValue := 9.901 - assert.Assert(t, checkSingleValue(floatValue, pattern)) + assert.NilError(t, checkSingleValue(floatValue, pattern)) } func TestCheckSingleValue_CheckOperatorLessFail(t *testing.T) { pattern := " < 10 " value := 10 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = "<10.0001" floatValue := 19.901 - assert.Assert(t, !checkSingleValue(floatValue, pattern)) + assert.Assert(t, checkSingleValue(floatValue, pattern) != nil) } func TestCheckSingleValue_CheckOperatorNotEqual(t *testing.T) { pattern := " != 10 " value := 9.99999 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = "!=10.0001" floatValue := 10.0000 - assert.Assert(t, checkSingleValue(floatValue, pattern)) + assert.NilError(t, checkSingleValue(floatValue, pattern)) } func TestCheckSingleValue_CheckOperatorNotEqualFail(t *testing.T) { pattern := " != 9.99999 " value := 9.99999 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = "!=10" floatValue := 10 - assert.Assert(t, !checkSingleValue(floatValue, pattern)) + assert.Assert(t, checkSingleValue(floatValue, pattern) != nil) } func TestCheckSingleValue_CheckOperatorEqual(t *testing.T) { pattern := " 10.000001 " value := 10.000001 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = "10.000000" floatValue := 10 - assert.Assert(t, checkSingleValue(floatValue, pattern)) + assert.NilError(t, checkSingleValue(floatValue, pattern)) } func TestCheckSingleValue_CheckOperatorEqualFail(t *testing.T) { pattern := " 10.000000 " value := 10.000001 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = "10.000001" floatValue := 10 - assert.Assert(t, !checkSingleValue(floatValue, pattern)) + assert.Assert(t, checkSingleValue(floatValue, pattern) != nil) } func TestCheckSingleValue_CheckSeveralOperators(t *testing.T) { pattern := " <-1 | 10.000001 " value := 10.000001 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) value = -30 - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) value = 5 - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) } func TestCheckSingleValue_CheckWildcard(t *testing.T) { pattern := "nirmata_*" value := "nirmata_awesome" - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) pattern = "nirmata_*" value = "spasex_awesome" - assert.Assert(t, !checkSingleValue(value, pattern)) + assert.Assert(t, checkSingleValue(value, pattern) != nil) pattern = "g?t" value = "git" - assert.Assert(t, checkSingleValue(value, pattern)) + assert.NilError(t, checkSingleValue(value, pattern)) } func TestSkipArrayObject_OneAnchor(t *testing.T) { @@ -330,7 +332,7 @@ func TestValidateMapElement_TwoElementsInArrayOnePass(t *testing.T) { json.Unmarshal(rawPattern, &pattern) json.Unmarshal(rawMap, &resource) - assert.Assert(t, validateMapElement(resource, pattern)) + assert.NilError(t, validateMapElement(resource, pattern)) } func TestValidateMapElement_OneElementInArrayPass(t *testing.T) { @@ -341,7 +343,7 @@ func TestValidateMapElement_OneElementInArrayPass(t *testing.T) { json.Unmarshal(rawPattern, &pattern) json.Unmarshal(rawMap, &resource) - assert.Assert(t, validateMapElement(resource, pattern)) + assert.NilError(t, validateMapElement(resource, pattern)) } func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) { @@ -352,5 +354,19 @@ func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) { json.Unmarshal(rawPattern, &pattern) json.Unmarshal(rawMap, &resource) - assert.Assert(t, !validateMapElement(resource, pattern)) + assert.Assert(t, validateMapElement(resource, pattern) != nil) +} + +func TestValidate_ServiceTest(t *testing.T) { + rawPolicy := []byte(`{ "apiVersion": "kubepolicy.nirmata.io/v1alpha1", "kind": "Policy", "metadata": { "name": "policy-service" }, "spec": { "rules": [ { "name": "ps1", "resource": { "kind": "Service", "name": "game-service*" }, "mutate": { "patches": [ { "path": "/metadata/labels/isMutated", "op": "add", "value": "true" }, { "path": "/metadata/labels/secretLabel", "op": "replace", "value": "weKnow" }, { "path": "/metadata/labels/originalLabel", "op": "remove" }, { "path": "/spec/selector/app", "op": "replace", "value": "mutedApp" } ] }, "validate": { "message": "This resource is broken", "pattern": { "spec": { "ports": [ { "name": "hs", "protocol": 32 } ] } } } } ] } }`) + rawResource := []byte(`{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "game-service", "labels": { "originalLabel": "isHere", "secretLabel": "thisIsMySecret" } }, "spec": { "selector": { "app": "MyApp" }, "ports": [ { "name": "http", "protocol": "TCP", "port": 80, "targetPort": 9376 } ] } }`) + + var policy kubepolicy.Policy + json.Unmarshal(rawPolicy, &policy) + + gvk := metav1.GroupVersionKind{ + Kind: "Service", + } + + assert.Assert(t, Validate(policy, rawResource, gvk) != nil) } diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 61a6e5cba5..e671af98fa 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -176,7 +176,7 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 for _, policy := range policies { ws.logger.Printf("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) - if ok := engine.Validate(*policy, request.Object.Raw, request.Kind); !ok { + if err := engine.Validate(*policy, request.Object.Raw, request.Kind); err != nil { ws.logger.Printf("Validation has failed: %v\n", err) utilruntime.HandleError(err) allowed = false From 8f3361e96b3caae2e90e707188883d2ed1731b64 Mon Sep 17 00:00:00 2001 From: Maxim Goncharenko Date: Mon, 20 May 2019 15:41:23 +0300 Subject: [PATCH 06/11] Fixed issue with no message on errorness validation for user --- pkg/engine/validation.go | 41 +++++++++++++++++++++++----------------- pkg/webhooks/server.go | 25 ++++++++++++++---------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 7cc1ec9e49..35d40393ab 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -45,6 +45,13 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers } if err := validateMap(resource, rule.Validation.Pattern); err != nil { + message := *rule.Validation.Message + if len(message) == 0 { + message = fmt.Sprintf("%v", err) + } else { + message = fmt.Sprintf("%s, %s", message, err.Error()) + } + return fmt.Errorf("%s: %s", *rule.Validation.Message, err.Error()) } } @@ -56,12 +63,12 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers func validateMap(resourcePart, patternPart interface{}) error { pattern, ok := patternPart.(map[string]interface{}) if !ok { - return fmt.Errorf("Expected map, found %T", patternPart) + return fmt.Errorf("expected map, found %T", patternPart) } resource, ok := resourcePart.(map[string]interface{}) if !ok { - return fmt.Errorf("Expected map, found %T", resourcePart) + return fmt.Errorf("expected map, found %T", resourcePart) } for key, value := range pattern { @@ -80,12 +87,12 @@ func validateMap(resourcePart, patternPart interface{}) error { func validateArray(resourcePart, patternPart interface{}) error { patternArray, ok := patternPart.([]interface{}) if !ok { - return fmt.Errorf("Expected array, found %T", patternPart) + return fmt.Errorf("expected array, found %T", patternPart) } resourceArray, ok := resourcePart.([]interface{}) if !ok { - return fmt.Errorf("Expected array, found %T", resourcePart) + return fmt.Errorf("expected array, found %T", resourcePart) } switch pattern := patternArray[0].(type) { @@ -98,7 +105,7 @@ func validateArray(resourcePart, patternPart interface{}) error { for _, value := range resourceArray { resource, ok := value.(map[string]interface{}) if !ok { - return fmt.Errorf("Expected array, found %T", resourcePart) + return fmt.Errorf("expected array, found %T", resourcePart) } if skipArrayObject(resource, anchors) { @@ -125,14 +132,14 @@ func validateMapElement(resourcePart, patternPart interface{}) error { case map[string]interface{}: dictionary, ok := resourcePart.(map[string]interface{}) if !ok { - return fmt.Errorf("Expected %T, found %T", patternPart, resourcePart) + return fmt.Errorf("expected %T, found %T", patternPart, resourcePart) } return validateMap(dictionary, pattern) case []interface{}: array, ok := resourcePart.([]interface{}) if !ok { - return fmt.Errorf("Expected %T, found %T", patternPart, resourcePart) + return fmt.Errorf("expected %T, found %T", patternPart, resourcePart) } return validateArray(array, pattern) @@ -140,12 +147,12 @@ func validateMapElement(resourcePart, patternPart interface{}) error { str, ok := resourcePart.(string) if !ok { - return fmt.Errorf("Expected %T, found %T", patternPart, resourcePart) + return fmt.Errorf("expected %T, found %T", patternPart, resourcePart) } return checkSingleValue(str, pattern) default: - return fmt.Errorf("Validating error: unknown type in map: %T", patternPart) + return fmt.Errorf("validating error: unknown type in map: %T", patternPart) } } @@ -189,28 +196,28 @@ func checkSingleValue(value, pattern interface{}) error { case int: return checkForOperator(float64(typedValue), typedPattern) default: - return fmt.Errorf("Expected string or numerical type, found %T, pattern: %s", value, typedPattern) + return fmt.Errorf("expected string or numerical type, found %T, pattern: %s", value, typedPattern) } case float64: num, ok := value.(float64) if !ok { - return fmt.Errorf("Expected float, found %T", value) + return fmt.Errorf("expected float, found %T", value) } if typedPattern != num { - return fmt.Errorf("Value %f is not equal to pattern %f", value, typedPattern) + return fmt.Errorf("value %f is not equal to pattern %f", value, typedPattern) } case int: num, ok := value.(int) if !ok { - return fmt.Errorf("Expected int, found %T", value) + return fmt.Errorf("expected int, found %T", value) } if typedPattern != num { - return fmt.Errorf("Value %d is not equal to pattern %d", num, typedPattern) + return fmt.Errorf("value %d is not equal to pattern %d", num, typedPattern) } default: - return fmt.Errorf("Expected pattern (string or numerical type), found %T", pattern) + return fmt.Errorf("expected pattern (string or numerical type), found %T", pattern) } return nil @@ -218,7 +225,7 @@ func checkSingleValue(value, pattern interface{}) error { func checkForWildcard(value, pattern string) error { if !wildcard.Match(pattern, value) { - return fmt.Errorf("Wildcard check has failed. Pattern: \"%s\". Value: \"%s\"", pattern, value) + return fmt.Errorf("wildcard check has failed. Pattern: \"%s\". Value: \"%s\"", pattern, value) } return nil @@ -236,7 +243,7 @@ func checkForOperator(value float64, pattern string) error { } } - return fmt.Errorf("Operator check has failed. Pattern: \"%s\". Value: \"%f\"", pattern, value) + return fmt.Errorf("operator check has failed. Pattern: \"%s\". Value: \"%f\"", pattern, value) } func checkSingleOperator(value float64, pattern string) bool { diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index e671af98fa..4842004989 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -18,8 +18,8 @@ import ( "github.com/nirmata/kube-policy/pkg/engine/mutation" tlsutils "github.com/nirmata/kube-policy/pkg/tls" v1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) // WebhookServer contains configured TLS server with MutationWebhook. @@ -135,7 +135,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be policies, err := ws.policyLister.List(labels.NewSelector()) if err != nil { - utilruntime.HandleError(err) + ws.logger.Printf("%v", err) return nil } @@ -168,26 +168,31 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 policies, err := ws.policyLister.List(labels.NewSelector()) if err != nil { - utilruntime.HandleError(err) + ws.logger.Printf("%v", err) return nil } - allowed := true for _, policy := range policies { ws.logger.Printf("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) if err := engine.Validate(*policy, request.Object.Raw, request.Kind); err != nil { - ws.logger.Printf("Validation has failed: %v\n", err) - utilruntime.HandleError(err) - allowed = false - } else { - ws.logger.Println("Validation is successful") + message := fmt.Sprintf("validation has failed: %s", err.Error()) + ws.logger.Println(message) + + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: message, + }, + } } } + ws.logger.Println("Validation is successful") return &v1beta1.AdmissionResponse{ - Allowed: allowed, + Allowed: true, } + } // bodyToAdmissionReview creates AdmissionReview object from request body From 0aebb2a88e7b564b506628d2d9a27a4660e61978 Mon Sep 17 00:00:00 2001 From: Maxim Goncharenko Date: Mon, 20 May 2019 17:07:09 +0300 Subject: [PATCH 07/11] Fixed int and float types mismatch --- pkg/engine/validation.go | 28 ++++++++++++++++++++++++++++ pkg/engine/validation_test.go | 14 ++++++++++++++ pkg/webhooks/server.go | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 35d40393ab..77521deee7 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -151,9 +151,37 @@ func validateMapElement(resourcePart, patternPart interface{}) error { } return checkSingleValue(str, pattern) + case float64: + switch num := resourcePart.(type) { + case float64: + if num != pattern { + return fmt.Errorf("%f not equal %f", num, pattern) + } + case int64: + if float64(num) != pattern { + return fmt.Errorf("%d not equal %f", num, pattern) + } + default: + return fmt.Errorf("expected %T, found %T", patternPart, resourcePart) + } + case int64: + switch num := resourcePart.(type) { + case float64: + if num != float64(pattern) { + return fmt.Errorf("%f not equal %d", num, pattern) + } + case int64: + if float64(num) != float64(num) { + return fmt.Errorf("%d not equal %d", num, pattern) + } + default: + return fmt.Errorf("expected %T, found %T", patternPart, resourcePart) + } default: return fmt.Errorf("validating error: unknown type in map: %T", patternPart) } + + return nil } func getAnchorsFromMap(pattern map[string]interface{}) (map[string]interface{}, error) { diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 93f5ca7360..eac1a884bf 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -370,3 +370,17 @@ func TestValidate_ServiceTest(t *testing.T) { assert.Assert(t, Validate(policy, rawResource, gvk) != nil) } + +func TestValidate_MapHasFloats(t *testing.T) { + rawPolicy := []byte(`{ "apiVersion": "kubepolicy.nirmata.io/v1alpha1", "kind": "Policy", "metadata": { "name": "policy-deployment-changed" }, "spec": { "rules": [ { "name": "First policy v2", "resource": { "kind": "Deployment", "name": "nginx-*" }, "mutate": { "patches": [ { "path": "/metadata/labels/isMutated", "op": "add", "value": "true" }, { "path": "/metadata/labels/app", "op": "replace", "value": "nginx_is_mutated" } ] }, "validate": { "message": "replicas number is wrong", "pattern": { "metadata": { "labels": { "app": "*" } }, "spec": { "replicas": 3 } } } } ] } }`) + rawResource := []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "name": "nginx-deployment", "labels": { "app": "nginx" } }, "spec": { "replicas": 3, "selector": { "matchLabels": { "app": "nginx" } }, "template": { "metadata": { "labels": { "app": "nginx" } }, "spec": { "containers": [ { "name": "nginx", "image": "nginx:1.7.9", "ports": [ { "containerPort": 80 } ] } ] } } } }`) + + var policy kubepolicy.Policy + json.Unmarshal(rawPolicy, &policy) + + gvk := metav1.GroupVersionKind{ + Kind: "Deployment", + } + + assert.NilError(t, Validate(policy, rawResource, gvk)) +} diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 4842004989..4d85a2ca02 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -149,7 +149,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be if len(policyPatches) > 0 { namespace := engine.ParseNamespaceFromObject(request.Object.Raw) name := engine.ParseNameFromObject(request.Object.Raw) - ws.logger.Printf("Policy %s applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name) + ws.logger.Printf("Mutation from policy %s has applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name) } } From 500e8d7e164cb2d8c307b4bc4a6a5faf4a72d249 Mon Sep 17 00:00:00 2001 From: Maxim Goncharenko Date: Mon, 20 May 2019 18:28:54 +0300 Subject: [PATCH 08/11] Fixed string and float type mismatches --- pkg/engine/validation.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 77521deee7..a6b55cadaf 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -144,13 +144,7 @@ func validateMapElement(resourcePart, patternPart interface{}) error { return validateArray(array, pattern) case string: - str, ok := resourcePart.(string) - - if !ok { - return fmt.Errorf("expected %T, found %T", patternPart, resourcePart) - } - - return checkSingleValue(str, pattern) + return checkSingleValue(resourcePart, patternPart) case float64: switch num := resourcePart.(type) { case float64: From ffe644f8217db8167618c4397202679b3c51e218 Mon Sep 17 00:00:00 2001 From: shuting Date: Mon, 20 May 2019 13:02:55 -0700 Subject: [PATCH 09/11] Support Mutate from command line --- Gopkg.toml | 4 + cmd/build | 1 + cmd/kyverno.go | 17 ++++ pkg/engine/mutation.go | 8 +- pkg/engine/mutation/patches.go | 11 +-- pkg/kyverno/apply/apply.go | 148 +++++++++++++++++++++++++++++++++ pkg/kyverno/cmd.go | 25 ++++++ pkg/webhooks/server.go | 2 +- 8 files changed, 207 insertions(+), 9 deletions(-) create mode 100755 cmd/build create mode 100644 cmd/kyverno.go create mode 100644 pkg/kyverno/apply/apply.go create mode 100644 pkg/kyverno/cmd.go diff --git a/Gopkg.toml b/Gopkg.toml index cd12f08501..b80ad21c0b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -31,3 +31,7 @@ required = ["k8s.io/code-generator/cmd/client-gen"] [[override]] name = "github.com/golang/protobuf" version = "v1.2.1" + +[[constraint]] + name = "gopkg.in/yaml.v2" + version = "2.2.2" diff --git a/cmd/build b/cmd/build new file mode 100755 index 0000000000..691afc14f2 --- /dev/null +++ b/cmd/build @@ -0,0 +1 @@ +go build -o kyverno \ No newline at end of file diff --git a/cmd/kyverno.go b/cmd/kyverno.go new file mode 100644 index 0000000000..9f3d33d66f --- /dev/null +++ b/cmd/kyverno.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" + + kyverno "github.com/nirmata/kube-policy/pkg/kyverno" +) + +func main() { + cmd := kyverno.NewDefaultKyvernoCommand() + + if err := cmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index ec946f10db..e81abc88f7 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -10,8 +10,10 @@ import ( // Mutate performs mutation. Overlay first and then mutation patches // TODO: return events and violations -func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) []mutation.PatchBytes { +func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([]mutation.PatchBytes, []byte) { var policyPatches []mutation.PatchBytes + var processedPatches []mutation.PatchBytes + patchedDocument := rawResource for i, rule := range policy.Spec.Rules { @@ -55,7 +57,7 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio // Process Patches if rule.Mutation.Patches != nil { - processedPatches, err := mutation.ProcessPatches(rule.Mutation.Patches, rawResource) + processedPatches, patchedDocument, err = mutation.ProcessPatches(rule.Mutation.Patches, patchedDocument) if err != nil { log.Printf("Patches application failed: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) } else { @@ -64,5 +66,5 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio } } - return policyPatches + return policyPatches, patchedDocument } diff --git a/pkg/engine/mutation/patches.go b/pkg/engine/mutation/patches.go index 0c298352aa..db8316d6dc 100644 --- a/pkg/engine/mutation/patches.go +++ b/pkg/engine/mutation/patches.go @@ -13,19 +13,20 @@ type PatchBytes []byte // ProcessPatches Returns array from separate patches that can be applied to the document // Returns error ONLY in case when creation of resource should be denied. -func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, error) { +func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, []byte, error) { if len(resource) == 0 { - return nil, errors.New("Source document for patching is empty") + return nil, nil, errors.New("Source document for patching is empty") } var appliedPatches []PatchBytes + patchedDocument := resource for i, patch := range patches { patchRaw, err := json.Marshal(patch) if err != nil { - return nil, err + return nil, nil, err } - _, err = applyPatch(resource, patchRaw) + patchedDocument, err = applyPatch(patchedDocument, patchRaw) if err != nil { // TODO: continue on error if one of the patches fails, will add the failure event in such case log.Printf("Patch failed: patch number = %d, patch Operation = %s, err: %v", i, patch.Operation, err) @@ -34,7 +35,7 @@ func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, appliedPatches = append(appliedPatches, patchRaw) } - return appliedPatches, nil + return appliedPatches, patchedDocument, nil } // JoinPatches joins array of serialized JSON patches to the single JSONPatch array diff --git a/pkg/kyverno/apply/apply.go b/pkg/kyverno/apply/apply.go new file mode 100644 index 0000000000..dd4815dc8b --- /dev/null +++ b/pkg/kyverno/apply/apply.go @@ -0,0 +1,148 @@ +package apply + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "strings" + + kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + "github.com/nirmata/kube-policy/pkg/engine" + "github.com/spf13/cobra" + yamlv2 "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + yaml "k8s.io/apimachinery/pkg/util/yaml" +) + +const applyExample = ` # Apply a policy to the resource. + kyverno apply @policy.yaml @resource.yaml` + +// NewCmdApply returns the apply command for kyverno +func NewCmdApply(in io.Reader, out, errout io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "apply", + Short: "Apply policy on the resource", + Example: applyExample, + Run: func(cmd *cobra.Command, args []string) { + // TODO: add pre-checks for policy and resource manifest + // order for policy and resource in args could be disordered + + if len(args) != 2 { + log.Printf("Missing policy and/or resource manifest.") + return + } + + // extract policy + policyDir := validateDir(args[0]) + policy, err := extractPolicy(policyDir) + if err != nil { + log.Printf("failed to extract policy: %v", err) + os.Exit(1) + } + + // fmt.Printf("policy name=%s, rule name=%s, %s/%s\n", policy.ObjectMeta.Name, policy.Spec.Rules[0].Name, + // policy.Spec.Rules[0].ResourceDescription.Kind, *policy.Spec.Rules[0].ResourceDescription.Name) + + // extract rawResource + resourceDir := validateDir(args[1]) + rawResource, gvk, err := extractResource(resourceDir) + if err != nil { + log.Printf("failed to load resource: %v", err) + os.Exit(1) + } + + _, patchedDocument := engine.Mutate(*policy, rawResource, *gvk) + out, err := prettyPrint(patchedDocument) + if err != nil { + fmt.Printf("JSON parse error: %v\n", err) + fmt.Printf("%v\n", string(patchedDocument)) + return + } + + fmt.Printf("%v\n", string(out)) + }, + } + return cmd +} + +func extractPolicy(fileDir string) (*kubepolicy.Policy, error) { + policy := &kubepolicy.Policy{} + + file, err := loadFile(fileDir) + if err != nil { + return nil, fmt.Errorf("failed to load file: %v", err) + } + + policyBytes, err := yaml.ToJSON(file) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(policyBytes, policy); err != nil { + return nil, fmt.Errorf("failed to decode policy %s, err: %v", policy.Name, err) + } + + return policy, nil +} + +func extractResource(fileDir string) ([]byte, *metav1.GroupVersionKind, error) { + file, err := loadFile(fileDir) + if err != nil { + return nil, nil, fmt.Errorf("failed to load file: %v", err) + } + + data := make(map[interface{}]interface{}) + + if err = yamlv2.Unmarshal([]byte(file), &data); err != nil { + return nil, nil, fmt.Errorf("failed to parse resource: %v", err) + } + + apiVersion, ok := data["apiVersion"].(string) + if !ok { + return nil, nil, fmt.Errorf("failed to parse apiversion: %v", err) + } + + kind, ok := data["kind"].(string) + if !ok { + return nil, nil, fmt.Errorf("failed to parse kind of resource: %v", err) + } + + var gvk *metav1.GroupVersionKind + gv := strings.Split(apiVersion, "/") + if len(gv) == 2 { + gvk = &metav1.GroupVersionKind{Group: gv[0], Version: gv[1], Kind: kind} + } else { + gvk = &metav1.GroupVersionKind{Version: gv[0], Kind: kind} + } + + json, err := yaml.ToJSON(file) + + return json, gvk, err +} + +func loadFile(fileDir string) ([]byte, error) { + if _, err := os.Stat(fileDir); os.IsNotExist(err) { + return nil, err + } + + return ioutil.ReadFile(fileDir) +} + +func validateDir(dir string) string { + if strings.HasPrefix(dir, "@") { + return dir[1:] + } + return dir +} + +func prettyPrint(data []byte) ([]byte, error) { + out := make(map[interface{}]interface{}) + if err := yamlv2.Unmarshal(data, &out); err != nil { + return nil, err + } + + return yamlv2.Marshal(&out) +} diff --git a/pkg/kyverno/cmd.go b/pkg/kyverno/cmd.go new file mode 100644 index 0000000000..0f663e28a6 --- /dev/null +++ b/pkg/kyverno/cmd.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "io" + "os" + + "github.com/nirmata/kube-policy/pkg/kyverno/apply" + "github.com/spf13/cobra" +) + +// NewDefaultKyvernoCommand ... +func NewDefaultKyvernoCommand() *cobra.Command { + return NewKyvernoCommand(os.Stdin, os.Stdout, os.Stderr) +} + +// NewKyvernoCommand returns the new kynerno command +func NewKyvernoCommand(in io.Reader, out, errout io.Writer) *cobra.Command { + cmds := &cobra.Command{ + Use: "kyverno", + Short: "kyverno manages native policies of Kubernetes", + } + + cmds.AddCommand(apply.NewCmdApply(in, out, errout)) + return cmds +} diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 59735f1c2e..14d04056c8 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -147,7 +147,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be for _, policy := range policies { ws.logger.Printf("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) - policyPatches := engine.Mutate(*policy, request.Object.Raw, request.Kind) + policyPatches, _ := engine.Mutate(*policy, request.Object.Raw, request.Kind) allPatches = append(allPatches, policyPatches...) if len(policyPatches) > 0 { From 91b7a1b9aca1f0ad55cdbf4e28443e13f4c1ff22 Mon Sep 17 00:00:00 2001 From: shuting Date: Mon, 20 May 2019 15:14:01 -0700 Subject: [PATCH 10/11] - handle operation remove case: if path does not exist - remove duplicate log - support validate in CLI --- pkg/engine/mutation.go | 1 + pkg/engine/mutation/patches.go | 9 ++++- pkg/engine/mutation/patches_test.go | 18 ++++----- pkg/engine/validation.go | 1 - pkg/kyverno/apply/apply.go | 63 +++++++++++++++++------------ 5 files changed, 55 insertions(+), 37 deletions(-) diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 63bf86358e..1474c4364f 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -13,6 +13,7 @@ import ( func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([]mutation.PatchBytes, []byte) { var policyPatches []mutation.PatchBytes var processedPatches []mutation.PatchBytes + var err error patchedDocument := rawResource for _, rule := range policy.Spec.Rules { diff --git a/pkg/engine/mutation/patches.go b/pkg/engine/mutation/patches.go index db8316d6dc..068125c4fa 100644 --- a/pkg/engine/mutation/patches.go +++ b/pkg/engine/mutation/patches.go @@ -29,6 +29,9 @@ func ProcessPatches(patches []kubepolicy.Patch, resource []byte) ([]PatchBytes, patchedDocument, err = applyPatch(patchedDocument, patchRaw) if err != nil { // TODO: continue on error if one of the patches fails, will add the failure event in such case + if patch.Operation == "remove" { + continue + } log.Printf("Patch failed: patch number = %d, patch Operation = %s, err: %v", i, patch.Operation, err) continue } @@ -66,5 +69,9 @@ func applyPatch(resource []byte, patchRaw []byte) ([]byte, error) { return nil, err } - return patch.Apply(resource) + patchedDocument, err := patch.Apply(resource) + if err != nil { + return resource, err + } + return patchedDocument, err } diff --git a/pkg/engine/mutation/patches_test.go b/pkg/engine/mutation/patches_test.go index 825a5723cb..e4d26e5a1a 100644 --- a/pkg/engine/mutation/patches_test.go +++ b/pkg/engine/mutation/patches_test.go @@ -35,7 +35,7 @@ const endpointsDocument string = `{ func TestProcessPatches_EmptyPatches(t *testing.T) { var empty []types.Patch - patches, err := ProcessPatches(empty, []byte(endpointsDocument)) + patches, _, err := ProcessPatches(empty, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patches) == 0) } @@ -51,13 +51,13 @@ func makeAddIsMutatedLabelPatch() types.Patch { func TestProcessPatches_EmptyDocument(t *testing.T) { var patches []types.Patch patches = append(patches, makeAddIsMutatedLabelPatch()) - patchesBytes, err := ProcessPatches(patches, nil) + patchesBytes, _, err := ProcessPatches(patches, nil) assert.Assert(t, err != nil) assert.Assert(t, len(patchesBytes) == 0) } func TestProcessPatches_AllEmpty(t *testing.T) { - patchesBytes, err := ProcessPatches(nil, nil) + patchesBytes, _, err := ProcessPatches(nil, nil) assert.Assert(t, err != nil) assert.Assert(t, len(patchesBytes) == 0) } @@ -66,7 +66,7 @@ func TestProcessPatches_AddPathDoesntExist(t *testing.T) { patch := makeAddIsMutatedLabelPatch() patch.Path = "/metadata/additional/is-mutated" patches := []types.Patch{patch} - patchesBytes, err := ProcessPatches(patches, []byte(endpointsDocument)) + patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patchesBytes) == 0) } @@ -74,7 +74,7 @@ func TestProcessPatches_AddPathDoesntExist(t *testing.T) { func TestProcessPatches_RemovePathDoesntExist(t *testing.T) { patch := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"} patches := []types.Patch{patch} - patchesBytes, err := ProcessPatches(patches, []byte(endpointsDocument)) + patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patchesBytes) == 0) } @@ -83,7 +83,7 @@ func TestProcessPatches_AddAndRemovePathsDontExist_EmptyResult(t *testing.T) { patch1 := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"} patch2 := types.Patch{Path: "/spec/labels/label3", Operation: "add", Value: "label3Value"} patches := []types.Patch{patch1, patch2} - patchesBytes, err := ProcessPatches(patches, []byte(endpointsDocument)) + patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patchesBytes) == 0) } @@ -93,7 +93,7 @@ func TestProcessPatches_AddAndRemovePathsDontExist_ContinueOnError_NotEmptyResul patch2 := types.Patch{Path: "/spec/labels/label2", Operation: "remove", Value: "label2Value"} patch3 := types.Patch{Path: "/metadata/labels/label3", Operation: "add", Value: "label3Value"} patches := []types.Patch{patch1, patch2, patch3} - patchesBytes, err := ProcessPatches(patches, []byte(endpointsDocument)) + patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patchesBytes) == 1) assertEqStringAndData(t, `{"path":"/metadata/labels/label3","op":"add","value":"label3Value"}`, patchesBytes[0]) @@ -102,7 +102,7 @@ func TestProcessPatches_AddAndRemovePathsDontExist_ContinueOnError_NotEmptyResul func TestProcessPatches_RemovePathDoesntExist_EmptyResult(t *testing.T) { patch := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"} patches := []types.Patch{patch} - patchesBytes, err := ProcessPatches(patches, []byte(endpointsDocument)) + patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patchesBytes) == 0) } @@ -111,7 +111,7 @@ func TestProcessPatches_RemovePathDoesntExist_NotEmptyResult(t *testing.T) { patch1 := types.Patch{Path: "/metadata/labels/is-mutated", Operation: "remove"} patch2 := types.Patch{Path: "/metadata/labels/label2", Operation: "add", Value: "label2Value"} patches := []types.Patch{patch1, patch2} - patchesBytes, err := ProcessPatches(patches, []byte(endpointsDocument)) + patchesBytes, _, err := ProcessPatches(patches, []byte(endpointsDocument)) assert.NilError(t, err) assert.Assert(t, len(patchesBytes) == 1) assertEqStringAndData(t, `{"path":"/metadata/labels/label2","op":"add","value":"label2Value"}`, patchesBytes[0]) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index a6b55cadaf..c3f4d788cd 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -56,7 +56,6 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers } } - log.Println("Validation is successful") return nil } diff --git a/pkg/kyverno/apply/apply.go b/pkg/kyverno/apply/apply.go index dd4815dc8b..dfd8a0ca36 100644 --- a/pkg/kyverno/apply/apply.go +++ b/pkg/kyverno/apply/apply.go @@ -27,32 +27,7 @@ func NewCmdApply(in io.Reader, out, errout io.Writer) *cobra.Command { Short: "Apply policy on the resource", Example: applyExample, Run: func(cmd *cobra.Command, args []string) { - // TODO: add pre-checks for policy and resource manifest - // order for policy and resource in args could be disordered - - if len(args) != 2 { - log.Printf("Missing policy and/or resource manifest.") - return - } - - // extract policy - policyDir := validateDir(args[0]) - policy, err := extractPolicy(policyDir) - if err != nil { - log.Printf("failed to extract policy: %v", err) - os.Exit(1) - } - - // fmt.Printf("policy name=%s, rule name=%s, %s/%s\n", policy.ObjectMeta.Name, policy.Spec.Rules[0].Name, - // policy.Spec.Rules[0].ResourceDescription.Kind, *policy.Spec.Rules[0].ResourceDescription.Name) - - // extract rawResource - resourceDir := validateDir(args[1]) - rawResource, gvk, err := extractResource(resourceDir) - if err != nil { - log.Printf("failed to load resource: %v", err) - os.Exit(1) - } + policy, rawResource, gvk := complete(args) _, patchedDocument := engine.Mutate(*policy, rawResource, *gvk) out, err := prettyPrint(patchedDocument) @@ -62,12 +37,48 @@ func NewCmdApply(in io.Reader, out, errout io.Writer) *cobra.Command { return } + if err := engine.Validate(*policy, rawResource, *gvk); err != nil { + fmt.Println(err) + return + } + fmt.Printf("%v\n", string(out)) }, } return cmd } +func complete(args []string) (*kubepolicy.Policy, []byte, *metav1.GroupVersionKind) { + // TODO: add pre-checks for policy and resource manifest + // order for policy and resource in args could be disordered + + if len(args) != 2 { + log.Printf("Missing policy and/or resource manifest.") + return nil, nil, nil + } + + // extract policy + policyDir := validateDir(args[0]) + policy, err := extractPolicy(policyDir) + if err != nil { + log.Printf("failed to extract policy: %v", err) + os.Exit(1) + } + + // fmt.Printf("policy name=%s, rule name=%s, %s/%s\n", policy.ObjectMeta.Name, policy.Spec.Rules[0].Name, + // policy.Spec.Rules[0].ResourceDescription.Kind, *policy.Spec.Rules[0].ResourceDescription.Name) + + // extract rawResource + resourceDir := validateDir(args[1]) + rawResource, gvk, err := extractResource(resourceDir) + if err != nil { + log.Printf("failed to load resource: %v", err) + os.Exit(1) + } + + return policy, rawResource, gvk +} + func extractPolicy(fileDir string) (*kubepolicy.Policy, error) { policy := &kubepolicy.Policy{} From 771dcd358e1021f0fdb82e46a6f984ebeecda1b6 Mon Sep 17 00:00:00 2001 From: shuting Date: Mon, 20 May 2019 17:59:13 -0700 Subject: [PATCH 11/11] support policy apply to multiple resources --- .../CLI/deployment/policy-deployment.yaml | 30 +++ examples/CLI/deployment/resource/d1.yaml | 23 ++ examples/CLI/deployment/resource/d2.yaml | 35 ++++ pkg/kyverno/apply/apply.go | 198 ++++++++++++------ 4 files changed, 226 insertions(+), 60 deletions(-) create mode 100644 examples/CLI/deployment/policy-deployment.yaml create mode 100644 examples/CLI/deployment/resource/d1.yaml create mode 100644 examples/CLI/deployment/resource/d2.yaml diff --git a/examples/CLI/deployment/policy-deployment.yaml b/examples/CLI/deployment/policy-deployment.yaml new file mode 100644 index 0000000000..ef8ab8b9bf --- /dev/null +++ b/examples/CLI/deployment/policy-deployment.yaml @@ -0,0 +1,30 @@ +apiVersion : kubepolicy.nirmata.io/v1alpha1 +kind : Policy +metadata : + name : policy-deployment +spec : + rules: + - name: deployment-policy + resource: + kind : Deployment + selector : + matchLabels : + cli: test + mutate: + patches: + - path: /metadata/labels/isMutated + op: add + value: "true" + - path: /metadata/labels/app + op: replace + value: "nginx_is_mutated" + validate: + message: "The imagePullPolicy shoud set to Always" + pattern: + spec: + template: + spec: + containers: + - (name): "*" + imagePullPolicy: Always + diff --git a/examples/CLI/deployment/resource/d1.yaml b/examples/CLI/deployment/resource/d1.yaml new file mode 100644 index 0000000000..db1db6b186 --- /dev/null +++ b/examples/CLI/deployment/resource/d1.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx + cli: test +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + imagePullPolicy: Always + ports: + - containerPort: 80 diff --git a/examples/CLI/deployment/resource/d2.yaml b/examples/CLI/deployment/resource/d2.yaml new file mode 100644 index 0000000000..8459622f2d --- /dev/null +++ b/examples/CLI/deployment/resource/d2.yaml @@ -0,0 +1,35 @@ +kind: "Deployment" +apiVersion: "extensions/v1beta1" +metadata: + name: "ghost" + labels: + nirmata.io/deployment.name: "ghost" + nirmata.io/application.name: "ghost" + nirmata.io/component: "ghost" + cli: test +spec: + replicas: 1 + revisionHistoryLimit: 5 + selector: + matchLabels: + nirmata.io/application.name: "ghost" + nirmata.io/component: "ghost" + strategy: + type: "RollingUpdate" + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + nirmata.io/deployment.name: "ghost" + nirmata.io/application.name: "ghost" + nirmata.io/component: "ghost" + spec: + containers: + - name: "ghost" + image: "ghost:2.9.1-alpine" + imagePullPolicy: Always + ports: + - containerPort: 8080 + protocol: "TCP" diff --git a/pkg/kyverno/apply/apply.go b/pkg/kyverno/apply/apply.go index dfd8a0ca36..d4e147aa80 100644 --- a/pkg/kyverno/apply/apply.go +++ b/pkg/kyverno/apply/apply.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "strings" kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" @@ -18,65 +19,73 @@ import ( ) const applyExample = ` # Apply a policy to the resource. - kyverno apply @policy.yaml @resource.yaml` + kyverno apply @policy.yaml @resource.yaml + kyverno apply @policy.yaml @resourceDir/` // NewCmdApply returns the apply command for kyverno func NewCmdApply(in io.Reader, out, errout io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "apply", - Short: "Apply policy on the resource", + Short: "Apply policy on the resource(s)", Example: applyExample, Run: func(cmd *cobra.Command, args []string) { - policy, rawResource, gvk := complete(args) + var output string + policy, resources := complete(args) - _, patchedDocument := engine.Mutate(*policy, rawResource, *gvk) - out, err := prettyPrint(patchedDocument) - if err != nil { - fmt.Printf("JSON parse error: %v\n", err) - fmt.Printf("%v\n", string(patchedDocument)) - return + for _, resource := range resources { + patchedDocument, err := applyPolicy(policy, resource.rawResource, resource.gvk) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + out, err := prettyPrint(patchedDocument) + if err != nil { + fmt.Printf("JSON parse error: %v\n", err) + fmt.Printf("%v\n", string(patchedDocument)) + return + } + + output = output + fmt.Sprintf("---\n%s", string(out)) } - - if err := engine.Validate(*policy, rawResource, *gvk); err != nil { - fmt.Println(err) - return - } - - fmt.Printf("%v\n", string(out)) + fmt.Printf("%v\n", output) }, } return cmd } -func complete(args []string) (*kubepolicy.Policy, []byte, *metav1.GroupVersionKind) { - // TODO: add pre-checks for policy and resource manifest - // order for policy and resource in args could be disordered +func complete(args []string) (*kubepolicy.Policy, []*resourceInfo) { - if len(args) != 2 { - log.Printf("Missing policy and/or resource manifest.") - return nil, nil, nil + policyDir, resourceDir, err := validateDir(args) + if err != nil { + fmt.Printf("Failed to parse file path, err: %v\n", err) + os.Exit(1) } // extract policy - policyDir := validateDir(args[0]) policy, err := extractPolicy(policyDir) if err != nil { log.Printf("failed to extract policy: %v", err) os.Exit(1) } - // fmt.Printf("policy name=%s, rule name=%s, %s/%s\n", policy.ObjectMeta.Name, policy.Spec.Rules[0].Name, - // policy.Spec.Rules[0].ResourceDescription.Kind, *policy.Spec.Rules[0].ResourceDescription.Name) - // extract rawResource - resourceDir := validateDir(args[1]) - rawResource, gvk, err := extractResource(resourceDir) + resources, err := extractResource(resourceDir) if err != nil { - log.Printf("failed to load resource: %v", err) + log.Printf("failed to parse resource: %v", err) os.Exit(1) } - return policy, rawResource, gvk + return policy, resources +} + +func applyPolicy(policy *kubepolicy.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]byte, error) { + _, patchedDocument := engine.Mutate(*policy, rawResource, *gvk) + + if err := engine.Validate(*policy, rawResource, *gvk); err != nil { + return nil, err + } + return patchedDocument, nil } func extractPolicy(fileDir string) (*kubepolicy.Policy, error) { @@ -96,42 +105,72 @@ func extractPolicy(fileDir string) (*kubepolicy.Policy, error) { return nil, fmt.Errorf("failed to decode policy %s, err: %v", policy.Name, err) } + if policy.TypeMeta.Kind != "Policy" { + return nil, fmt.Errorf("failed to parse policy") + } + return policy, nil } -func extractResource(fileDir string) ([]byte, *metav1.GroupVersionKind, error) { - file, err := loadFile(fileDir) +type resourceInfo struct { + rawResource []byte + gvk *metav1.GroupVersionKind +} + +func extractResource(fileDir string) ([]*resourceInfo, error) { + var files []string + var resources []*resourceInfo + // check if applied on multiple resources + isDir, err := isDir(fileDir) if err != nil { - return nil, nil, fmt.Errorf("failed to load file: %v", err) + return nil, err } - data := make(map[interface{}]interface{}) - - if err = yamlv2.Unmarshal([]byte(file), &data); err != nil { - return nil, nil, fmt.Errorf("failed to parse resource: %v", err) - } - - apiVersion, ok := data["apiVersion"].(string) - if !ok { - return nil, nil, fmt.Errorf("failed to parse apiversion: %v", err) - } - - kind, ok := data["kind"].(string) - if !ok { - return nil, nil, fmt.Errorf("failed to parse kind of resource: %v", err) - } - - var gvk *metav1.GroupVersionKind - gv := strings.Split(apiVersion, "/") - if len(gv) == 2 { - gvk = &metav1.GroupVersionKind{Group: gv[0], Version: gv[1], Kind: kind} + if isDir { + files, err = ScanDir(fileDir) + if err != nil { + return nil, err + } } else { - gvk = &metav1.GroupVersionKind{Version: gv[0], Kind: kind} + files = []string{fileDir} } - json, err := yaml.ToJSON(file) + for _, dir := range files { + file, err := loadFile(dir) + if err != nil { + return nil, fmt.Errorf("failed to load file: %v", err) + } - return json, gvk, err + data := make(map[interface{}]interface{}) + + if err = yamlv2.Unmarshal([]byte(file), &data); err != nil { + return nil, fmt.Errorf("failed to parse resource: %v", err) + } + + apiVersion, ok := data["apiVersion"].(string) + if !ok { + return nil, fmt.Errorf("failed to parse apiversion: %v", err) + } + + kind, ok := data["kind"].(string) + if !ok { + return nil, fmt.Errorf("failed to parse kind of resource: %v", err) + } + + var gvkInfo *metav1.GroupVersionKind + gv := strings.Split(apiVersion, "/") + if len(gv) == 2 { + gvkInfo = &metav1.GroupVersionKind{Group: gv[0], Version: gv[1], Kind: kind} + } else { + gvkInfo = &metav1.GroupVersionKind{Version: gv[0], Kind: kind} + } + + json, err := yaml.ToJSON(file) + + resources = append(resources, &resourceInfo{rawResource: json, gvk: gvkInfo}) + } + + return resources, err } func loadFile(fileDir string) ([]byte, error) { @@ -142,11 +181,19 @@ func loadFile(fileDir string) ([]byte, error) { return ioutil.ReadFile(fileDir) } -func validateDir(dir string) string { - if strings.HasPrefix(dir, "@") { - return dir[1:] +func validateDir(args []string) (policyDir, resourceDir string, err error) { + if len(args) != 2 { + return "", "", fmt.Errorf("missing policy and/or resource manifest") } - return dir + + if strings.HasPrefix(args[0], "@") { + policyDir = args[0][1:] + } + + if strings.HasPrefix(args[1], "@") { + resourceDir = args[1][1:] + } + return } func prettyPrint(data []byte) ([]byte, error) { @@ -157,3 +204,34 @@ func prettyPrint(data []byte) ([]byte, error) { return yamlv2.Marshal(&out) } + +func isDir(dir string) (bool, error) { + fi, err := os.Stat(dir) + if err != nil { + return false, err + } + + return fi.IsDir(), nil +} + +func ScanDir(dir string) ([]string, error) { + var res []string + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", dir, err) + return err + } + /* if len(strings.Split(path, "/")) == 4 { + fmt.Println(path) + } */ + res = append(res, path) + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error walking the path %q: %v", dir, err) + } + + return res[1:], nil +}