From 97be6ad379e36872d4ad9817c420dba41adc389c Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Mon, 10 Jun 2019 17:06:31 +0300 Subject: [PATCH 01/37] Added comments and refactored array processing validation logic --- pkg/engine/utils.go | 10 ++++ pkg/engine/validation.go | 116 +++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 339af5b233..4bf529a1be 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -153,3 +153,13 @@ func skipArrayObject(object, anchors map[string]interface{}) bool { return false } + +func removeAnchor(key string) string { + if wrappedWithParentheses(key) { + return key[1 : len(key)-1] + } + + // TODO: Add logic for other anchors here + + return key +} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index de4416a6e3..431772c3a4 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -33,8 +33,8 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers validationResult := validateResourceWithPattern(resource, rule.Validation.Pattern) if result.Success != validationResult.Reason { - ruleApplicationResult.MergeWith(&validationResult) ruleApplicationResult.AddMessagef(*rule.Validation.Message) + ruleApplicationResult.MergeWith(&validationResult) } else { ruleApplicationResult.AddMessagef("Success") } @@ -45,68 +45,79 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers return policyResult } +// validateResourceWithPattern is a start of element-by-element validation process +// It assumes that validation is started from root, so "/" is passed func validateResourceWithPattern(resource, pattern interface{}) result.RuleApplicationResult { return validateResourceElement(resource, pattern, "/") } -func validateResourceElement(value, pattern interface{}, path string) result.RuleApplicationResult { +// validateResourceElement detects the element type (map, array, nil, string, int, bool, float) +// and calls corresponding handler +// Pattern tree and resource tree can have different structure. In this case validation fails +func validateResourceElement(resourceElement, patternElement interface{}, path string) result.RuleApplicationResult { res := result.NewRuleApplicationResult("") // TODO: Move similar message templates to message package - switch typedPattern := pattern.(type) { + switch typedPatternElement := patternElement.(type) { + // map case map[string]interface{}: - typedValue, ok := value.(map[string]interface{}) + typedResourceElement, ok := resourceElement.(map[string]interface{}) if !ok { - res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, pattern, value) + res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement) return res } - return validateMap(typedValue, typedPattern, path) + return validateMap(typedResourceElement, typedPatternElement, path) + // array case []interface{}: - typedValue, ok := value.([]interface{}) + typedResourceElement, ok := resourceElement.([]interface{}) if !ok { - res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, pattern, value) + res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement) return res } - return validateArray(typedValue, typedPattern, path) + return validateArray(typedResourceElement, typedPatternElement, path) + // elementary values case string, float64, int, int64, bool, nil: - if !ValidateValueWithPattern(value, pattern) { - res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path) + if !ValidateValueWithPattern(resourceElement, patternElement) { + res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", resourceElement, patternElement, path) } return res default: - res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", pattern, path) + res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", patternElement, path) return res } } -func validateMap(valueMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult { +// If validateResourceElement detects map element inside resource and pattern trees, it goes to validateMap +// For each element of the map we must detect the type again, so we pass this elements to validateResourceElement +func validateMap(resourceMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult { res := result.NewRuleApplicationResult("") - for key, pattern := range patternMap { - if wrappedWithParentheses(key) { - key = key[1 : len(key)-1] - } + for key, patternElement := range patternMap { + key = removeAnchor(key) - if pattern == "*" && valueMap[key] != nil { + // The '*' pattern means that key exists and has value + if patternElement == "*" && resourceMap[key] != nil { continue - } else if pattern == "*" && valueMap[key] == nil { + } else if patternElement == "*" && resourceMap[key] == nil { res.FailWithMessagef("Field %s is not present", key) } else { - elementResult := validateResourceElement(valueMap[key], pattern, path+key+"/") + elementResult := validateResourceElement(resourceMap[key], patternElement, path+key+"/") if result.Failed == elementResult.Reason { res.Reason = elementResult.Reason res.Messages = append(res.Messages, elementResult.Messages...) } } - } return res } +// If validateResourceElement detects array element inside resource and pattern trees, it goes to validateArray +// Unlike the validateMap, we should check the array elements type on-site, because in case of maps, we should +// get anchors and check each array element with it. func validateArray(resourceArray, patternArray []interface{}, path string) result.RuleApplicationResult { res := result.NewRuleApplicationResult("") @@ -114,35 +125,44 @@ func validateArray(resourceArray, patternArray []interface{}, path string) resul return res } - switch pattern := patternArray[0].(type) { + switch typedPatternElement := patternArray[0].(type) { case map[string]interface{}: - anchors := getAnchorsFromMap(pattern) - for i, value := range resourceArray { - currentPath := path + strconv.Itoa(i) + "/" - resource, ok := value.(map[string]interface{}) - if !ok { - res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, pattern, value) - return res - } - - if skipArrayObject(resource, anchors) { - continue - } - - mapValidationResult := validateMap(resource, pattern, currentPath) - if result.Failed == mapValidationResult.Reason { - res.Reason = mapValidationResult.Reason - res.Messages = append(res.Messages, mapValidationResult.Messages...) - } - } - case string, float64, int, int64, bool, nil: - for _, value := range resourceArray { - if !ValidateValueWithPattern(value, pattern) { - res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path) - } - } + // This is special case, because maps in arrays can have anchors that must be + // processed with the special way affecting the entire array + arrayResult := validateArrayOfMaps(resourceArray, typedPatternElement, path) + res.MergeWith(&arrayResult) default: - res.FailWithMessagef("Array element pattern of unknown type %T. Path: %s", pattern, path) + // In all other cases - detect type and handle each array element with validateResourceElement + for i, patternElement := range patternArray { + currentPath := path + strconv.Itoa(i) + "/" + elementResult := validateResourceElement(resourceArray[i], patternElement, currentPath) + res.MergeWith(&elementResult) + } + } + + return res +} + +// validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic +// and then validates each map due to the pattern +func validateArrayOfMaps(resourceMapArray []interface{}, patternMap map[string]interface{}, path string) result.RuleApplicationResult { + res := result.NewRuleApplicationResult("") + anchors := getAnchorsFromMap(patternMap) + + for i, resourceElement := range resourceMapArray { + currentPath := path + strconv.Itoa(i) + "/" + typedResourceElement, ok := resourceElement.(map[string]interface{}) + if !ok { + res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternMap, resourceElement) + return res + } + + if skipArrayObject(typedResourceElement, anchors) { + continue + } + + mapValidationResult := validateMap(typedResourceElement, patternMap, currentPath) + res.MergeWith(&mapValidationResult) } return res From c9df93ce99a490690ae6b42fd72ea3c3d47de2d8 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Mon, 10 Jun 2019 17:10:05 +0300 Subject: [PATCH 02/37] Added merge of results in map validation logic --- pkg/engine/validation.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 431772c3a4..6ae0b6585d 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -105,10 +105,7 @@ func validateMap(resourceMap, patternMap map[string]interface{}, path string) re res.FailWithMessagef("Field %s is not present", key) } else { elementResult := validateResourceElement(resourceMap[key], patternElement, path+key+"/") - if result.Failed == elementResult.Reason { - res.Reason = elementResult.Reason - res.Messages = append(res.Messages, elementResult.Messages...) - } + res.MergeWith(&elementResult) } } From 7897f1db05369426665c8d18d2fdb8f0b2205386 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Mon, 10 Jun 2019 17:32:26 +0300 Subject: [PATCH 03/37] Refactored pattern.go --- pkg/engine/pattern.go | 67 +++++++++++-------------------------------- pkg/engine/utils.go | 27 ++++++++++++++++- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/pkg/engine/pattern.go b/pkg/engine/pattern.go index 5e6b287d30..7efa9f4640 100644 --- a/pkg/engine/pattern.go +++ b/pkg/engine/pattern.go @@ -3,7 +3,6 @@ package engine import ( "math" "regexp" - "strconv" "strings" "github.com/golang/glog" @@ -58,6 +57,7 @@ func ValidateValueWithPattern(value, pattern interface{}) bool { } } +// Handler for int values during validation process func validateValueWithIntPattern(value interface{}, pattern int64) bool { switch typedValue := value.(type) { case int: @@ -78,6 +78,7 @@ func validateValueWithIntPattern(value interface{}, pattern int64) bool { } } +// Handler for float values during validation process func validateValueWithFloatPattern(value interface{}, pattern float64) bool { switch typedValue := value.(type) { case int: @@ -96,6 +97,7 @@ func validateValueWithFloatPattern(value interface{}, pattern float64) bool { } } +// Handler for nil values during validation process func validateValueWithNilPattern(value interface{}) bool { switch typed := value.(type) { case float64: @@ -119,6 +121,7 @@ func validateValueWithNilPattern(value interface{}) bool { } } +// Handler for pattern values during validation process func validateValueWithStringPatterns(value interface{}, pattern string) bool { statements := strings.Split(pattern, "|") for _, statement := range statements { @@ -131,6 +134,8 @@ func validateValueWithStringPatterns(value interface{}, pattern string) bool { return false } +// Handler for single pattern value during validation process +// Detects if pattern has a number func validateValueWithStringPattern(value interface{}, pattern string) bool { operator := getOperatorFromStringPattern(pattern) pattern = pattern[len(operator):] @@ -143,6 +148,7 @@ func validateValueWithStringPattern(value interface{}, pattern string) bool { return validateNumberWithStr(value, number, str, operator) } +// Handler for string values func validateString(value interface{}, pattern string, operator Operator) bool { if NotEqual == operator || Equal == operator { strValue, ok := value.(string) @@ -164,6 +170,7 @@ func validateString(value interface{}, pattern string, operator Operator) bool { return false } +// validateNumberWithStr applies wildcard to suffix and operator to numerical part func validateNumberWithStr(value interface{}, patternNumber, patternStr string, operator Operator) bool { // pattern has suffix if "" != patternStr { @@ -179,51 +186,21 @@ func validateNumberWithStr(value interface{}, patternNumber, patternStr string, return false } - valueParsedNumber, err := parseNumber(valueNumber) - if err != nil { - return false - } - - return validateNumber(valueParsedNumber, patternNumber, operator) + return validateNumber(valueNumber, patternNumber, operator) } return validateNumber(value, patternNumber, operator) } +// validateNumber compares two numbers with operator func validateNumber(value, pattern interface{}, operator Operator) bool { - var floatPattern, floatValue float64 - - switch typed := value.(type) { - case string: - var err error - floatValue, err = strconv.ParseFloat(typed, 64) - if err != nil { - return false - } - case float64: - floatValue = typed - case int64: - floatValue = float64(typed) - case int: - floatValue = float64(typed) - default: + floatPattern, err := convertToFloat(pattern) + if err != nil { return false } - switch typed := pattern.(type) { - case string: - var err error - floatPattern, err = strconv.ParseFloat(typed, 64) - if err != nil { - return false - } - case float64: - floatPattern = typed - case int64: - floatPattern = float64(typed) - case int: - floatPattern = float64(typed) - default: + floatValue, err := convertToFloat(value) + if err != nil { return false } @@ -245,6 +222,7 @@ func validateNumber(value, pattern interface{}, operator Operator) bool { return false } +// getOperatorFromStringPattern parses opeartor from pattern func getOperatorFromStringPattern(pattern string) Operator { if len(pattern) < 2 { return Equal @@ -273,6 +251,7 @@ func getOperatorFromStringPattern(pattern string) Operator { return Equal } +// detects numerical and string parts in pattern and returns them func getNumberAndStringPartsFromPattern(pattern string) (number, str string) { regexpStr := `^(\d*(\.\d+)?)(.*)` re := regexp.MustCompile(regexpStr) @@ -280,17 +259,3 @@ func getNumberAndStringPartsFromPattern(pattern string) (number, str string) { match := matches[0] return match[1], match[3] } - -func parseNumber(number string) (interface{}, error) { - var err error - - if floatValue, err := strconv.ParseFloat(number, 64); err == nil { - return floatValue, nil - } - - if intValue, err := strconv.ParseInt(number, 10, 64); err == nil { - return intValue, nil - } - - return nil, err -} diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 4bf529a1be..ceae2aff37 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -2,6 +2,8 @@ package engine import ( "encoding/json" + "fmt" + "strconv" "strings" "github.com/minio/minio/pkg/wildcard" @@ -98,7 +100,7 @@ func ParseNamespaceFromObject(bytes []byte) string { return "" } -// returns true if policyResourceName is a regexp +// ParseRegexPolicyResourceName returns true if policyResourceName is a regexp func ParseRegexPolicyResourceName(policyResourceName string) (string, bool) { regex := strings.Split(policyResourceName, "regex:") if len(regex) == 1 { @@ -154,6 +156,7 @@ func skipArrayObject(object, anchors map[string]interface{}) bool { return false } +// removeAnchor remove special characters around anchored key func removeAnchor(key string) string { if wrappedWithParentheses(key) { return key[1 : len(key)-1] @@ -163,3 +166,25 @@ func removeAnchor(key string) string { return key } + +// convertToFloat converts string and any other value to float64 +func convertToFloat(value interface{}) (float64, error) { + switch typed := value.(type) { + case string: + var err error + floatValue, err := strconv.ParseFloat(typed, 64) + if err != nil { + return 0, err + } + + return floatValue, nil + case float64: + return typed, nil + case int64: + return float64(typed), nil + case int: + return float64(typed), nil + default: + return 0, fmt.Errorf("Could not convert %T to float64", value) + } +} From 1013a8a637cca4c53b809a4d075ca39c8b1dafa2 Mon Sep 17 00:00:00 2001 From: shuting Date: Mon, 10 Jun 2019 18:10:51 -0700 Subject: [PATCH 04/37] Allow user to run Kyverno in debug mode --- definitions/install_debug.yaml | 126 ++++++++++++++++++ init.go | 10 +- main.go | 4 +- pkg/config/config.go | 10 +- pkg/webhooks/registration.go | 88 +++++++++++- scripts/deploy-controller-debug.sh | 36 +++++ ...te-self-signed-cert-and-k8secrets-debug.sh | 71 ++++++++++ 7 files changed, 336 insertions(+), 9 deletions(-) create mode 100644 definitions/install_debug.yaml create mode 100755 scripts/deploy-controller-debug.sh create mode 100755 scripts/generate-self-signed-cert-and-k8secrets-debug.sh diff --git a/definitions/install_debug.yaml b/definitions/install_debug.yaml new file mode 100644 index 0000000000..313c348b65 --- /dev/null +++ b/definitions/install_debug.yaml @@ -0,0 +1,126 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: policies.kyverno.io +spec: + group: kyverno.io + versions: + - name: v1alpha1 + served: true + storage: true + scope: Cluster + names: + kind: Policy + plural: policies + singular: policy + subresources: + status: {} + validation: + openAPIV3Schema: + properties: + spec: + required: + - rules + properties: + rules: + type: array + items: + type: object + required: + - name + - resource + properties: + name: + type: string + resource: + type: object + required: + - kinds + properties: + kinds: + type: array + items: + type: string + name: + type: string + selector: + properties: + matchLabels: + type: object + additionalProperties: + type: string + matchExpressions: + type: array + items: + type: object + required: + - key + - operator + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + mutate: + type: object + properties: + overlay: + AnyValue: {} + patches: + type: array + items: + type: object + required: + - path + - op + properties: + path: + type: string + op: + type: string + enum: + - add + - replace + - remove + value: + AnyValue: {} + validate: + type: object + required: + - pattern + properties: + message: + type: string + pattern: + AnyValue: {} + generate: + type: object + required: + - kind + - name + properties: + kind: + type: string + name: + type: string + clone: + type: object + required: + - namespace + - name + properties: + namespace: + type: string + name: + type: string + data: + AnyValue: {} +--- +kind: Namespace +apiVersion: v1 +metadata: + name: "kyverno" \ No newline at end of file diff --git a/init.go b/init.go index e8ea2eb37f..7e172ce1e6 100644 --- a/init.go +++ b/init.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/golang/glog" client "github.com/nirmata/kyverno/pkg/dclient" tls "github.com/nirmata/kyverno/pkg/tls" @@ -40,10 +42,12 @@ func initTLSPemPair(configuration *rest.Config, client *client.Client) (*tls.Tls if err != nil { return nil, err } - err = client.WriteTlsPair(certProps, tlsPair) - if err != nil { - glog.Errorf("Unable to save TLS pair to the cluster: %v", err) + if err = client.WriteTlsPair(certProps, tlsPair); err != nil { + return nil, fmt.Errorf("Unable to save TLS pair to the cluster: %v", err) } + return tlsPair, nil } + + glog.Infoln("Using existing TLS key/certificate pair") return tlsPair, nil } diff --git a/main.go b/main.go index 2bb5383247..9f5ad59302 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( var ( kubeconfig string + serverIP string ) func main() { @@ -55,7 +56,7 @@ func main() { glog.Fatalf("Unable to create webhook server: %v\n", err) } - webhookRegistrationClient, err := webhooks.NewWebhookRegistrationClient(clientConfig, client) + webhookRegistrationClient, err := webhooks.NewWebhookRegistrationClient(clientConfig, client, serverIP) if err != nil { glog.Fatalf("Unable to register admission webhooks on cluster: %v\n", err) } @@ -81,6 +82,7 @@ func main() { func init() { flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + flag.StringVar(&serverIP, "serverIP", "", "IP address where Kyverno controller runs. Only required if out-of-cluster.") config.LogDefaultFlags() flag.Parse() } diff --git a/pkg/config/config.go b/pkg/config/config.go index 42c6148891..99509d2f78 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -7,11 +7,13 @@ const ( KubePolicyNamespace = "kyverno" WebhookServiceName = "kyverno-svc" - MutatingWebhookConfigurationName = "kyverno-mutating-webhook-cfg" - MutatingWebhookName = "nirmata.kyverno.mutating-webhook" + MutatingWebhookConfigurationName = "kyverno-mutating-webhook-cfg" + MutatingWebhookConfigurationDebug = "kyverno-mutating-webhook-cfg-debug" + MutatingWebhookName = "nirmata.kyverno.mutating-webhook" - ValidatingWebhookConfigurationName = "kyverno-validating-webhook-cfg" - ValidatingWebhookName = "nirmata.kyverno.validating-webhook" + ValidatingWebhookConfigurationName = "kyverno-validating-webhook-cfg" + ValidatingWebhookConfigurationDebug = "kyverno-validating-webhook-cfg-debug" + ValidatingWebhookName = "nirmata.kyverno.validating-webhook" // Due to kubernetes issue, we must use next literal constants instead of deployment TypeMeta fields // Issue: https://github.com/kubernetes/kubernetes/pull/63972 diff --git a/pkg/webhooks/registration.go b/pkg/webhooks/registration.go index b269f94227..710940df80 100644 --- a/pkg/webhooks/registration.go +++ b/pkg/webhooks/registration.go @@ -2,8 +2,10 @@ package webhooks import ( "errors" + "fmt" "io/ioutil" + "github.com/golang/glog" "github.com/nirmata/kyverno/pkg/config" client "github.com/nirmata/kyverno/pkg/dclient" @@ -18,10 +20,12 @@ type WebhookRegistrationClient struct { registrationClient *admregclient.AdmissionregistrationV1beta1Client client *client.Client clientConfig *rest.Config + // serverIP should be used if running Kyverno out of clutser + serverIP string } // NewWebhookRegistrationClient creates new WebhookRegistrationClient instance -func NewWebhookRegistrationClient(clientConfig *rest.Config, client *client.Client) (*WebhookRegistrationClient, error) { +func NewWebhookRegistrationClient(clientConfig *rest.Config, client *client.Client, serverIP string) (*WebhookRegistrationClient, error) { registrationClient, err := admregclient.NewForConfig(clientConfig) if err != nil { return nil, err @@ -31,11 +35,15 @@ func NewWebhookRegistrationClient(clientConfig *rest.Config, client *client.Clie registrationClient: registrationClient, client: client, clientConfig: clientConfig, + serverIP: serverIP, }, nil } // Register creates admission webhooks configs on cluster func (wrc *WebhookRegistrationClient) Register() error { + if wrc.serverIP != "" { + glog.Infof("Registering webhook with url https://%s\n", wrc.serverIP) + } // For the case if cluster already has this configs wrc.Deregister() @@ -66,6 +74,12 @@ func (wrc *WebhookRegistrationClient) Register() error { // This function does not fail on error: // Register will fail if the config exists, so there is no need to fail on error func (wrc *WebhookRegistrationClient) Deregister() { + if wrc.serverIP != "" { + wrc.registrationClient.MutatingWebhookConfigurations().Delete(config.MutatingWebhookConfigurationDebug, &meta.DeleteOptions{}) + wrc.registrationClient.ValidatingWebhookConfigurations().Delete(config.ValidatingWebhookConfigurationDebug, &meta.DeleteOptions{}) + return + } + wrc.registrationClient.MutatingWebhookConfigurations().Delete(config.MutatingWebhookConfigurationName, &meta.DeleteOptions{}) wrc.registrationClient.ValidatingWebhookConfigurations().Delete(config.ValidatingWebhookConfigurationName, &meta.DeleteOptions{}) } @@ -83,6 +97,10 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati return nil, errors.New("Unable to extract CA data from configuration") } + if wrc.serverIP != "" { + return wrc.contructDebugMutatingWebhookConfig(caData), nil + } + return &admregapi.MutatingWebhookConfiguration{ ObjectMeta: meta.ObjectMeta{ Name: config.MutatingWebhookConfigurationName, @@ -100,6 +118,24 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati }, nil } +func (wrc *WebhookRegistrationClient) contructDebugMutatingWebhookConfig(caData []byte) *admregapi.MutatingWebhookConfiguration { + url := fmt.Sprintf("https://%s%s", wrc.serverIP, config.MutatingWebhookServicePath) + glog.V(3).Infof("Debug MutatingWebhookConfig is registered with url %s\n", url) + + return &admregapi.MutatingWebhookConfiguration{ + ObjectMeta: meta.ObjectMeta{ + Name: config.MutatingWebhookConfigurationDebug, + Labels: config.KubePolicyAppLabels, + }, + Webhooks: []admregapi.Webhook{ + constructDebugWebhook( + config.MutatingWebhookName, + url, + caData), + }, + } +} + func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configuration *rest.Config) (*admregapi.ValidatingWebhookConfiguration, error) { // Check if ca is defined in the secret tls-ca // assume the key and signed cert have been defined in secret tls.kyverno @@ -112,6 +148,10 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura return nil, errors.New("Unable to extract CA data from configuration") } + if wrc.serverIP != "" { + return wrc.contructDebugValidatingWebhookConfig(caData), nil + } + return &admregapi.ValidatingWebhookConfiguration{ ObjectMeta: meta.ObjectMeta{ Name: config.ValidatingWebhookConfigurationName, @@ -129,6 +169,24 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura }, nil } +func (wrc *WebhookRegistrationClient) contructDebugValidatingWebhookConfig(caData []byte) *admregapi.ValidatingWebhookConfiguration { + url := fmt.Sprintf("https://%s%s", wrc.serverIP, config.ValidatingWebhookServicePath) + glog.V(3).Infof("Debug ValidatingWebhookConfig is registered with url %s\n", url) + + return &admregapi.ValidatingWebhookConfiguration{ + ObjectMeta: meta.ObjectMeta{ + Name: config.ValidatingWebhookConfigurationName, + Labels: config.KubePolicyAppLabels, + }, + Webhooks: []admregapi.Webhook{ + constructDebugWebhook( + config.ValidatingWebhookName, + url, + caData), + }, + } +} + func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook { return admregapi.Webhook{ Name: name, @@ -161,6 +219,34 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook } } +func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook { + return admregapi.Webhook{ + Name: name, + ClientConfig: admregapi.WebhookClientConfig{ + URL: &url, + CABundle: caData, + }, + Rules: []admregapi.RuleWithOperations{ + admregapi.RuleWithOperations{ + Operations: []admregapi.OperationType{ + admregapi.Create, + }, + Rule: admregapi.Rule{ + APIGroups: []string{ + "*", + }, + APIVersions: []string{ + "*", + }, + Resources: []string{ + "*/*", + }, + }, + }, + }, + } +} + func (wrc *WebhookRegistrationClient) constructOwner() meta.OwnerReference { kubePolicyDeployment, err := wrc.client.GetKubePolicyDeployment() diff --git a/scripts/deploy-controller-debug.sh b/scripts/deploy-controller-debug.sh new file mode 100755 index 0000000000..2eea11e6bf --- /dev/null +++ b/scripts/deploy-controller-debug.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +for i in "$@" +do +case $i in + --service=*) + service="${i#*=}" + shift + ;; + --serverIP=*) + serverIP="${i#*=}" + shift + ;; +esac +done + +if [ -z "${serverIP}" ]; then + echo -e "Please specify '--serverIP' where Kyverno controller runs." + exit 1 +fi + +if [ -z "${service}" ]; then + service="localhost" +fi + +echo "service is $service" +echo "serverIP is $serverIP" + +echo "Generating certificate for the service ${service}..." + +certsGenerator="./scripts/generate-self-signed-cert-and-k8secrets-debug.sh" +chmod +x "${certsGenerator}" + +${certsGenerator} "--service=${service}" "--serverIP=${serverIP}" || exit 2 +echo -e "\n### You can build and run kyverno project locally.\n### To check its work, run it with flags --kubeconfig and --serverIP parameters." + diff --git a/scripts/generate-self-signed-cert-and-k8secrets-debug.sh b/scripts/generate-self-signed-cert-and-k8secrets-debug.sh new file mode 100755 index 0000000000..52f15b756c --- /dev/null +++ b/scripts/generate-self-signed-cert-and-k8secrets-debug.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +for i in "$@" +do +case $i in + --service=*) + service="${i#*=}" + shift + ;; + --serverIP=*) + serverIP="${i#*=}" + shift + ;; +esac +done + +destdir="certs" +if [ ! -d "$destdir" ]; then + mkdir ${destdir} || exit 1 +fi + +tmpdir=$(mktemp -d) +cat <> ${tmpdir}/csr.conf +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = ${service} +IP.1 = ${serverIP} +EOF + +if [ ! -z "${service}" ]; then + subjectCN="${service}" +else + subjectCN=${serverIP} +fi + +echo "Generating self-signed certificate for CN=${subjectCN}" +# generate priv key for root CA +openssl genrsa -out ${destdir}/rootCA.key 4096 +# generate root CA +openssl req -x509 -new -nodes -key ${destdir}/rootCA.key -sha256 -days 1024 -out ${destdir}/rootCA.crt -subj "/CN=${subjectCN}" +# generate priv key +openssl genrsa -out ${destdir}/webhook.key 4096 +# generate certificate +openssl req -new -key ${destdir}/webhook.key -out ${destdir}/webhook.csr -subj "/CN=${subjectCN}" -config ${tmpdir}/csr.conf +# sign the certificate using the root CA +openssl x509 -req -in ${destdir}/webhook.csr -CA ${destdir}/rootCA.crt -CAkey ${destdir}/rootCA.key -CAcreateserial -out ${destdir}/webhook.crt -days 1024 -sha256 -extensions v3_req -extfile ${tmpdir}/csr.conf + + +kubectl delete -f definitions/install_debug.yaml 2>/dev/null +kubectl delete csr,MutatingWebhookConfiguration,ValidatingWebhookConfiguration --all 2>/dev/null + +echo "Generating corresponding kubernetes secrets for TLS pair and root CA" +# create project namespace +kubectl create ns kyverno +# create tls pair secret +kubectl -n kyverno create secret tls kyverno-svc.kyverno.svc.kyverno-tls-pair --cert=${destdir}/webhook.crt --key=${destdir}/webhook.key +# annotate tls pair secret to specify use of self-signed certificates and check if root CA is created as secret +kubectl annotate secret kyverno-svc.kyverno.svc.kyverno-tls-pair -n kyverno self-signed-cert=true +# create root CA secret +kubectl -n kyverno create secret generic kyverno-svc.kyverno.svc.kyverno-tls-ca --from-file=${destdir}/rootCA.crt + +echo "Creating CRD" +kubectl apply -f definitions/install_debug.yaml From 2e116aa00fcb1b18f7e026ca82ee3e8a945ce9a7 Mon Sep 17 00:00:00 2001 From: shuting Date: Mon, 10 Jun 2019 18:28:33 -0700 Subject: [PATCH 05/37] Update installation doc to run in debug mode --- documentation/installation.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/documentation/installation.md b/documentation/installation.md index b7d72cb5d5..32459ee1eb 100644 --- a/documentation/installation.md +++ b/documentation/installation.md @@ -102,13 +102,14 @@ kubectl logs -n kyverno Here is a script that generates a self-signed CA, a TLS certificate-key pair, and the corresponding kubernetes secrets: [helper script](/scripts/generate-self-signed-cert-and-k8secrets.sh) -# Installing in a Development Environment +# Installing outside of the cluster (debug mode) -To build and run Kyverno in a development environment see: https://github.com/nirmata/kyverno/wiki/Building +To build Kyverno in a development environment see: https://github.com/nirmata/kyverno/wiki/Building -To check if the controller is working, find it in the list of kyverno pods: +To run controller in this mode you should prepare TLS key/certificate pair for debug webhook, then start controller with kubeconfig and the server address. -`kubectl get pods -n kyverno` +1. Run scripts/deploy-controller-debug.sh --service=localhost --serverIP=, where is the IP address of the host where controller runs. This scripts will generate TLS certificate for debug webhook server and register this webhook in the cluster. Also it registers CustomResource Policy. +2. Start the controller using the following command: sudo kyverno --kubeconfig=~/.kube/config --serverIP= # Try Kyverno without a Kubernetes cluster From 804d610fc7d6165bd38443aeafbba141cfe536a7 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Tue, 11 Jun 2019 16:54:19 +0300 Subject: [PATCH 06/37] Refactored overlays and updated tests. Now new element in array is added to the end --- pkg/engine/overlay.go | 393 ++++++++++++++++++++----------------- pkg/engine/overlay_test.go | 17 +- 2 files changed, 217 insertions(+), 193 deletions(-) diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index 2d6ab1501e..da183519d9 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -24,182 +24,254 @@ func ProcessOverlay(rule kubepolicy.Rule, rawResource []byte, gvk metav1.GroupVe var appliedPatches []PatchBytes json.Unmarshal(rawResource, &resource) - patch := applyOverlay(resource, *rule.Mutation.Overlay, "/", &overlayApplicationResult) + patches, res := mutateResourceWithOverlay(resource, *rule.Mutation.Overlay) + overlayApplicationResult.MergeWith(&res) + if overlayApplicationResult.GetReason() == result.Success { - appliedPatches = append(appliedPatches, patch...) + appliedPatches = append(appliedPatches, patches...) } return appliedPatches, overlayApplicationResult } -// goes down through overlay and resource trees and applies overlay -func applyOverlay(resource, overlay interface{}, path string, res *result.RuleApplicationResult) []PatchBytes { +// OverlayProcessor stores context for overlay mutation process +type OverlayProcessor struct { + appliedPatches []PatchBytes + result result.RuleApplicationResult +} + +// mutateResourceWithOverlay is a start of overlaying process +// It assumes that mutation is started from root, so "/" is passed +func mutateResourceWithOverlay(resource, pattern interface{}) ([]PatchBytes, result.RuleApplicationResult) { + return applyOverlay(resource, pattern, "/") +} + +// applyOverlay detects type of current item and goes down through overlay and resource trees applying overlay +func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { var appliedPatches []PatchBytes + overlayResult := result.NewRuleApplicationResult("") // resource item exists but has different type - replace // all subtree within this path by overlay if reflect.TypeOf(resource) != reflect.TypeOf(overlay) { - patch := replaceSubtree(overlay, path, res) - if res.Reason == result.Success { + patch, res := replaceSubtree(overlay, path) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { appliedPatches = append(appliedPatches, patch) } - return appliedPatches + return appliedPatches, overlayResult } + return applyOverlayForSameTypes(resource, overlay, path) +} + +// applyOverlayForSameTypes is applyOverlay for cases when TypeOf(resource) == TypeOf(overlay) +func applyOverlayForSameTypes(resource, overlay interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { + var appliedPatches []PatchBytes + overlayResult := result.NewRuleApplicationResult("") + + // detect the type of resource and overlay and select corresponding handler switch typedOverlay := overlay.(type) { + // map case map[string]interface{}: typedResource := resource.(map[string]interface{}) + patches, res := applyOverlayToMap(typedResource, typedOverlay, path) + overlayResult.MergeWith(&res) - for key, value := range typedOverlay { - if wrappedWithParentheses(key) { - continue - } - currentPath := path + key + "/" - resourcePart, ok := typedResource[key] - - if ok { - patches := applyOverlay(resourcePart, value, currentPath, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patches...) - } - - } else { - patch := insertSubtree(value, currentPath, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patch) - } - - appliedPatches = append(appliedPatches, patch) - } - } - case []interface{}: - typedResource := resource.([]interface{}) - patches := applyOverlayToArray(typedResource, typedOverlay, path, res) - if res.Reason == result.Success { + if result.Success == overlayResult.GetReason() { appliedPatches = append(appliedPatches, patches...) } + // array + case []interface{}: + typedResource := resource.([]interface{}) + patches, res := applyOverlayToArray(typedResource, typedOverlay, path) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patches...) + } + // elementary types case string, float64, int64, bool: - patch := replaceSubtree(overlay, path, res) - if res.Reason == result.Success { + patch, res := replaceSubtree(overlay, path) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { appliedPatches = append(appliedPatches, patch) } default: - res.FailWithMessagef("Overlay has unsupported type: %T", overlay) - return nil + overlayResult.FailWithMessagef("Overlay has unsupported type: %T", overlay) + return nil, overlayResult } - return appliedPatches + return appliedPatches, overlayResult } -// for each overlay and resource array elements and applies overlay -func applyOverlayToArray(resource, overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes { +// for each overlay and resource map elements applies overlay +func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { var appliedPatches []PatchBytes - if len(overlay) == 0 { - res.FailWithMessagef("Empty array detected in the overlay") - return nil + overlayResult := result.NewRuleApplicationResult("") + + for key, value := range overlayMap { + // skip anchor element because it has condition, not + // the value that must replace resource value + if wrappedWithParentheses(key) { + continue + } + + currentPath := path + key + "/" + resourcePart, ok := resourceMap[key] + + if ok { + // Key exists - go down through the overlay and resource trees + patches, res := applyOverlay(resourcePart, value, currentPath) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patches...) + } + } else { + // Key does not exist - insert entire overlay subtree + patch, res := insertSubtree(value, currentPath) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patch) + } + } } - if len(resource) == 0 { - return fillEmptyArray(overlay, path, res) + return appliedPatches, overlayResult +} + +// for each overlay and resource array elements applies overlay +func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { + var appliedPatches []PatchBytes + overlayResult := result.NewRuleApplicationResult("") + + if 0 == len(overlay) { + overlayResult.FailWithMessagef("Empty array detected in the overlay") + return nil, overlayResult + } + + if 0 == len(resource) { + patch, res := insertSubtree(overlay, path) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patch) + } + + return appliedPatches, res } if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) { - res.FailWithMessagef("overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0]) - return nil + overlayResult.FailWithMessagef("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0]) + return nil, overlayResult } - switch overlay[0].(type) { - case map[string]interface{}: - for _, overlayElement := range overlay { - typedOverlay := overlayElement.(map[string]interface{}) - anchors := getAnchorsFromMap(typedOverlay) - if len(anchors) > 0 { - for i, resourceElement := range resource { - typedResource := resourceElement.(map[string]interface{}) - - currentPath := path + strconv.Itoa(i) + "/" - if !skipArrayObject(typedResource, anchors) { - patches := applyOverlay(resourceElement, overlayElement, currentPath, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patches...) - } - } - - } - } else if hasNestedAnchors(overlayElement) { - for i, resourceElement := range resource { - currentPath := path + strconv.Itoa(i) + "/" - patches := applyOverlay(resourceElement, overlayElement, currentPath, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patches...) - } - } - } else { - currentPath := path + "0/" - patch := insertSubtree(overlayElement, currentPath, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patch) - } - } - } - default: - path += "0/" - for _, value := range overlay { - patch := insertSubtree(value, path, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patch) - } - } - } - - return appliedPatches + return applyOverlayToArrayOfSameTypes(resource, overlay, path) } -// In case of empty resource array -// append all non-anchor items to front -func fillEmptyArray(overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes { +func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { var appliedPatches []PatchBytes - if len(overlay) == 0 { - res.FailWithMessagef("Empty array detected in the overlay") - return nil - } - - path += "0/" + overlayResult := result.NewRuleApplicationResult("") switch overlay[0].(type) { case map[string]interface{}: - for _, overlayElement := range overlay { - typedOverlay := overlayElement.(map[string]interface{}) - anchors := getAnchorsFromMap(typedOverlay) - - if len(anchors) == 0 { - patch := insertSubtree(overlayElement, path, res) - if res.Reason == result.Success { - appliedPatches = append(appliedPatches, patch) - } - } - } + return applyOverlayToArrayOfMaps(resource, overlay, path) default: - for _, overlayElement := range overlay { - patch := insertSubtree(overlayElement, path, res) - if res.Reason == result.Success { + lastElementIdx := len(resource) + + // Add elements to the end + for i, value := range overlay { + currentPath := path + strconv.Itoa(lastElementIdx+i) + "/" + patch, res := insertSubtree(value, currentPath) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { appliedPatches = append(appliedPatches, patch) } } } - return appliedPatches + return appliedPatches, overlayResult } -func insertSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte { - return processSubtree(overlay, path, "add", res) +func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { + var appliedPatches []PatchBytes + overlayResult := result.NewRuleApplicationResult("") + + lastElementIdx := len(resource) + for i, overlayElement := range overlay { + typedOverlay := overlayElement.(map[string]interface{}) + anchors := getAnchorsFromMap(typedOverlay) + + if len(anchors) > 0 { + // If we have anchors - choose corresponding resource element and mutate it + patches, res := applyOverlayWithAnchors(resource, overlayElement, anchors, path) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patches...) + } + } else if hasNestedAnchors(overlayElement) { + // If we have anchors on the lower level - continue traversing overlay and resource trees + for j, resourceElement := range resource { + currentPath := path + strconv.Itoa(j) + "/" + patches, res := applyOverlay(resourceElement, overlayElement, currentPath) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patches...) + } + } + } else { + // Overlay subtree has no anchors - insert new element + currentPath := path + strconv.Itoa(lastElementIdx+i) + "/" + patch, res := insertSubtree(overlayElement, currentPath) + overlayResult.MergeWith(&res) + + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patch) + } + } + } + + return appliedPatches, overlayResult } -func replaceSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte { - return processSubtree(overlay, path, "replace", res) +func applyOverlayWithAnchors(resource []interface{}, overlay interface{}, anchors map[string]interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { + var appliedPatches []PatchBytes + overlayResult := result.NewRuleApplicationResult("") + + for i, resourceElement := range resource { + typedResource := resourceElement.(map[string]interface{}) + + currentPath := path + strconv.Itoa(i) + "/" + if !skipArrayObject(typedResource, anchors) { + patches, res := applyOverlay(resourceElement, overlay, currentPath) + overlayResult.MergeWith(&res) + if result.Success == overlayResult.GetReason() { + appliedPatches = append(appliedPatches, patches...) + } + } + } + + return appliedPatches, overlayResult } -func processSubtree(overlay interface{}, path string, op string, res *result.RuleApplicationResult) PatchBytes { +func insertSubtree(overlay interface{}, path string) (PatchBytes, result.RuleApplicationResult) { + return processSubtree(overlay, path, "add") +} + +func replaceSubtree(overlay interface{}, path string) (PatchBytes, result.RuleApplicationResult) { + return processSubtree(overlay, path, "replace") +} + +func processSubtree(overlay interface{}, path string, op string) (PatchBytes, result.RuleApplicationResult) { + overlayResult := result.NewRuleApplicationResult("") + if len(path) > 1 && path[len(path)-1] == '/' { path = path[:len(path)-1] } @@ -214,76 +286,26 @@ func processSubtree(overlay interface{}, path string, op string, res *result.Rul // check the patch _, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]")) if err != nil { - res.FailWithMessagef("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path) - return nil + overlayResult.FailWithMessagef("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path) + return nil, overlayResult } - return PatchBytes(patchStr) + return PatchBytes(patchStr), overlayResult } -// TODO: Overlay is already in JSON, remove this code // converts overlay to JSON string to be inserted into the JSON Patch func prepareJSONValue(overlay interface{}) string { - switch typed := overlay.(type) { - case map[string]interface{}: - if len(typed) == 0 { - return "" - } + jsonOverlay, err := json.Marshal(overlay) - if hasOnlyAnchors(overlay) { - return "" - } - - result := "" - for key, value := range typed { - jsonValue := prepareJSONValue(value) - - pair := fmt.Sprintf(`"%s":%s`, key, jsonValue) - - if result != "" { - result += ", " - } - - result += pair - } - - result = fmt.Sprintf(`{ %s }`, result) - return result - case []interface{}: - if len(typed) == 0 { - return "" - } - - if hasOnlyAnchors(overlay) { - return "" - } - - result := "" - for _, value := range typed { - jsonValue := prepareJSONValue(value) - - if result != "" { - result += ", " - } - - result += jsonValue - } - - result = fmt.Sprintf(`[ %s ]`, result) - return result - case string: - return fmt.Sprintf(`"%s"`, typed) - case float64: - return fmt.Sprintf("%f", typed) - case int64: - return fmt.Sprintf("%d", typed) - case bool: - return fmt.Sprintf("%t", typed) - default: + if err != nil || hasOnlyAnchors(overlay) { return "" } + + return string(jsonOverlay) } +// Anchor has pattern value, so resource shouldn't be mutated with it +// If entire subtree has only anchor keys - we func hasOnlyAnchors(overlay interface{}) bool { switch typed := overlay.(type) { case map[string]interface{}: @@ -296,13 +318,20 @@ func hasOnlyAnchors(overlay interface{}) bool { return false } } - - return true + case []interface{}: + for _, value := range typed { + if !hasOnlyAnchors(value) { + return false + } + } default: return false } + + return true } +// Checks if subtree has anchors func hasNestedAnchors(overlay interface{}) bool { switch typed := overlay.(type) { case map[string]interface{}: diff --git a/pkg/engine/overlay_test.go b/pkg/engine/overlay_test.go index 19dc2c2a92..7ba2b9abfa 100644 --- a/pkg/engine/overlay_test.go +++ b/pkg/engine/overlay_test.go @@ -2,7 +2,6 @@ package engine import ( "encoding/json" - "github.com/nirmata/kyverno/pkg/result" "reflect" "testing" @@ -66,8 +65,7 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) { json.Unmarshal(resourceRaw, &resource) json.Unmarshal(overlayRaw, &overlay) - res := result.NewRuleApplicationResult("") - patches := applyOverlay(resource, overlay, "/", &res) + patches, res := applyOverlay(resource, overlay, "/") assert.NilError(t, res.ToError()) assert.Assert(t, patches != nil) @@ -140,8 +138,7 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) { json.Unmarshal(resourceRaw, &resource) json.Unmarshal(overlayRaw, &overlay) - res := result.NewRuleApplicationResult("") - patches := applyOverlay(resource, overlay, "/", &res) + patches, res := applyOverlay(resource, overlay, "/") assert.NilError(t, res.ToError()) assert.Assert(t, patches != nil) @@ -155,7 +152,7 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) { assert.NilError(t, err) assert.Assert(t, patched != nil) - expectedResult := []byte(`{"apiVersion":"v1","kind":"Endpoints","metadata":{"name":"test-endpoint","labels":{"label":"test"}},"subsets":[{"addresses":[{"ip":"192.168.10.172"},{"ip":"192.168.10.173"}],"ports":[{"name":"insecure-connection","port":80.000000,"protocol":"UDP"}]},{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":443,"protocol":"TCP"}]}]}`) + expectedResult := []byte(`{ "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":443, "protocol":"TCP" } ] }, { "addresses":[ { "ip":"192.168.10.172" }, { "ip":"192.168.10.173" } ], "ports":[ { "name":"insecure-connection", "port":80, "protocol":"UDP" } ] } ] }`) compareJsonAsMap(t, expectedResult, patched) } @@ -219,8 +216,7 @@ func TestApplyOverlay_TestInsertToArray(t *testing.T) { json.Unmarshal(resourceRaw, &resource) json.Unmarshal(overlayRaw, &overlay) - res := result.NewRuleApplicationResult("") - patches := applyOverlay(resource, overlay, "/", &res) + patches, res := applyOverlay(resource, overlay, "/") assert.NilError(t, res.ToError()) assert.Assert(t, patches != nil) @@ -303,13 +299,12 @@ func TestApplyOverlay_ImagePullPolicy(t *testing.T) { json.Unmarshal(resourceRaw, &resource) json.Unmarshal(overlayRaw, &overlay) - res := result.NewRuleApplicationResult("") - patches := applyOverlay(resource, overlay, "/", &res) + patches, res := applyOverlay(resource, overlay, "/") assert.NilError(t, res.ToError()) assert.Assert(t, len(patches) != 0) doc, err := ApplyPatches(resourceRaw, patches) assert.NilError(t, err) - expectedResult := []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"nginx-deployment","labels":{"app":"nginx"}},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}},"spec":{"containers":[{"image":"nginx:latest","imagePullPolicy":"IfNotPresent","name":"nginx","ports":[{"containerPort":8080.000000},{"containerPort":80}]},{"image":"ghost:latest","imagePullPolicy":"IfNotPresent","name":"ghost","ports":[{"containerPort":8080.000000}]}]}}}}`) + expectedResult := []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"nginx-deployment","labels":{"app":"nginx"}},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}},"spec":{"containers":[{"image":"nginx:latest","imagePullPolicy":"IfNotPresent","name":"nginx","ports":[{"containerPort":80},{"containerPort":8080}]},{"image":"ghost:latest","imagePullPolicy":"IfNotPresent","name":"ghost","ports":[{"containerPort":8080}]}]}}}}`) compareJsonAsMap(t, expectedResult, doc) } From b5fa5b256085cbca4467d6d92b0848e645e5d431 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Tue, 11 Jun 2019 18:46:35 -0700 Subject: [PATCH 07/37] update broken links and CLI text --- documentation/installation.md | 2 +- documentation/testing-policies.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/installation.md b/documentation/installation.md index 088f61587e..018cce826e 100644 --- a/documentation/installation.md +++ b/documentation/installation.md @@ -123,7 +123,7 @@ To run controller in this mode you should prepare TLS key/certificate pair for d # Try Kyverno without a Kubernetes cluster -The [Kyverno CLI](documentation/testing-policies-cli.md) allows you to write and test policies without installing Kyverno in a Kubernetes cluster. Some features are not supported without a Kubernetes cluster. +The [Kyverno CLI](documentation/testing-policies.md#test-using-the-kyverno-cli) allows you to write and test policies without installing Kyverno in a Kubernetes cluster. Some features are not supported without a Kubernetes cluster. diff --git a/documentation/testing-policies.md b/documentation/testing-policies.md index 40767d7b1c..b935f05279 100644 --- a/documentation/testing-policies.md +++ b/documentation/testing-policies.md @@ -22,7 +22,7 @@ kubectl get -f CM.yaml -o yaml ## Test using the Kyverno CLI -The Kyverno Command Line Interface (CLI) tool enables writing and testing policies without requiring Kubernetes clusters and without having to apply local policy changes to a cluster. +The Kyverno Command Line Interface (CLI) tool allows writing and testing policies without having to apply local policy changes to a cluster. You can also test policies without a Kubernetes clusters, but results may vary as default values will not be filled in. ### Building the CLI From 5c6982d62b0cecf333e2037a45df8d81ac420871 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Tue, 11 Jun 2019 19:16:15 -0700 Subject: [PATCH 08/37] update bug# and remove fixed issue --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 0bafa1b5d0..e1976bb41a 100644 --- a/README.md +++ b/README.md @@ -148,9 +148,8 @@ Here are some the major features we plan on completing before a 1.0 release: * [Events](https://github.com/nirmata/kyverno/issues/14) * [Policy Violations](https://github.com/nirmata/kyverno/issues/24) -* [Generate any resource](https://github.com/nirmata/kyverno/issues/21) * [Conditionals on existing resources](https://github.com/nirmata/kyverno/issues/57) -* [Extend CLI to operate on cluster resources ](https://github.com/nirmata/kyverno/issues/25) +* [Extend CLI to operate on cluster resources ](https://github.com/nirmata/kyverno/issues/164) ## Getting help From f89168f77a4c7708a3c8127c9c21f6f5d5399bac Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Wed, 12 Jun 2019 12:21:52 +0300 Subject: [PATCH 09/37] Wrote additional comments --- pkg/engine/overlay.go | 17 ++--- pkg/engine/overlay_test.go | 128 ++++++++++++++++++++++++++++++++++++- pkg/engine/validation.go | 2 +- 3 files changed, 135 insertions(+), 12 deletions(-) diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index da183519d9..cf2ffe85cb 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -34,15 +34,9 @@ func ProcessOverlay(rule kubepolicy.Rule, rawResource []byte, gvk metav1.GroupVe return appliedPatches, overlayApplicationResult } -// OverlayProcessor stores context for overlay mutation process -type OverlayProcessor struct { - appliedPatches []PatchBytes - result result.RuleApplicationResult -} - // mutateResourceWithOverlay is a start of overlaying process -// It assumes that mutation is started from root, so "/" is passed func mutateResourceWithOverlay(resource, pattern interface{}) ([]PatchBytes, result.RuleApplicationResult) { + // It assumes that mutation is started from root, so "/" is passed return applyOverlay(resource, pattern, "/") } @@ -155,6 +149,7 @@ func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchB } if 0 == len(resource) { + // If array resource is empty, insert part from overlay patch, res := insertSubtree(overlay, path) overlayResult.MergeWith(&res) @@ -173,6 +168,7 @@ func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchB return applyOverlayToArrayOfSameTypes(resource, overlay, path) } +// applyOverlayToArrayOfSameTypes applies overlay to array elements if they (resource and overlay elements) have same type func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { var appliedPatches []PatchBytes overlayResult := result.NewRuleApplicationResult("") @@ -186,6 +182,7 @@ func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string // Add elements to the end for i, value := range overlay { currentPath := path + strconv.Itoa(lastElementIdx+i) + "/" + // currentPath example: /spec/template/spec/containers/3/ patch, res := insertSubtree(value, currentPath) overlayResult.MergeWith(&res) @@ -198,6 +195,7 @@ func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string return appliedPatches, overlayResult } +// func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { var appliedPatches []PatchBytes overlayResult := result.NewRuleApplicationResult("") @@ -219,6 +217,7 @@ func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([] // If we have anchors on the lower level - continue traversing overlay and resource trees for j, resourceElement := range resource { currentPath := path + strconv.Itoa(j) + "/" + // currentPath example: /spec/template/spec/containers/3/ patches, res := applyOverlay(resourceElement, overlayElement, currentPath) overlayResult.MergeWith(&res) @@ -229,6 +228,7 @@ func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([] } else { // Overlay subtree has no anchors - insert new element currentPath := path + strconv.Itoa(lastElementIdx+i) + "/" + // currentPath example: /spec/template/spec/containers/3/ patch, res := insertSubtree(overlayElement, currentPath) overlayResult.MergeWith(&res) @@ -249,6 +249,7 @@ func applyOverlayWithAnchors(resource []interface{}, overlay interface{}, anchor typedResource := resourceElement.(map[string]interface{}) currentPath := path + strconv.Itoa(i) + "/" + // currentPath example: /spec/template/spec/containers/3/ if !skipArrayObject(typedResource, anchors) { patches, res := applyOverlay(resourceElement, overlay, currentPath) overlayResult.MergeWith(&res) @@ -305,7 +306,7 @@ func prepareJSONValue(overlay interface{}) string { } // Anchor has pattern value, so resource shouldn't be mutated with it -// If entire subtree has only anchor keys - we +// If entire subtree has only anchor keys - we should skip inserting it func hasOnlyAnchors(overlay interface{}) bool { switch typed := overlay.(type) { case map[string]interface{}: diff --git a/pkg/engine/overlay_test.go b/pkg/engine/overlay_test.go index 7ba2b9abfa..449c5641b7 100644 --- a/pkg/engine/overlay_test.go +++ b/pkg/engine/overlay_test.go @@ -78,7 +78,34 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) { assert.NilError(t, err) assert.Assert(t, patched != nil) - expectedResult := []byte(`{"apiVersion":"v1","kind":"Endpoints","metadata":{"name":"test-endpoint","labels":{"label":"test"}},"subsets":[{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":444.000000,"protocol":"UDP"}]}]}`) + expectedResult := []byte(` + { + "apiVersion":"v1", + "kind":"Endpoints", + "metadata":{ + "name":"test-endpoint", + "labels":{ + "label":"test" + } + }, + "subsets":[ + { + "addresses":[ + { + "ip":"192.168.10.171" + } + ], + "ports":[ + { + "name":"secure-connection", + "port":444.000000, + "protocol":"UDP" + } + ] + } + ] + }`) + compareJsonAsMap(t, expectedResult, patched) } @@ -152,7 +179,50 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) { assert.NilError(t, err) assert.Assert(t, patched != nil) - expectedResult := []byte(`{ "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":443, "protocol":"TCP" } ] }, { "addresses":[ { "ip":"192.168.10.172" }, { "ip":"192.168.10.173" } ], "ports":[ { "name":"insecure-connection", "port":80, "protocol":"UDP" } ] } ] }`) + expectedResult := []byte(`{ + "apiVersion":"v1", + "kind":"Endpoints", + "metadata":{ + "name":"test-endpoint", + "labels":{ + "label":"test" + } + }, + "subsets":[ + { + "addresses":[ + { + "ip":"192.168.10.171" + } + ], + "ports":[ + { + "name":"secure-connection", + "port":443, + "protocol":"TCP" + } + ] + }, + { + "addresses":[ + { + "ip":"192.168.10.172" + }, + { + "ip":"192.168.10.173" + } + ], + "ports":[ + { + "name":"insecure-connection", + "port":80, + "protocol":"UDP" + } + ] + } + ] + }`) + compareJsonAsMap(t, expectedResult, patched) } @@ -305,6 +375,58 @@ func TestApplyOverlay_ImagePullPolicy(t *testing.T) { doc, err := ApplyPatches(resourceRaw, patches) assert.NilError(t, err) - expectedResult := []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"nginx-deployment","labels":{"app":"nginx"}},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}},"spec":{"containers":[{"image":"nginx:latest","imagePullPolicy":"IfNotPresent","name":"nginx","ports":[{"containerPort":80},{"containerPort":8080}]},{"image":"ghost:latest","imagePullPolicy":"IfNotPresent","name":"ghost","ports":[{"containerPort":8080}]}]}}}}`) + expectedResult := []byte(`{ + "apiVersion":"apps/v1", + "kind":"Deployment", + "metadata":{ + "name":"nginx-deployment", + "labels":{ + "app":"nginx" + } + }, + "spec":{ + "replicas":1, + "selector":{ + "matchLabels":{ + "app":"nginx" + } + }, + "template":{ + "metadata":{ + "labels":{ + "app":"nginx" + } + }, + "spec":{ + "containers":[ + { + "image":"nginx:latest", + "imagePullPolicy":"IfNotPresent", + "name":"nginx", + "ports":[ + { + "containerPort":80 + }, + { + "containerPort":8080 + } + ] + }, + { + "image":"ghost:latest", + "imagePullPolicy":"IfNotPresent", + "name":"ghost", + "ports":[ + { + "containerPort":8080 + } + ] + } + ] + } + } + } + }`) + compareJsonAsMap(t, expectedResult, doc) } diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 6ae0b6585d..d76c12e418 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -91,7 +91,7 @@ func validateResourceElement(resourceElement, patternElement interface{}, path s } // If validateResourceElement detects map element inside resource and pattern trees, it goes to validateMap -// For each element of the map we must detect the type again, so we pass this elements to validateResourceElement +// For each element of the map we must detect the type again, so we pass these elements to validateResourceElement func validateMap(resourceMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult { res := result.NewRuleApplicationResult("") From 5339f4450492e7feef116fb6cc91ab162dc16a2e Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Wed, 12 Jun 2019 13:12:36 +0300 Subject: [PATCH 10/37] Added comment for applyOverlayToArrayOfMaps --- pkg/engine/overlay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index cf2ffe85cb..157d7a5f46 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -195,7 +195,7 @@ func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string return appliedPatches, overlayResult } -// +// Array of maps needs special handling as far as it can have anchors. func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) { var appliedPatches []PatchBytes overlayResult := result.NewRuleApplicationResult("") From 1d851a8968eb43909f8958b3b7c615981613fff6 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 12 Jun 2019 08:49:29 -0700 Subject: [PATCH 11/37] add polaris --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e1976bb41a..a89ca3d56e 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,10 @@ Additional examples are available in [examples](/examples). [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) is a general-purpose policy engine that can be used as a Kubernetes admission controller. It supports a large set of use cases. Policies are written using [Rego](https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies#what-is-rego) a custom query language. +### Polaris + +[Polaris](https://github.com/reactiveops/polaris) validates configurations for best practices. It includes several checks across health, networking, security, etc. Checks can be assigned a severity. A dashboard reports the overall score. + ### External configuration management tools Tools like [Kustomize](https://github.com/kubernetes-sigs/kustomize) can be used to manage variations in configurations outside of clusters. There are several advantages to this approach when used to produce variations of the same base configuration. However, such solutions cannot be used to validate or enforce configurations. From 43835a25f1d3c22b2a00004e614f444467481011 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 12 Jun 2019 09:39:37 -0700 Subject: [PATCH 12/37] add code of conduct & contributing section --- CODE_OF_CONDUCT.md | 39 +++++++++++++++++++++++++++++++++++++++ README.md | 22 ++++++++++++++++------ 2 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..bb60496999 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,39 @@ +# Kyverno Community Code of Conduct v1.0 + +### Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering +an open and welcoming community, we pledge to respect all people who contribute +through reporting issues, posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are not +aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers +commit themselves to fairly and consistently applying these principles to every aspect +of managing this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior in Kubernetes may be reported by contacting the project maintainer(s). + +This Code of Conduct is adapted from the the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md) and the Contributor Covenant +(http://contributor-covenant.org), version 1.2.0, available at +http://contributor-covenant.org/version/1/2/0/ diff --git a/README.md b/README.md index a89ca3d56e..9b5081bcb7 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,14 @@ spec: Additional examples are available in [examples](/examples). +## License + +[Apache License 2.0](https://github.com/nirmata/kyverno/blob/master/LICENSE) + +## Status + +*Kyverno is under active development and not ready for production use. Key components and policy definitions are likely to change as we complete core features.* + ## Alternatives ### Open Policy Agent @@ -128,12 +136,6 @@ Additional examples are available in [examples](/examples). Tools like [Kustomize](https://github.com/kubernetes-sigs/kustomize) can be used to manage variations in configurations outside of clusters. There are several advantages to this approach when used to produce variations of the same base configuration. However, such solutions cannot be used to validate or enforce configurations. - -## Status - -*Kyverno is under active development and not ready for production use. Key components and policy definitions are likely to change as we complete core features.* - - ## Documentation * [Getting Started](documentation/installation.md) @@ -160,3 +162,11 @@ Here are some the major features we plan on completing before a 1.0 release: * For feature requests and bugs, file an [issue](https://github.com/nirmata/kyverno/issues). * For discussions or questions, join the [mailing list](https://groups.google.com/forum/#!forum/kyverno) +## Contributing + +Welcome to our community and thanks for contributing! + + * Please review and agree to abide with the [Code of Conduct](/CODE_OF_CONDUCT.md) before contributing. + * See the [Wiki](https://github.com/nirmata/kyverno/wiki) for developer documentation. + * Browse through the [open issues](https://github.com/nirmata/kyverno/issues) + From fb20de60217cd9f968609d339b0e352170f395ec Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 12 Jun 2019 13:50:08 -0700 Subject: [PATCH 13/37] fix indexing and update overlay docs --- documentation/writing-policies-generate.md | 2 +- documentation/writing-policies-mutate.md | 97 ++++++++++++++-------- documentation/writing-policies-validate.md | 2 +- documentation/writing-policies.md | 20 ++--- 4 files changed, 73 insertions(+), 48 deletions(-) diff --git a/documentation/writing-policies-generate.md b/documentation/writing-policies-generate.md index 6fcabefd4d..cc19c9c695 100644 --- a/documentation/writing-policies-generate.md +++ b/documentation/writing-policies-generate.md @@ -2,7 +2,7 @@ # Generate Configurations -```generate``` feature can be applied to created namespaces to create new resources in them. This feature is useful when every namespace in a cluster must contain some basic required resources. The feature is available for policy rules in which the resource kind is Namespace. +```generate``` is used to create default resources for a namespace. This feature is useful for managing resources that are required in each namespace. ## Example 1 diff --git a/documentation/writing-policies-mutate.md b/documentation/writing-policies-mutate.md index 1c4c044c56..c1f1c89174 100644 --- a/documentation/writing-policies-mutate.md +++ b/documentation/writing-policies-mutate.md @@ -2,13 +2,13 @@ # Mutate Configurations -The ```mutate``` rule contains actions that should be applied to the resource before its creation. Mutation can be made using patches or overlay. Using ```patches``` in the JSONPatch format, you can make point changes to the created resource, and ```overlays``` are designed to bring the resource to the desired view according to a specific pattern. +The ```mutate``` rule contains actions that will be applied to matching resource before their creation. A mutate rule can be written as a JSON Patch or as an overlay. By using a ```patch``` in the (JSONPatch - RFC 6902)[http://jsonpatch.com/] format, you can make precise changes to the resource being created. Using an ```overlay``` is convenient for describing the desired state of the resource. -Resource mutation occurs before validation, so the validation rules should not contradict the changes set in the mutation section. +Resource mutation occurs before validation, so the validation rules should not contradict the changes performed by the mutation section. ## Patches -The patches are used to make direct changes in the created resource. In the next example the patch will be applied to all Deployments that contain a word "nirmata" in the name. +This patch adds an init container to all deployments. ````yaml apiVersion : kyverno.io/v1alpha1 @@ -17,29 +17,27 @@ metadata : name : policy-v1 spec : rules: - - name: "Deployment of *nirmata* images" + - name: "add-init-secrets" resource: - kind: Deployment - # Name is optional. By default validation policy is applicable to any resource of supported kind. - # Name supports wildcards * and ? - name: "*nirmata*" + kinds: + - Deployment mutate: patches: - # This patch adds sidecar container to every deployment that matches this policy - - path: "/spec/template/spec/containers/0/" + - path: "/spec/template/spec/initContainers/0/" op: add value: - - image: "nirmata.io/sidecar:latest" - imagePullPolicy: "Always" - ports: - - containerPort: 443 + - image: "nirmata.io/kube-vault-client:v2" + name: "init-secrets" + ```` -There is one patch in the rule, it will add the new image to the "containers" list with specified parameters. Patch is described in [JSONPatch](http://jsonpatch.com/) format and support the operations ('op' field): +[JSONPatch](http://jsonpatch.com/) supports the following operations (in the 'op' field): * **add** * **replace** * **remove** -Here is the example with of a patch which removes a label from the secret: +With Kyverno, the add and replace have the same behavior i.e. both operations will add or replace the target element. + +Here is the example of a patch that removes a label from the secret: ````yaml apiVersion : kyverno.io/v1alpha1 kind : Policy @@ -49,7 +47,6 @@ spec : rules: - name: "Remove unwanted label" resource: - # Will be applied to all secrets, because name and selector are not specified kind: Secret mutate: patches: @@ -61,9 +58,9 @@ Note, that if **remove** operation cannot be applied, then this **remove** opera ## Overlay -The Mutation Overlay is the desired form of resource. The existing resource parameters are replaced with the parameters described in the overlay. If there are no such parameters in the target resource, they are copied to the resource from the overlay. The overlay is not used to delete the properties of a resource: use **patches** for this purpose. +An mutation overlay describes the desired form of resource. The existing resource values are replaced with the values specified in the overlay. If a value is specified in the overlay but not present in the target resource, then it will be added to the resource. The overlay cannot be used to delete values in a resource: use **patches** for this purpose. -The next overlay will add or change the hard limit for memory to 2 gigabytes in every ResourceQuota with label ```quota: low```: +The following mutation overlay will add (or replace) the memory request and limit to 10Gi for every Pod with a label ```memory: high```: ````yaml apiVersion : kyverno.io/v1alpha1 @@ -74,22 +71,27 @@ spec : rules: - name: "Set hard memory limit to 2Gi" resource: - # Will be applied to all secrets, because name and selector are not specified - kind: ResourceQuota + kind: Pod selector: matchLabels: - quota: low + memory: high mutate: overlay: spec: - hard: - limits.memory: 2Gi + containers: + # the wildcard * will match all containers in the list + - name: * + resources: + requests: + memory: "10Gi" + limits: + memory: "10Gi" + ```` -The ```overlay``` keyword under ```mutate``` feature describes the desired form of ResourceQuota. ### Working with lists -The application of an overlay to the list without additional settings is pretty straightforward: the new items will be added to the list exсept of those that totally equal to existent items. For example, the next overlay will add IP "192.168.10.172" to all addresses in all Endpoints: +Applying overlays to a list type without is fairly straightforward: new items will be added to the list, unless they already ecist. For example, the next overlay will add IP "192.168.10.172" to all addresses in all Endpoints: ````yaml apiVersion: policy.nirmata.io/v1alpha1 @@ -99,7 +101,6 @@ metadata: spec: rules: - resource: - # Applied to all endpoints kind : Endpoints mutate: overlay: @@ -108,16 +109,21 @@ spec: - ip: 192.168.10.172 ```` -You can use overlays to merge objects inside lists using **anchor** items marked by parentheses. For example, this overlay will add/replace port to 6443 in all ports with name that start from the word "secure": + +### Conditional logic using anchors + +An **anchor** field, marked by parentheses, allows conditional processing of configurations. Processing stops when the anchor value does not match. Once processing stops, any child elements or any remaining siblings in a list, will not be processed. + + For example, this overlay will add or replace the value 6443 for the port field, for all ports with a name value that starts with "secure": + ````yaml apiVersion : policy.nirmata.io/v1alpha1 kind : Policy metadata : - name : policy-endpoints-should-be-more-secure + name : policy-set-port spec : rules: - resource: - # Applied to all endpoints kind : Endpoints mutate: overlay: @@ -127,13 +133,36 @@ spec : port: 6443 ```` -The **anchors** marked in parentheses support **wildcards**: +The **anchors** values support **wildcards**: 1. `*` - matches zero or more alphanumeric characters 2. `?` - matches a single alphanumeric character -## Details -The behavior of overlays described more detailed in the project's wiki: [Mutation Overlay](https://github.com/nirmata/kyverno/wiki/Mutation-Overlay) +### Add if not present + +A variation of an anchor, is to add a field value if it is not already defined. This is done by using the ````+(...)```` notation for the field. + + For example, this overlay will set the port to 6443, if a port is not already defined: + +````yaml +apiVersion : policy.nirmata.io/v1alpha1 +kind : Policy +metadata : + name : policy-set-port +spec : + rules: + - resource: + kind : Endpoints + mutate: + overlay: + subsets: + - ports: + +(port): 6443 +```` + +## Additional Details + +Additional details on mutation overlay behaviors are available on the wiki: [Mutation Overlay](https://github.com/nirmata/kyverno/wiki/Mutation-Overlay) --- -*Read Next >> [Validate](/documentation/writing-policies-validate.md)* +*Read Next >> [Generate](/documentation/writing-policies-generate.md)* diff --git a/documentation/writing-policies-validate.md b/documentation/writing-policies-validate.md index 402fbc71e4..36e0d9eaae 100644 --- a/documentation/writing-policies-validate.md +++ b/documentation/writing-policies-validate.md @@ -68,4 +68,4 @@ Additional examples are available in [examples](/examples/) --- -*Read Next >> [Generate](/documentation/writing-policies-generate.md)* +*Read Next >> [Generate](/documentation/writing-policies-mutate.md)* diff --git a/documentation/writing-policies.md b/documentation/writing-policies.md index 6bbefb2846..72a80dbae3 100644 --- a/documentation/writing-policies.md +++ b/documentation/writing-policies.md @@ -10,29 +10,25 @@ kind : Policy metadata : name : policy spec : - # Each policy has a list of rules applied in declaration order rules: - - # Rules must have a name - - name: "check-pod-controller-labels" - + # Rules must have a unique name + - name: "check-pod-controller-labels" # Each rule matches specific resource described by "resource" field. resource: - kind: Deployment, StatefulSet, DaemonSet - # Name is optional. By default validation policy is applicable to any resource of supported kinds. - # Name supports wildcards * and ? + kinds: + - Deployment + - StatefulSet + - DaemonSet + # A resource name is optional. Name supports wildcards * and ? name: "*" - # Selector is optional and can be used to match specific resources - # Selector values support wildcards * and ? + # A resoucre selector is optional. Selector values support wildcards * and ? selector: - # A selector can use match matchLabels: app: mongodb matchExpressions: - {key: tier, operator: In, values: [database]} - # Each rule can contain a single validate, mutate, or generate directive ... ```` From ec41030765b6c18a08a618cf06b047a588d3bafa Mon Sep 17 00:00:00 2001 From: shuting Date: Wed, 12 Jun 2019 16:33:12 -0700 Subject: [PATCH 14/37] update doc for CLI --- documentation/testing-policies.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/documentation/testing-policies.md b/documentation/testing-policies.md index b935f05279..a877430846 100644 --- a/documentation/testing-policies.md +++ b/documentation/testing-policies.md @@ -49,14 +49,22 @@ go get -u https://github.com/nirmata/kyverno/cmd/kyverno ### Using the CLI +The CLI loads default kubeconfig ($HOME/.kube/config) to test policies in Kubernetes cluster. If no kubeconfig is found, the CLI will test policies on raw resources. + To test a policy using the CLI type: -`kyverno ` +`kyverno apply @ @` For example: ```bash -kyverno ../../examples/cli/policy-deployment.yaml ../../examples/cli/resources +kyverno apply @../../examples/cli/policy-deployment.yaml @../../examples/cli/resources ``` -In future releases, the CLI will support complete validation of policies and will allow testing policies against resources in Kubernetes clusters. +To test a policy with the specific kubeconfig: + +```bash +kyverno apply @../../examples/cli/policy-deployment.yaml @../../examples/cli/resources --kubeconfig $PATH_TO_KUBECONFIG_FILE +``` + +In future releases, the CLI will support complete validation and generation of policies. From 66fd48a463d0f9770194d09c2612768ad8aed8cb Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 12 Jun 2019 16:47:22 -0700 Subject: [PATCH 15/37] fix name and text --- documentation/writing-policies-generate.md | 8 ++++---- documentation/writing-policies-mutate.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/documentation/writing-policies-generate.md b/documentation/writing-policies-generate.md index cc19c9c695..883d1e1b92 100644 --- a/documentation/writing-policies-generate.md +++ b/documentation/writing-policies-generate.md @@ -46,8 +46,8 @@ spec: ```` In this example, when this policy is applied, any new namespace that satisfies the label selector will receive 2 new resources after its creation: -* ConfigMap copied from default/config-template. -* Secret with values DB_USER and DB_PASSWORD, and label ```purpose: mongo```. + * ConfigMap copied from default/config-template. + * Secret with values DB_USER and DB_PASSWORD, and label ```purpose: mongo```. ## Example 2 @@ -73,11 +73,11 @@ spec: matchExpressions: [] policyTypes: [] metadata: - annotations: {} labels: policyname: "default" ```` -In this example, when this policy is applied, any new namespace will receive a new NetworkPolicy resource based on the specified template that by default denies all inbound and outbound traffic. + +In this example, when the policy is applied, any new namespace will receive a nNtworkPolicy based on the specified template that by default denies all inbound and outbound traffic. --- *Read Next >> [Testing Policies](/documentation/testing-policies.md)* diff --git a/documentation/writing-policies-mutate.md b/documentation/writing-policies-mutate.md index c1f1c89174..924da8b0b8 100644 --- a/documentation/writing-policies-mutate.md +++ b/documentation/writing-policies-mutate.md @@ -97,7 +97,7 @@ Applying overlays to a list type without is fairly straightforward: new items wi apiVersion: policy.nirmata.io/v1alpha1 kind: Policy metadata: - name: policy-endpoints- + name: policy-endpoints spec: rules: - resource: From 044ca7a408d2e801850b6f1a98e1c6f1a9983b94 Mon Sep 17 00:00:00 2001 From: shuting Date: Wed, 12 Jun 2019 18:23:16 -0700 Subject: [PATCH 16/37] - addd cleanup script - remove imagepullpolicy in install.yaml --- definitions/install.yaml | 1 - scripts/cleanup.sh | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100755 scripts/cleanup.sh diff --git a/definitions/install.yaml b/definitions/install.yaml index 743a16f1eb..48ebe3c706 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -176,7 +176,6 @@ spec: containers: - name: kyverno image: nirmata/kyverno:latest - imagePullPolicy: IfNotPresent ports: - containerPort: 443 securityContext: diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh new file mode 100755 index 0000000000..6ea055fd8e --- /dev/null +++ b/scripts/cleanup.sh @@ -0,0 +1,2 @@ +kubectl delete -f definitions/install.yaml +kubectl delete csr,MutatingWebhookConfiguration,ValidatingWebhookConfiguration --all \ No newline at end of file From e3033b5684f7e7b767d18085ecd8a2ac6076784e Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Wed, 12 Jun 2019 21:06:15 -0700 Subject: [PATCH 17/37] fix label example --- documentation/writing-policies-validate.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/documentation/writing-policies-validate.md b/documentation/writing-policies-validate.md index 36e0d9eaae..d15161e62a 100644 --- a/documentation/writing-policies-validate.md +++ b/documentation/writing-policies-validate.md @@ -43,24 +43,26 @@ metadata : name : validation-example spec : rules: - - resource: + - name: check-label + resource: # Kind specifies one or more resource types to match kinds: - Deployment - StatefuleSet - DaemonSet # Name is optional and can use wildcards - name: * + name: "*" # Selector is optional selector: validate: # Message is optional - message: "The label app is required" + message: "The label app is required" pattern: spec: - selector: - matchLabels: - app: "?*" + template: + metadata: + labels: + app: "?*" ```` From 9bacfe43639822bd9342385d7cb66998bcce637e Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Thu, 13 Jun 2019 17:20:00 +0300 Subject: [PATCH 18/37] Implemented at least one exists logic --- pkg/engine/anchor.go | 160 ++++++++++++++++++++++++++++++++++ pkg/engine/overlay.go | 2 +- pkg/engine/utils.go | 31 ++++++- pkg/engine/utils_test.go | 63 +++++++++++++ pkg/engine/validation.go | 23 +---- pkg/engine/validation_test.go | 35 -------- 6 files changed, 256 insertions(+), 58 deletions(-) create mode 100644 pkg/engine/anchor.go diff --git a/pkg/engine/anchor.go b/pkg/engine/anchor.go new file mode 100644 index 0000000000..888ba441fe --- /dev/null +++ b/pkg/engine/anchor.go @@ -0,0 +1,160 @@ +package engine + +import ( + "strconv" + + "github.com/nirmata/kyverno/pkg/result" +) + +// CreateAnchorHandler is a factory that create anchor handlers +func CreateAnchorHandler(anchor string, pattern interface{}, path string) ValidationAnchorHandler { + switch { + case isConditionAnchor(anchor): + return NewConditionAnchorValidationHandler(anchor, pattern, path) + case isExistanceAnchor(anchor): + return NewExistanceAnchorValidationHandler(anchor, pattern, path) + default: + return NewNoAnchorValidationHandler(path) + } +} + +// ValidationAnchorHandler is an interface that represents +// a family of anchor handlers for array of maps +// resourcePart must be an array of dictionaries +// patternPart must be a dictionary with anchors +type ValidationAnchorHandler interface { + Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult +} + +// NoAnchorValidationHandler just calls validateMap +// because no anchors were found in the pattern map +type NoAnchorValidationHandler struct { + path string +} + +// NewNoAnchorValidationHandler creates new instance of +// NoAnchorValidationHandler +func NewNoAnchorValidationHandler(path string) ValidationAnchorHandler { + return &NoAnchorValidationHandler{ + path: path, + } +} + +// Handle performs validation in context of NoAnchorValidationHandler +func (navh *NoAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult { + handlingResult := result.NewRuleApplicationResult("") + + for i, resourceElement := range resourcePart { + currentPath := navh.path + strconv.Itoa(i) + "/" + + typedResourceElement, ok := resourceElement.(map[string]interface{}) + if !ok { + handlingResult.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternPart, resourceElement) + return handlingResult + } + + res := validateMap(typedResourceElement, patternPart, currentPath) + handlingResult.MergeWith(&res) + } + + return handlingResult +} + +// ConditionAnchorValidationHandler performs +// validation only for array elements that +// pass condition in the anchor +// (key): value +type ConditionAnchorValidationHandler struct { + anchor string + pattern interface{} + path string +} + +// NewConditionAnchorValidationHandler creates new instance of +// NoAnchorValidationHandler +func NewConditionAnchorValidationHandler(anchor string, pattern interface{}, path string) ValidationAnchorHandler { + return &ConditionAnchorValidationHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle performs validation in context of ConditionAnchorValidationHandler +func (cavh *ConditionAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult { + _, handlingResult := handleConditionCases(resourcePart, patternPart, cavh.anchor, cavh.pattern, cavh.path) + + return handlingResult +} + +// ExistanceAnchorValidationHandler performs +// validation only for array elements that +// pass condition in the anchor +// AND requires an existance of at least one +// element that passes this condition +// ^(key): value +type ExistanceAnchorValidationHandler struct { + anchor string + pattern interface{} + path string +} + +// NewExistanceAnchorValidationHandler creates new instance of +// NoAnchorValidationHandler +func NewExistanceAnchorValidationHandler(anchor string, pattern interface{}, path string) ValidationAnchorHandler { + return &ExistanceAnchorValidationHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle performs validation in context of ExistanceAnchorValidationHandler +func (eavh *ExistanceAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult { + anchoredEtries, handlingResult := handleConditionCases(resourcePart, patternPart, eavh.anchor, eavh.pattern, eavh.path) + + if 0 == anchoredEtries { + handlingResult.FailWithMessagef("Existance anchor %s used, but no suitable entries were found", eavh.anchor) + } + + return handlingResult +} + +// check if array element fits the anchor +func checkForAnchorCondition(anchor string, pattern interface{}, resourceMap map[string]interface{}) bool { + anchorKey := removeAnchor(anchor) + + if value, ok := resourceMap[anchorKey]; ok { + return ValidateValueWithPattern(value, pattern) + } + + return false +} + +// both () and ^() are checking conditions and have a lot of similar logic +// the only difference is that ^() requires existace of one element +// anchoredEtries var counts this occurences. +func handleConditionCases(resourcePart []interface{}, patternPart map[string]interface{}, anchor string, pattern interface{}, path string) (int, result.RuleApplicationResult) { + handlingResult := result.NewRuleApplicationResult("") + anchoredEtries := 0 + + for i, resourceElement := range resourcePart { + currentPath := path + strconv.Itoa(i) + "/" + + typedResourceElement, ok := resourceElement.(map[string]interface{}) + if !ok { + handlingResult.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternPart, resourceElement) + break + } + + if !checkForAnchorCondition(anchor, pattern, typedResourceElement) { + continue + } + + anchoredEtries++ + res := validateMap(typedResourceElement, patternPart, currentPath) + handlingResult.MergeWith(&res) + } + + return anchoredEtries, handlingResult +} diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index 157d7a5f46..f9ece15207 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -109,7 +109,7 @@ func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path stri for key, value := range overlayMap { // skip anchor element because it has condition, not // the value that must replace resource value - if wrappedWithParentheses(key) { + if isConditionAnchor(key) { continue } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index ceae2aff37..51b4d6ddd0 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -113,7 +113,7 @@ func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} result := make(map[string]interface{}) for key, value := range anchorsMap { - if wrappedWithParentheses(key) { + if isConditionAnchor(key) || isExistanceAnchor(key) { result[key] = value } } @@ -121,6 +121,16 @@ func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} return result } +func getAnchorFromMap(anchorsMap map[string]interface{}) (string, interface{}) { + for key, value := range anchorsMap { + if isConditionAnchor(key) || isExistanceAnchor(key) { + return key, value + } + } + + return "", nil +} + func findKind(kinds []string, kindGVK string) bool { for _, kind := range kinds { if kind == kindGVK { @@ -130,7 +140,7 @@ func findKind(kinds []string, kindGVK string) bool { return false } -func wrappedWithParentheses(str string) bool { +func isConditionAnchor(str string) bool { if len(str) < 2 { return false } @@ -138,6 +148,17 @@ func wrappedWithParentheses(str string) bool { return (str[0] == '(' && str[len(str)-1] == ')') } +func isExistanceAnchor(str string) bool { + left := "^(" + right := ")" + + if len(str) < len(left)+len(right) { + return false + } + + return (str[:len(left)] == left && str[len(str)-len(right):] == right) +} + // Checks if array object matches anchors. If not - skip - return true func skipArrayObject(object, anchors map[string]interface{}) bool { for key, pattern := range anchors { @@ -158,10 +179,14 @@ func skipArrayObject(object, anchors map[string]interface{}) bool { // removeAnchor remove special characters around anchored key func removeAnchor(key string) string { - if wrappedWithParentheses(key) { + if isConditionAnchor(key) { return key[1 : len(key)-1] } + if isExistanceAnchor(key) { + return key[2 : len(key)-1] + } + // TODO: Add logic for other anchors here return key diff --git a/pkg/engine/utils_test.go b/pkg/engine/utils_test.go index bf0bce425e..b5b638bee8 100644 --- a/pkg/engine/utils_test.go +++ b/pkg/engine/utils_test.go @@ -331,3 +331,66 @@ func TestResourceMeetsDescription_MatchLabelsAndMatchExpressions(t *testing.T) { assert.Assert(t, false == ResourceMeetsDescription(rawResource, resourceDescription, groupVersionKind)) } + +func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { + str := "(something)" + assert.Assert(t, isConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) { + str := "()" + assert.Assert(t, isConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) { + str := "something" + assert.Assert(t, !isConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) { + str := "(something" + assert.Assert(t, !isConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) { + str := "something)" + assert.Assert(t, !isConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) { + str := "so)m(et(hin)g" + assert.Assert(t, !isConditionAnchor(str)) +} + +func TestWrappedWithParentheses_Empty(t *testing.T) { + str := "" + assert.Assert(t, !isConditionAnchor(str)) +} + +func TestIsExistanceAnchor_Yes(t *testing.T) { + assert.Assert(t, isExistanceAnchor("^(abc)")) +} + +func TestIsExistanceAnchor_NoRightBracket(t *testing.T) { + assert.Assert(t, !isExistanceAnchor("^(abc")) +} + +func TestIsExistanceAnchor_OnlyHat(t *testing.T) { + assert.Assert(t, !isExistanceAnchor("^abc")) +} + +func TestIsExistanceAnchor_ConditionAnchor(t *testing.T) { + assert.Assert(t, !isExistanceAnchor("(abc)")) +} + +func TestRemoveAnchor_ConditionAnchor(t *testing.T) { + assert.Equal(t, removeAnchor("(abc)"), "abc") +} + +func TestRemoveAnchor_ExistanceAnchor(t *testing.T) { + assert.Equal(t, removeAnchor("^(abc)"), "abc") +} + +func TestRemoveAnchor_EmptyExistanceAnchor(t *testing.T) { + assert.Equal(t, removeAnchor("^()"), "") +} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index d76c12e418..f273c71984 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -143,24 +143,9 @@ func validateArray(resourceArray, patternArray []interface{}, path string) resul // validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic // and then validates each map due to the pattern func validateArrayOfMaps(resourceMapArray []interface{}, patternMap map[string]interface{}, path string) result.RuleApplicationResult { - res := result.NewRuleApplicationResult("") - anchors := getAnchorsFromMap(patternMap) + anchor, pattern := getAnchorFromMap(patternMap) + delete(patternMap, anchor) - for i, resourceElement := range resourceMapArray { - currentPath := path + strconv.Itoa(i) + "/" - typedResourceElement, ok := resourceElement.(map[string]interface{}) - if !ok { - res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternMap, resourceElement) - return res - } - - if skipArrayObject(typedResourceElement, anchors) { - continue - } - - mapValidationResult := validateMap(typedResourceElement, patternMap, currentPath) - res.MergeWith(&mapValidationResult) - } - - return res + handler := CreateAnchorHandler(anchor, pattern, path) + return handler.Handle(resourceMapArray, patternMap) } diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index b3f6e61bd3..127e736565 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -9,41 +9,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { - str := "(something)" - assert.Assert(t, wrappedWithParentheses(str)) -} - -func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) { - str := "()" - assert.Assert(t, wrappedWithParentheses(str)) -} - -func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) { - str := "something" - assert.Assert(t, !wrappedWithParentheses(str)) -} - -func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) { - str := "(something" - assert.Assert(t, !wrappedWithParentheses(str)) -} - -func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) { - str := "something)" - assert.Assert(t, !wrappedWithParentheses(str)) -} - -func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) { - str := "so)m(et(hin)g" - assert.Assert(t, !wrappedWithParentheses(str)) -} - -func TestWrappedWithParentheses_Empty(t *testing.T) { - str := "" - assert.Assert(t, !wrappedWithParentheses(str)) -} - func TestValidateString_AsteriskTest(t *testing.T) { pattern := "*" value := "anything" From ed7dfb63d60517f3910663aa7345d565e9ac4856 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Thu, 13 Jun 2019 17:33:58 +0300 Subject: [PATCH 19/37] Fixed issue with nested anchors --- pkg/engine/validation.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index f273c71984..db01189ea4 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -144,7 +144,6 @@ func validateArray(resourceArray, patternArray []interface{}, path string) resul // and then validates each map due to the pattern func validateArrayOfMaps(resourceMapArray []interface{}, patternMap map[string]interface{}, path string) result.RuleApplicationResult { anchor, pattern := getAnchorFromMap(patternMap) - delete(patternMap, anchor) handler := CreateAnchorHandler(anchor, pattern, path) return handler.Handle(resourceMapArray, patternMap) From 1a08ab4bdad01ac3686ab0f3133e6784a3be24c7 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Fri, 14 Jun 2019 17:19:32 +0300 Subject: [PATCH 20/37] Implemented logic "Add if not exist" --- pkg/engine/overlay.go | 11 ++++++---- pkg/engine/overlay_test.go | 43 ++++++++++++++++++++++++++++++++++++++ pkg/engine/utils.go | 15 ++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index 157d7a5f46..866c231702 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -113,10 +113,11 @@ func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path stri continue } - currentPath := path + key + "/" - resourcePart, ok := resourceMap[key] + noAnchorKey := removeAnchor(key) + currentPath := path + noAnchorKey + "/" + resourcePart, ok := resourceMap[noAnchorKey] - if ok { + if ok && !isAddingAnchor(key) { // Key exists - go down through the overlay and resource trees patches, res := applyOverlay(resourcePart, value, currentPath) overlayResult.MergeWith(&res) @@ -124,7 +125,9 @@ func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path stri if result.Success == overlayResult.GetReason() { appliedPatches = append(appliedPatches, patches...) } - } else { + } + + if !ok { // Key does not exist - insert entire overlay subtree patch, res := insertSubtree(value, currentPath) overlayResult.MergeWith(&res) diff --git a/pkg/engine/overlay_test.go b/pkg/engine/overlay_test.go index 449c5641b7..c05ee7be44 100644 --- a/pkg/engine/overlay_test.go +++ b/pkg/engine/overlay_test.go @@ -430,3 +430,46 @@ func TestApplyOverlay_ImagePullPolicy(t *testing.T) { compareJsonAsMap(t, expectedResult, doc) } + +func TestApplyOverlay_AddingAnchor(t *testing.T) { + overlayRaw := []byte(`{ + "metadata": { + "name": "nginx-deployment", + "labels": { + "+(app)": "should-not-be-here", + "+(key1)": "value1" + } + } + }`) + resourceRaw := []byte(`{ + "metadata": { + "name": "nginx-deployment", + "labels": { + "app": "nginx" + } + } + }`) + + var resource, overlay interface{} + + json.Unmarshal(resourceRaw, &resource) + json.Unmarshal(overlayRaw, &overlay) + + patches, res := applyOverlay(resource, overlay, "/") + assert.NilError(t, res.ToError()) + assert.Assert(t, len(patches) != 0) + + doc, err := ApplyPatches(resourceRaw, patches) + assert.NilError(t, err) + expectedResult := []byte(`{ + "metadata":{ + "labels":{ + "app":"nginx", + "key1":"value1" + }, + "name":"nginx-deployment" + } + }`) + + compareJsonAsMap(t, expectedResult, doc) +} diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index ceae2aff37..732faad7f8 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -138,6 +138,17 @@ func wrappedWithParentheses(str string) bool { return (str[0] == '(' && str[len(str)-1] == ')') } +func isAddingAnchor(key string) bool { + const left = "+(" + const right = ")" + + if len(key) < len(left)+len(right) { + return false + } + + return left == key[:len(left)] && right == key[len(key)-len(right):] +} + // Checks if array object matches anchors. If not - skip - return true func skipArrayObject(object, anchors map[string]interface{}) bool { for key, pattern := range anchors { @@ -162,7 +173,9 @@ func removeAnchor(key string) string { return key[1 : len(key)-1] } - // TODO: Add logic for other anchors here + if isAddingAnchor(key) { + return key[2 : len(key)-1] + } return key } From 45d44cef88d557e193ad1d58d1ec77451d077c88 Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Fri, 14 Jun 2019 16:02:28 -0700 Subject: [PATCH 21/37] re-structure unit tests --- pkg/dclient/client_test.go | 136 ++++++++++++++++++------------------- pkg/dclient/utils.go | 20 ++++++ 2 files changed, 88 insertions(+), 68 deletions(-) diff --git a/pkg/dclient/client_test.go b/pkg/dclient/client_test.go index 984c316203..b3a7c7df6f 100644 --- a/pkg/dclient/client_test.go +++ b/pkg/dclient/client_test.go @@ -1,13 +1,11 @@ package client import ( - "fmt" "testing" policytypes "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) @@ -21,33 +19,19 @@ import ( // - kubernetes client // - objects to initialize the client -func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": apiVersion, - "kind": kind, - "metadata": map[string]interface{}{ - "namespace": namespace, - "name": name, - }, - }, - } +type fixture struct { + t *testing.T + regresources map[string]string + objects []runtime.Object + client *Client } -func newUnstructuredWithSpec(apiVersion, kind, namespace, name string, spec map[string]interface{}) *unstructured.Unstructured { - u := newUnstructured(apiVersion, kind, namespace, name) - u.Object["spec"] = spec - return u -} - -func TestClient(t *testing.T) { - scheme := runtime.NewScheme() - // init groupversion +func newFixture(t *testing.T) *fixture { regresource := map[string]string{"group/version": "thekinds", "group2/version": "thekinds", "v1": "namespaces", "apps/v1": "deployments"} - // init resources + objects := []runtime.Object{newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"), newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), @@ -55,8 +39,8 @@ func TestClient(t *testing.T) { newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"), newUnstructured("apps/v1", "Deployment", "kyverno", "kyverno-deployment"), } - - // Mock Client + scheme := runtime.NewScheme() + // Create mock client client, err := NewMockClient(scheme, objects...) if err != nil { t.Fatal(err) @@ -64,77 +48,92 @@ func TestClient(t *testing.T) { // set discovery Client client.SetDiscovery(NewFakeDiscoveryClient(regresource)) + + f := fixture{ + t: t, + regresources: regresource, + objects: objects, + client: client, + } + return &f + +} + +func TestCRUDResource(t *testing.T) { + f := newFixture(t) // Get Resource - res, err := client.GetResource("thekinds", "ns-foo", "name-foo") + _, err := f.client.GetResource("thekinds", "ns-foo", "name-foo") if err != nil { - t.Fatal(err) + t.Errorf("GetResource not working: %s", err) } - fmt.Println(res) // List Resources - list, err := client.ListResource("thekinds", "ns-foo") + _, err = f.client.ListResource("thekinds", "ns-foo") if err != nil { - t.Fatal(err) + t.Errorf("ListResource not working: %s", err) } - fmt.Println(len(list.Items)) // DeleteResouce - err = client.DeleteResouce("thekinds", "ns-foo", "name-bar") + err = f.client.DeleteResouce("thekinds", "ns-foo", "name-bar") if err != nil { - t.Fatal(err) + t.Errorf("DeleteResouce not working: %s", err) } // CreateResource - res, err = client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1")) + _, err = f.client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1")) if err != nil { - t.Fatal(err) + t.Errorf("CreateResource not working: %s", err) } // UpdateResource - res, err = client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"})) + _, err = f.client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"})) if err != nil { - t.Fatal(err) + t.Errorf("UpdateResource not working: %s", err) } - // UpdateStatusResource - res, err = client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"})) + _, err = f.client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"})) if err != nil { - t.Fatal(err) + t.Errorf("UpdateStatusResource not working: %s", err) } +} - iEvent, err := client.GetEventsInterface() +func TestEventInterface(t *testing.T) { + f := newFixture(t) + iEvent, err := f.client.GetEventsInterface() if err != nil { - t.Fatal(err) + t.Errorf("GetEventsInterface not working: %s", err) } - eventList, err := iEvent.List(meta.ListOptions{}) + _, err = iEvent.List(meta.ListOptions{}) if err != nil { - t.Fatal(err) + t.Errorf("Testing Event interface not working: %s", err) } - fmt.Println(eventList.Items) - - iCSR, err := client.GetCSRInterface() - if err != nil { - t.Fatal(err) - } - csrList, err := iCSR.List(meta.ListOptions{}) - if err != nil { - t.Fatal(err) - } - fmt.Println(csrList.Items) +} +func TestCSRInterface(t *testing.T) { + f := newFixture(t) + iCSR, err := f.client.GetCSRInterface() + if err != nil { + t.Errorf("GetCSRInterface not working: %s", err) + } + _, err = iCSR.List(meta.ListOptions{}) + if err != nil { + t.Errorf("Testing CSR interface not working: %s", err) + } +} +func TestGenerateResource(t *testing.T) { + f := newFixture(t) //GenerateResource -> copy From // 1 create namespace // 2 generate resource - // create namespace - ns, err := client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1")) + ns, err := f.client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1")) if err != nil { t.Fatal(err) } gen := policytypes.Generation{Kind: "TheKind", Name: "gen-kind", Clone: &policytypes.CloneFrom{Namespace: "ns-foo", Name: "name-foo"}} - err = client.GenerateResource(gen, ns.GetName()) + err = f.client.GenerateResource(gen, ns.GetName()) if err != nil { t.Fatal(err) } - res, err = client.GetResource("thekinds", "ns1", "gen-kind") + _, err = f.client.GetResource("thekinds", "ns1", "gen-kind") if err != nil { t.Fatal(err) } @@ -142,19 +141,20 @@ func TestClient(t *testing.T) { gen = policytypes.Generation{Kind: "TheKind", Name: "name2-baz-new", Data: newUnstructured("group2/version", "TheKind", "ns1", "name2-baz-new")} - err = client.GenerateResource(gen, ns.GetName()) + err = f.client.GenerateResource(gen, ns.GetName()) if err != nil { t.Fatal(err) } - res, err = client.GetResource("thekinds", "ns1", "name2-baz-new") + _, err = f.client.GetResource("thekinds", "ns1", "name2-baz-new") + if err != nil { + t.Fatal(err) + } +} + +func TestKubePolicyDeployment(t *testing.T) { + f := newFixture(t) + _, err := f.client.GetKubePolicyDeployment() if err != nil { t.Fatal(err) } - - // Get Kube Policy Deployment - deploy, err := client.GetKubePolicyDeployment() - if err != nil { - t.Fatal(err) - } - fmt.Println(deploy.GetName()) } diff --git a/pkg/dclient/utils.go b/pkg/dclient/utils.go index 080f2739c6..1650f20081 100644 --- a/pkg/dclient/utils.go +++ b/pkg/dclient/utils.go @@ -4,6 +4,7 @@ import ( "strings" "time" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic/fake" @@ -65,3 +66,22 @@ func (c *fakeDiscoveryClient) getGVRFromKind(kind string) schema.GroupVersionRes resource := strings.ToLower(kind) + "s" return c.getGVR(resource) } + +func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "namespace": namespace, + "name": name, + }, + }, + } +} + +func newUnstructuredWithSpec(apiVersion, kind, namespace, name string, spec map[string]interface{}) *unstructured.Unstructured { + u := newUnstructured(apiVersion, kind, namespace, name) + u.Object["spec"] = spec + return u +} From b3bf9b02d27938ecf37ad6e8c89c469d387bc2ff Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Fri, 14 Jun 2019 16:15:23 -0700 Subject: [PATCH 22/37] skip test due to random fails --- pkg/dclient/client_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/dclient/client_test.go b/pkg/dclient/client_test.go index b3a7c7df6f..5c7cd6577e 100644 --- a/pkg/dclient/client_test.go +++ b/pkg/dclient/client_test.go @@ -60,6 +60,7 @@ func newFixture(t *testing.T) *fixture { } func TestCRUDResource(t *testing.T) { + t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") f := newFixture(t) // Get Resource _, err := f.client.GetResource("thekinds", "ns-foo", "name-foo") @@ -117,6 +118,8 @@ func TestCSRInterface(t *testing.T) { } func TestGenerateResource(t *testing.T) { + t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") + f := newFixture(t) //GenerateResource -> copy From // 1 create namespace @@ -124,18 +127,18 @@ func TestGenerateResource(t *testing.T) { // create namespace ns, err := f.client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1")) if err != nil { - t.Fatal(err) + t.Errorf("CreateResource not working: %s", err) } gen := policytypes.Generation{Kind: "TheKind", Name: "gen-kind", Clone: &policytypes.CloneFrom{Namespace: "ns-foo", Name: "name-foo"}} err = f.client.GenerateResource(gen, ns.GetName()) if err != nil { - t.Fatal(err) + t.Errorf("GenerateResource not working: %s", err) } _, err = f.client.GetResource("thekinds", "ns1", "gen-kind") if err != nil { - t.Fatal(err) + t.Errorf("GetResource not working: %s", err) } // GenerateResource -> data gen = policytypes.Generation{Kind: "TheKind", @@ -143,11 +146,11 @@ func TestGenerateResource(t *testing.T) { Data: newUnstructured("group2/version", "TheKind", "ns1", "name2-baz-new")} err = f.client.GenerateResource(gen, ns.GetName()) if err != nil { - t.Fatal(err) + t.Errorf("GenerateResource not working: %s", err) } _, err = f.client.GetResource("thekinds", "ns1", "name2-baz-new") if err != nil { - t.Fatal(err) + t.Errorf("GetResource not working: %s", err) } } From fb280f2bfb754abb971de4c98b534956ae3b0365 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Sun, 16 Jun 2019 21:09:37 -0700 Subject: [PATCH 23/37] update static docs page (fix typo) --- gh-pages/config.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gh-pages/config.toml b/gh-pages/config.toml index 0f74deb7f5..e9b9c06d0f 100644 --- a/gh-pages/config.toml +++ b/gh-pages/config.toml @@ -8,10 +8,9 @@ relativeURLs=true [params] description = "Kubernetes Native Policy Management" long_description = ''' - Manage policies as Kuberneres resources using YAML or JSON. Easily validate, - mutate, or generate Kubernetes resources. Match resources based on label selectors - and wildcards. View policy results as events, and policy violations as events or - in policy status.''' + Manage policies as Kubernetes resources to validate, mutate, or generate any configuration. + Select resources based on labels and wildcards. View policy enforcement as events. Detect + policy violations for existing resources.''' author_name = "Nirmata" author_url = "https://nirmata.com" project_url = "https://github.com/nirmata/kyverno/" From 0a90f26f5119cdeafa070e029de87c4938be45b7 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Sun, 16 Jun 2019 21:34:57 -0700 Subject: [PATCH 24/37] update static page text --- gh-pages/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gh-pages/config.toml b/gh-pages/config.toml index e9b9c06d0f..e1419187e4 100644 --- a/gh-pages/config.toml +++ b/gh-pages/config.toml @@ -8,7 +8,7 @@ relativeURLs=true [params] description = "Kubernetes Native Policy Management" long_description = ''' - Manage policies as Kubernetes resources to validate, mutate, or generate any configuration. + Manage policies as Kubernetes resources. Validate, mutate, and generate configurations. Select resources based on labels and wildcards. View policy enforcement as events. Detect policy violations for existing resources.''' author_name = "Nirmata" From 640ffd7258edb7ad765117b4dfd8260a4bd24afb Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Mon, 17 Jun 2019 10:31:51 -0700 Subject: [PATCH 25/37] re factor --- pkg/dclient/client_test.go | 33 +++++++++++++++--------------- pkg/dclient/utils.go | 42 ++++++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/pkg/dclient/client_test.go b/pkg/dclient/client_test.go index 5c7cd6577e..62b798382e 100644 --- a/pkg/dclient/client_test.go +++ b/pkg/dclient/client_test.go @@ -7,6 +7,7 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ) // GetResource @@ -20,18 +21,19 @@ import ( // - objects to initialize the client type fixture struct { - t *testing.T - regresources map[string]string - objects []runtime.Object - client *Client + t *testing.T + objects []runtime.Object + client *Client } func newFixture(t *testing.T) *fixture { - regresource := map[string]string{"group/version": "thekinds", - "group2/version": "thekinds", - "v1": "namespaces", - "apps/v1": "deployments"} - + // init groupversion + regResource := []schema.GroupVersionResource{ + schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}, + schema.GroupVersionResource{Group: "group2", Version: "version", Resource: "thekinds"}, + schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}, + schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, + } objects := []runtime.Object{newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"), newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), @@ -47,20 +49,19 @@ func newFixture(t *testing.T) *fixture { } // set discovery Client - client.SetDiscovery(NewFakeDiscoveryClient(regresource)) + client.SetDiscovery(NewFakeDiscoveryClient(regResource)) f := fixture{ - t: t, - regresources: regresource, - objects: objects, - client: client, + t: t, + objects: objects, + client: client, } return &f } func TestCRUDResource(t *testing.T) { - t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") + // t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") f := newFixture(t) // Get Resource _, err := f.client.GetResource("thekinds", "ns-foo", "name-foo") @@ -118,7 +119,7 @@ func TestCSRInterface(t *testing.T) { } func TestGenerateResource(t *testing.T) { - t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") + // t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") f := newFixture(t) //GenerateResource -> copy From diff --git a/pkg/dclient/utils.go b/pkg/dclient/utils.go index 1650f20081..c87030a563 100644 --- a/pkg/dclient/utils.go +++ b/pkg/dclient/utils.go @@ -37,16 +37,21 @@ func NewMockClient(scheme *runtime.Scheme, objects ...runtime.Object) (*Client, } // NewFakeDiscoveryClient returns a fakediscovery client -func NewFakeDiscoveryClient(regResources map[string]string) *fakeDiscoveryClient { - registeredResources := make([]schema.GroupVersionResource, len(regResources)) - for groupVersion, resource := range regResources { - gv, err := schema.ParseGroupVersion(groupVersion) - if err != nil { - continue - } - registeredResources = append(registeredResources, gv.WithResource(resource)) +func NewFakeDiscoveryClient(registeredResouces []schema.GroupVersionResource) *fakeDiscoveryClient { + // Load some-preregistd resources + res := []schema.GroupVersionResource{ + schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, + schema.GroupVersionResource{Version: "v1", Resource: "endpoints"}, + schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}, + schema.GroupVersionResource{Version: "v1", Resource: "resourcequotas"}, + schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, + schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, + schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}, + schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, + schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}, } - return &fakeDiscoveryClient{registeredResouces: registeredResources} + registeredResouces = append(registeredResouces, res...) + return &fakeDiscoveryClient{registeredResouces: registeredResouces} } type fakeDiscoveryClient struct { @@ -85,3 +90,22 @@ func newUnstructuredWithSpec(apiVersion, kind, namespace, name string, spec map[ u.Object["spec"] = spec return u } + +func retry(attempts int, sleep time.Duration, fn func() error) error { + if err := fn(); err != nil { + if s, ok := err.(stop); ok { + return s.error + } + if attempts--; attempts > 0 { + time.Sleep(sleep) + return retry(attempts, 2*sleep, fn) + } + return err + } + return nil +} + +// Custom error +type stop struct { + error +} From 860d2f8df18b5a8ff2e81af3817cdd65af95c3cf Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Mon, 17 Jun 2019 18:35:40 -0700 Subject: [PATCH 26/37] enable test --- pkg/dclient/client_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/dclient/client_test.go b/pkg/dclient/client_test.go index 62b798382e..a7526c2c51 100644 --- a/pkg/dclient/client_test.go +++ b/pkg/dclient/client_test.go @@ -61,7 +61,6 @@ func newFixture(t *testing.T) *fixture { } func TestCRUDResource(t *testing.T) { - // t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") f := newFixture(t) // Get Resource _, err := f.client.GetResource("thekinds", "ns-foo", "name-foo") @@ -119,8 +118,6 @@ func TestCSRInterface(t *testing.T) { } func TestGenerateResource(t *testing.T) { - // t.Skip("Under Development. Reason: delay in generation of resources, so test fails at times") - f := newFixture(t) //GenerateResource -> copy From // 1 create namespace From ba43509b4390ca5317f2750b59bcdb2c14cf26f5 Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Mon, 17 Jun 2019 18:42:23 -0700 Subject: [PATCH 27/37] fix format --- pkg/controller/controller_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 8a300f1683..95223e0ffb 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -10,6 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/sample-controller/pkg/signals" ) @@ -78,7 +79,10 @@ func (f *fixture) setupFixture() { if err != nil { f.t.Fatal(err) } - regresource := map[string]string{"kyverno.io/v1alpha1": "policys"} + regresource := []schema.GroupVersionResource{ + schema.GroupVersionResource{Group: "kyverno.io", + Version: "v1alpha1", + Resource: "policys"}} fclient.SetDiscovery(client.NewFakeDiscoveryClient(regresource)) } From 6565cf43d05424ba94aa17eab44296896162ea64 Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Mon, 17 Jun 2019 19:03:35 -0700 Subject: [PATCH 28/37] apply policy only if kind matches --- pkg/webhooks/server.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 81fd2b23d9..554bc4778f 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -124,19 +124,21 @@ func (ws *WebhookServer) Stop() { // HandleMutation handles mutating webhook admission request func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - glog.Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) policies, err := ws.policyLister.List(labels.NewSelector()) if err != nil { glog.Warning(err) return nil } + glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) admissionResult := result.NewAdmissionResult(string(request.UID)) var allPatches []engine.PatchBytes for _, policy := range policies { - + if policy.Kind != request.Kind.Kind { + continue + } glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) policyPatches, mutationResult := engine.Mutate(*policy, request.Object.Raw, request.Kind) From 6fd7cba0eabab7548365bb5e61e14343ad910abf Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Mon, 17 Jun 2019 23:41:18 -0700 Subject: [PATCH 29/37] initial prototype commit --- main.go | 23 ++++++++++++++++++++--- pkg/webhooks/server.go | 27 +++++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 9f5ad59302..e1d9562710 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "strings" "github.com/golang/glog" "github.com/nirmata/kyverno/pkg/config" @@ -15,8 +16,9 @@ import ( ) var ( - kubeconfig string - serverIP string + kubeconfig string + serverIP string + filterK8Kinds arrayFlags ) func main() { @@ -51,7 +53,7 @@ func main() { glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err) } - server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory) + server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, filterK8Kinds) if err != nil { glog.Fatalf("Unable to create webhook server: %v\n", err) } @@ -80,9 +82,24 @@ func main() { policyController.Stop() } +type arrayFlags []string + +func (i *arrayFlags) String() string { + var sb strings.Builder + for _, str := range *i { + sb.WriteString(str) + } + return sb.String() +} + +func (i *arrayFlags) Set(value string) error { + *i = append(*i, value) + return nil +} func init() { flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") flag.StringVar(&serverIP, "serverIP", "", "IP address where Kyverno controller runs. Only required if out-of-cluster.") + flag.Var(&filterK8Kinds, "filterKind", "k8 kinds where polcies are not to be applied on") config.LogDefaultFlags() flag.Parse() } diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 554bc4778f..a73dd1fc31 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -29,6 +29,7 @@ type WebhookServer struct { server http.Server client *client.Client policyLister v1alpha1.PolicyLister + filterKinds []string } // NewWebhookServer creates new instance of WebhookServer accordingly to given configuration @@ -36,7 +37,8 @@ type WebhookServer struct { func NewWebhookServer( client *client.Client, tlsPair *tlsutils.TlsPemPair, - shareInformer sharedinformer.PolicyInformer) (*WebhookServer, error) { + shareInformer sharedinformer.PolicyInformer, + filterKinds []string) (*WebhookServer, error) { if tlsPair == nil { return nil, errors.New("NewWebhookServer is not initialized properly") @@ -52,6 +54,7 @@ func NewWebhookServer( ws := &WebhookServer{ client: client, policyLister: shareInformer.GetLister(), + filterKinds: filterKinds, } mux := http.NewServeMux() @@ -79,11 +82,14 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { admissionReview.Response = &v1beta1.AdmissionResponse{ Allowed: true, } - switch r.URL.Path { - case config.MutatingWebhookServicePath: - admissionReview.Response = ws.HandleMutation(admissionReview.Request) - case config.ValidatingWebhookServicePath: - admissionReview.Response = ws.HandleValidation(admissionReview.Request) + if !stringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) { + + switch r.URL.Path { + case config.MutatingWebhookServicePath: + admissionReview.Response = ws.HandleMutation(admissionReview.Request) + case config.ValidatingWebhookServicePath: + admissionReview.Response = ws.HandleValidation(admissionReview.Request) + } } admissionReview.Response.UID = admissionReview.Request.UID @@ -101,6 +107,15 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { } } +func stringInSlice(kind string, list []string) bool { + for _, b := range list { + if b == kind { + return true + } + } + return false +} + // RunAsync TLS server in separate thread and returns control immediately func (ws *WebhookServer) RunAsync() { go func(ws *WebhookServer) { From e467b596e1f67919bd705731748f87da5b253e94 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Tue, 18 Jun 2019 13:07:55 +0300 Subject: [PATCH 30/37] Fixed typo "anchoredEtries" --- pkg/engine/anchor.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/engine/anchor.go b/pkg/engine/anchor.go index 888ba441fe..d10c4e2759 100644 --- a/pkg/engine/anchor.go +++ b/pkg/engine/anchor.go @@ -111,9 +111,9 @@ func NewExistanceAnchorValidationHandler(anchor string, pattern interface{}, pat // Handle performs validation in context of ExistanceAnchorValidationHandler func (eavh *ExistanceAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult { - anchoredEtries, handlingResult := handleConditionCases(resourcePart, patternPart, eavh.anchor, eavh.pattern, eavh.path) + anchoredEntries, handlingResult := handleConditionCases(resourcePart, patternPart, eavh.anchor, eavh.pattern, eavh.path) - if 0 == anchoredEtries { + if 0 == anchoredEntries { handlingResult.FailWithMessagef("Existance anchor %s used, but no suitable entries were found", eavh.anchor) } @@ -133,10 +133,10 @@ func checkForAnchorCondition(anchor string, pattern interface{}, resourceMap map // both () and ^() are checking conditions and have a lot of similar logic // the only difference is that ^() requires existace of one element -// anchoredEtries var counts this occurences. +// anchoredEntries var counts this occurences. func handleConditionCases(resourcePart []interface{}, patternPart map[string]interface{}, anchor string, pattern interface{}, path string) (int, result.RuleApplicationResult) { handlingResult := result.NewRuleApplicationResult("") - anchoredEtries := 0 + anchoredEntries := 0 for i, resourceElement := range resourcePart { currentPath := path + strconv.Itoa(i) + "/" @@ -151,10 +151,10 @@ func handleConditionCases(resourcePart []interface{}, patternPart map[string]int continue } - anchoredEtries++ + anchoredEntries++ res := validateMap(typedResourceElement, patternPart, currentPath) handlingResult.MergeWith(&res) } - return anchoredEtries, handlingResult + return anchoredEntries, handlingResult } From faf1309a553a3440b8513d2f7ea8986eda95d6b3 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Tue, 18 Jun 2019 13:51:36 +0300 Subject: [PATCH 31/37] Added tests for addingKey inside lists --- pkg/engine/overlay_test.go | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/pkg/engine/overlay_test.go b/pkg/engine/overlay_test.go index c05ee7be44..91171c8639 100644 --- a/pkg/engine/overlay_test.go +++ b/pkg/engine/overlay_test.go @@ -473,3 +473,68 @@ func TestApplyOverlay_AddingAnchor(t *testing.T) { compareJsonAsMap(t, expectedResult, doc) } + +func TestApplyOverlay_AddingAnchorInsideListElement(t *testing.T) { + overlayRaw := []byte(` + { + "list" : [ + { + "(entity)": "*able", + "+(event)": "process" + } + ] + }`) + resourceRaw := []byte(` + { + "list" : [ + { + "entity": "movable" + }, + { + "entity": "collisionable", + "event": "skip" + }, + { + "entity": "any", + "event": "delete" + }, + { + "entity": "none" + } + ] + }`) + + var resource, overlay interface{} + + json.Unmarshal(resourceRaw, &resource) + json.Unmarshal(overlayRaw, &overlay) + + patches, res := applyOverlay(resource, overlay, "/") + assert.NilError(t, res.ToError()) + assert.Assert(t, len(patches) != 0) + + doc, err := ApplyPatches(resourceRaw, patches) + assert.NilError(t, err) + expectedResult := []byte(` + { + "list":[ + { + "entity":"movable", + "event":"process" + }, + { + "entity":"collisionable", + "event":"skip" + }, + { + "entity":"any", + "event":"delete" + }, + { + "entity":"none" + } + ] + }`) + + compareJsonAsMap(t, expectedResult, doc) +} From 41a932bc10e3ad969bb9b5bf33f3adc37bc2ce55 Mon Sep 17 00:00:00 2001 From: Denis Belyshev Date: Tue, 18 Jun 2019 15:02:41 +0300 Subject: [PATCH 32/37] Added real test for adding anchor --- pkg/engine/overlay_test.go | 137 ++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 41 deletions(-) diff --git a/pkg/engine/overlay_test.go b/pkg/engine/overlay_test.go index 91171c8639..c0d670054e 100644 --- a/pkg/engine/overlay_test.go +++ b/pkg/engine/overlay_test.go @@ -477,31 +477,62 @@ func TestApplyOverlay_AddingAnchor(t *testing.T) { func TestApplyOverlay_AddingAnchorInsideListElement(t *testing.T) { overlayRaw := []byte(` { - "list" : [ - { - "(entity)": "*able", - "+(event)": "process" + "spec": { + "template": { + "spec": { + "containers": [ + { + "(image)": "*:latest", + "+(imagePullPolicy)": "IfNotPresent" + } + ] + } } - ] + } }`) resourceRaw := []byte(` - { - "list" : [ - { - "entity": "movable" - }, - { - "entity": "collisionable", - "event": "skip" - }, - { - "entity": "any", - "event": "delete" - }, - { - "entity": "none" + { + "apiVersion":"apps/v1", + "kind":"Deployment", + "metadata":{ + "name":"nginx-deployment", + "labels":{ + "app":"nginx" } - ] + }, + "spec":{ + "replicas":1, + "selector":{ + "matchLabels":{ + "app":"nginx" + } + }, + "template":{ + "metadata":{ + "labels":{ + "app":"nginx" + } + }, + "spec":{ + "containers":[ + { + "image":"nginx:latest" + }, + { + "image":"ghost:latest", + "imagePullPolicy":"Always" + }, + { + "image":"debian:10" + }, + { + "image":"ubuntu:18.04", + "imagePullPolicy":"Always" + } + ] + } + } + } }`) var resource, overlay interface{} @@ -516,25 +547,49 @@ func TestApplyOverlay_AddingAnchorInsideListElement(t *testing.T) { doc, err := ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(` - { - "list":[ - { - "entity":"movable", - "event":"process" - }, - { - "entity":"collisionable", - "event":"skip" - }, - { - "entity":"any", - "event":"delete" - }, - { - "entity":"none" - } - ] - }`) - + { + "apiVersion":"apps/v1", + "kind":"Deployment", + "metadata":{ + "name":"nginx-deployment", + "labels":{ + "app":"nginx" + } + }, + "spec":{ + "replicas":1, + "selector":{ + "matchLabels":{ + "app":"nginx" + } + }, + "template":{ + "metadata":{ + "labels":{ + "app":"nginx" + } + }, + "spec":{ + "containers":[ + { + "image":"nginx:latest", + "imagePullPolicy":"IfNotPresent" + }, + { + "image":"ghost:latest", + "imagePullPolicy":"Always" + }, + { + "image":"debian:10" + }, + { + "image":"ubuntu:18.04", + "imagePullPolicy":"Always" + } + ] + } + } + } + }`) compareJsonAsMap(t, expectedResult, doc) } From 50b0da48a10b4cec509da834d7788a7b610613b9 Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Tue, 18 Jun 2019 11:47:45 -0700 Subject: [PATCH 33/37] support comma seperated kinds --- main.go | 20 ++----------------- pkg/webhooks/server.go | 14 ++----------- pkg/webhooks/utils.go | 45 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 pkg/webhooks/utils.go diff --git a/main.go b/main.go index e1d9562710..22ed432fe7 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "flag" - "strings" "github.com/golang/glog" "github.com/nirmata/kyverno/pkg/config" @@ -18,7 +17,7 @@ import ( var ( kubeconfig string serverIP string - filterK8Kinds arrayFlags + filterK8Kinds webhooks.ArrayFlags ) func main() { @@ -52,7 +51,6 @@ func main() { if err != nil { glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err) } - server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, filterK8Kinds) if err != nil { glog.Fatalf("Unable to create webhook server: %v\n", err) @@ -82,24 +80,10 @@ func main() { policyController.Stop() } -type arrayFlags []string - -func (i *arrayFlags) String() string { - var sb strings.Builder - for _, str := range *i { - sb.WriteString(str) - } - return sb.String() -} - -func (i *arrayFlags) Set(value string) error { - *i = append(*i, value) - return nil -} func init() { flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") flag.StringVar(&serverIP, "serverIP", "", "IP address where Kyverno controller runs. Only required if out-of-cluster.") - flag.Var(&filterK8Kinds, "filterKind", "k8 kinds where polcies are not to be applied on") + flag.Var(&filterK8Kinds, "filterKind", "k8 kind where policy is not evaluated by the admission webhook. example --filterKind \"Event\" --filterKind \"TokenReview,ClusterRole\"") config.LogDefaultFlags() flag.Parse() } diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index a73dd1fc31..7455409ad8 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -54,9 +54,8 @@ func NewWebhookServer( ws := &WebhookServer{ client: client, policyLister: shareInformer.GetLister(), - filterKinds: filterKinds, + filterKinds: parseKinds(filterKinds), } - mux := http.NewServeMux() mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve) mux.HandleFunc(config.ValidatingWebhookServicePath, ws.serve) @@ -82,7 +81,7 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { admissionReview.Response = &v1beta1.AdmissionResponse{ Allowed: true, } - if !stringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) { + if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) { switch r.URL.Path { case config.MutatingWebhookServicePath: @@ -107,15 +106,6 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { } } -func stringInSlice(kind string, list []string) bool { - for _, b := range list { - if b == kind { - return true - } - } - return false -} - // RunAsync TLS server in separate thread and returns control immediately func (ws *WebhookServer) RunAsync() { go func(ws *WebhookServer) { diff --git a/pkg/webhooks/utils.go b/pkg/webhooks/utils.go new file mode 100644 index 0000000000..1b1786b384 --- /dev/null +++ b/pkg/webhooks/utils.go @@ -0,0 +1,45 @@ +package webhooks + +import ( + "strings" +) + +//StringInSlice checks if string is present in slice of strings +func StringInSlice(kind string, list []string) bool { + for _, b := range list { + if b == kind { + return true + } + } + return false +} + +//parseKinds parses the kinds if a single string contains comma seperated kinds +// {"1,2,3","4","5"} => {"1","2","3","4","5"} +func parseKinds(list []string) []string { + kinds := []string{} + for _, k := range list { + args := strings.Split(k, ",") + for _, arg := range args { + if arg != "" { + kinds = append(kinds, strings.TrimSpace(arg)) + } + } + } + return kinds +} + +type ArrayFlags []string + +func (i *ArrayFlags) String() string { + var sb strings.Builder + for _, str := range *i { + sb.WriteString(str) + } + return sb.String() +} + +func (i *ArrayFlags) Set(value string) error { + *i = append(*i, value) + return nil +} From dfeaf4184551b48b9f6412a5dcd929ebf3f0a23e Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Tue, 18 Jun 2019 13:52:12 -0700 Subject: [PATCH 34/37] support dryRun parameter in client api --- pkg/dclient/certificates.go | 8 ++++---- pkg/dclient/client.go | 34 +++++++++++++++++++++++++--------- pkg/violation/builder.go | 2 +- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/pkg/dclient/certificates.go b/pkg/dclient/certificates.go index c72e0289e4..8cbf23113f 100644 --- a/pkg/dclient/certificates.go +++ b/pkg/dclient/certificates.go @@ -58,7 +58,7 @@ func (c *Client) submitAndApproveCertificateRequest(req *certificates.Certificat for _, csr := range csrList.Items { if csr.GetName() == req.ObjectMeta.Name { - err := c.DeleteResouce(CSRs, "", csr.GetName()) + err := c.DeleteResouce(CSRs, "", csr.GetName(), false) if err != nil { return nil, fmt.Errorf("Unable to delete existing certificate request: %v", err) } @@ -67,7 +67,7 @@ func (c *Client) submitAndApproveCertificateRequest(req *certificates.Certificat } } - unstrRes, err := c.CreateResource(CSRs, "", req) + unstrRes, err := c.CreateResource(CSRs, "", req, false) if err != nil { return nil, err } @@ -209,7 +209,7 @@ func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPem Type: v1.SecretTypeTLS, } - _, err := c.CreateResource(Secrets, props.Namespace, secret) + _, err := c.CreateResource(Secrets, props.Namespace, secret, false) if err == nil { glog.Infof("Secret %s is created", name) } @@ -223,7 +223,7 @@ func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPem secret.Data[v1.TLSCertKey] = pemPair.Certificate secret.Data[v1.TLSPrivateKeyKey] = pemPair.PrivateKey - _, err = c.UpdateResource(Secrets, props.Namespace, secret) + _, err = c.UpdateResource(Secrets, props.Namespace, secret, false) if err != nil { return err } diff --git a/pkg/dclient/client.go b/pkg/dclient/client.go index ee504644ed..f859759068 100644 --- a/pkg/dclient/client.go +++ b/pkg/dclient/client.go @@ -115,34 +115,50 @@ func (c *Client) ListResource(resource string, namespace string) (*unstructured. } // DeleteResouce deletes the specified resource -func (c *Client) DeleteResouce(resource string, namespace string, name string) error { - return c.getResourceInterface(resource, namespace).Delete(name, &meta.DeleteOptions{}) +func (c *Client) DeleteResouce(resource string, namespace string, name string, dryRun bool) error { + options := meta.DeleteOptions{} + if dryRun { + options = meta.DeleteOptions{DryRun: []string{meta.DryRunAll}} + } + return c.getResourceInterface(resource, namespace).Delete(name, &options) } // CreateResource creates object for the specified resource/namespace -func (c *Client) CreateResource(resource string, namespace string, obj interface{}) (*unstructured.Unstructured, error) { +func (c *Client) CreateResource(resource string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) { + options := meta.CreateOptions{} + if dryRun { + options = meta.CreateOptions{DryRun: []string{meta.DryRunAll}} + } // convert typed to unstructured obj if unstructuredObj := convertToUnstructured(obj); unstructuredObj != nil { - return c.getResourceInterface(resource, namespace).Create(unstructuredObj, meta.CreateOptions{}) + return c.getResourceInterface(resource, namespace).Create(unstructuredObj, options) } return nil, fmt.Errorf("Unable to create resource ") } // UpdateResource updates object for the specified resource/namespace -func (c *Client) UpdateResource(resource string, namespace string, obj interface{}) (*unstructured.Unstructured, error) { +func (c *Client) UpdateResource(resource string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) { + options := meta.UpdateOptions{} + if dryRun { + options = meta.UpdateOptions{DryRun: []string{meta.DryRunAll}} + } // convert typed to unstructured obj if unstructuredObj := convertToUnstructured(obj); unstructuredObj != nil { - return c.getResourceInterface(resource, namespace).Update(unstructuredObj, meta.UpdateOptions{}) + return c.getResourceInterface(resource, namespace).Update(unstructuredObj, options) } return nil, fmt.Errorf("Unable to update resource ") } // UpdateStatusResource updates the resource "status" subresource -func (c *Client) UpdateStatusResource(resource string, namespace string, obj interface{}) (*unstructured.Unstructured, error) { +func (c *Client) UpdateStatusResource(resource string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) { + options := meta.UpdateOptions{} + if dryRun { + options = meta.UpdateOptions{DryRun: []string{meta.DryRunAll}} + } // convert typed to unstructured obj if unstructuredObj := convertToUnstructured(obj); unstructuredObj != nil { - return c.getResourceInterface(resource, namespace).UpdateStatus(unstructuredObj, meta.UpdateOptions{}) + return c.getResourceInterface(resource, namespace).UpdateStatus(unstructuredObj, options) } return nil, fmt.Errorf("Unable to update resource ") } @@ -204,7 +220,7 @@ func (c *Client) GenerateResource(generator types.Generation, namespace string) glog.Errorf("Can't create a resource %s: %v", generator.Name, err) return nil } - _, err = c.CreateResource(rGVR.Resource, namespace, resource) + _, err = c.CreateResource(rGVR.Resource, namespace, resource, false) if err != nil { return err } diff --git a/pkg/violation/builder.go b/pkg/violation/builder.go index 3c7b9f0049..0c64843500 100644 --- a/pkg/violation/builder.go +++ b/pkg/violation/builder.go @@ -82,7 +82,7 @@ func (b *builder) processViolation(info Info) error { modifiedPolicy.Status.Violations = modifiedViolations // Violations are part of the status sub resource, so we can use the Update Status api instead of updating the policy object - _, err = b.client.UpdateStatusResource("policies/status", namespace, modifiedPolicy) + _, err = b.client.UpdateStatusResource("policies/status", namespace, modifiedPolicy, false) if err != nil { return err } From 39a17911c39e337b19741c084444c6b815b13090 Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Wed, 19 Jun 2019 14:05:23 -0700 Subject: [PATCH 35/37] code review corrections --- definitions/install.yaml | 1 + pkg/webhooks/server.go | 27 +++++++++++++++++++-------- pkg/webhooks/utils.go | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/definitions/install.yaml b/definitions/install.yaml index 48ebe3c706..b68f50b5be 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -176,6 +176,7 @@ spec: containers: - name: kyverno image: nirmata/kyverno:latest + args: ["--filterKind","Nodes,Events,APIService,SubjectAccessReview"] ports: - containerPort: 443 securityContext: diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 7455409ad8..cc7129155a 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -81,6 +81,7 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { admissionReview.Response = &v1beta1.AdmissionResponse{ Allowed: true, } + // Do not process the admission requests for kinds that are in filterKinds for filtering if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) { switch r.URL.Path { @@ -135,15 +136,19 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be glog.Warning(err) return nil } - glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) admissionResult := result.NewAdmissionResult(string(request.UID)) var allPatches []engine.PatchBytes for _, policy := range policies { - if policy.Kind != request.Kind.Kind { + + // check if policy has a rule for the admission request kind + if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { continue } + + glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) policyPatches, mutationResult := engine.Mutate(*policy, request.Object.Raw, request.Kind) @@ -159,10 +164,10 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be name := engine.ParseNameFromObject(request.Object.Raw) glog.Infof("Mutation from policy %s has applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name) } + glog.Info(admissionResult.String()) } message := "\n" + admissionResult.String() - glog.Info(message) if admissionResult.GetReason() == result.Success { patchType := v1beta1.PatchTypeJSONPatch @@ -183,8 +188,6 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be // HandleValidation handles validating webhook admission request func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { - glog.Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", - request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) policies, err := ws.policyLister.List(labels.NewSelector()) if err != nil { @@ -194,6 +197,14 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 admissionResult := result.NewAdmissionResult(string(request.UID)) for _, policy := range policies { + + if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) { + continue + } + + glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s", + request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation) + glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules)) validationResult := engine.Validate(*policy, request.Object.Raw, request.Kind) admissionResult = result.Append(admissionResult, validationResult) @@ -201,10 +212,10 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 if validationError := validationResult.ToError(); validationError != nil { glog.Warningf(validationError.Error()) } + glog.Info(admissionResult.String()) } message := "\n" + admissionResult.String() - glog.Info(message) // Generation loop after all validation succeeded var response *v1beta1.AdmissionResponse @@ -213,7 +224,7 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1 for _, policy := range policies { engine.Generate(ws.client, *policy, request.Object.Raw, request.Kind) } - glog.Info("Validation is successful") + glog.V(3).Info("Validation is successful") response = &v1beta1.AdmissionResponse{ Allowed: true, diff --git a/pkg/webhooks/utils.go b/pkg/webhooks/utils.go index 1b1786b384..116c06053a 100644 --- a/pkg/webhooks/utils.go +++ b/pkg/webhooks/utils.go @@ -2,6 +2,8 @@ package webhooks import ( "strings" + + "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" ) //StringInSlice checks if string is present in slice of strings @@ -43,3 +45,21 @@ func (i *ArrayFlags) Set(value string) error { *i = append(*i, value) return nil } + +// extract the kinds that the policy rules apply to +func getApplicableKindsForPolicy(p *v1alpha1.Policy) []string { + kindsMap := map[string]interface{}{} + kinds := []string{} + // iterate over the rules an identify all kinds + for _, rule := range p.Spec.Rules { + for _, k := range rule.ResourceDescription.Kinds { + kindsMap[k] = nil + } + } + + // get the kinds + for k := range kindsMap { + kinds = append(kinds, k) + } + return kinds +} From 9c1b606892de493a00c379519c93e583b4e6860d Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Wed, 19 Jun 2019 14:21:03 -0700 Subject: [PATCH 36/37] update documentation --- documentation/installation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/documentation/installation.md b/documentation/installation.md index 018cce826e..edf95e80f6 100644 --- a/documentation/installation.md +++ b/documentation/installation.md @@ -126,6 +126,11 @@ To run controller in this mode you should prepare TLS key/certificate pair for d The [Kyverno CLI](documentation/testing-policies.md#test-using-the-kyverno-cli) allows you to write and test policies without installing Kyverno in a Kubernetes cluster. Some features are not supported without a Kubernetes cluster. +# Filter kuberenetes resources that admission webhook should not process + +The admission webhook checks if a policy is applicable on all admission requests. The kubernetes kinds that are not be processed can be filtered by using the command line argument 'filterKind'. + +By default we have specified Nodes, Events, APIService & SubjectAccessReview as the kinds to be skipped in the [install.yaml](https://github.com/nirmata/kyverno/raw/master/definitions/install.yaml). --- *Read Next >> [Writing Policies](/documentation/writing-policies.md)* From 096ea1b7cfaa5d7de3c4fa609f67b1b8d181c329 Mon Sep 17 00:00:00 2001 From: shivdudhani Date: Wed, 19 Jun 2019 14:40:34 -0700 Subject: [PATCH 37/37] add false for dryrun after api change --- pkg/dclient/client_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/dclient/client_test.go b/pkg/dclient/client_test.go index a7526c2c51..0cdbf9f71e 100644 --- a/pkg/dclient/client_test.go +++ b/pkg/dclient/client_test.go @@ -73,22 +73,22 @@ func TestCRUDResource(t *testing.T) { t.Errorf("ListResource not working: %s", err) } // DeleteResouce - err = f.client.DeleteResouce("thekinds", "ns-foo", "name-bar") + err = f.client.DeleteResouce("thekinds", "ns-foo", "name-bar", false) if err != nil { t.Errorf("DeleteResouce not working: %s", err) } // CreateResource - _, err = f.client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1")) + _, err = f.client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"), false) if err != nil { t.Errorf("CreateResource not working: %s", err) } // UpdateResource - _, err = f.client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"})) + _, err = f.client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"}), false) if err != nil { t.Errorf("UpdateResource not working: %s", err) } // UpdateStatusResource - _, err = f.client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"})) + _, err = f.client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"}), false) if err != nil { t.Errorf("UpdateStatusResource not working: %s", err) } @@ -123,7 +123,7 @@ func TestGenerateResource(t *testing.T) { // 1 create namespace // 2 generate resource // create namespace - ns, err := f.client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1")) + ns, err := f.client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1"), false) if err != nil { t.Errorf("CreateResource not working: %s", err) }