1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-15 17:51:20 +00:00
kyverno/pkg/webhooks/server.go

506 lines
17 KiB
Go
Raw Normal View History

2019-05-13 18:33:01 +00:00
package webhooks
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"k8s.io/apimachinery/pkg/labels"
2020-03-17 18:05:20 +00:00
"github.com/go-logr/logr"
2020-04-27 13:08:03 +00:00
"github.com/julienschmidt/httprouter"
2020-04-10 17:54:54 +00:00
v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/checker"
2019-08-17 16:58:14 +00:00
kyvernoclient "github.com/nirmata/kyverno/pkg/client/clientset/versioned"
kyvernoinformer "github.com/nirmata/kyverno/pkg/client/informers/externalversions/kyverno/v1"
2019-11-13 21:41:08 +00:00
kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1"
"github.com/nirmata/kyverno/pkg/config"
client "github.com/nirmata/kyverno/pkg/dclient"
context2 "github.com/nirmata/kyverno/pkg/engine/context"
2019-06-27 01:04:50 +00:00
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/openapi"
"github.com/nirmata/kyverno/pkg/policystatus"
2019-11-12 22:41:29 +00:00
"github.com/nirmata/kyverno/pkg/policyviolation"
tlsutils "github.com/nirmata/kyverno/pkg/tls"
2019-11-11 22:52:09 +00:00
userinfo "github.com/nirmata/kyverno/pkg/userinfo"
"github.com/nirmata/kyverno/pkg/utils"
"github.com/nirmata/kyverno/pkg/webhookconfig"
"github.com/nirmata/kyverno/pkg/webhooks/generate"
v1beta1 "k8s.io/api/admission/v1beta1"
2019-08-24 01:34:23 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
rbacinformer "k8s.io/client-go/informers/rbac/v1"
rbaclister "k8s.io/client-go/listers/rbac/v1"
2019-08-12 17:02:07 +00:00
"k8s.io/client-go/tools/cache"
)
// WebhookServer contains configured TLS server with MutationWebhook.
type WebhookServer struct {
2019-11-15 23:59:37 +00:00
server http.Server
client *client.Client
kyvernoClient *kyvernoclient.Clientset
2019-11-15 23:59:37 +00:00
// list/get cluster policy resource
pLister kyvernolister.ClusterPolicyLister
2019-11-15 23:59:37 +00:00
// returns true if the cluster policy store has synced atleast
pSynced cache.InformerSynced
2019-11-15 23:59:37 +00:00
// list/get role binding resource
rbLister rbaclister.RoleBindingLister
2019-11-15 23:59:37 +00:00
// return true if role bining store has synced atleast once
rbSynced cache.InformerSynced
2019-11-15 23:59:37 +00:00
// list/get cluster role binding resource
crbLister rbaclister.ClusterRoleBindingLister
2019-11-15 23:59:37 +00:00
// return true if cluster role binding store has synced atleast once
crbSynced cache.InformerSynced
2019-11-15 23:59:37 +00:00
// generate events
eventGen event.Interface
2019-11-15 23:59:37 +00:00
// webhook registration client
webhookRegistrationClient *webhookconfig.WebhookRegistrationClient
// API to send policy stats for aggregation
statusListener policystatus.Listener
2019-10-19 00:38:46 +00:00
// helpers to validate against current loaded configuration
configHandler config.Interface
2019-10-19 00:38:46 +00:00
// channel for cleanup notification
cleanUp chan<- struct{}
// last request time
lastReqTime *checker.LastReqTime
2019-11-12 22:41:29 +00:00
// policy violation generator
pvGenerator policyviolation.GeneratorInterface
// generate request generator
grGenerator *generate.Generator
resourceWebhookWatcher *webhookconfig.ResourceWebhookRegister
2020-03-17 18:05:20 +00:00
log logr.Logger
2020-03-27 13:36:06 +00:00
openAPIController *openapi.Controller
}
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
// Policy Controller and Kubernetes Client should be initialized in configuration
func NewWebhookServer(
kyvernoClient *kyvernoclient.Clientset,
client *client.Client,
tlsPair *tlsutils.TlsPemPair,
pInformer kyvernoinformer.ClusterPolicyInformer,
rbInformer rbacinformer.RoleBindingInformer,
crbInformer rbacinformer.ClusterRoleBindingInformer,
eventGen event.Interface,
webhookRegistrationClient *webhookconfig.WebhookRegistrationClient,
statusSync policystatus.Listener,
configHandler config.Interface,
pvGenerator policyviolation.GeneratorInterface,
grGenerator *generate.Generator,
resourceWebhookWatcher *webhookconfig.ResourceWebhookRegister,
2020-03-17 18:05:20 +00:00
cleanUp chan<- struct{},
log logr.Logger,
2020-03-27 13:36:06 +00:00
openAPIController *openapi.Controller,
2020-03-17 18:05:20 +00:00
) (*WebhookServer, error) {
if tlsPair == nil {
return nil, errors.New("NewWebhookServer is not initialized properly")
}
var tlsConfig tls.Config
pair, err := tls.X509KeyPair(tlsPair.Certificate, tlsPair.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{pair}
ws := &WebhookServer{
client: client,
kyvernoClient: kyvernoClient,
pLister: pInformer.Lister(),
pSynced: pInformer.Informer().HasSynced,
rbLister: rbInformer.Lister(),
rbSynced: rbInformer.Informer().HasSynced,
crbLister: crbInformer.Lister(),
crbSynced: crbInformer.Informer().HasSynced,
eventGen: eventGen,
webhookRegistrationClient: webhookRegistrationClient,
statusListener: statusSync,
configHandler: configHandler,
cleanUp: cleanUp,
lastReqTime: resourceWebhookWatcher.LastReqTime,
pvGenerator: pvGenerator,
grGenerator: grGenerator,
resourceWebhookWatcher: resourceWebhookWatcher,
2020-03-17 18:05:20 +00:00
log: log,
2020-03-27 13:36:06 +00:00
openAPIController: openAPIController,
}
2020-04-27 13:08:03 +00:00
mux := httprouter.New()
mux.HandlerFunc("POST", config.MutatingWebhookServicePath, ws.handlerFunc(ws.resourceMutation, true))
mux.HandlerFunc("POST", config.ValidatingWebhookServicePath, ws.handlerFunc(ws.resourceValidation, true))
mux.HandlerFunc("POST", config.PolicyMutatingWebhookServicePath, ws.handlerFunc(ws.policyMutation, true))
mux.HandlerFunc("POST", config.PolicyValidatingWebhookServicePath, ws.handlerFunc(ws.policyValidation, true))
mux.HandlerFunc("POST", config.VerifyMutatingWebhookServicePath, ws.handlerFunc(ws.verifyHandler, false))
// Handle Liveness responds to a Kubernetes Liveness probe
// Fail this request if Kubernetes should restart this instance
mux.HandlerFunc("GET", config.LivenessServicePath, func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.WriteHeader(http.StatusOK)
})
// Handle Readiness responds to a Kubernetes Readiness probe
// Fail this request if this instance can't accept traffic, but Kubernetes shouldn't restart it
mux.HandlerFunc("GET", config.ReadinessServicePath, func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.WriteHeader(http.StatusOK)
})
ws.server = http.Server{
Addr: ":443", // Listen on port for HTTPS requests
TLSConfig: &tlsConfig,
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
return ws, nil
}
2020-03-29 02:06:18 +00:00
func (ws *WebhookServer) handlerFunc(handler func(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse, filter bool) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
2020-03-29 02:06:18 +00:00
startTime := time.Now()
ws.lastReqTime.SetTime(startTime)
admissionReview := ws.bodyToAdmissionReview(r, rw)
2020-03-29 02:06:18 +00:00
if admissionReview == nil {
ws.log.Info("failed to parse admission review request", "request", r)
2020-03-29 02:06:18 +00:00
return
2020-03-25 05:23:03 +00:00
}
2020-03-29 02:11:58 +00:00
logger := ws.log.WithValues("kind", admissionReview.Request.Kind, "namespace", admissionReview.Request.Namespace, "name", admissionReview.Request.Name)
2020-03-29 02:06:18 +00:00
admissionReview.Response = &v1beta1.AdmissionResponse{
Allowed: true,
UID: admissionReview.Request.UID,
2020-03-25 05:23:03 +00:00
}
2020-03-29 02:06:18 +00:00
// Do not process the admission requests for kinds that are in filterKinds for filtering
request := admissionReview.Request
if filter && ws.configHandler.ToFilter(request.Kind.Kind, request.Namespace, request.Name) {
writeResponse(rw, admissionReview)
2020-03-29 02:06:18 +00:00
return
}
2020-04-22 14:45:16 +00:00
admissionReview.Response = handler(request)
writeResponse(rw, admissionReview)
2020-05-19 01:30:39 +00:00
logger.V(4).Info("request processed", "processingTime", time.Since(startTime).Milliseconds())
return
}
}
func writeResponse(rw http.ResponseWriter, admissionReview *v1beta1.AdmissionReview) {
responseJSON, err := json.Marshal(admissionReview)
if err != nil {
http.Error(rw, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := rw.Write(responseJSON); err != nil {
http.Error(rw, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
}
}
2020-04-10 17:54:54 +00:00
func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
2020-05-19 03:01:20 +00:00
if excludeKyvernoResources(request.Kind.Kind) {
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Success",
},
}
}
logger := ws.log.WithName("resourceMutation").WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation)
policies, err := ws.pLister.ListResources(labels.NewSelector())
if err != nil {
// Unable to connect to policy Lister to access policies
2020-03-17 18:05:20 +00:00
logger.Error(err, "failed to list policies. Policies are NOT being applied")
return &v1beta1.AdmissionResponse{Allowed: true}
}
2019-11-11 22:52:09 +00:00
// getRoleRef only if policy has roles/clusterroles defined
2020-04-10 17:54:54 +00:00
var roles, clusterRoles []string
2019-11-11 22:52:09 +00:00
if containRBACinfo(policies) {
roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request)
2019-11-11 22:52:09 +00:00
if err != nil {
// TODO(shuting): continue apply policy if error getting roleRef?
2020-03-17 18:05:20 +00:00
logger.Error(err, "failed to get RBAC infromation for request")
2019-11-11 22:52:09 +00:00
}
2019-11-09 02:56:24 +00:00
}
// convert RAW to unstructured
resource, err := utils.ConvertResource(request.Object.Raw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
if err != nil {
2020-03-17 18:05:20 +00:00
logger.Error(err, "failed to convert RAW resource to unstructured format")
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: "Failure",
Message: err.Error(),
},
}
}
2020-04-10 17:54:54 +00:00
userRequestInfo := v1.RequestInfo{
Roles: roles,
ClusterRoles: clusterRoles,
AdmissionUserInfo: request.UserInfo}
// build context
ctx := context2.NewContext()
err = ctx.AddRequest(request)
if err != nil {
logger.Error(err, "failed to load incoming request in context")
}
2020-04-10 17:54:54 +00:00
err = ctx.AddUserInfo(userRequestInfo)
if err != nil {
logger.Error(err, "failed to load userInfo in context")
}
err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username)
if err != nil {
logger.Error(err, "failed to load service account in context")
}
var patches []byte
patchedResource := request.Object.Raw
2020-05-19 01:30:39 +00:00
higherVersion := utils.HigherThanKubernetesVersion(ws.client, ws.log, 1, 14, 0)
if higherVersion {
// MUTATION
// mutation failure should not block the resource creation
// any mutation failure is reported as the violation
2020-05-20 20:43:12 +00:00
patches = ws.HandleMutation(request, resource, policies, ctx, userRequestInfo)
2020-05-21 00:08:30 +00:00
logger.V(6).Info("", "generated patches", string(patches))
// patch the resource with patches before handling validation rules
patchedResource = processResourceWithPatches(patches, request.Object.Raw, logger)
2020-05-21 00:08:30 +00:00
logger.V(6).Info("", "patchedResource", string(patchedResource))
if ws.resourceWebhookWatcher != nil && ws.resourceWebhookWatcher.RunValidationInMutatingWebhook == "true" {
// VALIDATION
ok, msg := ws.HandleValidation(request, policies, patchedResource, ctx, userRequestInfo)
if !ok {
logger.Info("admission request denied")
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: "Failure",
Message: msg,
},
}
2020-01-11 13:03:11 +00:00
}
2019-08-24 01:34:23 +00:00
}
2020-05-21 00:08:30 +00:00
} else {
2020-05-21 15:29:35 +00:00
logger.Info("mutate and validate rules are not supported prior to Kubernetes 1.14.0")
}
// GENERATE
// Only applied during resource creation and update
// Success -> Generate Request CR created successsfully
// Failed -> Failed to create Generate Request CR
if request.Operation == v1beta1.Create || request.Operation == v1beta1.Update {
2020-04-10 17:54:54 +00:00
ok, msg := ws.HandleGenerate(request, policies, ctx, userRequestInfo)
if !ok {
2020-03-17 18:05:20 +00:00
logger.Info("admission request denied")
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: "Failure",
Message: msg,
},
}
}
}
2019-08-24 01:34:23 +00:00
// Succesfful processing of mutation & validation rules in policy
patchType := v1beta1.PatchTypeJSONPatch
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Success",
},
Patch: patches,
PatchType: &patchType,
}
}
2020-04-10 17:54:54 +00:00
func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
2020-05-21 00:08:30 +00:00
logger := ws.log.WithName("resourceValidation").WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation)
if ok := utils.HigherThanKubernetesVersion(ws.client, ws.log, 1, 14, 0); !ok {
2020-05-21 15:29:35 +00:00
logger.Info("mutate and validate rules are not supported prior to Kubernetes 1.14.0")
2020-05-21 00:08:30 +00:00
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Success",
},
}
}
2020-05-19 03:01:20 +00:00
if excludeKyvernoResources(request.Kind.Kind) {
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Success",
},
}
}
policies, err := ws.pLister.ListResources(labels.NewSelector())
2020-01-11 13:03:11 +00:00
if err != nil {
// Unable to connect to policy Lister to access policies
2020-03-17 18:05:20 +00:00
logger.Error(err, "failed to list policies. Policies are NOT being applied")
2020-01-11 13:03:11 +00:00
return &v1beta1.AdmissionResponse{Allowed: true}
}
var roles, clusterRoles []string
// getRoleRef only if policy has roles/clusterroles defined
if containRBACinfo(policies) {
roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request)
2020-01-11 13:03:11 +00:00
if err != nil {
// TODO(shuting): continue apply policy if error getting roleRef?
logger.Error(err, "failed to get RBAC information for request")
2020-01-11 13:03:11 +00:00
}
}
2020-04-10 17:54:54 +00:00
userRequestInfo := v1.RequestInfo{
Roles: roles,
ClusterRoles: clusterRoles,
AdmissionUserInfo: request.UserInfo}
// build context
ctx := context2.NewContext()
err = ctx.AddRequest(request)
if err != nil {
logger.Error(err, "failed to load incoming request in context")
}
2020-04-10 17:54:54 +00:00
err = ctx.AddUserInfo(userRequestInfo)
if err != nil {
logger.Error(err, "failed to load userInfo in context")
}
err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username)
if err != nil {
logger.Error(err, "failed to load service account in context")
}
2020-05-21 00:08:30 +00:00
ok, msg := ws.HandleValidation(request, policies, nil, ctx, userRequestInfo)
if !ok {
logger.Info("admission request denied")
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Status: "Failure",
Message: msg,
},
2020-01-11 13:03:11 +00:00
}
}
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Success",
},
}
}
// RunAsync TLS server in separate thread and returns control immediately
2019-10-25 21:55:48 +00:00
func (ws *WebhookServer) RunAsync(stopCh <-chan struct{}) {
2020-03-17 18:05:20 +00:00
logger := ws.log
if !cache.WaitForCacheSync(stopCh, ws.pSynced, ws.rbSynced, ws.crbSynced) {
2020-03-17 18:05:20 +00:00
logger.Info("failed to sync informer cache")
2019-11-15 23:59:37 +00:00
}
go func(ws *WebhookServer) {
2020-03-17 18:05:20 +00:00
logger.V(3).Info("started serving requests", "addr", ws.server.Addr)
2019-08-28 00:00:16 +00:00
if err := ws.server.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
2020-03-17 18:05:20 +00:00
logger.Error(err, "failed to listen to requests")
}
}(ws)
2020-03-17 18:05:20 +00:00
logger.Info("starting")
// verifys if the admission control is enabled and active
// resync: 60 seconds
// deadline: 60 seconds (send request)
// max deadline: deadline*3 (set the deployment annotation as false)
go ws.lastReqTime.Run(ws.pLister, ws.eventGen, ws.client, checker.DefaultResync, checker.DefaultDeadline, stopCh)
}
// Stop TLS server and returns control after the server is shut down
func (ws *WebhookServer) Stop(ctx context.Context) {
2020-03-17 18:05:20 +00:00
logger := ws.log
// cleanUp
// remove the static webhookconfigurations
go ws.webhookRegistrationClient.RemoveWebhookConfigurations(ws.cleanUp)
// shutdown http.Server with context timeout
err := ws.server.Shutdown(ctx)
if err != nil {
// Error from closing listeners, or context timeout:
2020-03-17 18:05:20 +00:00
logger.Error(err, "shutting down server")
ws.server.Close()
}
}
// bodyToAdmissionReview creates AdmissionReview object from request body
// Answers to the http.ResponseWriter if request is not valid
func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview {
2020-03-17 18:05:20 +00:00
logger := ws.log
if request.Body == nil {
logger.Info("empty body", "req", request.URL.String())
http.Error(writer, "empty body", http.StatusBadRequest)
return nil
}
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
logger.Info("failed to read HTTP body", "req", request.URL.String())
http.Error(writer, "failed to read HTTP body", http.StatusBadRequest)
}
contentType := request.Header.Get("Content-Type")
if contentType != "application/json" {
2020-03-17 18:05:20 +00:00
logger.Info("invalid Content-Type", "contextType", contentType)
http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
return nil
}
admissionReview := &v1beta1.AdmissionReview{}
if err := json.Unmarshal(body, &admissionReview); err != nil {
2020-03-17 18:05:20 +00:00
logger.Error(err, "failed to decode request body to type 'AdmissionReview")
http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed)
return nil
}
2019-06-06 00:43:59 +00:00
return admissionReview
}