1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 07:57:07 +00:00
kyverno/pkg/webhooks/resource/handlers.go
Charles-Edouard Brétéché 018d45cb29
feat: add reports circuit breaker (#10499)
* feat: add reports circuit breaker

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* improve metrics and granularity

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
2024-06-25 11:16:30 +08:00

312 lines
12 KiB
Go

package resource
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/alitto/pond"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/client/clientset/versioned"
kyvernov1informers "github.com/kyverno/kyverno/pkg/client/informers/externalversions/kyverno/v1"
kyvernov1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v1"
kyvernov2listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v2"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
"github.com/kyverno/kyverno/pkg/d4f"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/jmespath"
"github.com/kyverno/kyverno/pkg/engine/policycontext"
"github.com/kyverno/kyverno/pkg/event"
"github.com/kyverno/kyverno/pkg/metrics"
"github.com/kyverno/kyverno/pkg/policycache"
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/handlers"
"github.com/kyverno/kyverno/pkg/webhooks/resource/imageverification"
"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"
"k8s.io/apimachinery/pkg/runtime/schema"
corev1listers "k8s.io/client-go/listers/core/v1"
)
type resourceHandlers struct {
// clients
client dclient.Interface
kyvernoClient versioned.Interface
engine engineapi.Engine
// config
configuration config.Configuration
metricsConfig metrics.MetricsConfigManager
// cache
pCache policycache.Cache
// listers
nsLister corev1listers.NamespaceLister
urLister kyvernov2listers.UpdateRequestNamespaceLister
cpolLister kyvernov1listers.ClusterPolicyLister
polLister kyvernov1listers.PolicyLister
urGenerator webhookgenerate.Generator
eventGen event.Interface
pcBuilder webhookutils.PolicyContextBuilder
admissionReports bool
backgroundServiceAccountName string
auditPool *pond.WorkerPool
reportsBreaker d4f.Breaker
}
func NewHandlers(
engine engineapi.Engine,
client dclient.Interface,
kyvernoClient versioned.Interface,
configuration config.Configuration,
metricsConfig metrics.MetricsConfigManager,
pCache policycache.Cache,
nsLister corev1listers.NamespaceLister,
urLister kyvernov2listers.UpdateRequestNamespaceLister,
cpolInformer kyvernov1informers.ClusterPolicyInformer,
polInformer kyvernov1informers.PolicyInformer,
urGenerator webhookgenerate.Generator,
eventGen event.Interface,
admissionReports bool,
backgroundServiceAccountName string,
jp jmespath.Interface,
maxAuditWorkers int,
maxAuditCapacity int,
reportsBreaker d4f.Breaker,
) webhooks.ResourceHandlers {
return &resourceHandlers{
engine: engine,
client: client,
kyvernoClient: kyvernoClient,
configuration: configuration,
metricsConfig: metricsConfig,
pCache: pCache,
nsLister: nsLister,
urLister: urLister,
cpolLister: cpolInformer.Lister(),
polLister: polInformer.Lister(),
urGenerator: urGenerator,
eventGen: eventGen,
pcBuilder: webhookutils.NewPolicyContextBuilder(configuration, jp),
admissionReports: admissionReports,
backgroundServiceAccountName: backgroundServiceAccountName,
auditPool: pond.New(maxAuditWorkers, maxAuditCapacity, pond.Strategy(pond.Lazy())),
reportsBreaker: reportsBreaker,
}
}
func (h *resourceHandlers) Validate(ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, failurePolicy string, startTime time.Time) handlers.AdmissionResponse {
kind := request.Kind.Kind
logger = logger.WithValues("kind", kind).WithValues("URLParams", request.URLParams)
logger.V(4).Info("received an admission request in validating webhook")
policies, mutatePolicies, generatePolicies, _, err := h.retrieveAndCategorizePolicies(ctx, logger, request, failurePolicy, false)
if err != nil {
return errorResponse(logger, request.UID, err, "failed to fetch policy with key")
}
if len(policies) == 0 && len(mutatePolicies) == 0 && len(generatePolicies) == 0 {
logger.V(4).Info("no policies matched admission request")
}
logger.V(4).Info("processing policies for validate admission request", "validate", len(policies), "mutate", len(mutatePolicies), "generate", len(generatePolicies))
vh := validation.NewValidationHandler(
logger,
h.kyvernoClient,
h.engine,
h.pCache,
h.pcBuilder,
h.eventGen,
h.admissionReports,
h.metricsConfig,
h.configuration,
h.nsLister,
h.reportsBreaker,
)
var wg sync.WaitGroup
var ok bool
var msg string
var warnings []string
wg.Add(1)
go func() {
defer wg.Done()
ok, msg, warnings = vh.HandleValidationEnforce(ctx, request, policies, startTime)
}()
go h.auditPool.Submit(func() {
vh.HandleValidationAudit(ctx, request)
})
if !admissionutils.IsDryRun(request.AdmissionRequest) {
h.handleBackgroundApplies(ctx, logger, request, generatePolicies, mutatePolicies, startTime, nil)
}
if len(policies) == 0 {
return admissionutils.ResponseSuccess(request.UID)
}
wg.Wait()
if !ok {
logger.Info("admission request denied")
return admissionutils.Response(request.UID, errors.New(msg), warnings...)
}
return admissionutils.ResponseSuccess(request.UID, warnings...)
}
func (h *resourceHandlers) Mutate(ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, failurePolicy string, startTime time.Time) handlers.AdmissionResponse {
kind := request.Kind.Kind
logger = logger.WithValues("kind", kind).WithValues("URLParams", request.URLParams)
logger.V(4).Info("received an admission request in mutating webhook")
_, mutatePolicies, _, verifyImagesPolicies, err := h.retrieveAndCategorizePolicies(ctx, logger, request, failurePolicy, true)
if err != nil {
return errorResponse(logger, request.UID, err, "failed to fetch policy with key")
}
if len(mutatePolicies) == 0 && len(verifyImagesPolicies) == 0 {
logger.V(4).Info("no policies matched mutate admission request")
return admissionutils.ResponseSuccess(request.UID)
}
logger.V(4).Info("processing policies for mutate admission request", "mutatePolicies", len(mutatePolicies), "verifyImagesPolicies", len(verifyImagesPolicies))
policyContext, err := h.pcBuilder.Build(request.AdmissionRequest, request.Roles, request.ClusterRoles, request.GroupVersionKind)
if err != nil {
logger.Error(err, "failed to build policy context")
return admissionutils.Response(request.UID, err)
}
mh := mutation.NewMutationHandler(logger, h.engine, h.eventGen, h.nsLister, h.metricsConfig)
mutatePatches, mutateWarnings, err := mh.HandleMutation(ctx, request.AdmissionRequest, mutatePolicies, policyContext, startTime)
if err != nil {
logger.Error(err, "mutation failed")
return admissionutils.Response(request.UID, err)
}
newRequest := patchRequest(mutatePatches, request.AdmissionRequest, logger)
// rebuild context to process images updated via mutate policies
policyContext, err = h.pcBuilder.Build(newRequest, request.Roles, request.ClusterRoles, request.GroupVersionKind)
if err != nil {
logger.Error(err, "failed to build policy context")
return admissionutils.Response(request.UID, err)
}
ivh := imageverification.NewImageVerificationHandler(
logger,
h.kyvernoClient,
h.engine,
h.eventGen,
h.admissionReports,
h.configuration,
h.nsLister,
h.reportsBreaker,
)
imagePatches, imageVerifyWarnings, err := ivh.Handle(ctx, newRequest, verifyImagesPolicies, policyContext)
if err != nil {
logger.Error(err, "image verification failed")
return admissionutils.Response(request.UID, err)
}
patch := jsonutils.JoinPatches(mutatePatches, imagePatches)
var warnings []string
warnings = append(warnings, mutateWarnings...)
warnings = append(warnings, imageVerifyWarnings...)
return admissionutils.MutationResponse(request.UID, patch, warnings...)
}
func (h *resourceHandlers) retrieveAndCategorizePolicies(
ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, failurePolicy string, mutation bool) (
[]kyvernov1.PolicyInterface, []kyvernov1.PolicyInterface, []kyvernov1.PolicyInterface, []kyvernov1.PolicyInterface, error,
) {
var policies, mutatePolicies, generatePolicies, imageVerifyValidatePolicies []kyvernov1.PolicyInterface
if request.URLParams == "" {
gvr := schema.GroupVersionResource(request.Resource)
policies = filterPolicies(ctx, failurePolicy, h.pCache.GetPolicies(policycache.ValidateEnforce, gvr, request.SubResource, request.Namespace)...)
mutatePolicies = filterPolicies(ctx, failurePolicy, h.pCache.GetPolicies(policycache.Mutate, gvr, request.SubResource, request.Namespace)...)
generatePolicies = filterPolicies(ctx, failurePolicy, h.pCache.GetPolicies(policycache.Generate, gvr, request.SubResource, request.Namespace)...)
if mutation {
imageVerifyValidatePolicies = filterPolicies(ctx, failurePolicy, h.pCache.GetPolicies(policycache.VerifyImagesMutate, gvr, request.SubResource, request.Namespace)...)
} else {
imageVerifyValidatePolicies = filterPolicies(ctx, failurePolicy, h.pCache.GetPolicies(policycache.VerifyImagesValidate, gvr, request.SubResource, request.Namespace)...)
policies = append(policies, imageVerifyValidatePolicies...)
}
} else {
meta := strings.Split(request.URLParams, "/")
polName := meta[1]
polNamespace := ""
if len(meta) >= 3 {
polNamespace = meta[1]
polName = meta[2]
}
var policy kyvernov1.PolicyInterface
var err error
if polNamespace == "" {
policy, err = h.cpolLister.Get(polName)
} else {
policy, err = h.polLister.Policies(polNamespace).Get(polName)
}
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("key %s/%s: %v", polNamespace, polName, err)
}
filteredPolicies := filterPolicies(ctx, failurePolicy, policy)
if len(filteredPolicies) == 0 {
logger.V(4).Info("no policy found with key", "namespace", polNamespace, "name", polName)
return nil, nil, nil, nil, nil
}
policy = filteredPolicies[0]
spec := policy.GetSpec()
if spec.HasValidate() {
policies = append(policies, policy)
}
if spec.HasGenerate() {
generatePolicies = append(generatePolicies, policy)
}
if spec.HasMutate() {
mutatePolicies = append(mutatePolicies, policy)
}
if spec.HasVerifyImages() {
policies = append(policies, policy)
}
}
return policies, mutatePolicies, generatePolicies, imageVerifyValidatePolicies, nil
}
func (h *resourceHandlers) buildPolicyContextFromAdmissionRequest(logger logr.Logger, request handlers.AdmissionRequest) (*policycontext.PolicyContext, error) {
policyContext, err := h.pcBuilder.Build(request.AdmissionRequest, request.Roles, request.ClusterRoles, request.GroupVersionKind)
if err != nil {
return nil, err
}
namespaceLabels := make(map[string]string)
if request.Kind.Kind != "Namespace" && request.Namespace != "" {
namespaceLabels = engineutils.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, h.nsLister, logger)
}
policyContext = policyContext.WithNamespaceLabels(namespaceLabels)
return policyContext, nil
}
func filterPolicies(ctx context.Context, failurePolicy string, policies ...kyvernov1.PolicyInterface) []kyvernov1.PolicyInterface {
var results []kyvernov1.PolicyInterface
for _, policy := range policies {
if failurePolicy == "fail" {
if policy.GetSpec().GetFailurePolicy(ctx) == kyvernov1.Fail {
results = append(results, policy)
}
} else if failurePolicy == "ignore" {
if policy.GetSpec().GetFailurePolicy(ctx) == kyvernov1.Ignore {
results = append(results, policy)
}
} else {
results = append(results, policy)
}
}
return results
}