diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 623d50c573..707809e583 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,11 +1,9 @@ name: prereleaser - on: push: tags: - - '*' + - '*' - jobs: releaser: runs-on: ubuntu-latest @@ -36,6 +34,8 @@ jobs: access-token: ${{ secrets.ACCESS_TOKEN }} deploy-branch: gh-pages charts-folder: charts + - name: Update new version in krew-index + uses: rajatjindal/krew-release-bot@v0.0.38 diff --git a/.goreleaser.yml b/.goreleaser.yml index 2595128777..34ad611465 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,6 @@ builds: - windows goarch: - amd64 - goarm: [6, 7] archives: - id: kyverno-cli-archive name_template: |- @@ -26,12 +25,12 @@ archives: {{- end -}} builds: - kyverno-cli - replacements: - 386: i386 - amd64: x86_64 format_overrides: - goos: windows format: zip + replacements: + 386: i386 + amd64: x86_64 files: ["LICENSE"] checksum: name_template: "checksums.txt" diff --git a/.krew.yaml b/.krew.yaml new file mode 100644 index 0000000000..5a49128f63 --- /dev/null +++ b/.krew.yaml @@ -0,0 +1,46 @@ +apiVersion: krew.googlecontainertools.github.com/v1alpha2 +kind: Plugin +metadata: + name: kyverno +spec: + version: {{ .TagName }} + homepage: https://github.com/nirmata/kyverno + platforms: + - selector: + matchLabels: + os: linux + arch: amd64 + {{addURIAndSha "https://github.com/nirmata/kyverno/releases/download/{{ .TagName }}/kyverno-cli_{{ .TagName }}_linux_x86_64.tar.gz" .TagName }} + files: + - from: kyverno + to: . + - from: LICENSE + to: . + bin: kyverno + - selector: + matchLabels: + os: darwin + arch: amd64 + {{addURIAndSha "https://github.com/nirmata/kyverno/releases/download/{{ .TagName }}/kyverno-cli_{{ .TagName }}_darwin_x86_64.tar.gz" .TagName }} + files: + - from: kyverno + to: . + - from: LICENSE + to: . + bin: kyverno + - selector: + matchLabels: + os: windows + arch: amd64 + {{addURIAndSha "https://github.com/nirmata/kyverno/releases/download/{{ .TagName }}/kyverno-cli_{{ .TagName }}_windows_x86_64.zip" .TagName }} + files: + - from: kyverno.exe + to: . + - from: LICENSE + to: . + bin: kyverno.exe + shortDescription: Kyverno is a policy engine for kubernetes + description: |+2 + Kyverno is used to test kyverno policies and apply policies to resources files + caveats: | + The plugin requires access to create Policy and CustomResources diff --git a/cmd/kyverno/main.go b/cmd/kyverno/main.go index 1dbb7b65fb..66bd944ea7 100644 --- a/cmd/kyverno/main.go +++ b/cmd/kyverno/main.go @@ -209,7 +209,6 @@ func main() { pInformer.Kyverno().V1().ClusterPolicies(), pInformer.Kyverno().V1().GenerateRequests(), eventGenerator, - pvgen, kubedynamicInformer, statusSync.Listener, log.Log.WithName("GenerateController"), @@ -231,6 +230,16 @@ func main() { log.Log.WithName("PolicyCacheController"), ) + auditHandler := webhooks.NewValidateAuditHandler( + pCacheController.Cache, + eventGenerator, + statusSync.Listener, + pvgen, + kubeInformer.Rbac().V1().RoleBindings(), + kubeInformer.Rbac().V1().ClusterRoleBindings(), + log.Log.WithName("ValidateAuditHandler"), + ) + // CONFIGURE CERTIFICATES tlsPair, err := client.InitTLSPemPair(clientConfig, fqdncn) if err != nil { @@ -282,6 +291,7 @@ func main() { pvgen, grgen, rWebhookWatcher, + auditHandler, supportMudateValidate, cleanUp, log.Log.WithName("WebhookServer"), @@ -307,6 +317,7 @@ func main() { go pvgen.Run(1, stopCh) go statusSync.Run(1, stopCh) go pCacheController.Run(1, stopCh) + go auditHandler.Run(10, stopCh) openAPISync.Run(1, stopCh) // verifys if the admission control is enabled and active diff --git a/documentation/kyverno-cli.md b/documentation/kyverno-cli.md index 85ff9d3b7f..b399f8175d 100644 --- a/documentation/kyverno-cli.md +++ b/documentation/kyverno-cli.md @@ -1,13 +1,10 @@ -_[documentation](/README.md#documentation) / kyverno-cli_ +*[documentation](/README.md#documentation) / kyverno-cli* + # Kyverno CLI The Kyverno Command Line Interface (CLI) is designed to validate policies and test the behavior of applying policies to resources before adding the policy to a cluster. It can be used as a kubectl plugin and as a standalone CLI. -## Install the CLI - -The Kyverno CLI binary is distributed with each release. You can install the CLI for your platform from the [releases](https://github.com/nirmata/kyverno/releases) site. - ## Build the CLI You can build the CLI binary locally, then move the binary into a directory in your PATH. @@ -19,10 +16,14 @@ make cli mv ./cmd/cli/kubectl-kyverno/kyverno /usr/local/bin/kyverno ``` -You can also use curl to install kyverno-cli - +You can also use [Krew](https://github.com/kubernetes-sigs/krew) ```bash -curl -L https://raw.githubusercontent.com/nirmata/kyverno/master/scripts/install-cli.sh | bash +# Install kyverno using krew plugin manager +kubectl krew install kyverno + +#example +kuberctl kyverno version + ``` ## Install via AUR (archlinux) @@ -39,55 +40,39 @@ yay -S kyverno-git Prints the version of kyverno used by the CLI. -Example: - +Example: ``` kyverno version ``` #### Validate - -Validates a policy, can validate multiple policy resource description files or even an entire folder containing policy resource description -files. Currently supports files with resource description in YAML. +Validates a policy, can validate multiple policy resource description files or even an entire folder containing policy resource description +files. Currently supports files with resource description in yaml. Example: - ``` kyverno validate /path/to/policy1.yaml /path/to/policy2.yaml /path/to/folderFullOfPolicies ``` #### Apply - Applies policies on resources, and supports applying multiple policies on multiple resources in a single command. Also supports applying the given policies to an entire cluster. The current kubectl context will be used to access the cluster. -Will return results to stdout. + Will return results to stdout. Apply to a resource: - -```bash +``` kyverno apply /path/to/policy.yaml --resource /path/to/resource.yaml ``` Apply to all matching resources in a cluster: - -```bash +``` kyverno apply /path/to/policy.yaml --cluster > policy-results.txt ``` Apply multiple policies to multiple resources: - -```bash +``` kyverno apply /path/to/policy1.yaml /path/to/folderFullOfPolicies --resource /path/to/resource1.yaml --resource /path/to/resource2.yaml --cluster ``` -##### Exit Codes -The CLI exits with diffenent exit codes: - -| Message | Exit Code | -| ------------------------------------- | --------- | -| executes successfully | 0 | -| one or more policy rules are violated | 1 | -| policy validation failed | 2 | - -_Read Next >> [Sample Policies](/samples/README.md)_ +*Read Next >> [Sample Policies](/samples/README.md)* diff --git a/go.mod b/go.mod index 0c734f61ed..4534fef0c4 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/minio/minio v0.0.0-20200114012931-30922148fbb5 github.com/ory/go-acc v0.2.1 // indirect + github.com/pkg/errors v0.8.1 github.com/prometheus/common v0.4.1 github.com/rogpeppe/godef v1.1.2 // indirect github.com/spf13/cobra v0.0.5 diff --git a/go.sum b/go.sum index bf2c17eaab..a05d684930 100644 --- a/go.sum +++ b/go.sum @@ -606,6 +606,7 @@ github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5 github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA= github.com/minio/minio v0.0.0-20200114012931-30922148fbb5 h1:CjDeQ78sVdDrENJff3EUwVMUv9GfTL4NyLvjE/Bvrd8= github.com/minio/minio v0.0.0-20200114012931-30922148fbb5/go.mod h1:HH1U0HOUzfjsCGlGCncWDh8L3zPejMhMDDsLAETqXs0= +github.com/minio/minio-go/v6 v6.0.44 h1:CVwVXw+uCOcyMi7GvcOhxE8WgV+Xj8Vkf2jItDf/EGI= github.com/minio/minio-go/v6 v6.0.44/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg= github.com/minio/parquet-go v0.0.0-20191231003236-20b3c07bcd2c/go.mod h1:sl82d+TnCE7qeaNJazHdNoG9Gpyl9SZYfleDAQWrsls= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= @@ -748,6 +749,7 @@ github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2Ry github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM= github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= diff --git a/pkg/engine/forceMutate.go b/pkg/engine/forceMutate.go index ba677a5f72..0fae6085c0 100644 --- a/pkg/engine/forceMutate.go +++ b/pkg/engine/forceMutate.go @@ -55,6 +55,9 @@ func mutateResourceWithOverlay(resource unstructured.Unstructured, overlay inter // ForceMutate does not check any conditions, it simply mutates the given resource func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (unstructured.Unstructured, error) { var err error + logger := log.Log.WithName("EngineForceMutate").WithValues("policy", policy.Name, "kind", resource.GetKind(), + "namespace", resource.GetNamespace(), "name", resource.GetName()) + for _, rule := range policy.Spec.Rules { if !rule.HasMutate() { continue @@ -81,7 +84,7 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour if rule.Mutation.Patches != nil { var resp response.RuleResponse - resp, resource = mutate.ProcessPatches(log.Log, rule, resource) + resp, resource = mutate.ProcessPatches(logger.WithValues("rule", rule.Name), rule, resource) if !resp.Success { return unstructured.Unstructured{}, fmt.Errorf(resp.Message) } diff --git a/pkg/engine/mutate/overlay.go b/pkg/engine/mutate/overlay.go index 256efa2871..a1c0c88f82 100644 --- a/pkg/engine/mutate/overlay.go +++ b/pkg/engine/mutate/overlay.go @@ -29,7 +29,7 @@ func ProcessOverlay(log logr.Logger, ruleName string, overlay interface{}, resou resp.Type = utils.Mutation.String() defer func() { resp.RuleStats.ProcessingTime = time.Since(startTime) - logger.V(4).Info("finished applying overlay rule", "processingTime", resp.RuleStats.ProcessingTime) + logger.V(4).Info("finished applying overlay rule", "processingTime", resp.RuleStats.ProcessingTime.String()) }() patches, overlayerr := processOverlayPatches(logger, resource.UnstructuredContent(), overlay) @@ -351,8 +351,8 @@ func processSubtree(overlay interface{}, path string, op string) ([]byte, error) // explicitly handle boolean type in annotation // keep the type boolean as it is in any other fields - if strings.Contains(path, "/metadata/annotations") || - strings.Contains(path, "/metadata/labels"){ + if strings.Contains(path, "/metadata/annotations") || + strings.Contains(path, "/metadata/labels") { patchStr = wrapBoolean(patchStr) } diff --git a/pkg/engine/mutate/overlayCondition.go b/pkg/engine/mutate/overlayCondition.go index 97d0210e80..641169fa75 100755 --- a/pkg/engine/mutate/overlayCondition.go +++ b/pkg/engine/mutate/overlayCondition.go @@ -27,9 +27,9 @@ func checkConditions(log logr.Logger, resource, overlay interface{}, path string // condition never be true in this case if reflect.TypeOf(resource) != reflect.TypeOf(overlay) { if hasNestedAnchors(overlay) { - log.V(4).Info(fmt.Sprintf("Found anchor on different types of element at path %s: overlay %T, resource %T", path, overlay, resource)) + log.V(4).Info(fmt.Sprintf("element type mismatch at path %s: overlay %T, resource %T", path, overlay, resource)) return path, newOverlayError(conditionFailure, - fmt.Sprintf("Found anchor on different types of element at path %s: overlay %T %v, resource %T %v", path, overlay, overlay, resource, resource)) + fmt.Sprintf("element type mismatch at path %s: overlay %T %v, resource %T %v", path, overlay, overlay, resource, resource)) } return "", overlayError{} @@ -112,8 +112,8 @@ func validateConditionAnchorMap(resourceMap, anchors map[string]interface{}, pat // resource - A: B2 func compareOverlay(resource, overlay interface{}, path string) (string, overlayError) { if reflect.TypeOf(resource) != reflect.TypeOf(overlay) { - log.Log.V(4).Info("Found anchor on different types of element", "overlay", overlay, "resource", resource) - return path, newOverlayError(conditionFailure, fmt.Sprintf("Found anchor on different types of element: overlay %T, resource %T", overlay, resource)) + log.Log.V(4).Info("element type mismatch", "overlay", overlay, "resource", resource) + return path, newOverlayError(conditionFailure, fmt.Sprintf("element type mismatch: overlay %T, resource %T", overlay, resource)) } switch typedOverlay := overlay.(type) { diff --git a/pkg/engine/mutate/overlayCondition_test.go b/pkg/engine/mutate/overlayCondition_test.go index 8da6eefee9..4c9d43d14c 100644 --- a/pkg/engine/mutate/overlayCondition_test.go +++ b/pkg/engine/mutate/overlayCondition_test.go @@ -150,7 +150,7 @@ func TestMeetConditions_DifferentTypes(t *testing.T) { // anchor exist _, err = meetConditions(log.Log, resource, overlay) - assert.Assert(t, strings.Contains(err.Error(), "Found anchor on different types of element at path /subsets/")) + assert.Assert(t, strings.Contains(err.Error(), "element type mismatch at path /subsets/")) } func TestMeetConditions_anchosInSameObject(t *testing.T) { diff --git a/pkg/engine/mutate/patches.go b/pkg/engine/mutate/patches.go index 7b294bbca9..51aaaae026 100644 --- a/pkg/engine/mutate/patches.go +++ b/pkg/engine/mutate/patches.go @@ -28,7 +28,7 @@ func ProcessPatches(log logr.Logger, rule kyverno.Rule, resource unstructured.Un resp.Type = utils.Mutation.String() defer func() { resp.RuleStats.ProcessingTime = time.Since(startTime) - logger.V(4).Info("finished JSON patch", "processingTime", resp.RuleStats.ProcessingTime) + logger.V(4).Info("applied JSON patch", "processingTime", resp.RuleStats.ProcessingTime.String()) }() // convert to RAW diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 6d4fa3f102..712aa16821 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -22,23 +22,24 @@ const ( PodControllersAnnotation = "pod-policies.kyverno.io/autogen-controllers" //PodTemplateAnnotation defines the annotation key for Pod-Template PodTemplateAnnotation = "pod-policies.kyverno.io/autogen-applied" - PodControllerRuleName = "autogen-pod-ctrl-annotation" ) // Mutate performs mutation. Overlay first and then mutation patches func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { startTime := time.Now() policy := policyContext.Policy - resource := policyContext.NewResource + patchedResource := policyContext.NewResource ctx := policyContext.Context - logger := log.Log.WithName("Mutate").WithValues("policy", policy.Name, "kind", resource.GetKind(), "namespace", resource.GetNamespace(), "name", resource.GetName()) + logger := log.Log.WithName("EngineMutate").WithValues("policy", policy.Name, "kind", patchedResource.GetKind(), + "namespace", patchedResource.GetNamespace(), "name", patchedResource.GetName()) + logger.V(4).Info("start policy processing", "startTime", startTime) - startMutateResultResponse(&resp, policy, resource) + + startMutateResultResponse(&resp, policy, patchedResource) defer endMutateResultResponse(logger, &resp, startTime) - patchedResource := policyContext.NewResource - - if autoGenAnnotationApplied(patchedResource) && policy.HasAutoGenAnnotation() { + if policy.HasAutoGenAnnotation() && excludePod(patchedResource) { + logger.V(5).Info("Skip applying policy, Pod has ownerRef set", "policy", policy.GetName()) resp.PatchedResource = patchedResource return } @@ -47,14 +48,14 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { var ruleResponse response.RuleResponse logger := logger.WithValues("rule", rule.Name) //TODO: to be checked before calling the resources as well - if !rule.HasMutate() && !strings.Contains(PodControllers, resource.GetKind()) { + if !rule.HasMutate() && !strings.Contains(PodControllers, patchedResource.GetKind()) { continue } // check if the resource satisfies the filter conditions defined in the rule //TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that // dont satisfy a policy rule resource description - if err := MatchesResourceDescription(resource, rule, policyContext.AdmissionInfo); err != nil { + if err := MatchesResourceDescription(patchedResource, rule, policyContext.AdmissionInfo); err != nil { logger.V(3).Info("resource not matched", "reason", err.Error()) continue } @@ -112,22 +113,6 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) { return resp } - // skip inserting on existing resource - if policy.HasAutoGenAnnotation() && strings.Contains(PodControllers, resource.GetKind()) { - if !patchedResourceHasPodControllerAnnotation(patchedResource) { - var ruleResponse response.RuleResponse - ruleResponse, patchedResource = mutate.ProcessOverlay(logger, PodControllerRuleName, podTemplateRule.Mutation.Overlay, patchedResource) - if !ruleResponse.Success { - logger.Info("failed to insert annotation for podTemplate", "error", ruleResponse.Message) - } else { - if ruleResponse.Success && ruleResponse.Patches != nil { - logger.V(3).Info("inserted annotation for podTemplate") - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse) - } - } - } - } - // send the patched resource resp.PatchedResource = patchedResource return resp @@ -170,23 +155,5 @@ func startMutateResultResponse(resp *response.EngineResponse, policy kyverno.Clu func endMutateResultResponse(logger logr.Logger, resp *response.EngineResponse, startTime time.Time) { resp.PolicyResponse.ProcessingTime = time.Since(startTime) - logger.V(4).Info("finished processing policy", "processingTime", resp.PolicyResponse.ProcessingTime, "mutationRulesApplied", resp.PolicyResponse.RulesAppliedCount) -} - -// podTemplateRule mutate pod template with annotation -// pod-policies.kyverno.io/autogen-applied=true -var podTemplateRule = kyverno.Rule{ - Mutation: kyverno.Mutation{ - Overlay: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": map[string]interface{}{ - "+(" + PodTemplateAnnotation + ")": "true", - }, - }, - }, - }, - }, - }, + logger.V(4).Info("finished processing policy", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "mutationRulesApplied", resp.PolicyResponse.RulesAppliedCount) } diff --git a/pkg/engine/response/response.go b/pkg/engine/response/response.go index 8d4440ca03..eeb37dd2cb 100644 --- a/pkg/engine/response/response.go +++ b/pkg/engine/response/response.go @@ -113,7 +113,7 @@ func (er EngineResponse) GetSuccessRules() []string { func (er EngineResponse) getRules(success bool) []string { var rules []string for _, r := range er.PolicyResponse.Rules { - if r.Success == success{ + if r.Success == success { rules = append(rules, r.Name) } } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 79c252e823..cc6179ce2e 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -201,11 +201,10 @@ func copyConditions(original []kyverno.Condition) []kyverno.Condition { return copy } -// autoGenAnnotationApplied checks if a Pod has annotation "pod-policies.kyverno.io/autogen-applied" -func autoGenAnnotationApplied(resource unstructured.Unstructured) bool { +// excludePod checks if a Pod has ownerRef set +func excludePod(resource unstructured.Unstructured) bool { if resource.GetKind() == "Pod" { - ann := resource.GetAnnotations() - if _, ok := ann[PodTemplateAnnotation]; ok { + if len(resource.GetOwnerReferences()) > 0 { return true } } diff --git a/pkg/engine/validate/validate.go b/pkg/engine/validate/validate.go index f148f88f85..d31a8c075b 100644 --- a/pkg/engine/validate/validate.go +++ b/pkg/engine/validate/validate.go @@ -125,15 +125,18 @@ func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, o } default: // In all other cases - detect type and handle each array element with validateResourceElement - for i, patternElement := range patternArray { - currentPath := path + strconv.Itoa(i) + "/" - path, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath) - if err != nil { - return path, err + if len(resourceArray) >= len(patternArray) { + for i, patternElement := range patternArray { + currentPath := path + strconv.Itoa(i) + "/" + path, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath) + if err != nil { + return path, err + } } + }else{ + return "", fmt.Errorf("Validate Array failed, array length mismatch, resource Array len is %d and pattern Array len is %d", len(resourceArray), len(patternArray)) } } - return "", nil } diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index e86e87130c..07ac7fff56 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -24,7 +24,7 @@ func Validate(policyContext PolicyContext) (resp response.EngineResponse) { oldR := policyContext.OldResource ctx := policyContext.Context admissionInfo := policyContext.AdmissionInfo - logger := log.Log.WithName("Validate").WithValues("policy", policy.Name) + logger := log.Log.WithName("EngineValidate").WithValues("policy", policy.Name) if reflect.DeepEqual(newR, unstructured.Unstructured{}) { logger = logger.WithValues("kind", oldR.GetKind(), "namespace", oldR.GetNamespace(), "name", oldR.GetName()) @@ -93,7 +93,7 @@ func startResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPo func endResultResponse(log logr.Logger, resp *response.EngineResponse, startTime time.Time) { resp.PolicyResponse.ProcessingTime = time.Since(startTime) - log.V(4).Info("finshed processing", "processingTime", resp.PolicyResponse.ProcessingTime, "validationRulesApplied", resp.PolicyResponse.RulesAppliedCount) + log.V(4).Info("finshed processing", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "validationRulesApplied", resp.PolicyResponse.RulesAppliedCount) } func incrementAppliedCount(resp *response.EngineResponse) { @@ -103,6 +103,10 @@ func incrementAppliedCount(resp *response.EngineResponse) { func isRequestDenied(log logr.Logger, ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, admissionInfo kyverno.RequestInfo) *response.EngineResponse { resp := &response.EngineResponse{} + if policy.HasAutoGenAnnotation() && excludePod(resource) { + log.V(5).Info("Skip applying policy, Pod has ownerRef set", "policy", policy.GetName()) + return resp + } for _, rule := range policy.Spec.Rules { if !rule.HasValidate() { @@ -142,7 +146,8 @@ func isRequestDenied(log logr.Logger, ctx context.EvalInterface, policy kyverno. func validateResource(log logr.Logger, ctx context.EvalInterface, policy kyverno.ClusterPolicy, resource unstructured.Unstructured, admissionInfo kyverno.RequestInfo) *response.EngineResponse { resp := &response.EngineResponse{} - if autoGenAnnotationApplied(resource) && policy.HasAutoGenAnnotation() { + if policy.HasAutoGenAnnotation() && excludePod(resource) { + log.V(5).Info("Skip applying policy, Pod has ownerRef set", "policy", policy.GetName()) return resp } @@ -226,7 +231,7 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr resp.Type = utils.Validation.String() defer func() { resp.RuleStats.ProcessingTime = time.Since(startTime) - logger.V(4).Info("finished processing rule", "processingTime", resp.RuleStats.ProcessingTime) + logger.V(4).Info("finished processing rule", "processingTime", resp.RuleStats.ProcessingTime.String()) }() // work on a copy of validation rule diff --git a/pkg/generate/cleanup/controller.go b/pkg/generate/cleanup/controller.go index 2bfe339232..43c6d84f7d 100644 --- a/pkg/generate/cleanup/controller.go +++ b/pkg/generate/cleanup/controller.go @@ -248,7 +248,7 @@ func (c *Controller) syncGenerateRequest(key string) error { startTime := time.Now() logger.Info("started syncing generate request", "startTime", startTime) defer func() { - logger.V(4).Info("finished syncying generate request", "processingTIme", time.Since(startTime)) + logger.V(4).Info("finished syncying generate request", "processingTIme", time.Since(startTime).String()) }() _, grName, err := cache.SplitMetaNamespaceKey(key) if errors.IsNotFound(err) { diff --git a/pkg/generate/controller.go b/pkg/generate/controller.go index af5121e6b1..61f7967640 100644 --- a/pkg/generate/controller.go +++ b/pkg/generate/controller.go @@ -13,7 +13,6 @@ import ( dclient "github.com/nirmata/kyverno/pkg/dclient" "github.com/nirmata/kyverno/pkg/event" "github.com/nirmata/kyverno/pkg/policystatus" - "github.com/nirmata/kyverno/pkg/policyviolation" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -52,8 +51,6 @@ type Controller struct { pSynced cache.InformerSynced // grSynced returns true if the Generate Request store has been synced at least once grSynced cache.InformerSynced - // policy violation generator - pvGenerator policyviolation.GeneratorInterface // dyanmic sharedinformer factory dynamicInformer dynamicinformer.DynamicSharedInformerFactory //TODO: list of generic informers @@ -70,7 +67,6 @@ func NewController( pInformer kyvernoinformer.ClusterPolicyInformer, grInformer kyvernoinformer.GenerateRequestInformer, eventGen event.Interface, - pvGenerator policyviolation.GeneratorInterface, dynamicInformer dynamicinformer.DynamicSharedInformerFactory, policyStatus policystatus.Listener, log logr.Logger, @@ -79,7 +75,6 @@ func NewController( client: client, kyvernoClient: kyvernoclient, eventGen: eventGen, - pvGenerator: pvGenerator, //TODO: do the math for worst case back off and make sure cleanup runs after that // as we dont want a deleted GR to be re-queue queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(1, 30), "generate-request"), @@ -268,7 +263,7 @@ func (c *Controller) syncGenerateRequest(key string) error { startTime := time.Now() logger.Info("started sync", "key", key, "startTime", startTime) defer func() { - logger.V(4).Info("finished sync", "key", key, "processingTime", time.Since(startTime)) + logger.V(4).Info("finished sync", "key", key, "processingTime", time.Since(startTime).String()) }() _, grName, err := cache.SplitMetaNamespaceKey(key) if err != nil { diff --git a/pkg/policy/apply.go b/pkg/policy/apply.go index 59f8d448f7..b7cc8c6fd6 100644 --- a/pkg/policy/apply.go +++ b/pkg/policy/apply.go @@ -28,7 +28,7 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure name = ns + "/" + name } - logger.V(3).Info("applyPolicy", "resource", name, "processingTime", time.Since(startTime)) + logger.V(3).Info("applyPolicy", "resource", name, "processingTime", time.Since(startTime).String()) }() var engineResponses []response.EngineResponse @@ -81,10 +81,6 @@ func getFailedOverallRuleInfo(resource unstructured.Unstructured, engineResponse for index, rule := range engineResponse.PolicyResponse.Rules { log.V(4).Info("verifying if policy rule was applied before", "rule", rule.Name) - if rule.Name == engine.PodControllerRuleName { - continue - } - patches := rule.Patches patch, err := jsonpatch.DecodePatch(utils.JoinPatches(patches)) diff --git a/pkg/policy/controller.go b/pkg/policy/controller.go index 172ee743ae..0fc91fcfc1 100644 --- a/pkg/policy/controller.go +++ b/pkg/policy/controller.go @@ -304,7 +304,7 @@ func (pc *PolicyController) syncPolicy(key string) error { startTime := time.Now() logger.V(4).Info("started syncing policy", "key", key, "startTime", startTime) defer func() { - logger.V(4).Info("finished syncing policy", "key", key, "processingTime", time.Since(startTime)) + logger.V(4).Info("finished syncing policy", "key", key, "processingTime", time.Since(startTime).String()) }() policy, err := pc.pLister.Get(key) diff --git a/pkg/policy/namespacedpv.go b/pkg/policy/namespacedpv.go index 90335c52f3..05c887225e 100644 --- a/pkg/policy/namespacedpv.go +++ b/pkg/policy/namespacedpv.go @@ -109,6 +109,7 @@ func (pc *PolicyController) getPolicyForNamespacedPolicyViolation(pv *kyverno.Po logger := pc.log.WithValues("kind", pv.Kind, "namespace", pv.Namespace, "name", pv.Name) policies, err := pc.pLister.GetPolicyForNamespacedPolicyViolation(pv) if err != nil || len(policies) == 0 { + logger.V(4).Info("missing policy for namespaced policy violation", "reason", err.Error()) return nil } // Because all PolicyViolations's belonging to a Policy should have a unique label key, diff --git a/pkg/policycache/cache.go b/pkg/policycache/cache.go index 685417d35a..7354046df6 100644 --- a/pkg/policycache/cache.go +++ b/pkg/policycache/cache.go @@ -12,6 +12,9 @@ import ( type pMap struct { sync.RWMutex dataMap map[PolicyType][]*kyverno.ClusterPolicy + + // nameCacheMap stores the names of all existing policies in dataMap + nameCacheMap map[PolicyType]map[string]bool } // policyCache ... @@ -29,9 +32,17 @@ type Interface interface { // newPolicyCache ... func newPolicyCache(log logr.Logger) Interface { + namesCache := map[PolicyType]map[string]bool{ + Mutate: make(map[string]bool), + ValidateEnforce: make(map[string]bool), + ValidateAudit: make(map[string]bool), + Generate: make(map[string]bool), + } + return &policyCache{ pMap{ - dataMap: make(map[PolicyType][]*kyverno.ClusterPolicy), + dataMap: make(map[PolicyType][]*kyverno.ClusterPolicy), + nameCacheMap: namesCache, }, log, } @@ -54,29 +65,15 @@ func (pc *policyCache) Remove(policy *kyverno.ClusterPolicy) { pc.Logger.V(4).Info("policy is removed from cache", "name", policy.GetName()) } -// buildCacheMap builds the map to store the names of all existing -// policies in the cache, it is used to aviod adding duplicate policies -func buildCacheMap(policies []*kyverno.ClusterPolicy) map[string]bool { - cacheMap := make(map[string]bool) - - for _, p := range policies { - name := p.GetName() - if !cacheMap[name] { - cacheMap[p.GetName()] = true - } - } - - return cacheMap -} - func (m *pMap) add(policy *kyverno.ClusterPolicy) { m.Lock() defer m.Unlock() enforcePolicy := policy.Spec.ValidationFailureAction == "enforce" - mutateMap := buildCacheMap(m.dataMap[Mutate]) - validateMap := buildCacheMap(m.dataMap[ValidateEnforce]) - generateMap := buildCacheMap(m.dataMap[Generate]) + mutateMap := m.nameCacheMap[Mutate] + validateEnforceMap := m.nameCacheMap[ValidateEnforce] + validateAuditMap := m.nameCacheMap[ValidateAudit] + generateMap := m.nameCacheMap[Generate] pName := policy.GetName() for _, rule := range policy.Spec.Rules { @@ -90,12 +87,23 @@ func (m *pMap) add(policy *kyverno.ClusterPolicy) { continue } - if rule.HasValidate() && enforcePolicy { - if !validateMap[pName] { - validateMap[pName] = true + if rule.HasValidate() { + if enforcePolicy { + if !validateEnforceMap[pName] { + validateEnforceMap[pName] = true - validatePolicy := m.dataMap[ValidateEnforce] - m.dataMap[ValidateEnforce] = append(validatePolicy, policy) + validatePolicy := m.dataMap[ValidateEnforce] + m.dataMap[ValidateEnforce] = append(validatePolicy, policy) + } + continue + } + + // ValidateAudit + if !validateAuditMap[pName] { + validateAuditMap[pName] = true + + validatePolicy := m.dataMap[ValidateAudit] + m.dataMap[ValidateAudit] = append(validatePolicy, policy) } continue } @@ -110,6 +118,11 @@ func (m *pMap) add(policy *kyverno.ClusterPolicy) { continue } } + + m.nameCacheMap[Mutate] = mutateMap + m.nameCacheMap[ValidateEnforce] = validateEnforceMap + m.nameCacheMap[ValidateAudit] = validateAuditMap + m.nameCacheMap[Generate] = generateMap } func (m *pMap) get(key PolicyType) []*kyverno.ClusterPolicy { diff --git a/pkg/policycache/cache_test.go b/pkg/policycache/cache_test.go index 03a2b7886f..dd2d0346db 100644 --- a/pkg/policycache/cache_test.go +++ b/pkg/policycache/cache_test.go @@ -55,6 +55,26 @@ func Test_Add_Duplicate_Policy(t *testing.T) { } } +func Test_Add_Validate_Audit(t *testing.T) { + pCache := newPolicyCache(log.Log) + policy := newPolicy(t) + + pCache.Add(policy) + pCache.Add(policy) + + policy.Spec.ValidationFailureAction = "audit" + pCache.Add(policy) + pCache.Add(policy) + + if len(pCache.Get(ValidateEnforce)) != 1 { + t.Errorf("expected 1 validate enforce policy, found %v", len(pCache.Get(ValidateEnforce))) + } + + if len(pCache.Get(ValidateAudit)) != 1 { + t.Errorf("expected 1 validate audit policy, found %v", len(pCache.Get(ValidateAudit))) + } +} + func Test_Remove_From_Empty_Cache(t *testing.T) { pCache := newPolicyCache(log.Log) policy := newPolicy(t) diff --git a/pkg/policycache/type.go b/pkg/policycache/type.go index 1349b0c5de..ae75481193 100644 --- a/pkg/policycache/type.go +++ b/pkg/policycache/type.go @@ -5,5 +5,6 @@ type PolicyType uint8 const ( Mutate PolicyType = 1 << iota ValidateEnforce + ValidateAudit Generate ) diff --git a/pkg/policystatus/main.go b/pkg/policystatus/main.go index 34d849cc55..451e17ac4b 100644 --- a/pkg/policystatus/main.go +++ b/pkg/policystatus/main.go @@ -47,10 +47,10 @@ func (l Listener) Send(s statusUpdater) { //since it contains access to all the persistant data present //in this package. type Sync struct { - cache *cache - Listener Listener - client *versioned.Clientset - lister kyvernolister.ClusterPolicyLister + cache *cache + Listener Listener + client *versioned.Clientset + lister kyvernolister.ClusterPolicyLister } type cache struct { @@ -66,9 +66,9 @@ func NewSync(c *versioned.Clientset, lister kyvernolister.ClusterPolicyLister) * data: make(map[string]v1.PolicyStatus), keyToMutex: newKeyToMutex(), }, - client: c, - lister: lister, - Listener: make(chan statusUpdater, 20), + client: c, + lister: lister, + Listener: make(chan statusUpdater, 20), } } diff --git a/pkg/policystatus/status_test.go b/pkg/policystatus/status_test.go index 1f768c21e7..c69383b328 100644 --- a/pkg/policystatus/status_test.go +++ b/pkg/policystatus/status_test.go @@ -29,7 +29,6 @@ func (d dummyStatusUpdater) PolicyName() string { return "policy1" } - type dummyLister struct { } diff --git a/pkg/policyviolation/generator.go b/pkg/policyviolation/generator.go index f395ef1475..c574f9fb0f 100644 --- a/pkg/policyviolation/generator.go +++ b/pkg/policyviolation/generator.go @@ -159,7 +159,7 @@ func (gen *Generator) Run(workers int, stopCh <-chan struct{}) { } func (gen *Generator) runWorker() { - for gen.processNextWorkitem() { + for gen.processNextWorkItem() { } } @@ -186,7 +186,7 @@ func (gen *Generator) handleErr(err error, key interface{}) { logger.Error(err, "dropping key out of the queue", "key", key) } -func (gen *Generator) processNextWorkitem() bool { +func (gen *Generator) processNextWorkItem() bool { logger := gen.log obj, shutdown := gen.queue.Get() if shutdown { @@ -197,11 +197,13 @@ func (gen *Generator) processNextWorkitem() bool { defer gen.queue.Done(obj) var keyHash string var ok bool + if keyHash, ok = obj.(string); !ok { gen.queue.Forget(obj) logger.Info("incorrect type; expecting type 'string'", "obj", obj) return nil } + // lookup data store info := gen.dataStore.lookup(keyHash) if reflect.DeepEqual(info, Info{}) { @@ -210,14 +212,17 @@ func (gen *Generator) processNextWorkitem() bool { logger.Info("empty key") return nil } + err := gen.syncHandler(info) gen.handleErr(err, obj) return nil }(obj) + if err != nil { logger.Error(err, "failed to process item") return true } + return true } diff --git a/pkg/testrunner/testrunner_test.go b/pkg/testrunner/testrunner_test.go index 163c34cf13..d3a65421c8 100644 --- a/pkg/testrunner/testrunner_test.go +++ b/pkg/testrunner/testrunner_test.go @@ -6,9 +6,9 @@ func Test_Mutate_EndPoint(t *testing.T) { testScenario(t, "/test/scenarios/other/scenario_mutate_endpoint.yaml") } -// func Test_Mutate_Validate_qos(t *testing.T) { -// testScenario(t, "/test/scenarios/other/scenario_mutate_validate_qos.yaml") -// } +func Test_Mutate_Validate_qos(t *testing.T) { + testScenario(t, "/test/scenarios/other/scenario_mutate_validate_qos.yaml") +} func Test_disallow_root_user(t *testing.T) { testScenario(t, "test/scenarios/samples/best_practices/disallow_root_user.yaml") diff --git a/pkg/webhookconfig/registration.go b/pkg/webhookconfig/registration.go index 7f5cb305bd..5573af61b1 100644 --- a/pkg/webhookconfig/registration.go +++ b/pkg/webhookconfig/registration.go @@ -251,7 +251,7 @@ func (wrc *WebhookRegistrationClient) removeWebhookConfigurations() { startTime := time.Now() wrc.log.Info("removing prior webhook configurations") defer func() { - wrc.log.V(4).Info("removed webhookcongfigurations", "processingTime", time.Since(startTime)) + wrc.log.V(4).Info("removed webhookcongfigurations", "processingTime", time.Since(startTime).String()) }() var wg sync.WaitGroup diff --git a/pkg/webhookconfig/rwebhookregister.go b/pkg/webhookconfig/rwebhookregister.go index 9180d32f69..63d2d964c0 100644 --- a/pkg/webhookconfig/rwebhookregister.go +++ b/pkg/webhookconfig/rwebhookregister.go @@ -118,7 +118,7 @@ func (rww *ResourceWebhookRegister) Run(stopCh <-chan struct{}) { } // RemoveResourceWebhookConfiguration removes the resource webhook configurations -func (rww *ResourceWebhookRegister) RemoveResourceWebhookConfiguration() { +func (rww *ResourceWebhookRegister) RemoveResourceWebhookConfiguration() { rww.webhookRegistrationClient.RemoveResourceMutatingWebhookConfiguration() if rww.RunValidationInMutatingWebhook != "true" { diff --git a/pkg/webhooks/common.go b/pkg/webhooks/common.go index c6839b4e5a..a0839a6d94 100644 --- a/pkg/webhooks/common.go +++ b/pkg/webhooks/common.go @@ -29,11 +29,11 @@ func isResponseSuccesful(engineReponses []response.EngineResponse) bool { func toBlockResource(engineReponses []response.EngineResponse, log logr.Logger) bool { for _, er := range engineReponses { if !er.IsSuccessful() && er.PolicyResponse.ValidationFailureAction == Enforce { - log.Info("spec.ValidationFailureAction set to enforcel blocking resource request", "policy", er.PolicyResponse.Policy) + log.Info("spec.ValidationFailureAction set to enforce blocking resource request", "policy", er.PolicyResponse.Policy) return true } } - log.V(4).Info("sepc.ValidationFailureAction set to auit for all applicable policies;allowing resource reques; reporting with policy violation ") + log.V(4).Info("sepc.ValidationFailureAction set to auit for all applicable policies, won't block resource operation") return false } diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index b11caeafe3..8d57d76543 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -1,8 +1,10 @@ package webhooks import ( + "fmt" "reflect" "sort" + "strings" "time" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" @@ -11,25 +13,27 @@ import ( "github.com/nirmata/kyverno/pkg/engine/context" "github.com/nirmata/kyverno/pkg/engine/response" "github.com/nirmata/kyverno/pkg/engine/utils" + "github.com/nirmata/kyverno/pkg/event" "github.com/nirmata/kyverno/pkg/webhooks/generate" v1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) //HandleGenerate handles admission-requests for policies with generate rules -func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, policies []*kyverno.ClusterPolicy, ctx *context.Context, userRequestInfo kyverno.RequestInfo) (bool, string) { +func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, policies []*kyverno.ClusterPolicy, ctx *context.Context, userRequestInfo kyverno.RequestInfo) { logger := ws.log.WithValues("action", "generation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) logger.V(4).Info("incoming request") var engineResponses []response.EngineResponse if len(policies) == 0 { - return true, "" + return } // convert RAW to unstructured resource, err := utils.ConvertToUnstructured(request.Object.Raw) if err != nil { //TODO: skip applying the admission control ? logger.Error(err, "failed to convert RAR resource to unstructured format") - return true, "" + return } // CREATE resources, do not have name, assigned in admission-request @@ -52,10 +56,14 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic }) } } + // Adds Generate Request to a channel(queue size 1000) to generators - if err := applyGenerateRequest(ws.grGenerator, userRequestInfo, request.Operation, engineResponses...); err != nil { - //TODO: send appropriate error - return false, "Kyverno blocked: failed to create Generate Requests" + if failedResponse := applyGenerateRequest(ws.grGenerator, userRequestInfo, request.Operation, engineResponses...); err != nil { + // report failure event + for _, failedGR := range failedResponse { + events := failedEvents(fmt.Errorf("failed to create Generate Request: %v", failedGR.err), failedGR.gr, *resource) + ws.eventGen.Add(events...) + } } // Generate Stats wont be used here, as we delegate the generate rule @@ -66,16 +74,19 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic // HandleGeneration always returns success // Filter Policies - return true, "" + return } -func applyGenerateRequest(gnGenerator generate.GenerateRequests, userRequestInfo kyverno.RequestInfo, action v1beta1.Operation, engineResponses ...response.EngineResponse) error { +func applyGenerateRequest(gnGenerator generate.GenerateRequests, userRequestInfo kyverno.RequestInfo, + action v1beta1.Operation, engineResponses ...response.EngineResponse) (failedGenerateRequest []generateRequestResponse) { + for _, er := range engineResponses { - if err := gnGenerator.Apply(transform(userRequestInfo, er), action); err != nil { - return err + gr := transform(userRequestInfo, er) + if err := gnGenerator.Apply(gr, action); err != nil { + failedGenerateRequest = append(failedGenerateRequest, generateRequestResponse{gr: gr, err: err}) } } - return nil + return } func transform(userRequestInfo kyverno.RequestInfo, er response.EngineResponse) kyverno.GenerateRequestSpec { @@ -162,3 +173,41 @@ func updateAverageTime(newTime time.Duration, oldAverageTimeString string, avera newAverageTimeInNanoSeconds := numerator / denominator return time.Duration(newAverageTimeInNanoSeconds) * time.Nanosecond } + +type generateRequestResponse struct { + gr v1.GenerateRequestSpec + err error +} + +func (resp generateRequestResponse) info() string { + return strings.Join([]string{resp.gr.Resource.Kind, resp.gr.Resource.Namespace, resp.gr.Resource.Name}, "/") +} + +func (resp generateRequestResponse) error() string { + return resp.err.Error() +} + +func failedEvents(err error, gr kyverno.GenerateRequestSpec, resource unstructured.Unstructured) []event.Info { + var events []event.Info + // Cluster Policy + pe := event.Info{} + pe.Kind = "ClusterPolicy" + // cluserwide-resource + pe.Name = gr.Policy + pe.Reason = event.PolicyFailed.String() + pe.Source = event.GeneratePolicyController + pe.Message = fmt.Sprintf("policy failed to apply on resource %s/%s/%s: %v", resource.GetKind(), resource.GetNamespace(), resource.GetName(), err) + events = append(events, pe) + + // Resource + re := event.Info{} + re.Kind = resource.GetKind() + re.Namespace = resource.GetNamespace() + re.Name = resource.GetName() + re.Reason = event.PolicyFailed.String() + re.Source = event.GeneratePolicyController + re.Message = fmt.Sprintf("policy %s failed to apply: %v", gr.Policy, err) + events = append(events, re) + + return events +} diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index 790ae25c74..72fe530751 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -72,7 +72,7 @@ func (ws *WebhookServer) HandleMutation( if len(policyPatches) > 0 { patches = append(patches, policyPatches...) rules := engineResponse.GetSuccessRules() - logger.Info("mutation rules from policy applied successfully", "policy", policy.Name, "rules", rules) + logger.Info("mutation rules from policy applied successfully", "policy", policy.Name, "rules", rules) } policyContext.NewResource = engineResponse.PatchedResource diff --git a/pkg/webhooks/policyvalidation.go b/pkg/webhooks/policyvalidation.go index 5b2cf94c18..a9bbf81579 100644 --- a/pkg/webhooks/policyvalidation.go +++ b/pkg/webhooks/policyvalidation.go @@ -10,8 +10,11 @@ import ( //HandlePolicyValidation performs the validation check on policy resource func (ws *WebhookServer) policyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { + logger := ws.log.WithValues("action", "policyvalidation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) + //TODO: can this happen? wont this be picked by OpenAPI spec schema ? if err := policyvalidate.Validate(request.Object.Raw, ws.client, false, ws.openAPIController); err != nil { + logger.Error(err, "faield to validate policy") return &v1beta1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 123107fca7..75b559686a 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -7,11 +7,12 @@ import ( "errors" "fmt" "io/ioutil" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "net/http" "time" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/go-logr/logr" "github.com/julienschmidt/httprouter" v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" @@ -22,6 +23,7 @@ import ( "github.com/nirmata/kyverno/pkg/config" client "github.com/nirmata/kyverno/pkg/dclient" context2 "github.com/nirmata/kyverno/pkg/engine/context" + enginutils "github.com/nirmata/kyverno/pkg/engine/utils" "github.com/nirmata/kyverno/pkg/event" "github.com/nirmata/kyverno/pkg/openapi" "github.com/nirmata/kyverno/pkg/policycache" @@ -30,7 +32,6 @@ import ( tlsutils "github.com/nirmata/kyverno/pkg/tls" userinfo "github.com/nirmata/kyverno/pkg/userinfo" "github.com/nirmata/kyverno/pkg/utils" - enginutils "github.com/nirmata/kyverno/pkg/engine/utils" "github.com/nirmata/kyverno/pkg/webhookconfig" "github.com/nirmata/kyverno/pkg/webhooks/generate" v1beta1 "k8s.io/api/admission/v1beta1" @@ -98,8 +99,11 @@ type WebhookServer struct { grGenerator *generate.Generator resourceWebhookWatcher *webhookconfig.ResourceWebhookRegister - log logr.Logger - openAPIController *openapi.Controller + + auditHandler AuditHandler + + log logr.Logger + openAPIController *openapi.Controller supportMudateValidate bool } @@ -123,6 +127,7 @@ func NewWebhookServer( pvGenerator policyviolation.GeneratorInterface, grGenerator *generate.Generator, resourceWebhookWatcher *webhookconfig.ResourceWebhookRegister, + auditHandler AuditHandler, supportMudateValidate bool, cleanUp chan<- struct{}, log logr.Logger, @@ -161,6 +166,7 @@ func NewWebhookServer( pvGenerator: pvGenerator, grGenerator: grGenerator, resourceWebhookWatcher: resourceWebhookWatcher, + auditHandler: auditHandler, log: log, openAPIController: openAPIController, supportMudateValidate: supportMudateValidate, @@ -226,7 +232,7 @@ func (ws *WebhookServer) handlerFunc(handler func(request *v1beta1.AdmissionRequ admissionReview.Response = handler(request) writeResponse(rw, admissionReview) - logger.V(4).Info("request processed", "processingTime", time.Since(startTime).Milliseconds()) + logger.V(4).Info("request processed", "processingTime", time.Since(startTime).String()) return } @@ -258,7 +264,6 @@ func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1 } } - mutatePolicies := ws.pCache.Get(policycache.Mutate) validatePolicies := ws.pCache.Get(policycache.ValidateEnforce) generatePolicies := ws.pCache.Get(policycache.Generate) @@ -290,7 +295,7 @@ func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1 userRequestInfo := v1.RequestInfo{ Roles: roles, ClusterRoles: clusterRoles, - AdmissionUserInfo: request.UserInfo} + AdmissionUserInfo: *request.UserInfo.DeepCopy()} // build context ctx := context2.NewContext() @@ -325,8 +330,11 @@ func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1 logger.V(6).Info("", "patchedResource", string(patchedResource)) if ws.resourceWebhookWatcher != nil && ws.resourceWebhookWatcher.RunValidationInMutatingWebhook == "true" { + // push admission request to audit handler, this won't block the admission request + ws.auditHandler.Add(request.DeepCopy()) + // VALIDATION - ok, msg := ws.HandleValidation(request, validatePolicies, patchedResource, ctx, userRequestInfo) + ok, msg := HandleValidation(request, validatePolicies, nil, ctx, userRequestInfo, ws.statusListener, ws.eventGen, ws.pvGenerator, ws.log) if !ok { logger.Info("admission request denied") return &v1beta1.AdmissionResponse{ @@ -344,25 +352,14 @@ func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1 // GENERATE // Only applied during resource creation and update - // Success -> Generate Request CR created successsfully + // Success -> Generate Request CR created successfully // Failed -> Failed to create Generate Request CR if request.Operation == v1beta1.Create || request.Operation == v1beta1.Update { - - ok, msg := ws.HandleGenerate(request, generatePolicies, ctx, userRequestInfo) - if !ok { - logger.Info("admission request denied") - return &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Status: "Failure", - Message: msg, - }, - } - } + go ws.HandleGenerate(request.DeepCopy(), generatePolicies, ctx, userRequestInfo) } - // Succesfful processing of mutation & validation rules in policy + // Succesful processing of mutation & validation rules in policy patchType := v1beta1.PatchTypeJSONPatch return &v1beta1.AdmissionResponse{ Allowed: true, @@ -377,8 +374,8 @@ func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1 func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { logger := ws.log.WithName("resourceValidation").WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) - logger.V(4).Info("DEBUG","request",request) - checked, err := userinfo.IsRoleAuthorize(ws.rbLister, ws.crbLister,ws.rLister, ws.crLister, request) + logger.V(4).Info("DEBUG", "request", request) + checked, err := userinfo.IsRoleAuthorize(ws.rbLister, ws.crbLister, ws.rLister, ws.crLister, request) if err != nil { logger.Error(err, "failed to get RBAC infromation for request") } @@ -389,7 +386,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * var resource *unstructured.Unstructured if request.Operation == v1beta1.Delete { resource, err = enginutils.ConvertToUnstructured(request.OldObject.Raw) - }else{ + } else { resource, err = enginutils.ConvertToUnstructured(request.Object.Raw) } if err != nil { @@ -405,7 +402,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * } } - oldResource, err := ws.client.GetResource(resource.GetKind(),resource.GetNamespace(),resource.GetName()); + oldResource, err := ws.client.GetResource(resource.GetKind(), resource.GetNamespace(), resource.GetName()) if err != nil { if !apierrors.IsNotFound(err) { logger.Error(err, "failed to get resource") @@ -420,7 +417,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * } labels := oldResource.GetLabels() if labels != nil { - if labels["app.kubernetes.io/managed-by"] == "kyverno" && labels["app.kubernetes.io/synchronize"] == "enable" { + if labels["app.kubernetes.io/managed-by"] == "kyverno" && labels["app.kubernetes.io/synchronize"] == "enable" { return &v1beta1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ @@ -452,6 +449,9 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * } } + // push admission request to audit handler, this won't block the admission request + ws.auditHandler.Add(request.DeepCopy()) + policies := ws.pCache.Get(policycache.ValidateEnforce) if len(policies) == 0 { logger.V(4).Info("No enforce Validation policy found, returning") @@ -463,8 +463,14 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * if containRBACinfo(policies) { roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request) if err != nil { - // TODO(shuting): continue apply policy if error getting roleRef? logger.Error(err, "failed to get RBAC information for request") + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Status: "Failure", + Message: err.Error(), + }, + } } } @@ -489,7 +495,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * logger.Error(err, "failed to load service account in context") } - ok, msg := ws.HandleValidation(request, policies, nil, ctx, userRequestInfo) + ok, msg := HandleValidation(request, policies, nil, ctx, userRequestInfo, ws.statusListener, ws.eventGen, ws.pvGenerator, ws.log) if !ok { logger.Info("admission request denied") return &v1beta1.AdmissionResponse{ diff --git a/pkg/webhooks/validate_audit.go b/pkg/webhooks/validate_audit.go new file mode 100644 index 0000000000..ccef2ed936 --- /dev/null +++ b/pkg/webhooks/validate_audit.go @@ -0,0 +1,170 @@ +package webhooks + +import ( + "github.com/go-logr/logr" + "github.com/minio/minio/cmd/logger" + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + kyvernoclient "github.com/nirmata/kyverno/pkg/client/clientset/versioned" + "github.com/nirmata/kyverno/pkg/constant" + enginectx "github.com/nirmata/kyverno/pkg/engine/context" + "github.com/nirmata/kyverno/pkg/event" + "github.com/nirmata/kyverno/pkg/policycache" + "github.com/nirmata/kyverno/pkg/policystatus" + "github.com/nirmata/kyverno/pkg/policyviolation" + "github.com/nirmata/kyverno/pkg/userinfo" + "github.com/pkg/errors" + "k8s.io/api/admission/v1beta1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + rbacinformer "k8s.io/client-go/informers/rbac/v1" + rbaclister "k8s.io/client-go/listers/rbac/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" +) + +const ( + workQueueName = "validate-audit-handler" + workQueueRetryLimit = 3 +) + +// Handler applies validate audit policies to the admission request +// the handler adds the request to the work queue and returns immediately +// the request is processed in background, with the exact same logic +// when process the admission request in the webhook +type AuditHandler interface { + Add(request *v1beta1.AdmissionRequest) + Run(workers int, stopCh <-chan struct{}) +} + +type auditHandler struct { + client *kyvernoclient.Clientset + queue workqueue.RateLimitingInterface + pCache policycache.Interface + eventGen event.Interface + statusListener policystatus.Listener + pvGenerator policyviolation.GeneratorInterface + + rbLister rbaclister.RoleBindingLister + rbSynced cache.InformerSynced + crbLister rbaclister.ClusterRoleBindingLister + crbSynced cache.InformerSynced + + log logr.Logger +} + +// NewValidateAuditHandler returns a new instance of audit policy handler +func NewValidateAuditHandler(pCache policycache.Interface, + eventGen event.Interface, + statusListener policystatus.Listener, + pvGenerator policyviolation.GeneratorInterface, + rbInformer rbacinformer.RoleBindingInformer, + crbInformer rbacinformer.ClusterRoleBindingInformer, + log logr.Logger) AuditHandler { + + return &auditHandler{ + pCache: pCache, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), workQueueName), + eventGen: eventGen, + statusListener: statusListener, + pvGenerator: pvGenerator, + rbLister: rbInformer.Lister(), + rbSynced: rbInformer.Informer().HasSynced, + crbLister: crbInformer.Lister(), + crbSynced: crbInformer.Informer().HasSynced, + log: log, + } +} + +func (h *auditHandler) Add(request *v1beta1.AdmissionRequest) { + h.log.V(4).Info("admission request added", "uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) + h.queue.Add(request) +} + +func (h *auditHandler) Run(workers int, stopCh <-chan struct{}) { + h.log.V(4).Info("starting") + + defer func() { + utilruntime.HandleCrash() + h.log.V(4).Info("shutting down") + }() + + if !cache.WaitForCacheSync(stopCh, h.rbSynced, h.crbSynced) { + logger.Info("failed to sync informer cache") + } + + for i := 0; i < workers; i++ { + go wait.Until(h.runWorker, constant.GenerateControllerResync, stopCh) + } + + <-stopCh +} + +func (h *auditHandler) runWorker() { + for h.processNextWorkItem() { + } +} + +func (h *auditHandler) processNextWorkItem() bool { + obj, shutdown := h.queue.Get() + if shutdown { + return false + } + + defer h.queue.Done(obj) + + request, ok := obj.(*v1beta1.AdmissionRequest) + if !ok { + h.queue.Forget(obj) + logger.Info("incorrect type: expecting type 'AdmissionRequest'", "object", obj) + return false + } + + err := h.process(request) + h.handleErr(err) + + return true +} + +func (h *auditHandler) process(request *v1beta1.AdmissionRequest) error { + var roles, clusterRoles []string + var err error + + logger := h.log.WithName("process") + policies := h.pCache.Get(policycache.ValidateAudit) + + // getRoleRef only if policy has roles/clusterroles defined + if containRBACinfo(policies) { + roles, clusterRoles, err = userinfo.GetRoleRef(h.rbLister, h.crbLister, request) + if err != nil { + logger.Error(err, "failed to get RBAC information for request") + } + } + + userRequestInfo := v1.RequestInfo{ + Roles: roles, + ClusterRoles: clusterRoles, + AdmissionUserInfo: request.UserInfo} + + // build context + ctx := enginectx.NewContext() + err = ctx.AddRequest(request) + if err != nil { + return errors.Wrap(err, "failed to load incoming request in context") + } + + err = ctx.AddUserInfo(userRequestInfo) + if err != nil { + return errors.Wrap(err, "failed to load userInfo in context") + } + err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + if err != nil { + return errors.Wrap(err, "failed to load service account in context") + } + + HandleValidation(request, policies, nil, ctx, userRequestInfo, h.statusListener, h.eventGen, h.pvGenerator, logger) + return nil +} + +func (h *auditHandler) handleErr(err error) { + +} diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index 020620ceef..8e0cc2ad63 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -5,6 +5,9 @@ import ( "sort" "time" + "github.com/go-logr/logr" + "github.com/nirmata/kyverno/pkg/event" + "github.com/nirmata/kyverno/pkg/policystatus" "github.com/nirmata/kyverno/pkg/utils" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" @@ -21,12 +24,16 @@ import ( // HandleValidation handles validating webhook admission request // If there are no errors in validating rule we apply generation rules // patchedResource is the (resource + patches) after applying mutation rules -func (ws *WebhookServer) HandleValidation( +func HandleValidation( request *v1beta1.AdmissionRequest, policies []*kyverno.ClusterPolicy, patchedResource []byte, ctx *context.Context, - userRequestInfo kyverno.RequestInfo) (bool, string) { + userRequestInfo kyverno.RequestInfo, + statusListener policystatus.Listener, + eventGen event.Interface, + pvGenerator policyviolation.GeneratorInterface, + log logr.Logger) (bool, string) { if len(policies) == 0 { return true, "" @@ -37,7 +44,7 @@ func (ws *WebhookServer) HandleValidation( resourceName = request.Namespace + "/" + resourceName } - logger := ws.log.WithValues("action", "validate", "resource", resourceName, "operation", request.Operation) + logger := log.WithValues("action", "validate", "resource", resourceName, "operation", request.Operation) // Get new and old resource newR, oldR, err := utils.ExtractResources(patchedResource, request) @@ -76,7 +83,7 @@ func (ws *WebhookServer) HandleValidation( continue } engineResponses = append(engineResponses, engineResponse) - ws.statusListener.Send(validateStats{ + statusListener.Send(validateStats{ resp: engineResponse, }) if !engineResponse.IsSuccessful() { @@ -101,7 +108,7 @@ func (ws *WebhookServer) HandleValidation( // all policies were applied succesfully. // create an event on the resource events := generateEvents(engineResponses, blocked, (request.Operation == v1beta1.Update), logger) - ws.eventGen.Add(events...) + eventGen.Add(events...) if blocked { logger.V(4).Info("resource blocked") return false, getEnforceFailureErrorMsg(engineResponses) @@ -110,8 +117,8 @@ func (ws *WebhookServer) HandleValidation( // ADD POLICY VIOLATIONS // violations are created with resource on "audit" pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses, logger) - ws.pvGenerator.Add(pvInfos...) - // report time end + pvGenerator.Add(pvInfos...) + return true, "" }