1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 16:06:56 +00:00
kyverno/pkg/webhooks/handlers.go
Charles-Edouard Brétéché 6e813a6b9e
refactor: webhooks package (#3516)
* refactor: use more policy interface

Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>

* refactor: migrate to policy interface

Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>

* refactor: webhooks package

Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
2022-03-31 23:34:10 +08:00

225 lines
9.4 KiB
Go

package webhooks
import (
"fmt"
"net/http"
"reflect"
"strings"
"time"
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/common"
"github.com/kyverno/kyverno/pkg/engine"
enginectx "github.com/kyverno/kyverno/pkg/engine/context"
policyvalidate "github.com/kyverno/kyverno/pkg/policy"
"github.com/kyverno/kyverno/pkg/policycache"
"github.com/kyverno/kyverno/pkg/policymutation"
"github.com/kyverno/kyverno/pkg/userinfo"
"github.com/kyverno/kyverno/pkg/utils"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
"github.com/kyverno/kyverno/pkg/webhooks/handlers"
"k8s.io/api/admission/v1beta1"
)
// TODO: use admission review sub resource ?
func isStatusUpdate(old, new kyverno.PolicyInterface) bool {
if !reflect.DeepEqual(old.GetAnnotations(), new.GetAnnotations()) {
return false
}
if !reflect.DeepEqual(old.GetLabels(), new.GetLabels()) {
return false
}
if !reflect.DeepEqual(old.GetSpec(), new.GetSpec()) {
return false
}
return true
}
func errorResponse(logger logr.Logger, err error, message string) *v1beta1.AdmissionResponse {
logger.Error(err, message)
return admissionutils.ResponseFailure(false, message+": "+err.Error())
}
func setupLogger(logger logr.Logger, name string, request *v1beta1.AdmissionRequest) logr.Logger {
return logger.WithName("MutateWebhook").WithValues(
"uid", request.UID,
"kind", request.Kind,
"namespace", request.Namespace,
"name", request.Name,
"operation", request.Operation,
"gvk", request.Kind.String(),
)
}
func (ws *WebhookServer) admissionHandler(filter bool, inner handlers.AdmissionHandler) http.HandlerFunc {
if filter {
inner = handlers.Filter(ws.configHandler, inner)
}
return handlers.Monitor(ws.webhookMonitor, handlers.Admission(ws.log, inner))
}
func (ws *WebhookServer) policyMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
logger := setupLogger(ws.log, "policy mutation", request)
policy, oldPolicy, err := admissionutils.GetPolicies(request)
if err != nil {
logger.Error(err, "failed to unmarshal policies from admission request")
return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to default value, check kyverno controller logs for details: %v", err))
}
if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
logger.V(4).Info("skip policy mutation on status update")
return admissionutils.Response(true)
}
startTime := time.Now()
logger.V(3).Info("start policy change mutation")
defer logger.V(3).Info("finished policy change mutation", "time", time.Since(startTime).String())
// Generate JSON Patches for defaults
if patches, updateMsgs := policymutation.GenerateJSONPatchesForDefaults(policy, logger); len(patches) != 0 {
return admissionutils.ResponseWithMessageAndPatch(true, strings.Join(updateMsgs, "'"), patches)
}
return admissionutils.Response(true)
}
//policyValidation performs the validation check on policy resource
func (ws *WebhookServer) policyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
logger := setupLogger(ws.log, "policy validation", request)
policy, oldPolicy, err := admissionutils.GetPolicies(request)
if err != nil {
logger.Error(err, "failed to unmarshal policies from admission request")
return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to validate policy, check kyverno controller logs for details: %v", err))
}
if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
logger.V(4).Info("skip policy validation on status update")
return admissionutils.Response(true)
}
startTime := time.Now()
logger.V(3).Info("start policy change validation")
defer logger.V(3).Info("finished policy change validation", "time", time.Since(startTime).String())
response, err := policyvalidate.Validate(policy, ws.client, false, ws.openAPIController)
if err != nil {
logger.Error(err, "policy validation errors")
return admissionutils.ResponseWithMessage(true, err.Error())
}
if response != nil && len(response.Warnings) != 0 {
return response
}
return admissionutils.Response(true)
}
// resourceMutation mutates resource
func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
logger := setupLogger(ws.log, "resource mutation", request)
if excludeKyvernoResources(request.Kind.Kind) {
return admissionutils.ResponseSuccess(true, "")
}
if request.Operation == v1beta1.Delete {
resource, err := utils.ConvertResource(request.OldObject.Raw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
if err == nil {
ws.prGenerator.Add(buildDeletionPrInfo(resource))
} else {
logger.Info(fmt.Sprintf("Converting oldObject failed: %v", err))
}
return admissionutils.ResponseSuccess(true, "")
}
logger.V(4).Info("received an admission request in mutating webhook")
requestTime := time.Now().Unix()
mutatePolicies := ws.pCache.GetPolicies(policycache.Mutate, request.Kind.Kind, request.Namespace)
verifyImagesPolicies := ws.pCache.GetPolicies(policycache.VerifyImages, request.Kind.Kind, request.Namespace)
if len(mutatePolicies) == 0 && len(verifyImagesPolicies) == 0 {
logger.V(4).Info("no policies matched admission request")
return admissionutils.ResponseSuccess(true, "")
}
addRoles := containsRBACInfo(mutatePolicies)
policyContext, err := ws.buildPolicyContext(request, addRoles)
if err != nil {
logger.Error(err, "failed to build policy context")
return admissionutils.ResponseFailure(false, err.Error())
}
// update container images to a canonical form
if err := enginectx.MutateResourceWithImageInfo(request.Object.Raw, policyContext.JSONContext); err != nil {
ws.log.Error(err, "failed to patch images info to resource, policies that mutate images may be impacted")
}
mutatePatches := ws.applyMutatePolicies(request, policyContext, mutatePolicies, requestTime, logger)
newRequest := patchRequest(mutatePatches, request, logger)
imagePatches, err := ws.applyImageVerifyPolicies(newRequest, policyContext, verifyImagesPolicies, logger)
if err != nil {
logger.Error(err, "image verification failed")
return admissionutils.ResponseFailure(false, err.Error())
}
var patches = append(mutatePatches, imagePatches...)
return admissionutils.ResponseSuccessWithPatch(true, "", patches)
}
func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
logger := setupLogger(ws.log, "resource validation", request)
if request.Operation == v1beta1.Delete {
ws.handleDelete(request)
}
if excludeKyvernoResources(request.Kind.Kind) {
return admissionutils.ResponseSuccess(true, "")
}
logger.V(6).Info("received an admission request in validating webhook")
// timestamp at which this admission request got triggered
requestTime := time.Now().Unix()
policies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, "")
// Get namespace policies from the cache for the requested resource namespace
nsPolicies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, request.Namespace)
policies = append(policies, nsPolicies...)
generatePolicies := ws.pCache.GetPolicies(policycache.Generate, request.Kind.Kind, request.Namespace)
if len(generatePolicies) == 0 && request.Operation == v1beta1.Update {
// handle generate source resource updates
go ws.handleUpdatesForGenerateRules(request, []kyverno.PolicyInterface{})
}
var roles, clusterRoles []string
if containsRBACInfo(policies, generatePolicies) {
var err error
roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request, ws.configHandler)
if err != nil {
return errorResponse(logger, err, "failed to fetch RBAC data")
}
}
userRequestInfo := kyverno.RequestInfo{
Roles: roles,
ClusterRoles: clusterRoles,
AdmissionUserInfo: *request.UserInfo.DeepCopy(),
}
ctx, err := newVariablesContext(request, &userRequestInfo)
if err != nil {
return errorResponse(logger, err, "failed create policy rule context")
}
namespaceLabels := make(map[string]string)
if request.Kind.Kind != "Namespace" && request.Namespace != "" {
namespaceLabels = common.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, ws.nsLister, logger)
}
newResource, oldResource, err := utils.ExtractResources(nil, request)
if err != nil {
return errorResponse(logger, err, "failed create parse resource")
}
if err := ctx.AddImageInfo(&newResource); err != nil {
return errorResponse(logger, err, "failed add image information to policy rule context")
}
policyContext := &engine.PolicyContext{
NewResource: newResource,
OldResource: oldResource,
AdmissionInfo: userRequestInfo,
ExcludeGroupRole: ws.configHandler.GetExcludeGroupRole(),
ExcludeResourceFunc: ws.configHandler.ToFilter,
JSONContext: ctx,
Client: ws.client,
}
vh := &validationHandler{
log: ws.log,
eventGen: ws.eventGen,
prGenerator: ws.prGenerator,
}
ok, msg := vh.handleValidation(ws.promConfig, request, policies, policyContext, namespaceLabels, requestTime)
if !ok {
logger.Info("admission request denied")
return admissionutils.ResponseFailure(false, msg)
}
// push admission request to audit handler, this won't block the admission request
ws.auditHandler.Add(request.DeepCopy())
// process generate policies
ws.applyGeneratePolicies(request, policyContext, generatePolicies, requestTime, logger)
return admissionutils.ResponseSuccess(true, "")
}