From 1fa88e0dd03e018a82acf75ff4473012b10e9609 Mon Sep 17 00:00:00 2001 From: shravan Date: Fri, 6 Mar 2020 03:00:18 +0530 Subject: [PATCH] 536 workin cli --- cmd/cli/kubectl-kyverno/main.go | 9 + pkg/kyverno/apply/command.go | 380 ++++++++++++++++++++++++++++ pkg/kyverno/apply/helper.go | 37 +++ pkg/kyverno/main.go | 50 ++++ pkg/kyverno/sanitizedError/error.go | 20 ++ pkg/kyverno/validate/command.go | 147 +++++++++++ pkg/kyverno/version/command.go | 21 ++ pkg/openapi/validation.go | 6 + pkg/webhooks/policyvalidation.go | 3 +- 9 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 cmd/cli/kubectl-kyverno/main.go create mode 100644 pkg/kyverno/apply/command.go create mode 100644 pkg/kyverno/apply/helper.go create mode 100644 pkg/kyverno/main.go create mode 100644 pkg/kyverno/sanitizedError/error.go create mode 100644 pkg/kyverno/validate/command.go create mode 100644 pkg/kyverno/version/command.go diff --git a/cmd/cli/kubectl-kyverno/main.go b/cmd/cli/kubectl-kyverno/main.go new file mode 100644 index 0000000000..a5adfa5a9e --- /dev/null +++ b/cmd/cli/kubectl-kyverno/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/nirmata/kyverno/pkg/kyverno" +) + +func main() { + kyverno.CLI() +} diff --git a/pkg/kyverno/apply/command.go b/pkg/kyverno/apply/command.go new file mode 100644 index 0000000000..dd00f94ef7 --- /dev/null +++ b/pkg/kyverno/apply/command.go @@ -0,0 +1,380 @@ +package apply + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/nirmata/kyverno/pkg/kyverno/sanitizedError" + + policy2 "github.com/nirmata/kyverno/pkg/policy" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/runtime/schema" + + "k8s.io/client-go/discovery" + + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/nirmata/kyverno/pkg/engine" + + engineutils "github.com/nirmata/kyverno/pkg/engine/utils" + + "k8s.io/apimachinery/pkg/runtime" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/spf13/cobra" + yamlv2 "gopkg.in/yaml.v2" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes/scheme" +) + +func Command() *cobra.Command { + var cmd *cobra.Command + var resourcePaths []string + var cluster bool + + kubernetesConfig := genericclioptions.NewConfigFlags(true) + + cmd = &cobra.Command{ + Use: "apply", + Short: "Applies policies on resources", + Example: fmt.Sprintf("To apply on a resource:\nkyverno apply /path/to/policy.yaml /path/to/folderOfPolicies --resource=/path/to/resource1 --resource=/path/to/resource2\n\nTo apply on a cluster\nkyverno apply /path/to/policy.yaml /path/to/folderOfPolicies --cluster"), + RunE: func(cmd *cobra.Command, policyPaths []string) (err error) { + defer func() { + if err != nil { + if !sanitizedError.IsErrorSanitized(err) { + glog.V(4).Info(err) + err = fmt.Errorf("Internal error") + } + } + }() + + if len(resourcePaths) == 0 && !cluster { + return sanitizedError.New(fmt.Sprintf("Specify path to resource file or cluster name")) + } + + policies, err := getPolicies(policyPaths) + if err != nil { + if !sanitizedError.IsErrorSanitized(err) { + return sanitizedError.New("Could not parse policy paths") + } else { + return err + } + } + + for _, policy := range policies { + err := policy2.Validate(*policy) + if err != nil { + return sanitizedError.New(fmt.Sprintf("Policy %v is not valid", policy.Name)) + } + } + + var dClient discovery.CachedDiscoveryInterface + if cluster { + dClient, err = kubernetesConfig.ToDiscoveryClient() + if err != nil { + return sanitizedError.New(fmt.Errorf("Issues with kubernetes Config").Error()) + } + } + + resources, err := getResources(policies, resourcePaths, dClient) + if err != nil { + return sanitizedError.New(fmt.Errorf("Issues fetching resources").Error()) + } + + for i, policy := range policies { + for j, resource := range resources { + if !(j == 0 && i == 0) { + fmt.Printf("\n\n=======================================================================\n") + } + + err = applyPolicyOnResource(policy, resource) + if err != nil { + return sanitizedError.New(fmt.Errorf("Issues applying policy %v on resource %v", policy.Name, resource.GetName()).Error()) + } + } + } + + return nil + }, + } + + cmd.Flags().StringArrayVarP(&resourcePaths, "resource", "r", []string{}, "Path to resource files") + cmd.Flags().BoolVarP(&cluster, "cluster", "c", false, "Checks if policies should be applied to cluster in the current context") + + return cmd +} + +func getResources(policies []*v1.ClusterPolicy, resourcePaths []string, dClient discovery.CachedDiscoveryInterface) ([]*unstructured.Unstructured, error) { + var resources []*unstructured.Unstructured + var err error + + if dClient != nil { + var resourceTypesMap = make(map[string]bool) + var resourceTypes []string + for _, policy := range policies { + for _, rule := range policy.Spec.Rules { + for _, kind := range rule.MatchResources.Kinds { + resourceTypesMap[kind] = true + } + } + } + + for kind := range resourceTypesMap { + resourceTypes = append(resourceTypes, kind) + } + + resources, err = getResourcesOfTypeFromCluster(resourceTypes, dClient) + if err != nil { + return nil, err + } + } + + for _, resourcePath := range resourcePaths { + resource, err := getResource(resourcePath) + if err != nil { + return nil, err + } + + resources = append(resources, resource) + } + + return resources, nil +} + +func getResourcesOfTypeFromCluster(resourceTypes []string, dClient discovery.CachedDiscoveryInterface) ([]*unstructured.Unstructured, error) { + var resources []*unstructured.Unstructured + + for _, kind := range resourceTypes { + endpoint, err := getListEndpointForKind(kind) + if err != nil { + return nil, err + } + + listObjectRaw, err := dClient.RESTClient().Get().RequestURI(endpoint).Do().Raw() + if err != nil { + return nil, err + } + + listObject, err := engineutils.ConvertToUnstructured(listObjectRaw) + if err != nil { + return nil, err + } + + resourceList, err := listObject.ToList() + if err != nil { + return nil, err + } + + version := resourceList.GetAPIVersion() + for _, resource := range resourceList.Items { + resource.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Version: version, + Kind: kind, + }) + resources = append(resources, resource.DeepCopy()) + } + } + + return resources, nil +} + +func getPoliciesInDir(path string) ([]*v1.ClusterPolicy, error) { + var policies []*v1.ClusterPolicy + + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + for _, file := range files { + if file.IsDir() { + policiesFromDir, err := getPoliciesInDir(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + + policies = append(policies, policiesFromDir...) + } else { + policy, err := getPolicy(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + + policies = append(policies, policy) + } + } + + return policies, nil +} + +func getPolicies(paths []string) ([]*v1.ClusterPolicy, error) { + var policies = make([]*v1.ClusterPolicy, 0, len(paths)) + for _, path := range paths { + path = filepath.Clean(path) + + fileDesc, err := os.Stat(path) + if err != nil { + return nil, err + } + + if fileDesc.IsDir() { + policiesFromDir, err := getPoliciesInDir(path) + if err != nil { + return nil, err + } + + policies = append(policies, policiesFromDir...) + } else { + policy, err := getPolicy(path) + if err != nil { + return nil, err + } + + policies = append(policies, policy) + } + } + + for i := range policies { + setFalse := false + policies[i].Spec.Background = &setFalse + } + + return policies, nil +} + +func getPolicy(path string) (*v1.ClusterPolicy, error) { + policy := &v1.ClusterPolicy{} + + file, err := ioutil.ReadFile(path) + 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, sanitizedError.New(fmt.Sprintf("failed to decode policy in %s", path)) + } + + if policy.TypeMeta.Kind != "ClusterPolicy" { + return nil, sanitizedError.New(fmt.Sprintf("resource %v is not a cluster policy", policy.Name)) + } + + return policy, nil +} + +func getResource(path string) (*unstructured.Unstructured, error) { + + resourceYaml, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + decode := scheme.Codecs.UniversalDeserializer().Decode + resourceObject, metaData, err := decode(resourceYaml, nil, nil) + if err != nil { + return nil, err + } + + resourceUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&resourceObject) + if err != nil { + return nil, err + } + + resourceJSON, err := json.Marshal(resourceUnstructured) + if err != nil { + return nil, err + } + + resource, err := engineutils.ConvertToUnstructured(resourceJSON) + if err != nil { + return nil, err + } + + resource.SetGroupVersionKind(*metaData) + + if resource.GetNamespace() == "" { + resource.SetNamespace("default") + } + + return resource, nil +} + +func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unstructured) error { + + fmt.Printf("\n\nApplying Policy %s on Resource %s/%s/%s\n", policy.Name, resource.GetNamespace(), resource.GetKind(), resource.GetName()) + + mutateResponse := engine.Mutate(engine.PolicyContext{Policy: *policy, NewResource: *resource}) + if !mutateResponse.IsSuccesful() { + fmt.Printf("\n\nMutation:") + fmt.Printf("\nFailed to apply mutation") + for i, r := range mutateResponse.PolicyResponse.Rules { + fmt.Printf("\n%d. %s", i+1, r.Message) + } + fmt.Printf("\n\n") + } else { + if len(mutateResponse.PolicyResponse.Rules) > 0 { + fmt.Printf("\n\nMutation:") + fmt.Printf("\nMutation has been applied succesfully") + yamlEncodedResource, err := yamlv2.Marshal(mutateResponse.PatchedResource.Object) + if err != nil { + return err + } + + fmt.Printf("\n\n" + string(yamlEncodedResource)) + fmt.Printf("\n\n") + } + } + + validateResponse := engine.Validate(engine.PolicyContext{Policy: *policy, NewResource: mutateResponse.PatchedResource}) + if !validateResponse.IsSuccesful() { + fmt.Printf("\n\nValidation:") + fmt.Printf("\nResource is invalid") + for i, r := range validateResponse.PolicyResponse.Rules { + fmt.Printf("\n%d. %s", i+1, r.Message) + } + fmt.Printf("\n\n") + } else { + if len(validateResponse.PolicyResponse.Rules) > 0 { + fmt.Printf("\n\nValidation:") + fmt.Printf("\nResource is valid") + fmt.Printf("\n\n") + } + } + + var policyHasGenerate bool + for _, rule := range policy.Spec.Rules { + if rule.HasGenerate() { + policyHasGenerate = true + } + } + + if policyHasGenerate { + generateResponse := engine.Generate(engine.PolicyContext{Policy: *policy, NewResource: *resource}) + if len(generateResponse.PolicyResponse.Rules) > 0 { + fmt.Printf("\n\nGenerate:") + fmt.Printf("\nResource is valid") + fmt.Printf("\n\n") + } else { + fmt.Printf("\n\nGenerate:") + fmt.Printf("\nResource is invalid") + for i, r := range generateResponse.PolicyResponse.Rules { + fmt.Printf("\n%d. %s", i+1, r.Message) + } + fmt.Printf("\n\n") + } + } + + return nil +} diff --git a/pkg/kyverno/apply/helper.go b/pkg/kyverno/apply/helper.go new file mode 100644 index 0000000000..04dc142a9a --- /dev/null +++ b/pkg/kyverno/apply/helper.go @@ -0,0 +1,37 @@ +package apply + +import ( + "fmt" + "strings" + + "github.com/nirmata/kyverno/pkg/openapi" +) + +func getListEndpointForKind(kind string) (string, error) { + + definitionName := openapi.GetDefinitionNameFromKind(kind) + definitionNameWithoutPrefix := strings.Replace(definitionName, "io.k8s.", "", -1) + + parts := strings.Split(definitionNameWithoutPrefix, ".") + definitionPrefix := strings.Join(parts[:len(parts)-1], ".") + + defPrefixToApiPrefix := map[string]string{ + "api.core.v1": "/api/v1", + "api.apps.v1": "/apis/apps/v1", + "api.batch.v1": "/apis/batch/v1", + "api.admissionregistration.v1": "/apis/admissionregistration.k8s.io/v1", + "kube-aggregator.pkg.apis.apiregistration.v1": "/apis/apiregistration.k8s.io/v1", + "apiextensions-apiserver.pkg.apis.apiextensions.v1": "/apis/apiextensions.k8s.io/v1", + "api.autoscaling.v1": "/apis/autoscaling/v1/", + "api.storage.v1": "/apis/storage.k8s.io/v1", + "api.coordination.v1": "/apis/coordination.k8s.io/v1", + "api.scheduling.v1": "/apis/scheduling.k8s.io/v1", + "api.rbac.v1": "/apis/rbac.authorization.k8s.io/v1", + } + + if defPrefixToApiPrefix[definitionPrefix] == "" { + return "", fmt.Errorf("Unsupported resource type %v", kind) + } + + return defPrefixToApiPrefix[definitionPrefix] + "/" + strings.ToLower(kind) + "s", nil +} diff --git a/pkg/kyverno/main.go b/pkg/kyverno/main.go new file mode 100644 index 0000000000..d0d1163ef6 --- /dev/null +++ b/pkg/kyverno/main.go @@ -0,0 +1,50 @@ +package kyverno + +import ( + "flag" + "os" + + "github.com/nirmata/kyverno/pkg/kyverno/validate" + + "github.com/nirmata/kyverno/pkg/kyverno/apply" + + "github.com/nirmata/kyverno/pkg/kyverno/version" + + "github.com/spf13/cobra" +) + +func CLI() { + cli := &cobra.Command{ + Use: "kyverno", + Short: "kyverno manages native policies of Kubernetes", + } + + configureGlog(cli) + + commands := []*cobra.Command{ + version.Command(), + apply.Command(), + validate.Command(), + } + + cli.AddCommand(commands...) + + cli.SilenceUsage = true + + if err := cli.Execute(); err != nil { + os.Exit(1) + } +} + +func configureGlog(cli *cobra.Command) { + flag.Parse() + _ = flag.Set("logtostderr", "true") + + cli.PersistentFlags().AddGoFlagSet(flag.CommandLine) + _ = cli.PersistentFlags().MarkHidden("alsologtostderr") + _ = cli.PersistentFlags().MarkHidden("logtostderr") + _ = cli.PersistentFlags().MarkHidden("log_dir") + _ = cli.PersistentFlags().MarkHidden("log_backtrace_at") + _ = cli.PersistentFlags().MarkHidden("stderrthreshold") + _ = cli.PersistentFlags().MarkHidden("vmodule") +} diff --git a/pkg/kyverno/sanitizedError/error.go b/pkg/kyverno/sanitizedError/error.go new file mode 100644 index 0000000000..3c8ef003f7 --- /dev/null +++ b/pkg/kyverno/sanitizedError/error.go @@ -0,0 +1,20 @@ +package sanitizedError + +type customError struct { + message string +} + +func (c customError) Error() string { + return c.message +} + +func New(message string) error { + return customError{message: message} +} + +func IsErrorSanitized(err error) bool { + if _, ok := err.(customError); !ok { + return false + } + return true +} diff --git a/pkg/kyverno/validate/command.go b/pkg/kyverno/validate/command.go new file mode 100644 index 0000000000..bf20511e0d --- /dev/null +++ b/pkg/kyverno/validate/command.go @@ -0,0 +1,147 @@ +package validate + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/nirmata/kyverno/pkg/kyverno/sanitizedError" + + "github.com/golang/glog" + + policyvalidate "github.com/nirmata/kyverno/pkg/policy" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/yaml" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "validate", + Short: "Validates kyverno policies", + Example: "kyverno validate /path/to/policy.yaml /path/to/folderOfPolicies", + RunE: func(cmd *cobra.Command, policyPaths []string) (err error) { + defer func() { + if err != nil { + if !sanitizedError.IsErrorSanitized(err) { + glog.V(4).Info(err) + err = fmt.Errorf("Internal error") + } + } + }() + + policies, err := getPolicies(policyPaths) + if err != nil { + if !sanitizedError.IsErrorSanitized(err) { + return sanitizedError.New("Could not parse policy paths") + } else { + return err + } + } + + for _, policy := range policies { + err = policyvalidate.Validate(*policy) + if err != nil { + fmt.Println("Policy " + policy.Name + " is invalid") + } else { + fmt.Println("Policy " + policy.Name + " is valid") + } + } + + return nil + }, + } + + return cmd +} + +func getPoliciesInDir(path string) ([]*v1.ClusterPolicy, error) { + var policies []*v1.ClusterPolicy + + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + for _, file := range files { + if file.IsDir() { + policiesFromDir, err := getPoliciesInDir(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + + policies = append(policies, policiesFromDir...) + } else { + policy, err := getPolicy(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + + policies = append(policies, policy) + } + } + + return policies, nil +} + +func getPolicies(paths []string) ([]*v1.ClusterPolicy, error) { + var policies = make([]*v1.ClusterPolicy, 0, len(paths)) + for _, path := range paths { + path = filepath.Clean(path) + + fileDesc, err := os.Stat(path) + if err != nil { + return nil, err + } + + if fileDesc.IsDir() { + policiesFromDir, err := getPoliciesInDir(path) + if err != nil { + return nil, err + } + + policies = append(policies, policiesFromDir...) + } else { + policy, err := getPolicy(path) + if err != nil { + return nil, err + } + + policies = append(policies, policy) + } + } + + for i := range policies { + setFalse := false + policies[i].Spec.Background = &setFalse + } + + return policies, nil +} + +func getPolicy(path string) (*v1.ClusterPolicy, error) { + policy := &v1.ClusterPolicy{} + + file, err := ioutil.ReadFile(path) + 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, sanitizedError.New(fmt.Sprintf("failed to decode policy in %s", path)) + } + + if policy.TypeMeta.Kind != "ClusterPolicy" { + return nil, sanitizedError.New(fmt.Sprintf("resource %v is not a cluster policy", policy.Name)) + } + + return policy, nil +} diff --git a/pkg/kyverno/version/command.go b/pkg/kyverno/version/command.go new file mode 100644 index 0000000000..25d1324f2a --- /dev/null +++ b/pkg/kyverno/version/command.go @@ -0,0 +1,21 @@ +package version + +import ( + "fmt" + + "github.com/nirmata/kyverno/pkg/version" + "github.com/spf13/cobra" +) + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Shows current version of kyverno", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Version: %s\n", version.BuildVersion) + fmt.Printf("Time: %s\n", version.BuildTime) + fmt.Printf("Git commit ID: %s\n", version.BuildHash) + return nil + }, + } +} diff --git a/pkg/openapi/validation.go b/pkg/openapi/validation.go index 5f89d8499a..71b8fa4b43 100644 --- a/pkg/openapi/validation.go +++ b/pkg/openapi/validation.go @@ -115,6 +115,12 @@ func ValidateResource(patchedResource unstructured.Unstructured, kind string) er return nil } +func GetDefinitionNameFromKind(kind string) string { + openApiGlobalState.mutex.RLock() + defer openApiGlobalState.mutex.RUnlock() + return openApiGlobalState.kindToDefinitionName[kind] +} + func useOpenApiDocument(customDoc *openapi_v2.Document) error { openApiGlobalState.mutex.Lock() defer openApiGlobalState.mutex.Unlock() diff --git a/pkg/webhooks/policyvalidation.go b/pkg/webhooks/policyvalidation.go index ebe45e47cc..fefa9a11f7 100644 --- a/pkg/webhooks/policyvalidation.go +++ b/pkg/webhooks/policyvalidation.go @@ -4,9 +4,10 @@ import ( "encoding/json" "fmt" + policyvalidate "github.com/nirmata/kyverno/pkg/policy" + "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" - policyvalidate "github.com/nirmata/kyverno/pkg/policy" v1beta1 "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" )