diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index b147f5b490..8f76efca9f 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -1,6 +1,7 @@ package engine import ( + "encoding/json" "reflect" "strings" "time" @@ -101,18 +102,19 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { if reflect.DeepEqual(policyContext.AdmissionInfo, kyverno.RequestInfo{}) { continue } + } - if strings.Contains(PodControllers, resource.GetKind()) { + if strings.Contains(PodControllers, resource.GetKind()) { + if !patchedResourceHasPodControllerAnnotation(patchedResource) { var ruleResponse response.RuleResponse - ruleResponse, patchedResource = mutate.ProcessOverlay(logger, rule.Name, podTemplateRule, patchedResource) + ruleResponse, patchedResource = mutate.ProcessOverlay(logger, "podControllerAnnotation", podTemplateRule.Mutation.Overlay, patchedResource) if !ruleResponse.Success { logger.Info("failed to insert annotation for podTemplate", "error", ruleResponse.Message) - continue - } - - if ruleResponse.Success && ruleResponse.Patches != nil { - logger.V(2).Info("inserted annotation for podTemplate") - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse) + } else { + if ruleResponse.Success && ruleResponse.Patches != nil { + logger.V(2).Info("inserted annotation for podTemplate") + resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse) + } } } } @@ -120,6 +122,24 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { resp.PatchedResource = patchedResource return resp } + +func patchedResourceHasPodControllerAnnotation(resource unstructured.Unstructured) bool { + var podController struct { + Spec struct { + Template struct { + Metadata struct { + Annotations map[string]interface{} `json:"annotations"` + } `json:"metadata"` + } `json:"template"` + } `json:"spec"` + } + + resourceRaw, _ := json.Marshal(resource.Object) + _ = json.Unmarshal(resourceRaw, &podController) + + _, ok := podController.Spec.Template.Metadata.Annotations[PodTemplateAnnotation] + return ok +} func incrementAppliedRuleCount(resp *response.EngineResponse) { resp.PolicyResponse.RulesAppliedCount++ } @@ -150,7 +170,7 @@ var podTemplateRule = kyverno.Rule{ "template": map[string]interface{}{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ - "+(pod-policies.kyverno.io/autogen-applied)": "true", + "+(" + PodTemplateAnnotation + ")": "true", }, }, }, diff --git a/pkg/kyverno/apply/command.go b/pkg/kyverno/apply/command.go index 75ee885986..daea727a61 100644 --- a/pkg/kyverno/apply/command.go +++ b/pkg/kyverno/apply/command.go @@ -7,6 +7,9 @@ import ( "os" "path/filepath" "regexp" + "time" + + client "github.com/nirmata/kyverno/pkg/dclient" "github.com/nirmata/kyverno/pkg/utils" @@ -18,8 +21,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - "k8s.io/apimachinery/pkg/util/yaml" "github.com/nirmata/kyverno/pkg/engine" @@ -87,15 +88,19 @@ func Command() *cobra.Command { } } - var dClient discovery.CachedDiscoveryInterface + var dClient *client.Client if cluster { - dClient, err = kubernetesConfig.ToDiscoveryClient() + restConfig, err := kubernetesConfig.ToRESTConfig() if err != nil { - return sanitizedError.New(fmt.Errorf("Issues with kubernetes Config").Error()) + return err + } + dClient, err = client.NewClient(restConfig, 10*time.Second, make(chan struct{}), log.Log) + if err != nil { + return err } } - resources, err := getResources(policies, resourcePaths, dClient, openAPIController) + resources, err := getResources(policies, resourcePaths, dClient) if err != nil { return sanitizedError.New(fmt.Errorf("Issues fetching resources").Error()) } @@ -123,7 +128,7 @@ func Command() *cobra.Command { return cmd } -func getResources(policies []*v1.ClusterPolicy, resourcePaths []string, dClient discovery.CachedDiscoveryInterface, openAPIController *openapi.Controller) ([]*unstructured.Unstructured, error) { +func getResources(policies []*v1.ClusterPolicy, resourcePaths []string, dClient *client.Client) ([]*unstructured.Unstructured, error) { var resources []*unstructured.Unstructured var err error @@ -142,7 +147,7 @@ func getResources(policies []*v1.ClusterPolicy, resourcePaths []string, dClient resourceTypes = append(resourceTypes, kind) } - resources, err = getResourcesOfTypeFromCluster(resourceTypes, dClient, openAPIController) + resources, err = getResourcesOfTypeFromCluster(resourceTypes, dClient) if err != nil { return nil, err } @@ -160,27 +165,11 @@ func getResources(policies []*v1.ClusterPolicy, resourcePaths []string, dClient return resources, nil } -func getResourcesOfTypeFromCluster(resourceTypes []string, dClient discovery.CachedDiscoveryInterface, openAPIController *openapi.Controller) ([]*unstructured.Unstructured, error) { +func getResourcesOfTypeFromCluster(resourceTypes []string, dClient *client.Client) ([]*unstructured.Unstructured, error) { var resources []*unstructured.Unstructured for _, kind := range resourceTypes { - // TODO use lister interface - endpoint, err := getListEndpointForKind(kind, openAPIController) - 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() + resourceList, err := dClient.ListResource(kind, "", nil) if err != nil { return nil, err } diff --git a/pkg/kyverno/apply/helper.go b/pkg/kyverno/apply/helper.go deleted file mode 100644 index 4ad4b1b6d3..0000000000 --- a/pkg/kyverno/apply/helper.go +++ /dev/null @@ -1,37 +0,0 @@ -package apply - -import ( - "fmt" - "strings" - - "github.com/nirmata/kyverno/pkg/openapi" -) - -func getListEndpointForKind(kind string, openAPIController *openapi.Controller) (string, error) { - - definitionName := openAPIController.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/policy/validate.go b/pkg/policy/validate.go index e835fde5d9..abc3cfad8a 100644 --- a/pkg/policy/validate.go +++ b/pkg/policy/validate.go @@ -7,6 +7,8 @@ import ( "reflect" "strings" + "github.com/minio/minio/pkg/wildcard" + "github.com/nirmata/kyverno/pkg/openapi" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" @@ -42,15 +44,21 @@ func Validate(policyRaw []byte, client *dclient.Client, mock bool, openAPIContro } for i, rule := range p.Spec.Rules { - // only one type of rule is allowed per rule - if err := validateRuleType(rule); err != nil { - return fmt.Errorf("path: spec.rules[%d]: %v", i, err) - } - // validate resource description if path, err := validateResources(rule); err != nil { return fmt.Errorf("path: spec.rules[%d].%s: %v", i, path, err) } + // validate rule types + // only one type of rule is allowed per rule + if err := validateRuleType(rule); err != nil { + // as there are more than 1 operation in rule, not need to evaluate it further + return fmt.Errorf("path: spec.rules[%d]: %v", i, err) + } + + if doesMatchAndExcludeConflict(rule) { + return fmt.Errorf("path: spec.rules[%v]: rule is matching an empty set", rule.Name) + } + // validate rule actions // - Mutate // - Validate @@ -82,6 +90,121 @@ func Validate(policyRaw []byte, client *dclient.Client, mock bool, openAPIContro return nil } +// doesMatchAndExcludeConflict checks if the resultant +// of match and exclude block is not an empty set +func doesMatchAndExcludeConflict(rule kyverno.Rule) bool { + + if reflect.DeepEqual(rule.MatchResources, kyverno.MatchResources{}) { + return true + } + + if reflect.DeepEqual(rule.ExcludeResources, kyverno.ExcludeResources{}) { + return false + } + + excludeRoles := make(map[string]bool) + for _, role := range rule.ExcludeResources.UserInfo.Roles { + excludeRoles[role] = true + } + + excludeClusterRoles := make(map[string]bool) + for _, clusterRoles := range rule.ExcludeResources.UserInfo.ClusterRoles { + excludeClusterRoles[clusterRoles] = true + } + + excludeSubjects := make(map[string]bool) + for _, subject := range rule.ExcludeResources.UserInfo.Subjects { + subjectRaw, _ := json.Marshal(subject) + excludeSubjects[string(subjectRaw)] = true + } + + excludeKinds := make(map[string]bool) + for _, kind := range rule.ExcludeResources.ResourceDescription.Kinds { + excludeKinds[kind] = true + } + + excludeNamespaces := make(map[string]bool) + for _, namespace := range rule.ExcludeResources.ResourceDescription.Namespaces { + excludeNamespaces[namespace] = true + } + + excludeMatchExpressions := make(map[string]bool) + if rule.ExcludeResources.ResourceDescription.Selector != nil { + for _, matchExpression := range rule.ExcludeResources.ResourceDescription.Selector.MatchExpressions { + matchExpressionRaw, _ := json.Marshal(matchExpression) + excludeMatchExpressions[string(matchExpressionRaw)] = true + } + } + + if len(excludeRoles) > 0 { + for _, role := range rule.MatchResources.UserInfo.Roles { + if !excludeRoles[role] { + return false + } + } + } + + if len(excludeClusterRoles) > 0 { + for _, clusterRole := range rule.MatchResources.UserInfo.ClusterRoles { + if !excludeClusterRoles[clusterRole] { + return false + } + } + } + + if len(excludeSubjects) > 0 { + for _, subject := range rule.MatchResources.UserInfo.Subjects { + subjectRaw, _ := json.Marshal(subject) + if !excludeSubjects[string(subjectRaw)] { + return false + } + } + } + + if rule.ExcludeResources.ResourceDescription.Name != "" { + if !wildcard.Match(rule.ExcludeResources.ResourceDescription.Name, rule.MatchResources.ResourceDescription.Name) { + return false + } + } + + if len(excludeNamespaces) > 0 { + for _, namespace := range rule.MatchResources.ResourceDescription.Namespaces { + if !excludeNamespaces[namespace] { + return false + } + } + } + + if len(excludeKinds) > 0 { + for _, kind := range rule.MatchResources.ResourceDescription.Kinds { + if !excludeKinds[kind] { + return false + } + } + } + + if rule.MatchResources.ResourceDescription.Selector != nil && rule.ExcludeResources.ResourceDescription.Selector != nil { + if len(excludeMatchExpressions) > 0 { + for _, matchExpression := range rule.MatchResources.ResourceDescription.Selector.MatchExpressions { + matchExpressionRaw, _ := json.Marshal(matchExpression) + if !excludeMatchExpressions[string(matchExpressionRaw)] { + return false + } + } + } + + if len(rule.ExcludeResources.ResourceDescription.Selector.MatchLabels) > 0 { + for label, value := range rule.MatchResources.ResourceDescription.Selector.MatchLabels { + if rule.ExcludeResources.ResourceDescription.Selector.MatchLabels[label] != value { + return false + } + } + } + } + + return true +} + func ruleOnlyDealsWithResourceMetaData(rule kyverno.Rule) bool { overlayMap, _ := rule.Mutation.Overlay.(map[string]interface{}) for k := range overlayMap { diff --git a/pkg/policy/validate_test.go b/pkg/policy/validate_test.go index 787ffbfc75..0ca0778dd6 100644 --- a/pkg/policy/validate_test.go +++ b/pkg/policy/validate_test.go @@ -960,3 +960,76 @@ func Test_ruleOnlyDealsWithResourceMetaData(t *testing.T) { } } } + +func Test_doesMatchExcludeConflict(t *testing.T) { + testcases := []struct { + description string + rule []byte + expectedOutput bool + }{ + { + description: "Same match and exclude", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: true, + }, + { + description: "Failed to exclude kind", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude name", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something-*","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude namespace", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something3","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude labels", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"higha"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude expression", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["databases"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude subjects", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something2","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude clusterroles", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something3","something1"],"roles":["something","something1"]}}`), + expectedOutput: false, + }, + { + description: "Failed to exclude roles", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something3","something1"]}}`), + expectedOutput: false, + }, + { + description: "simple", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"]}},"exclude":{"resources":{"kinds":["Pod","Namespace","Job"],"name":"some*","namespaces":["something","something1","something2"]}}}`), + expectedOutput: true, + }, + { + description: "simple - fail", + rule: []byte(`{"name":"set-image-pull-policy-2","match":{"resources":{"kinds":["Pod","Namespace"],"name":"somxething","namespaces":["something","something1"]}},"exclude":{"resources":{"kinds":["Pod","Namespace","Job"],"name":"some*","namespaces":["something","something1","something2"]}}}`), + expectedOutput: false, + }, + } + + for i, testcase := range testcases { + var rule kyverno.Rule + _ = json.Unmarshal(testcase.rule, &rule) + + if doesMatchAndExcludeConflict(rule) != testcase.expectedOutput { + t.Errorf("Testcase [%d] failed - description - %v", i+1, testcase.description) + } + } +}