diff --git a/pkg/webhooks/resource/handlers.go b/pkg/webhooks/resource/handlers.go index 4d006ac23b..cfde95cc72 100644 --- a/pkg/webhooks/resource/handlers.go +++ b/pkg/webhooks/resource/handlers.go @@ -24,10 +24,10 @@ import ( "github.com/kyverno/kyverno/pkg/policyreport" "github.com/kyverno/kyverno/pkg/utils" admissionutils "github.com/kyverno/kyverno/pkg/utils/admission" - engineutils "github.com/kyverno/kyverno/pkg/utils/engine" jsonutils "github.com/kyverno/kyverno/pkg/utils/json" "github.com/kyverno/kyverno/pkg/webhooks" "github.com/kyverno/kyverno/pkg/webhooks/resource/audit" + "github.com/kyverno/kyverno/pkg/webhooks/resource/mutation" "github.com/kyverno/kyverno/pkg/webhooks/resource/validation" webhookgenerate "github.com/kyverno/kyverno/pkg/webhooks/updaterequest" webhookutils "github.com/kyverno/kyverno/pkg/webhooks/utils" @@ -189,7 +189,9 @@ func (h *handlers) Mutate(logger logr.Logger, request *admissionv1.AdmissionRequ if err := enginectx.MutateResourceWithImageInfo(request.Object.Raw, policyContext.JSONContext); err != nil { logger.Error(err, "failed to patch images info to resource, policies that mutate images may be impacted") } - mutatePatches, mutateWarnings, err := h.applyMutatePolicies(logger, request, policyContext, mutatePolicies, startTime) + + mh := mutation.NewMutationHandler(logger, h.eventGen, h.openAPIController, h.nsLister) + mutatePatches, mutateWarnings, err := mh.HandleMutation(h.metricsConfig, request, mutatePolicies, policyContext, startTime) if err != nil { logger.Error(err, "mutation failed") return admissionutils.ResponseFailure(err.Error()) @@ -211,114 +213,6 @@ func (h *handlers) Mutate(logger logr.Logger, request *admissionv1.AdmissionRequ return admissionResponse } -func (h *handlers) applyMutatePolicies(logger logr.Logger, request *admissionv1.AdmissionRequest, policyContext *engine.PolicyContext, policies []kyvernov1.PolicyInterface, ts time.Time) ([]byte, []string, error) { - mutatePatches, mutateEngineResponses, err := h.handleMutation(logger, request, policyContext, policies) - if err != nil { - return nil, nil, err - } - - logger.V(6).Info("", "generated patches", string(mutatePatches)) - - admissionReviewLatencyDuration := int64(time.Since(ts)) - go webhookutils.RegisterAdmissionReviewDurationMetricMutate(logger, h.metricsConfig, string(request.Operation), mutateEngineResponses, admissionReviewLatencyDuration) - go webhookutils.RegisterAdmissionRequestsMetricMutate(logger, h.metricsConfig, string(request.Operation), mutateEngineResponses) - - warnings := webhookutils.GetWarningMessages(mutateEngineResponses) - return mutatePatches, warnings, nil -} - -// handleMutation handles mutating webhook admission request -// return value: generated patches, triggered policies, engine responses correspdonding to the triggered policies -func (h *handlers) handleMutation(logger logr.Logger, request *admissionv1.AdmissionRequest, policyContext *engine.PolicyContext, policies []kyvernov1.PolicyInterface) ([]byte, []*response.EngineResponse, error) { - if len(policies) == 0 { - return nil, nil, nil - } - - if isResourceDeleted(policyContext) && request.Operation == admissionv1.Update { - return nil, nil, nil - } - - var patches [][]byte - var engineResponses []*response.EngineResponse - - for _, policy := range policies { - spec := policy.GetSpec() - if !spec.HasMutate() { - continue - } - logger.V(3).Info("applying policy mutate rules", "policy", policy.GetName()) - policyContext.Policy = policy - engineResponse, policyPatches, err := h.applyMutation(request, policyContext, logger) - if err != nil { - return nil, nil, fmt.Errorf("mutation policy %s error: %v", policy.GetName(), err) - } - - if len(policyPatches) > 0 { - patches = append(patches, policyPatches...) - rules := engineResponse.GetSuccessRules() - if len(rules) != 0 { - logger.Info("mutation rules from policy applied successfully", "policy", policy.GetName(), "rules", rules) - } - } - - policyContext.NewResource = engineResponse.PatchedResource - engineResponses = append(engineResponses, engineResponse) - - // registering the kyverno_policy_results_total metric concurrently - go webhookutils.RegisterPolicyResultsMetricMutation(logger, h.metricsConfig, string(request.Operation), policy, *engineResponse) - // registering the kyverno_policy_execution_duration_seconds metric concurrently - go webhookutils.RegisterPolicyExecutionDurationMetricMutate(logger, h.metricsConfig, string(request.Operation), policy, *engineResponse) - } - - // generate annotations - if annPatches := utils.GenerateAnnotationPatches(engineResponses, logger); annPatches != nil { - patches = append(patches, annPatches...) - } - - if !isResourceDeleted(policyContext) { - events := webhookutils.GenerateEvents(engineResponses, false) - h.eventGen.Add(events...) - } - - logMutationResponse(patches, engineResponses, logger) - - // patches holds all the successful patches, if no patch is created, it returns nil - return jsonutils.JoinPatches(patches...), engineResponses, nil -} - -func logMutationResponse(patches [][]byte, engineResponses []*response.EngineResponse, logger logr.Logger) { - if len(patches) != 0 { - logger.V(4).Info("created patches", "count", len(patches)) - } - - // if any of the policies fails, print out the error - if !engineutils.IsResponseSuccessful(engineResponses) { - logger.Error(errors.New(getErrorMsg(engineResponses)), "failed to apply mutation rules on the resource, reporting policy violation") - } -} - -func (h *handlers) applyMutation(request *admissionv1.AdmissionRequest, policyContext *engine.PolicyContext, logger logr.Logger) (*response.EngineResponse, [][]byte, error) { - if request.Kind.Kind != "Namespace" && request.Namespace != "" { - policyContext.NamespaceLabels = common.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, h.nsLister, logger) - } - - engineResponse := engine.Mutate(policyContext) - policyPatches := engineResponse.GetPatches() - - if !engineResponse.IsSuccessful() && len(engineResponse.GetFailedRules()) > 0 { - return nil, nil, fmt.Errorf("failed to apply policy %s rules %v", policyContext.Policy.GetName(), engineResponse.GetFailedRules()) - } - - if engineResponse.PatchedResource.GetKind() != "*" { - err := h.openAPIController.ValidateResource(*engineResponse.PatchedResource.DeepCopy(), engineResponse.PatchedResource.GetAPIVersion(), engineResponse.PatchedResource.GetKind()) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to validate resource mutated by policy %s", policyContext.Policy.GetName()) - } - } - - return engineResponse, policyPatches, nil -} - func (h *handlers) applyImageVerifyPolicies(logger logr.Logger, request *admissionv1.AdmissionRequest, policyContext *engine.PolicyContext, policies []kyvernov1.PolicyInterface) ([]byte, []string, error) { ok, message, imagePatches, warnings := h.handleVerifyImages(logger, request, policyContext, policies) if !ok { diff --git a/pkg/webhooks/resource/mutation/mutation.go b/pkg/webhooks/resource/mutation/mutation.go new file mode 100644 index 0000000000..fe8e801830 --- /dev/null +++ b/pkg/webhooks/resource/mutation/mutation.go @@ -0,0 +1,188 @@ +package mutation + +import ( + "fmt" + "reflect" + "time" + + "github.com/go-logr/logr" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + "github.com/kyverno/kyverno/pkg/common" + "github.com/kyverno/kyverno/pkg/engine" + "github.com/kyverno/kyverno/pkg/engine/response" + "github.com/kyverno/kyverno/pkg/event" + "github.com/kyverno/kyverno/pkg/metrics" + "github.com/kyverno/kyverno/pkg/openapi" + "github.com/kyverno/kyverno/pkg/utils" + engineutils "github.com/kyverno/kyverno/pkg/utils/engine" + jsonutils "github.com/kyverno/kyverno/pkg/utils/json" + webhookutils "github.com/kyverno/kyverno/pkg/webhooks/utils" + "github.com/pkg/errors" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + corev1listers "k8s.io/client-go/listers/core/v1" +) + +type MutationHandler interface { + // HandleMutation 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 + HandleMutation( + *metrics.MetricsConfig, + *admissionv1.AdmissionRequest, + []kyvernov1.PolicyInterface, + *engine.PolicyContext, + // map[string]string, + time.Time, + ) ([]byte, []string, error) +} + +func NewMutationHandler( + log logr.Logger, + eventGen event.Interface, + openAPIController openapi.ValidateInterface, + nsLister corev1listers.NamespaceLister, +) MutationHandler { + return &mutationHandler{ + log: log, + eventGen: eventGen, + openAPIController: openAPIController, + nsLister: nsLister, + } +} + +type mutationHandler struct { + log logr.Logger + eventGen event.Interface + openAPIController openapi.ValidateInterface + nsLister corev1listers.NamespaceLister +} + +func (h *mutationHandler) HandleMutation( + metricsConfig *metrics.MetricsConfig, + request *admissionv1.AdmissionRequest, + policies []kyvernov1.PolicyInterface, + policyContext *engine.PolicyContext, + admissionRequestTimestamp time.Time, +) ([]byte, []string, error) { + mutatePatches, mutateEngineResponses, err := h.applyMutations(metricsConfig, request, policies, policyContext) + if err != nil { + return nil, nil, err + } + + h.log.V(6).Info("", "generated patches", string(mutatePatches)) + + admissionReviewLatencyDuration := int64(time.Since(admissionRequestTimestamp)) + go webhookutils.RegisterAdmissionReviewDurationMetricMutate(h.log, metricsConfig, string(request.Operation), mutateEngineResponses, admissionReviewLatencyDuration) + go webhookutils.RegisterAdmissionRequestsMetricMutate(h.log, metricsConfig, string(request.Operation), mutateEngineResponses) + + return mutatePatches, webhookutils.GetWarningMessages(mutateEngineResponses), nil +} + +// applyMutations handles mutating webhook admission request +// return value: generated patches, triggered policies, engine responses correspdonding to the triggered policies +func (v *mutationHandler) applyMutations( + metricsConfig *metrics.MetricsConfig, + request *admissionv1.AdmissionRequest, + policies []kyvernov1.PolicyInterface, + policyContext *engine.PolicyContext, +) ([]byte, []*response.EngineResponse, error) { + if len(policies) == 0 { + return nil, nil, nil + } + + if isResourceDeleted(policyContext) && request.Operation == admissionv1.Update { + return nil, nil, nil + } + + var patches [][]byte + var engineResponses []*response.EngineResponse + + for _, policy := range policies { + spec := policy.GetSpec() + if !spec.HasMutate() { + continue + } + v.log.V(3).Info("applying policy mutate rules", "policy", policy.GetName()) + policyContext.Policy = policy + engineResponse, policyPatches, err := v.applyMutation(request, policyContext) + if err != nil { + return nil, nil, fmt.Errorf("mutation policy %s error: %v", policy.GetName(), err) + } + + if len(policyPatches) > 0 { + patches = append(patches, policyPatches...) + rules := engineResponse.GetSuccessRules() + if len(rules) != 0 { + v.log.Info("mutation rules from policy applied successfully", "policy", policy.GetName(), "rules", rules) + } + } + + policyContext.NewResource = engineResponse.PatchedResource + engineResponses = append(engineResponses, engineResponse) + + // registering the kyverno_policy_results_total metric concurrently + go webhookutils.RegisterPolicyResultsMetricMutation(v.log, metricsConfig, string(request.Operation), policy, *engineResponse) + // registering the kyverno_policy_execution_duration_seconds metric concurrently + go webhookutils.RegisterPolicyExecutionDurationMetricMutate(v.log, metricsConfig, string(request.Operation), policy, *engineResponse) + } + + // generate annotations + if annPatches := utils.GenerateAnnotationPatches(engineResponses, v.log); annPatches != nil { + patches = append(patches, annPatches...) + } + + if !isResourceDeleted(policyContext) { + events := webhookutils.GenerateEvents(engineResponses, false) + v.eventGen.Add(events...) + } + + logMutationResponse(patches, engineResponses, v.log) + + // patches holds all the successful patches, if no patch is created, it returns nil + return jsonutils.JoinPatches(patches...), engineResponses, nil +} + +func (h *mutationHandler) applyMutation(request *admissionv1.AdmissionRequest, policyContext *engine.PolicyContext) (*response.EngineResponse, [][]byte, error) { + if request.Kind.Kind != "Namespace" && request.Namespace != "" { + policyContext.NamespaceLabels = common.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, h.nsLister, h.log) + } + + engineResponse := engine.Mutate(policyContext) + policyPatches := engineResponse.GetPatches() + + if !engineResponse.IsSuccessful() && len(engineResponse.GetFailedRules()) > 0 { + return nil, nil, fmt.Errorf("failed to apply policy %s rules %v", policyContext.Policy.GetName(), engineResponse.GetFailedRules()) + } + + if engineResponse.PatchedResource.GetKind() != "*" { + err := h.openAPIController.ValidateResource(*engineResponse.PatchedResource.DeepCopy(), engineResponse.PatchedResource.GetAPIVersion(), engineResponse.PatchedResource.GetKind()) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to validate resource mutated by policy %s", policyContext.Policy.GetName()) + } + } + + return engineResponse, policyPatches, nil +} + +func logMutationResponse(patches [][]byte, engineResponses []*response.EngineResponse, logger logr.Logger) { + if len(patches) != 0 { + logger.V(4).Info("created patches", "count", len(patches)) + } + + // if any of the policies fails, print out the error + if !engineutils.IsResponseSuccessful(engineResponses) { + logger.Error(errors.New(webhookutils.GetErrorMsg(engineResponses)), "failed to apply mutation rules on the resource, reporting policy violation") + } +} + +func isResourceDeleted(policyContext *engine.PolicyContext) bool { + var deletionTimeStamp *metav1.Time + if reflect.DeepEqual(policyContext.NewResource, unstructured.Unstructured{}) { + deletionTimeStamp = policyContext.NewResource.GetDeletionTimestamp() + } else { + deletionTimeStamp = policyContext.OldResource.GetDeletionTimestamp() + } + return deletionTimeStamp != nil +} diff --git a/pkg/webhooks/resource/utils.go b/pkg/webhooks/resource/utils.go index 2e44aa9883..0d41617f3b 100644 --- a/pkg/webhooks/resource/utils.go +++ b/pkg/webhooks/resource/utils.go @@ -2,7 +2,6 @@ package resource import ( "encoding/json" - "fmt" "strings" "github.com/go-logr/logr" @@ -50,24 +49,6 @@ func processResourceWithPatches(patch []byte, resource []byte, log logr.Logger) return resource } -func getErrorMsg(engineReponses []*response.EngineResponse) string { - var str []string - var resourceInfo string - for _, er := range engineReponses { - if !er.IsSuccessful() { - // resource in engineReponses is identical as this was called per admission request - resourceInfo = fmt.Sprintf("%s/%s/%s", er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name) - str = append(str, fmt.Sprintf("failed policy %s:", er.PolicyResponse.Policy.Name)) - for _, rule := range er.PolicyResponse.Rules { - if rule.Status != response.RuleStatusPass { - str = append(str, rule.ToString()) - } - } - } - } - return fmt.Sprintf("Resource %s %s", resourceInfo, strings.Join(str, ";")) -} - func hasAnnotations(context *engine.PolicyContext) bool { annotations := context.NewResource.GetAnnotations() return len(annotations) != 0 diff --git a/pkg/webhooks/utils/error.go b/pkg/webhooks/utils/error.go new file mode 100644 index 0000000000..01aca9390a --- /dev/null +++ b/pkg/webhooks/utils/error.go @@ -0,0 +1,26 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/kyverno/kyverno/pkg/engine/response" +) + +func GetErrorMsg(engineReponses []*response.EngineResponse) string { + var str []string + var resourceInfo string + for _, er := range engineReponses { + if !er.IsSuccessful() { + // resource in engineReponses is identical as this was called per admission request + resourceInfo = fmt.Sprintf("%s/%s/%s", er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name) + str = append(str, fmt.Sprintf("failed policy %s:", er.PolicyResponse.Policy.Name)) + for _, rule := range er.PolicyResponse.Rules { + if rule.Status != response.RuleStatusPass { + str = append(str, rule.ToString()) + } + } + } + } + return fmt.Sprintf("Resource %s %s", resourceInfo, strings.Join(str, ";")) +}