From ffe644f8217db8167618c4397202679b3c51e218 Mon Sep 17 00:00:00 2001 From: shuting Date: Mon, 20 May 2019 13:02:55 -0700 Subject: [PATCH 1/3] 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 2/3] - 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 3/3] 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 +}