From c8ef9f9e37f7209068bf5c9aa1cf7ba08b8e9741 Mon Sep 17 00:00:00 2001 From: Wassim DHIF Date: Wed, 14 Aug 2024 11:01:52 +0200 Subject: [PATCH] feat(admission server): implement validating webhooks Signed-off-by: Wassim DHIF --- cmd/cluster-agent/admission/server.go | 99 ++++---- .../subcommands/start/command.go | 8 +- pkg/clusteragent/admission/common/const.go | 12 +- pkg/clusteragent/admission/common/global.go | 45 ++++ .../{mutate => }/common/label_selectors.go | 5 +- .../controllers/webhook/controller_base.go | 76 +++--- .../controllers/webhook/controller_v1.go | 222 +++++++++++++--- .../controllers/webhook/controller_v1_test.go | 178 +++++++------ .../controllers/webhook/controller_v1beta1.go | 237 +++++++++++++++--- .../webhook/controller_v1beta1_test.go | 188 +++++++------- pkg/clusteragent/admission/metrics/metrics.go | 14 ++ .../mutate/agent_sidecar/agent_sidecar.go | 34 +-- .../auto_instrumentation.go | 31 ++- .../mutate/autoscaling/autoscaling.go | 55 ++-- .../admission/mutate/config/config.go | 53 ++-- .../admission/mutate/config/config_test.go | 12 +- .../cwsinstrumentation/cws_instrumentation.go | 88 ++++--- pkg/clusteragent/admission/mutate/doc.go | 8 +- .../admission/mutate/tagsfromlabels/tags.go | 46 ++-- pkg/clusteragent/admission/start.go | 24 +- pkg/clusteragent/admission/status.go | 3 +- .../validate/alwaysadmit/alwaysadmit.go | 99 ++++++++ .../admission/validate/common/common.go | 43 ++++ pkg/clusteragent/admission/validate/doc.go | 73 ++++++ pkg/util/kubernetes/apiserver/types.go | 2 + 25 files changed, 1152 insertions(+), 503 deletions(-) rename pkg/clusteragent/admission/{mutate => }/common/label_selectors.go (95%) create mode 100644 pkg/clusteragent/admission/validate/alwaysadmit/alwaysadmit.go create mode 100644 pkg/clusteragent/admission/validate/common/common.go create mode 100644 pkg/clusteragent/admission/validate/doc.go diff --git a/cmd/cluster-agent/admission/server.go b/cmd/cluster-agent/admission/server.go index 9abd4eb15b3b12..fe124f283e5800 100644 --- a/cmd/cluster-agent/admission/server.go +++ b/cmd/cluster-agent/admission/server.go @@ -18,18 +18,17 @@ import ( "net/http" "time" + "github.com/cihub/seelog" authenticationv1 "k8s.io/api/authentication/v1" + admicommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/apiserver/common" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" "github.com/DataDog/datadog-agent/pkg/util/log" - "github.com/cihub/seelog" - admiv1 "k8s.io/api/admission/v1" admiv1beta1 "k8s.io/api/admission/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/dynamic" @@ -38,8 +37,8 @@ import ( const jsonContentType = "application/json" -// MutateRequest contains the information of a mutation request -type MutateRequest struct { +// Request contains the information of a validation request +type Request struct { // Raw is the raw request object Raw []byte // Name is the name of the object @@ -54,8 +53,8 @@ type MutateRequest struct { APIClient kubernetes.Interface } -// MutatingWebhookFunc is the function that runs the mutating webhook logic -type MutatingWebhookFunc func(request *MutateRequest) ([]byte, error) +// WebhookFunc is the generic function type that runs either a validating or mutating webhook logic +type WebhookFunc func(request *Request) *admiv1.AdmissionResponse // Server TODO type Server struct { @@ -90,11 +89,11 @@ func (s *Server) initDecoder() { s.decoder = serializer.NewCodecFactory(scheme).UniversalDeserializer() } -// Register adds an admission webhook handler. -// Register must be called to register the desired webhook handlers before calling Run. -func (s *Server) Register(uri string, webhookName string, f MutatingWebhookFunc, dc dynamic.Interface, apiClient kubernetes.Interface) { +// RegisterWebhook adds a Validating admission webhook handler. +// It must be called to register the desired webhook handlers before calling Run. +func (s *Server) RegisterWebhook(uri string, webhookName string, webhookType admicommon.WebhookType, f WebhookFunc, dc dynamic.Interface, apiClient kubernetes.Interface) { s.mux.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) { - s.mutateHandler(w, r, webhookName, f, dc, apiClient) + s.Handle(w, r, webhookName, webhookType, f, dc, apiClient) }) } @@ -134,22 +133,32 @@ func (s *Server) Run(mainCtx context.Context, client kubernetes.Interface) error return server.Shutdown(shutdownCtx) } -// mutateHandler contains the main logic responsible for handling mutation requests. +// Handle contains the main logic responsible for handling admission requests. // It supports both v1 and v1beta1 requests. -func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookName string, mutateFunc MutatingWebhookFunc, dc dynamic.Interface, apiClient kubernetes.Interface) { - metrics.MutatingWebhooksReceived.Inc(webhookName) +func (s *Server) Handle(w http.ResponseWriter, r *http.Request, webhookName string, webhookType admicommon.WebhookType, webhookFunc WebhookFunc, dc dynamic.Interface, apiClient kubernetes.Interface) { + // Increment the metrics for the received webhook + if webhookType == admicommon.ValidatingWebhook { + metrics.ValidatingWebhooksReceived.Inc(webhookName) + } else if webhookType == admicommon.MutatingWebhook { + metrics.MutatingWebhooksReceived.Inc(webhookName) + } + // Measure the time it takes to process the request start := time.Now() defer func() { - metrics.MutatingWebhooksResponseDuration.Observe(time.Since(start).Seconds(), webhookName) + if webhookType == admicommon.ValidatingWebhook { + metrics.ValidatingWebhooksResponseDuration.Observe(time.Since(start).Seconds(), webhookName) + } else if webhookType == admicommon.MutatingWebhook { + metrics.MutatingWebhooksResponseDuration.Observe(time.Since(start).Seconds(), webhookName) + } }() + // Validate admission request if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) log.Warnf("Invalid method %s, only POST requests are allowed", r.Method) return } - body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -157,13 +166,13 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa return } defer r.Body.Close() - if contentType := r.Header.Get("Content-Type"); contentType != jsonContentType { w.WriteHeader(http.StatusBadRequest) log.Warnf("Unsupported content type %s, only %s is supported", contentType, jsonContentType) return } + // Deserialize admission request obj, gvk, err := s.decoder.Decode(body, nil, nil) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -171,6 +180,7 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa return } + // Handle the request based on GroupVersionKind var response runtime.Object switch *gvk { case admiv1.SchemeGroupVersion.WithKind("AdmissionReview"): @@ -180,7 +190,7 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa } admissionReviewResp := &admiv1.AdmissionReview{} admissionReviewResp.SetGroupVersionKind(*gvk) - mutateRequest := MutateRequest{ + admissionRequest := Request{ Raw: admissionReviewReq.Request.Object.Raw, Name: admissionReviewReq.Request.Name, Namespace: admissionReviewReq.Request.Namespace, @@ -188,8 +198,18 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa DynamicClient: dc, APIClient: apiClient, } - jsonPatch, err := mutateFunc(&mutateRequest) - admissionReviewResp.Response = mutationResponse(jsonPatch, err) + + // Generate admission response + if webhookType == admicommon.ValidatingWebhook { + validation := webhookFunc(&admissionRequest) + admissionReviewResp.Response = validation + } else if webhookType == admicommon.MutatingWebhook { + mutationResponse := webhookFunc(&admissionRequest) + admissionReviewResp.Response = mutationResponse + } else { + log.Errorf("Invalid webhook type %v", webhookType) + w.WriteHeader(http.StatusInternalServerError) + } admissionReviewResp.Response.UID = admissionReviewReq.Request.UID response = admissionReviewResp case admiv1beta1.SchemeGroupVersion.WithKind("AdmissionReview"): @@ -199,7 +219,7 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa } admissionReviewResp := &admiv1beta1.AdmissionReview{} admissionReviewResp.SetGroupVersionKind(*gvk) - mutateRequest := MutateRequest{ + admissionRequest := Request{ Raw: admissionReviewReq.Request.Object.Raw, Name: admissionReviewReq.Request.Name, Namespace: admissionReviewReq.Request.Namespace, @@ -207,8 +227,18 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa DynamicClient: dc, APIClient: apiClient, } - jsonPatch, err := mutateFunc(&mutateRequest) - admissionReviewResp.Response = responseV1ToV1beta1(mutationResponse(jsonPatch, err)) + + // Generate admission response + if webhookType == admicommon.ValidatingWebhook { + validation := webhookFunc(&admissionRequest) + admissionReviewResp.Response = responseV1ToV1beta1(validation) + } else if webhookType == admicommon.MutatingWebhook { + mutationResponse := webhookFunc(&admissionRequest) + admissionReviewResp.Response = responseV1ToV1beta1(mutationResponse) + } else { + log.Errorf("Invalid webhook type %v", webhookType) + w.WriteHeader(http.StatusInternalServerError) + } admissionReviewResp.Response.UID = admissionReviewReq.Request.UID response = admissionReviewResp default: @@ -226,29 +256,6 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa } } -// mutationResponse returns the adequate v1.AdmissionResponse based on the mutation result. -func mutationResponse(jsonPatch []byte, err error) *admiv1.AdmissionResponse { - if err != nil { - log.Warnf("Failed to mutate: %v", err) - - return &admiv1.AdmissionResponse{ - Result: &metav1.Status{ - Message: err.Error(), - }, - Allowed: true, - } - - } - - patchType := admiv1.PatchTypeJSONPatch - - return &admiv1.AdmissionResponse{ - Patch: jsonPatch, - PatchType: &patchType, - Allowed: true, - } -} - // responseV1ToV1beta1 converts a v1.AdmissionResponse into a v1beta1.AdmissionResponse. func responseV1ToV1beta1(resp *admiv1.AdmissionResponse) *admiv1beta1.AdmissionResponse { var patchType *admiv1beta1.PatchType diff --git a/cmd/cluster-agent/subcommands/start/command.go b/cmd/cluster-agent/subcommands/start/command.go index 7ceef074e4cc09..a7ca020d43b05b 100644 --- a/cmd/cluster-agent/subcommands/start/command.go +++ b/cmd/cluster-agent/subcommands/start/command.go @@ -476,16 +476,16 @@ func start(log log.Component, StopCh: stopCh, } - mutatingWebhooks, err := admissionpkg.StartControllers(admissionCtx, wmeta, pa) + webhooks, err := admissionpkg.StartControllers(admissionCtx, wmeta, pa) if err != nil { pkglog.Errorf("Could not start admission controller: %v", err) } else { // Webhook and secret controllers are started successfully - // Setup the k8s admission webhook server + // Set up the k8s admission webhook server server := admissioncmd.NewServer() - for _, webhookConf := range mutatingWebhooks { - server.Register(webhookConf.Endpoint(), webhookConf.Name(), webhookConf.MutateFunc(), apiCl.DynamicCl, apiCl.Cl) + for _, webhookConf := range webhooks { + server.RegisterWebhook(webhookConf.Endpoint(), webhookConf.Name(), webhookConf.WebhookType(), webhookConf.WebhookFunc(), apiCl.DynamicCl, apiCl.Cl) } // Start the k8s admission webhook server diff --git a/pkg/clusteragent/admission/common/const.go b/pkg/clusteragent/admission/common/const.go index b216f69ffca749..d10759b00876cb 100644 --- a/pkg/clusteragent/admission/common/const.go +++ b/pkg/clusteragent/admission/common/const.go @@ -8,11 +8,21 @@ // Package common defines constants and types used by the Admission Controller. package common +// WebhookType is the type of the webhook. +type WebhookType int + +const ( + // ValidatingWebhook is type for Validating Webhooks. + ValidatingWebhook WebhookType = iota + // MutatingWebhook is type for Mutating Webhooks. + MutatingWebhook +) + const ( // EnabledLabelKey pod label to disable/enable mutations at the pod level. EnabledLabelKey = "admission.datadoghq.com/enabled" - // InjectionModeLabelKey pod label to chose the config injection at the pod level. + // InjectionModeLabelKey pod label to choose the config injection at the pod level. InjectionModeLabelKey = "admission.datadoghq.com/config.mode" // LibVersionAnnotKeyFormat is the format of the library version annotation diff --git a/pkg/clusteragent/admission/common/global.go b/pkg/clusteragent/admission/common/global.go index 78b5e5cd935f16..3f966bc22921c7 100644 --- a/pkg/clusteragent/admission/common/global.go +++ b/pkg/clusteragent/admission/common/global.go @@ -10,9 +10,54 @@ package common import ( "strconv" "time" + + "github.com/DataDog/datadog-agent/pkg/util/log" + + admiv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ( // ClusterAgentStartTime records the Cluster Agent start time ClusterAgentStartTime = strconv.FormatInt(time.Now().Unix(), 10) ) + +// ValidationResponse returns the result of the validation +func ValidationResponse(validation bool, err error) *admiv1.AdmissionResponse { + if err != nil { + log.Warnf("Failed to validate: %v", err) + + return &admiv1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + Allowed: false, + } + } + + return &admiv1.AdmissionResponse{ + Allowed: validation, + } +} + +// MutationResponse returns the result of the mutation +func MutationResponse(jsonPatch []byte, err error) *admiv1.AdmissionResponse { + if err != nil { + log.Warnf("Failed to mutate: %v", err) + + return &admiv1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + Allowed: true, + } + } + + patchType := admiv1.PatchTypeJSONPatch + + return &admiv1.AdmissionResponse{ + Patch: jsonPatch, + PatchType: &patchType, + Allowed: true, + } +} diff --git a/pkg/clusteragent/admission/mutate/common/label_selectors.go b/pkg/clusteragent/admission/common/label_selectors.go similarity index 95% rename from pkg/clusteragent/admission/mutate/common/label_selectors.go rename to pkg/clusteragent/admission/common/label_selectors.go index d305427ab782ba..2711cf2afdb970 100644 --- a/pkg/clusteragent/admission/mutate/common/label_selectors.go +++ b/pkg/clusteragent/admission/common/label_selectors.go @@ -10,7 +10,6 @@ package common import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/config" ) @@ -25,7 +24,7 @@ func DefaultLabelSelectors(useNamespaceSelector bool) (namespaceSelector, object labelSelector = metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { - Key: common.EnabledLabelKey, + Key: EnabledLabelKey, Operator: metav1.LabelSelectorOpNotIn, Values: []string{"false"}, }, @@ -35,7 +34,7 @@ func DefaultLabelSelectors(useNamespaceSelector bool) (namespaceSelector, object // Ignore all, accept pods if they're explicitly allowed labelSelector = metav1.LabelSelector{ MatchLabels: map[string]string{ - common.EnabledLabelKey: "true", + EnabledLabelKey: "true", }, } } diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_base.go b/pkg/clusteragent/admission/controllers/webhook/controller_base.go index bdcd7f7b9d1796..d8dac8513f2fdd 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_base.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_base.go @@ -10,7 +10,8 @@ package webhook import ( "fmt" - admiv1 "k8s.io/api/admissionregistration/v1" + admiv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,6 +24,7 @@ import ( "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" agentsidecar "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/agent_sidecar" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation" @@ -30,6 +32,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/config" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/tagsfromlabels" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/validate/alwaysadmit" "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -37,7 +40,7 @@ import ( // Controller is an interface implemented by ControllerV1 and ControllerV1beta1. type Controller interface { Run(stopCh <-chan struct{}) - EnabledMutatingWebhooks() []MutatingWebhook + EnabledWebhooks() []Webhook } // NewController returns the adequate implementation of the Controller interface. @@ -52,16 +55,17 @@ func NewController( pa workload.PodPatcher, ) Controller { if config.useAdmissionV1() { - return NewControllerV1(client, secretInformer, admissionInterface.V1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta, pa) + return NewControllerV1(client, secretInformer, admissionInterface.V1().ValidatingWebhookConfigurations(), admissionInterface.V1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta, pa) } - - return NewControllerV1beta1(client, secretInformer, admissionInterface.V1beta1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta, pa) + return NewControllerV1beta1(client, secretInformer, admissionInterface.V1beta1().ValidatingWebhookConfigurations(), admissionInterface.V1beta1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta, pa) } -// MutatingWebhook represents a mutating webhook -type MutatingWebhook interface { +// Webhook represents an admission webhook +type Webhook interface { // Name returns the name of the webhook Name() string + // WebhookType Type returns the type of the webhook + WebhookType() common.WebhookType // IsEnabled returns whether the webhook is enabled IsEnabled() bool // Endpoint returns the endpoint of the webhook @@ -71,33 +75,38 @@ type MutatingWebhook interface { Resources() []string // Operations returns the operations on the resources specified for which // the webhook should be invoked - Operations() []admiv1.OperationType + Operations() []admissionregistrationv1.OperationType // LabelSelectors returns the label selectors that specify when the webhook // should be invoked LabelSelectors(useNamespaceSelector bool) (namespaceSelector *metav1.LabelSelector, objectSelector *metav1.LabelSelector) - // MutateFunc returns the function that mutates the resources - MutateFunc() admission.MutatingWebhookFunc + // WebhookFunc runs the logic of the webhook and returns the admission response + WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse } -// mutatingWebhooks returns the list of mutating webhooks. Notice that the order -// of the webhooks returned is the order in which they will be executed. For -// now, the only restriction is that the agent sidecar webhook needs to go after -// the config one. The reason is that the volume mount for the APM socket added -// by the config webhook doesn't always work on Fargate (one of the envs where -// we use an agent sidecar), and the agent sidecar webhook needs to remove it. -func mutatingWebhooks(wmeta workloadmeta.Component, pa workload.PodPatcher) []MutatingWebhook { +// generateWebhooks returns the list of webhooks. The order of the webhooks returned +// is the order in which they will be executed. For now, the only restriction is that +// the agent sidecar webhook needs to go after the config one. +// The reason is that the volume mount for the APM socket added by the config webhook +// doesn't always work on Fargate (one of the envs where we use an agent sidecar), and +// the agent sidecar webhook needs to remove it. +func generateWebhooks(wmeta workloadmeta.Component, pa workload.PodPatcher) []Webhook { // Note: the auto_instrumentation pod injection filter is used across // multiple mutating webhooks, so we add it as a hard dependency to each // of the components that use it via the injectionFilter parameter. injectionFilter := autoinstrumentation.GetInjectionFilter() - webhooks := []MutatingWebhook{ + webhooks := []Webhook{ + // Validating webhooks + alwaysadmit.NewWebhook(), + + // Mutating webhooks config.NewWebhook(wmeta, injectionFilter), tagsfromlabels.NewWebhook(wmeta, injectionFilter), agentsidecar.NewWebhook(), autoscaling.NewWebhook(pa), } + // APM Instrumentation webhook needs to be registered after the config webhook. apm, err := autoinstrumentation.NewWebhook(wmeta, injectionFilter) if err == nil { webhooks = append(webhooks, apm) @@ -119,22 +128,23 @@ func mutatingWebhooks(wmeta workloadmeta.Component, pa workload.PodPatcher) []Mu // It contains the shared fields and provides shared methods. // For the nolint:structcheck see https://github.com/golangci/golangci-lint/issues/537 type controllerBase struct { - clientSet kubernetes.Interface //nolint:structcheck - config Config - secretsLister corelisters.SecretLister - secretsSynced cache.InformerSynced //nolint:structcheck - mutatingWebhooksSynced cache.InformerSynced //nolint:structcheck - queue workqueue.RateLimitingInterface - isLeaderFunc func() bool - isLeaderNotif <-chan struct{} - mutatingWebhooks []MutatingWebhook + clientSet kubernetes.Interface //nolint:structcheck + config Config + secretsLister corelisters.SecretLister + secretsSynced cache.InformerSynced //nolint:structcheck + validatingWebhooksSynced cache.InformerSynced //nolint:structcheck + mutatingWebhooksSynced cache.InformerSynced //nolint:structcheck + queue workqueue.RateLimitingInterface + isLeaderFunc func() bool + isLeaderNotif <-chan struct{} + webhooks []Webhook } -// EnabledMutatingWebhooks returns the list of enabled mutating webhooks. -func (c *controllerBase) EnabledMutatingWebhooks() []MutatingWebhook { - var res []MutatingWebhook +// EnabledWebhooks returns the list of enabled webhooks. +func (c *controllerBase) EnabledWebhooks() []Webhook { + var res []Webhook - for _, webhook := range c.mutatingWebhooks { + for _, webhook := range c.webhooks { if webhook.IsEnabled() { res = append(res, webhook) } @@ -257,13 +267,13 @@ func (c *controllerBase) processNextWorkItem(reconcile func() error) bool { if err := reconcile(); err != nil { c.requeue(key) - log.Infof("Couldn't reconcile Webhook %s: %v", c.config.getWebhookName(), err) + log.Infof("Couldn't reconcile webhook %s: %v", c.config.getWebhookName(), err) metrics.ReconcileErrors.Inc(metrics.WebhooksControllerName) return true } c.queue.Forget(key) - log.Debugf("Webhook %s reconciled successfully", c.config.getWebhookName()) + log.Debugf("webhook %s reconciled successfully", c.config.getWebhookName()) metrics.ReconcileSuccess.Inc(metrics.WebhooksControllerName) return true diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1.go index 64de1af48b3867..b54012286bd9d5 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/util/workqueue" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" "github.com/DataDog/datadog-agent/pkg/util/log" @@ -35,15 +36,18 @@ import ( // It uses the admissionregistration/v1 API. type ControllerV1 struct { controllerBase - mutatingWebhooksLister admissionlisters.MutatingWebhookConfigurationLister - mutatingWebhookTemplates []admiv1.MutatingWebhook + validatingWebhooksLister admissionlisters.ValidatingWebhookConfigurationLister + validatingWebhookTemplates []admiv1.ValidatingWebhook + mutatingWebhooksLister admissionlisters.MutatingWebhookConfigurationLister + mutatingWebhookTemplates []admiv1.MutatingWebhook } // NewControllerV1 returns a new Webhook Controller using admissionregistration/v1. func NewControllerV1( client kubernetes.Interface, secretInformer coreinformers.SecretInformer, - MutatingWebhookInformer admissioninformers.MutatingWebhookConfigurationInformer, + validatingWebhookInformer admissioninformers.ValidatingWebhookConfigurationInformer, + mutatingWebhookInformer admissioninformers.MutatingWebhookConfigurationInformer, isLeaderFunc func() bool, isLeaderNotif <-chan struct{}, config Config, @@ -55,12 +59,14 @@ func NewControllerV1( controller.config = config controller.secretsLister = secretInformer.Lister() controller.secretsSynced = secretInformer.Informer().HasSynced - controller.mutatingWebhooksLister = MutatingWebhookInformer.Lister() - controller.mutatingWebhooksSynced = MutatingWebhookInformer.Informer().HasSynced + controller.validatingWebhooksLister = validatingWebhookInformer.Lister() + controller.validatingWebhooksSynced = validatingWebhookInformer.Informer().HasSynced + controller.mutatingWebhooksLister = mutatingWebhookInformer.Lister() + controller.mutatingWebhooksSynced = mutatingWebhookInformer.Informer().HasSynced controller.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "webhooks") controller.isLeaderFunc = isLeaderFunc controller.isLeaderNotif = isLeaderNotif - controller.mutatingWebhooks = mutatingWebhooks(wmeta, pa) + controller.webhooks = generateWebhooks(wmeta, pa) controller.generateTemplates() if _, err := secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -71,12 +77,20 @@ func NewControllerV1( log.Errorf("cannot add event handler to secret informer: %v", err) } - if _, err := MutatingWebhookInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + if _, err := validatingWebhookInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.handleWebhook, - UpdateFunc: controller.handleMutatingWebhookUpdate, + UpdateFunc: controller.handleWebhookUpdate, DeleteFunc: controller.handleWebhook, }); err != nil { - log.Errorf("cannot add event handler to webhook informer: %v", err) + log.Errorf("cannot add event handler to validating webhook informer: %v", err) + } + + if _, err := mutatingWebhookInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: controller.handleWebhook, + UpdateFunc: controller.handleWebhookUpdate, + DeleteFunc: controller.handleWebhook, + }); err != nil { + log.Errorf("cannot add event handler to mutating webhook informer: %v", err) } return controller @@ -90,7 +104,7 @@ func (c *ControllerV1) Run(stopCh <-chan struct{}) { log.Infof("Starting webhook controller for secret %s/%s and webhook %s - Using admissionregistration/v1", c.config.getSecretNs(), c.config.getSecretName(), c.config.getWebhookName()) defer log.Infof("Stopping webhook controller for secret %s/%s and webhook %s", c.config.getSecretNs(), c.config.getSecretName(), c.config.getWebhookName()) - if ok := cache.WaitForCacheSync(stopCh, c.secretsSynced, c.mutatingWebhooksSynced); !ok { + if ok := cache.WaitForCacheSync(stopCh, c.secretsSynced, c.validatingWebhooksSynced, c.mutatingWebhooksSynced); !ok { return } @@ -109,30 +123,42 @@ func (c *ControllerV1) run() { } } -// handleMutatingWebhookUpdate handles the new Webhook reported in update events. +// handleWebhookUpdate handles the new Webhook reported in update events. // It can be a callback function for update events. -func (c *ControllerV1) handleMutatingWebhookUpdate(oldObj, newObj interface{}) { +func (c *ControllerV1) handleWebhookUpdate(oldObj, newObj interface{}) { if !c.isLeaderFunc() { return } - newWebhook, ok := newObj.(*admiv1.MutatingWebhookConfiguration) - if !ok { - log.Debugf("Expected MutatingWebhookConfiguration object, got: %v", newObj) - return - } + switch newObj.(type) { + case *admiv1.ValidatingWebhookConfiguration: + newWebhook, _ := newObj.(*admiv1.ValidatingWebhookConfiguration) + oldWebhook, ok := oldObj.(*admiv1.ValidatingWebhookConfiguration) + if !ok { + log.Debugf("Expected ValidatingWebhookConfiguration object, got: %v", oldObj) + return + } - oldWebhook, ok := oldObj.(*admiv1.MutatingWebhookConfiguration) - if !ok { - log.Debugf("Expected MutatingWebhookConfiguration object, got: %v", oldObj) - return - } + if newWebhook.ResourceVersion == oldWebhook.ResourceVersion { + return + } + c.handleWebhook(newObj) + case *admiv1.MutatingWebhookConfiguration: + newWebhook, _ := newObj.(*admiv1.MutatingWebhookConfiguration) + oldWebhook, ok := oldObj.(*admiv1.MutatingWebhookConfiguration) + if !ok { + log.Debugf("Expected MutatingWebhookConfiguration object, got: %v", oldObj) + return + } - if newWebhook.ResourceVersion == oldWebhook.ResourceVersion { + if newWebhook.ResourceVersion == oldWebhook.ResourceVersion { + return + } + c.handleWebhook(newObj) + default: + log.Debugf("Expected ValidatingWebhookConfiguration or MutatingWebhookConfiguration object, got: %v", newObj) return } - - c.handleWebhook(newObj) } // reconcile creates/updates the webhook object on new events. @@ -142,19 +168,78 @@ func (c *ControllerV1) reconcile() error { return err } + validatingWebhook, err := c.validatingWebhooksLister.Get(c.config.getWebhookName()) + if err != nil { + if errors.IsNotFound(err) { + log.Infof("Validating Webhook %s was not found, creating it", c.config.getWebhookName()) + err := c.createValidatingWebhook(secret) + if err != nil { + _ = log.Errorf("Failed to create Validating Webhook %s: %v", c.config.getWebhookName(), err) + } + } + } else { + log.Debugf("Validating Webhook %s was found, updating it", c.config.getWebhookName()) + err := c.updateValidatingWebhook(secret, validatingWebhook) + if err != nil { + _ = log.Errorf("Failed to update Validating Webhook %s: %v", c.config.getWebhookName(), err) + } + } + mutatingWebhook, err := c.mutatingWebhooksLister.Get(c.config.getWebhookName()) if err != nil { if errors.IsNotFound(err) { log.Infof("Mutating Webhook %s was not found, creating it", c.config.getWebhookName()) - return c.createMutatingWebhook(secret) + err := c.createMutatingWebhook(secret) + if err != nil { + _ = log.Errorf("Failed to create Mutating Webhook %s: %v", c.config.getWebhookName(), err) + } } + } else { + log.Debugf("Mutating Webhook %s was found, updating it", c.config.getWebhookName()) + err := c.updateMutatingWebhook(secret, mutatingWebhook) + if err != nil { + _ = log.Errorf("Failed to update Mutating Webhook %s: %v", c.config.getWebhookName(), err) + } + } - return err + return err +} + +// createValidatingWebhook creates a new ValidatingWebhookConfiguration object. +func (c *ControllerV1) createValidatingWebhook(secret *corev1.Secret) error { + webhook := &admiv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.config.getWebhookName(), + }, + Webhooks: c.newValidatingWebhooks(secret), } - log.Debugf("Mutating Webhook %s was found, updating it", c.config.getWebhookName()) + _, err := c.clientSet.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), webhook, metav1.CreateOptions{}) + if errors.IsAlreadyExists(err) { + log.Infof("Validating Webhook %s already exists", webhook.GetName()) + return nil + } - return c.updateMutatingWebhook(secret, mutatingWebhook) + return err +} + +// updateValidatingWebhook stores a new configuration in the ValidatingWebhookConfiguration object. +func (c *ControllerV1) updateValidatingWebhook(secret *corev1.Secret, webhook *admiv1.ValidatingWebhookConfiguration) error { + webhook = webhook.DeepCopy() + webhook.Webhooks = c.newValidatingWebhooks(secret) + _, err := c.clientSet.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), webhook, metav1.UpdateOptions{}) + return err +} + +// newValidatingWebhooks generates Webhook objects from config templates with updated CABundle from Secret. +func (c *ControllerV1) newValidatingWebhooks(secret *corev1.Secret) []admiv1.ValidatingWebhook { + webhooks := []admiv1.ValidatingWebhook{} + for _, tpl := range c.validatingWebhookTemplates { + tpl.ClientConfig.CABundle = certificate.GetCABundle(secret.Data) + webhooks = append(webhooks, tpl) + } + + return webhooks } // createMutatingWebhook creates a new MutatingWebhookConfiguration object. @@ -183,7 +268,7 @@ func (c *ControllerV1) updateMutatingWebhook(secret *corev1.Secret, webhook *adm return err } -// newMutatingWebhooks generates MutatingWebhook objects from config templates with updated CABundle from Secret. +// newMutatingWebhooks generates Webhook objects from config templates with updated CABundle from Secret. func (c *ControllerV1) newMutatingWebhooks(secret *corev1.Secret) []admiv1.MutatingWebhook { webhooks := []admiv1.MutatingWebhook{} for _, tpl := range c.mutatingWebhookTemplates { @@ -194,18 +279,38 @@ func (c *ControllerV1) newMutatingWebhooks(secret *corev1.Secret) []admiv1.Mutat return webhooks } +// generateTemplates generates the webhook templates from the configuration. func (c *ControllerV1) generateTemplates() { - webhooks := []admiv1.MutatingWebhook{} - - for _, webhook := range c.mutatingWebhooks { - if !webhook.IsEnabled() { + // Generate validating webhook templates + validatingWebhooks := []admiv1.ValidatingWebhook{} + for _, webhook := range c.webhooks { + if !webhook.IsEnabled() || webhook.WebhookType() != common.ValidatingWebhook { continue } - nsSelector, objSelector := webhook.LabelSelectors(c.config.useNamespaceSelector()) + validatingWebhooks = append( + validatingWebhooks, + c.getValidatingWebhookSkeleton( + webhook.Name(), + webhook.Endpoint(), + webhook.Operations(), + webhook.Resources(), + nsSelector, + objSelector, + ), + ) + } + c.validatingWebhookTemplates = validatingWebhooks - webhooks = append( - webhooks, + // Generate mutating webhook templates + mutatingWebhooks := []admiv1.MutatingWebhook{} + for _, webhook := range c.webhooks { + if !webhook.IsEnabled() || webhook.WebhookType() != common.MutatingWebhook { + continue + } + nsSelector, objSelector := webhook.LabelSelectors(c.config.useNamespaceSelector()) + mutatingWebhooks = append( + mutatingWebhooks, c.getMutatingWebhookSkeleton( webhook.Name(), webhook.Endpoint(), @@ -216,8 +321,45 @@ func (c *ControllerV1) generateTemplates() { ), ) } + c.mutatingWebhookTemplates = mutatingWebhooks +} - c.mutatingWebhookTemplates = webhooks +func (c *ControllerV1) getValidatingWebhookSkeleton(nameSuffix, path string, operations []admiv1.OperationType, resources []string, namespaceSelector, objectSelector *metav1.LabelSelector) admiv1.ValidatingWebhook { + matchPolicy := admiv1.Exact + sideEffects := admiv1.SideEffectClassNone + port := c.config.getServicePort() + timeout := c.config.getTimeout() + failurePolicy := c.getFailurePolicy() + webhook := admiv1.ValidatingWebhook{ + Name: c.config.configName(nameSuffix), + ClientConfig: admiv1.WebhookClientConfig{ + Service: &admiv1.ServiceReference{ + Namespace: c.config.getServiceNs(), + Name: c.config.getServiceName(), + Port: &port, + Path: &path, + }, + }, + Rules: []admiv1.RuleWithOperations{ + { + Operations: operations, + Rule: admiv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: resources, + }, + }, + }, + FailurePolicy: &failurePolicy, + MatchPolicy: &matchPolicy, + SideEffects: &sideEffects, + TimeoutSeconds: &timeout, + AdmissionReviewVersions: []string{"v1", "v1beta1"}, + NamespaceSelector: namespaceSelector, + ObjectSelector: objectSelector, + } + + return webhook } func (c *ControllerV1) getMutatingWebhookSkeleton(nameSuffix, path string, operations []admiv1.OperationType, resources []string, namespaceSelector, objectSelector *metav1.LabelSelector) admiv1.MutatingWebhook { @@ -225,7 +367,7 @@ func (c *ControllerV1) getMutatingWebhookSkeleton(nameSuffix, path string, opera sideEffects := admiv1.SideEffectClassNone port := c.config.getServicePort() timeout := c.config.getTimeout() - failurePolicy := c.getAdmiV1FailurePolicy() + failurePolicy := c.getFailurePolicy() reinvocationPolicy := c.getReinvocationPolicy() webhook := admiv1.MutatingWebhook{ Name: c.config.configName(nameSuffix), @@ -260,7 +402,7 @@ func (c *ControllerV1) getMutatingWebhookSkeleton(nameSuffix, path string, opera return webhook } -func (c *ControllerV1) getAdmiV1FailurePolicy() admiv1.FailurePolicyType { +func (c *ControllerV1) getFailurePolicy() admiv1.FailurePolicyType { policy := strings.ToLower(c.config.getFailurePolicy()) switch policy { case "ignore": diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go index c6141b9d9765e6..9b7ff66e36d128 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go @@ -29,7 +29,7 @@ import ( configComp "github.com/DataDog/datadog-agent/comp/core/config" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" workloadmetafxmock "github.com/DataDog/datadog-agent/comp/core/workloadmeta/fx-mock" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation" "github.com/DataDog/datadog-agent/pkg/config" configmock "github.com/DataDog/datadog-agent/pkg/config/mock" @@ -77,13 +77,19 @@ func TestCreateWebhookV1(t *testing.T) { defer close(stopCh) c := f.run(stopCh) - var webhook *admiv1.MutatingWebhookConfiguration + var validatingWebhookConfiguration *admiv1.ValidatingWebhookConfiguration require.Eventually(t, func() bool { - webhook, err = c.mutatingWebhooksLister.Get(v1Cfg.getWebhookName()) + validatingWebhookConfiguration, err = c.validatingWebhooksLister.Get(v1Cfg.getWebhookName()) return err == nil }, waitFor, tick) - if err := validateV1(webhook, secret); err != nil { + var mutatingWebhookConfiguration *admiv1.MutatingWebhookConfiguration + require.Eventually(t, func() bool { + mutatingWebhookConfiguration, err = c.mutatingWebhooksLister.Get(v1Cfg.getWebhookName()) + return err == nil + }, waitFor, tick) + + if err := validateV1(validatingWebhookConfiguration, mutatingWebhookConfiguration, secret); err != nil { t.Fatalf("Invalid Webhook: %v", err) } @@ -106,11 +112,11 @@ func TestUpdateOutdatedWebhookV1(t *testing.T) { secret := buildSecret(data, v1Cfg) f.populateSecretsCache(secret) - webhook := &admiv1.MutatingWebhookConfiguration{ + oldValidatingWebhookConfiguration := &admiv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: v1Cfg.getWebhookName(), }, - Webhooks: []admiv1.MutatingWebhook{ + Webhooks: []admiv1.ValidatingWebhook{ { Name: "webhook-foo", }, @@ -119,20 +125,40 @@ func TestUpdateOutdatedWebhookV1(t *testing.T) { }, }, } + f.populateValidatingWebhooksCache(oldValidatingWebhookConfiguration) - f.populateWebhooksCache(webhook) + oldMutatingWebhookConfiguration := &admiv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: v1Cfg.getWebhookName(), + }, + Webhooks: []admiv1.MutatingWebhook{ + { + Name: "webhook-foo", + }, + { + Name: "webhook-bar", + }, + }, + } + f.populateMutatingWebhooksCache(oldMutatingWebhookConfiguration) stopCh := make(chan struct{}) defer close(stopCh) c := f.run(stopCh) - var newWebhook *admiv1.MutatingWebhookConfiguration + var newValidatingWebhookConfiguration *admiv1.ValidatingWebhookConfiguration + require.Eventually(t, func() bool { + newValidatingWebhookConfiguration, err = c.validatingWebhooksLister.Get(v1Cfg.getWebhookName()) + return err == nil && !reflect.DeepEqual(oldValidatingWebhookConfiguration, newValidatingWebhookConfiguration) + }, waitFor, tick) + + var newMutatingWebhookConfiguration *admiv1.MutatingWebhookConfiguration require.Eventually(t, func() bool { - newWebhook, err = c.mutatingWebhooksLister.Get(v1Cfg.getWebhookName()) - return err == nil && !reflect.DeepEqual(webhook, newWebhook) + newMutatingWebhookConfiguration, err = c.mutatingWebhooksLister.Get(v1Cfg.getWebhookName()) + return err == nil && !reflect.DeepEqual(oldMutatingWebhookConfiguration, newMutatingWebhookConfiguration) }, waitFor, tick) - if err := validateV1(newWebhook, secret); err != nil { + if err := validateV1(newValidatingWebhookConfiguration, newMutatingWebhookConfiguration, secret); err != nil { t.Fatalf("Invalid Webhook: %v", err) } @@ -141,90 +167,44 @@ func TestUpdateOutdatedWebhookV1(t *testing.T) { }, waitFor, tick, "Work queue isn't empty") } -func TestAdmissionControllerFailureModeIgnore(t *testing.T) { - mockConfig := configmock.New(t) - f := newFixtureV1(t) - c, _ := f.createController() - c.config = NewConfig(true, false) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "Ignore") - c.config = NewConfig(true, false) - - webhookSkeleton := c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.Ignore, *webhookSkeleton.FailurePolicy) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "ignore") - c.config = NewConfig(true, false) - - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.Ignore, *webhookSkeleton.FailurePolicy) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "BadVal") - c.config = NewConfig(true, false) - - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.Ignore, *webhookSkeleton.FailurePolicy) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "") - c.config = NewConfig(true, false) - - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.Ignore, *webhookSkeleton.FailurePolicy) -} - -func TestAdmissionControllerFailureModeFail(t *testing.T) { +func TestAdmissionControllerFailureModeV1(t *testing.T) { mockConfig := configmock.New(t) f := newFixtureV1(t) c, _ := f.createController() - mockConfig.SetWithoutSource("admission_controller.failure_policy", "Fail") - c.config = NewConfig(true, false) + for _, value := range []string{"Ignore", "ignore", "BadVal", ""} { + mockConfig.SetWithoutSource("admission_controller.failure_policy", value) + c.config = NewConfig(true, false) - webhookSkeleton := c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.Fail, *webhookSkeleton.FailurePolicy) + validatingWebhookSkeleton := c.getValidatingWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1.Ignore, *validatingWebhookSkeleton.FailurePolicy) + mutatingWebhookSkeleton := c.getMutatingWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1.Ignore, *mutatingWebhookSkeleton.FailurePolicy) + } - mockConfig.SetWithoutSource("admission_controller.failure_policy", "fail") - c.config = NewConfig(true, false) + for _, value := range []string{"Fail", "fail"} { + mockConfig.SetWithoutSource("admission_controller.failure_policy", value) + c.config = NewConfig(true, false) - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.Fail, *webhookSkeleton.FailurePolicy) + validatingWebhookSkeleton := c.getValidatingWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1.Fail, *validatingWebhookSkeleton.FailurePolicy) + mutatingWebhookSkeleton := c.getMutatingWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1.Fail, *mutatingWebhookSkeleton.FailurePolicy) + } } func TestAdmissionControllerReinvocationPolicyV1(t *testing.T) { mockConfig := configmock.New(t) f := newFixtureV1(t) c, _ := f.createController() - c.config = NewConfig(true, false) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "IfNeeded") - c.config = NewConfig(true, false) - webhook := c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "ifneeded") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) + for _, value := range []string{"IfNeeded", "ifneeded", "Never", "never", "wrong", ""} { + mockConfig.SetWithoutSource("admission_controller.reinvocationpolicy", value) + c.config = NewConfig(true, false) - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "Never") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.NeverReinvocationPolicy, *webhook.ReinvocationPolicy) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "never") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.NeverReinvocationPolicy, *webhook.ReinvocationPolicy) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "wrong") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) + mutatingWebhookSkeleton := c.getMutatingWebhookSkeleton("foo", "/bar", []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1.IfNeededReinvocationPolicy, *mutatingWebhookSkeleton.ReinvocationPolicy) + } } func TestGenerateTemplatesV1(t *testing.T) { @@ -962,7 +942,7 @@ func TestGenerateTemplatesV1(t *testing.T) { c := &ControllerV1{} c.config = tt.configFunc() - c.mutatingWebhooks = mutatingWebhooks(wmeta, nil) + c.webhooks = generateWebhooks(wmeta, nil) c.generateTemplates() assert.EqualValues(t, tt.want(), c.mutatingWebhookTemplates) @@ -970,7 +950,7 @@ func TestGenerateTemplatesV1(t *testing.T) { } } -func TestGetWebhookSkeletonV1(t *testing.T) { +func TestGetMutatingWebhookSkeletonV1(t *testing.T) { mockConfig := configmock.New(t) defaultReinvocationPolicy := admiv1.IfNeededReinvocationPolicy failurePolicy := admiv1.Ignore @@ -1070,7 +1050,7 @@ func TestGetWebhookSkeletonV1(t *testing.T) { nsSelector, objSelector := common.DefaultLabelSelectors(tt.namespaceSelector) - assert.EqualValues(t, tt.want, c.getWebhookSkeleton(tt.args.nameSuffix, tt.args.path, []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nsSelector, objSelector)) + assert.EqualValues(t, tt.want, c.getMutatingWebhookSkeleton(tt.args.nameSuffix, tt.args.path, []admiv1.OperationType{admiv1.Create}, []string{"pods"}, nsSelector, objSelector)) }) } } @@ -1092,6 +1072,7 @@ func (f *fixtureV1) createController() (*ControllerV1, informers.SharedInformerF return NewControllerV1( f.client, factory.Core().V1().Secrets(), + factory.Admissionregistration().V1().ValidatingWebhookConfigurations(), factory.Admissionregistration().V1().MutatingWebhookConfigurations(), func() bool { return true }, make(chan struct{}), @@ -1110,22 +1091,37 @@ func (f *fixtureV1) run(stopCh chan struct{}) *ControllerV1 { return c } -func (f *fixtureV1) populateWebhooksCache(webhooks ...*admiv1.MutatingWebhookConfiguration) { +func (f *fixtureV1) populateValidatingWebhooksCache(webhooks ...*admiv1.ValidatingWebhookConfiguration) { + for _, w := range webhooks { + _, _ = f.client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), w, metav1.CreateOptions{}) + } +} + +func (f *fixtureV1) populateMutatingWebhooksCache(webhooks ...*admiv1.MutatingWebhookConfiguration) { for _, w := range webhooks { _, _ = f.client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), w, metav1.CreateOptions{}) } } -func validateV1(w *admiv1.MutatingWebhookConfiguration, s *corev1.Secret) error { - if len(w.Webhooks) != 3 { - return fmt.Errorf("Webhooks should contain 3 entries, got %d", len(w.Webhooks)) +func validateV1(validatingWebhooks *admiv1.ValidatingWebhookConfiguration, mutatingWebhooks *admiv1.MutatingWebhookConfiguration, s *corev1.Secret) error { + // Validate the number of webhooks. + if len(validatingWebhooks.Webhooks) != 1 { // TODO (wassim): set validatingWebhooks to 0 once testing is done and we remove the always-admit webhook. + return fmt.Errorf("validatingWebhooks should contain 1 entries, got %d", len(validatingWebhooks.Webhooks)) + } + if len(mutatingWebhooks.Webhooks) != 3 { + return fmt.Errorf("mutatingWebhooks should contain 3 entries, got %d", len(validatingWebhooks.Webhooks)) } - for i := 0; i < len(w.Webhooks); i++ { - if !reflect.DeepEqual(w.Webhooks[i].ClientConfig.CABundle, certificate.GetCABundle(s.Data)) { - return fmt.Errorf("The Webhook CABundle doesn't match the Secret: CABundle: %v, Secret: %v", w.Webhooks[i].ClientConfig.CABundle, s) + // Validate the CA bundle for webhooks. + for i := 0; i < len(validatingWebhooks.Webhooks); i++ { + if !reflect.DeepEqual(validatingWebhooks.Webhooks[i].ClientConfig.CABundle, certificate.GetCABundle(s.Data)) { + return fmt.Errorf("the webhook CA bundle doesn't match the secret. CA bundle: %v, Secret: %v", validatingWebhooks.Webhooks[i].ClientConfig.CABundle, s) + } + } + for i := 0; i < len(mutatingWebhooks.Webhooks); i++ { + if !reflect.DeepEqual(mutatingWebhooks.Webhooks[i].ClientConfig.CABundle, certificate.GetCABundle(s.Data)) { + return fmt.Errorf("the webhook CA bundle doesn't match the secret. CA bundle: %v, Secret: %v", mutatingWebhooks.Webhooks[i].ClientConfig.CABundle, s) } } - return nil } diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go index 5c590d02ea3acb..fd761e445354bb 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/util/workqueue" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" "github.com/DataDog/datadog-agent/pkg/util/log" @@ -35,23 +36,37 @@ import ( // It uses the admissionregistration/v1beta1 API. type ControllerV1beta1 struct { controllerBase - mutatingWebhooksLister admissionlisters.MutatingWebhookConfigurationLister - mutatingWebhookTemplates []admiv1beta1.MutatingWebhook + validatingWebhooksLister admissionlisters.ValidatingWebhookConfigurationLister + validatingWebhookTemplates []admiv1beta1.ValidatingWebhook + mutatingWebhooksLister admissionlisters.MutatingWebhookConfigurationLister + mutatingWebhookTemplates []admiv1beta1.MutatingWebhook } // NewControllerV1beta1 returns a new Webhook Controller using admissionregistration/v1beta1. -func NewControllerV1beta1(client kubernetes.Interface, secretInformer coreinformers.SecretInformer, webhookInformer admissioninformers.MutatingWebhookConfigurationInformer, isLeaderFunc func() bool, isLeaderNotif <-chan struct{}, config Config, wmeta workloadmeta.Component, pa workload.PodPatcher) *ControllerV1beta1 { +func NewControllerV1beta1( + client kubernetes.Interface, + secretInformer coreinformers.SecretInformer, + validatingWebhookInformer admissioninformers.ValidatingWebhookConfigurationInformer, + mutatingWebhookInformer admissioninformers.MutatingWebhookConfigurationInformer, + isLeaderFunc func() bool, + isLeaderNotif <-chan struct{}, + config Config, + wmeta workloadmeta.Component, + pa workload.PodPatcher, +) *ControllerV1beta1 { controller := &ControllerV1beta1{} controller.clientSet = client controller.config = config controller.secretsLister = secretInformer.Lister() controller.secretsSynced = secretInformer.Informer().HasSynced - controller.mutatingWebhooksLister = webhookInformer.Lister() - controller.mutatingWebhooksSynced = webhookInformer.Informer().HasSynced + controller.validatingWebhooksLister = validatingWebhookInformer.Lister() + controller.validatingWebhooksSynced = validatingWebhookInformer.Informer().HasSynced + controller.mutatingWebhooksLister = mutatingWebhookInformer.Lister() + controller.mutatingWebhooksSynced = mutatingWebhookInformer.Informer().HasSynced controller.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "webhooks") controller.isLeaderFunc = isLeaderFunc controller.isLeaderNotif = isLeaderNotif - controller.mutatingWebhooks = mutatingWebhooks(wmeta, pa) + controller.webhooks = generateWebhooks(wmeta, pa) controller.generateTemplates() if _, err := secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -62,9 +77,17 @@ func NewControllerV1beta1(client kubernetes.Interface, secretInformer coreinform log.Errorf("cannot add event handler to secret informer: %v", err) } - if _, err := webhookInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + if _, err := validatingWebhookInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.handleWebhook, - UpdateFunc: controller.handleMutatingWebhookUpdate, + UpdateFunc: controller.handleWebhookUpdate, + DeleteFunc: controller.handleWebhook, + }); err != nil { + log.Errorf("cannot add event handler to webhook informer: %v", err) + } + + if _, err := mutatingWebhookInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: controller.handleWebhook, + UpdateFunc: controller.handleWebhookUpdate, DeleteFunc: controller.handleWebhook, }); err != nil { log.Errorf("cannot add event handler to webhook informer: %v", err) @@ -81,7 +104,7 @@ func (c *ControllerV1beta1) Run(stopCh <-chan struct{}) { log.Infof("Starting webhook controller for secret %s/%s and webhook %s - Using admissionregistration/v1beta1", c.config.getSecretNs(), c.config.getSecretName(), c.config.getWebhookName()) defer log.Infof("Stopping webhook controller for secret %s/%s and webhook %s", c.config.getSecretNs(), c.config.getSecretName(), c.config.getWebhookName()) - if ok := cache.WaitForCacheSync(stopCh, c.secretsSynced, c.mutatingWebhooksSynced); !ok { + if ok := cache.WaitForCacheSync(stopCh, c.secretsSynced, c.validatingWebhooksSynced, c.mutatingWebhooksSynced); !ok { return } @@ -100,51 +123,125 @@ func (c *ControllerV1beta1) run() { } } -// handleMutatingWebhookUpdate handles the new Webhook reported in update events. +// handleWebhookUpdate handles the new Webhook reported in update events. // It can be a callback function for update events. -func (c *ControllerV1beta1) handleMutatingWebhookUpdate(oldObj, newObj interface{}) { +func (c *ControllerV1beta1) handleWebhookUpdate(oldObj, newObj interface{}) { if !c.isLeaderFunc() { return } - newWebhook, ok := newObj.(*admiv1beta1.MutatingWebhookConfiguration) - if !ok { - log.Debugf("Expected MutatingWebhookConfiguration object, got: %v", newObj) - return - } + switch newObj.(type) { + case *admiv1beta1.ValidatingWebhookConfiguration: + newWebhook, _ := newObj.(*admiv1beta1.ValidatingWebhookConfiguration) + oldWebhook, ok := oldObj.(*admiv1beta1.ValidatingWebhookConfiguration) + if !ok { + log.Debugf("Expected ValidatingWebhookConfiguration object, got: %v", oldObj) + return + } - oldWebhook, ok := oldObj.(*admiv1beta1.MutatingWebhookConfiguration) - if !ok { - log.Debugf("Expected MutatingWebhookConfiguration object, got: %v", oldObj) - return - } + if newWebhook.ResourceVersion == oldWebhook.ResourceVersion { + return + } + c.handleWebhook(newObj) + case *admiv1beta1.MutatingWebhookConfiguration: + newWebhook, _ := newObj.(*admiv1beta1.MutatingWebhookConfiguration) + oldWebhook, ok := oldObj.(*admiv1beta1.MutatingWebhookConfiguration) + if !ok { + log.Debugf("Expected MutatingWebhookConfiguration object, got: %v", oldObj) + return + } - if newWebhook.ResourceVersion == oldWebhook.ResourceVersion { + if newWebhook.ResourceVersion == oldWebhook.ResourceVersion { + return + } + c.handleWebhook(newObj) + default: + log.Debugf("Expected ValidatingWebhookConfiguration or MutatingWebhookConfiguration object, got: %v", newObj) return } - - c.handleWebhook(newObj) } // reconcile creates/updates the webhook object on new events. func (c *ControllerV1beta1) reconcile() error { + var err error + secret, err := c.getSecret() if err != nil { return err } + validatingWebhook, err := c.validatingWebhooksLister.Get(c.config.getWebhookName()) + if err != nil { + if errors.IsNotFound(err) { + log.Infof("Webhook %s was not found, creating it", c.config.getWebhookName()) + err = c.createValidatingWebhook(secret) + if err != nil { + _ = log.Errorf("Failed to create Validating Webhook %s: %v", c.config.getWebhookName(), err) + } + } + } else { + log.Debugf("The Webhook %s was found, updating it", c.config.getWebhookName()) + err = c.updateValidatingWebhook(secret, validatingWebhook) + if err != nil { + _ = log.Errorf("Failed to update Validating Webhook %s: %v", c.config.getWebhookName(), err) + } + } + mutatingWebhook, err := c.mutatingWebhooksLister.Get(c.config.getWebhookName()) if err != nil { if errors.IsNotFound(err) { log.Infof("Webhook %s was not found, creating it", c.config.getWebhookName()) - return c.createMutatingWebhook(secret) + err = c.createMutatingWebhook(secret) + if err != nil { + _ = log.Errorf("Failed to create Mutating Webhook %s: %v", c.config.getWebhookName(), err) + } + } + } else { + log.Debugf("The Webhook %s was found, updating it", c.config.getWebhookName()) + err = c.updateMutatingWebhook(secret, mutatingWebhook) + if err != nil { + _ = log.Errorf("Failed to update Mutating Webhook %s: %v", c.config.getWebhookName(), err) } - return err } - log.Debugf("The Webhook %s was found, updating it", c.config.getWebhookName()) + return err +} + +// createValidatingWebhook creates a new ValidatingWebhookConfiguration object. +func (c *ControllerV1beta1) createValidatingWebhook(secret *corev1.Secret) error { + webhook := &admiv1beta1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.config.getWebhookName(), + }, + Webhooks: c.newValidatingWebhooks(secret), + } + + _, err := c.clientSet.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(context.TODO(), webhook, metav1.CreateOptions{}) + if errors.IsAlreadyExists(err) { + log.Infof("Webhook %s already exists", webhook.GetName()) + return nil + } + + return err +} - return c.updateMutatingWebhook(secret, mutatingWebhook) +// updateValidatingWebhook stores a new configuration in the ValidatingWebhookConfiguration object. +func (c *ControllerV1beta1) updateValidatingWebhook(secret *corev1.Secret, webhook *admiv1beta1.ValidatingWebhookConfiguration) error { + webhook = webhook.DeepCopy() + webhook.Webhooks = c.newValidatingWebhooks(secret) + _, err := c.clientSet.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Update(context.TODO(), webhook, metav1.UpdateOptions{}) + return err +} + +// newValidatingWebhooks generates Webhook objects from config templates with updated CABundle from Secret. +func (c *ControllerV1beta1) newValidatingWebhooks(secret *corev1.Secret) []admiv1beta1.ValidatingWebhook { + webhooks := []admiv1beta1.ValidatingWebhook{} + for _, tpl := range c.validatingWebhookTemplates { + tpl.ClientConfig.CABundle = certificate.GetCABundle(secret.Data) + webhooks = append(webhooks, tpl) + } + + return webhooks } // createMutatingWebhook creates a new MutatingWebhookConfiguration object. @@ -174,7 +271,7 @@ func (c *ControllerV1beta1) updateMutatingWebhook(secret *corev1.Secret, webhook return err } -// newWebhooks generates MutatingWebhook objects from config templates with updated CABundle from Secret. +// newWebhooks generates Webhook objects from config templates with updated CABundle from Secret. func (c *ControllerV1beta1) newMutatingWebhooks(secret *corev1.Secret) []admiv1beta1.MutatingWebhook { webhooks := []admiv1beta1.MutatingWebhook{} for _, tpl := range c.mutatingWebhookTemplates { @@ -186,18 +283,18 @@ func (c *ControllerV1beta1) newMutatingWebhooks(secret *corev1.Secret) []admiv1b } func (c *ControllerV1beta1) generateTemplates() { - webhooks := []admiv1beta1.MutatingWebhook{} + validatingWebhooks := []admiv1beta1.ValidatingWebhook{} - for _, webhook := range c.mutatingWebhooks { - if !webhook.IsEnabled() { + for _, webhook := range c.webhooks { + if !webhook.IsEnabled() || webhook.WebhookType() != common.ValidatingWebhook { continue } nsSelector, objSelector := webhook.LabelSelectors(c.config.useNamespaceSelector()) - webhooks = append( - webhooks, - c.getWebhookSkeleton( + validatingWebhooks = append( + validatingWebhooks, + c.getValidatingWebhookSkeleton( webhook.Name(), webhook.Endpoint(), webhook.Operations(), @@ -208,15 +305,77 @@ func (c *ControllerV1beta1) generateTemplates() { ) } - c.mutatingWebhookTemplates = webhooks + c.validatingWebhookTemplates = validatingWebhooks + + mutatingWebhooks := []admiv1beta1.MutatingWebhook{} + + for _, webhook := range c.webhooks { + if !webhook.IsEnabled() || webhook.WebhookType() != common.MutatingWebhook { + continue + } + + nsSelector, objSelector := webhook.LabelSelectors(c.config.useNamespaceSelector()) + + mutatingWebhooks = append( + mutatingWebhooks, + c.getMutatingWebhookSkeleton( + webhook.Name(), + webhook.Endpoint(), + webhook.Operations(), + webhook.Resources(), + nsSelector, + objSelector, + ), + ) + } + + c.mutatingWebhookTemplates = mutatingWebhooks +} + +func (c *ControllerV1beta1) getValidatingWebhookSkeleton(nameSuffix, path string, operations []admiv1beta1.OperationType, resources []string, namespaceSelector, objectSelector *metav1.LabelSelector) admiv1beta1.ValidatingWebhook { + matchPolicy := admiv1beta1.Exact + sideEffects := admiv1beta1.SideEffectClassNone + port := c.config.getServicePort() + timeout := c.config.getTimeout() + failurePolicy := c.getFailurePolicy() + webhook := admiv1beta1.ValidatingWebhook{ + Name: c.config.configName(nameSuffix), + ClientConfig: admiv1beta1.WebhookClientConfig{ + Service: &admiv1beta1.ServiceReference{ + Namespace: c.config.getServiceNs(), + Name: c.config.getServiceName(), + Port: &port, + Path: &path, + }, + }, + Rules: []admiv1beta1.RuleWithOperations{ + { + Operations: operations, + Rule: admiv1beta1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: resources, + }, + }, + }, + FailurePolicy: &failurePolicy, + MatchPolicy: &matchPolicy, + SideEffects: &sideEffects, + TimeoutSeconds: &timeout, + AdmissionReviewVersions: []string{"v1beta1"}, + NamespaceSelector: namespaceSelector, + ObjectSelector: objectSelector, + } + + return webhook } -func (c *ControllerV1beta1) getWebhookSkeleton(nameSuffix, path string, operations []admiv1beta1.OperationType, resources []string, namespaceSelector, objectSelector *metav1.LabelSelector) admiv1beta1.MutatingWebhook { +func (c *ControllerV1beta1) getMutatingWebhookSkeleton(nameSuffix, path string, operations []admiv1beta1.OperationType, resources []string, namespaceSelector, objectSelector *metav1.LabelSelector) admiv1beta1.MutatingWebhook { matchPolicy := admiv1beta1.Exact sideEffects := admiv1beta1.SideEffectClassNone port := c.config.getServicePort() timeout := c.config.getTimeout() - failurePolicy := c.getAdmiV1Beta1FailurePolicy() + failurePolicy := c.getFailurePolicy() reinvocationPolicy := c.getReinvocationPolicy() webhook := admiv1beta1.MutatingWebhook{ Name: c.config.configName(nameSuffix), @@ -251,7 +410,7 @@ func (c *ControllerV1beta1) getWebhookSkeleton(nameSuffix, path string, operatio return webhook } -func (c *ControllerV1beta1) getAdmiV1Beta1FailurePolicy() admiv1beta1.FailurePolicyType { +func (c *ControllerV1beta1) getFailurePolicy() admiv1beta1.FailurePolicyType { policy := strings.ToLower(c.config.getFailurePolicy()) switch policy { case "ignore": diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go index 596ccdeedb3a1d..e536deec9fb4d3 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go @@ -10,13 +10,18 @@ package webhook import ( "context" "fmt" + configComp "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation" + "github.com/DataDog/datadog-agent/pkg/config" + configmock "github.com/DataDog/datadog-agent/pkg/config/mock" + "github.com/stretchr/testify/require" "reflect" "runtime" "testing" "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.uber.org/fx" admiv1beta1 "k8s.io/api/admissionregistration/v1beta1" corev1 "k8s.io/api/core/v1" @@ -26,13 +31,8 @@ import ( "k8s.io/client-go/kubernetes/fake" "github.com/DataDog/datadog-agent/comp/core" - configComp "github.com/DataDog/datadog-agent/comp/core/config" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" workloadmetafxmock "github.com/DataDog/datadog-agent/comp/core/workloadmeta/fx-mock" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation" - "github.com/DataDog/datadog-agent/pkg/config" - configmock "github.com/DataDog/datadog-agent/pkg/config/mock" "github.com/DataDog/datadog-agent/pkg/util/fxutil" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" ) @@ -72,13 +72,19 @@ func TestCreateWebhookV1beta1(t *testing.T) { defer close(stopCh) c := f.run(stopCh) - var webhook *admiv1beta1.MutatingWebhookConfiguration + var validatingWebhookConfiguration *admiv1beta1.ValidatingWebhookConfiguration require.Eventually(t, func() bool { - webhook, err = c.mutatingWebhooksLister.Get(v1beta1Cfg.getWebhookName()) + validatingWebhookConfiguration, err = c.validatingWebhooksLister.Get(v1beta1Cfg.getWebhookName()) return err == nil }, waitFor, tick) - if err := validateV1beta1(webhook, secret); err != nil { + var mutatingWebhookConfiguration *admiv1beta1.MutatingWebhookConfiguration + require.Eventually(t, func() bool { + mutatingWebhookConfiguration, err = c.mutatingWebhooksLister.Get(v1beta1Cfg.getWebhookName()) + return err == nil + }, waitFor, tick) + + if err := validateV1beta1(validatingWebhookConfiguration, mutatingWebhookConfiguration, secret); err != nil { t.Fatalf("Invalid Webhook: %v", err) } @@ -101,11 +107,11 @@ func TestUpdateOutdatedWebhookV1beta1(t *testing.T) { secret := buildSecret(data, v1beta1Cfg) f.populateSecretsCache(secret) - webhook := &admiv1beta1.MutatingWebhookConfiguration{ + oldValidatingWebhookConfiguration := &admiv1beta1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: v1beta1Cfg.getWebhookName(), + Name: v1Cfg.getWebhookName(), }, - Webhooks: []admiv1beta1.MutatingWebhook{ + Webhooks: []admiv1beta1.ValidatingWebhook{ { Name: "webhook-foo", }, @@ -114,20 +120,40 @@ func TestUpdateOutdatedWebhookV1beta1(t *testing.T) { }, }, } + f.populateValidatingWebhooksCache(oldValidatingWebhookConfiguration) - f.populateWebhooksCache(webhook) + oldMutatingWebhookConfiguration := &admiv1beta1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: v1Cfg.getWebhookName(), + }, + Webhooks: []admiv1beta1.MutatingWebhook{ + { + Name: "webhook-foo", + }, + { + Name: "webhook-bar", + }, + }, + } + f.populateMutatingWebhooksCache(oldMutatingWebhookConfiguration) stopCh := make(chan struct{}) defer close(stopCh) c := f.run(stopCh) - var newWebhook *admiv1beta1.MutatingWebhookConfiguration + var newValidatingWebhookConfiguration *admiv1beta1.ValidatingWebhookConfiguration + require.Eventually(t, func() bool { + newValidatingWebhookConfiguration, err = c.validatingWebhooksLister.Get(v1Cfg.getWebhookName()) + return err == nil && !reflect.DeepEqual(oldValidatingWebhookConfiguration, newValidatingWebhookConfiguration) + }, waitFor, tick) + + var newMutatingWebhookConfiguration *admiv1beta1.MutatingWebhookConfiguration require.Eventually(t, func() bool { - newWebhook, err = c.mutatingWebhooksLister.Get(v1beta1Cfg.getWebhookName()) - return err == nil && !reflect.DeepEqual(webhook, newWebhook) + newMutatingWebhookConfiguration, err = c.mutatingWebhooksLister.Get(v1Cfg.getWebhookName()) + return err == nil && !reflect.DeepEqual(oldMutatingWebhookConfiguration, newMutatingWebhookConfiguration) }, waitFor, tick) - if err := validateV1beta1(newWebhook, secret); err != nil { + if err := validateV1beta1(newValidatingWebhookConfiguration, newMutatingWebhookConfiguration, secret); err != nil { t.Fatalf("Invalid Webhook: %v", err) } @@ -136,90 +162,44 @@ func TestUpdateOutdatedWebhookV1beta1(t *testing.T) { }, waitFor, tick, "Work queue isn't empty") } -func TestAdmissionControllerFailureModeIgnoreV1beta1(t *testing.T) { +func TestAdmissionControllerFailureModeV1beta1(t *testing.T) { mockConfig := configmock.New(t) f := newFixtureV1beta1(t) c, _ := f.createController() - c.config = NewConfig(true, false) - mockConfig.SetWithoutSource("admission_controller.failure_policy", "Ignore") - c.config = NewConfig(true, false) + for _, value := range []string{"Ignore", "ignore", "BadVal", ""} { + mockConfig.SetWithoutSource("admission_controller.failure_policy", value) + c.config = NewConfig(true, false) - webhookSkeleton := c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.Ignore, *webhookSkeleton.FailurePolicy) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "ignore") - c.config = NewConfig(true, false) - - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.Ignore, *webhookSkeleton.FailurePolicy) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "BadVal") - c.config = NewConfig(true, false) - - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.Ignore, *webhookSkeleton.FailurePolicy) - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "") - c.config = NewConfig(true, false) - - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.Ignore, *webhookSkeleton.FailurePolicy) -} - -func TestAdmissionControllerFailureModeFailV1beta1(t *testing.T) { - mockConfig := configmock.New(t) - f := newFixtureV1beta1(t) - c, _ := f.createController() - - mockConfig.SetWithoutSource("admission_controller.failure_policy", "Fail") - c.config = NewConfig(true, false) - - webhookSkeleton := c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.Fail, *webhookSkeleton.FailurePolicy) + validatingWebhookSkeleton := c.getValidatingWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1beta1.Ignore, *validatingWebhookSkeleton.FailurePolicy) + mutatingWebhookSkeleton := c.getMutatingWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1beta1.Ignore, *mutatingWebhookSkeleton.FailurePolicy) + } - mockConfig.SetWithoutSource("admission_controller.failure_policy", "fail") - c.config = NewConfig(true, false) + for _, value := range []string{"Fail", "fail"} { + mockConfig.SetWithoutSource("admission_controller.failure_policy", value) + c.config = NewConfig(true, false) - webhookSkeleton = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.Fail, *webhookSkeleton.FailurePolicy) + validatingWebhookSkeleton := c.getValidatingWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1beta1.Fail, *validatingWebhookSkeleton.FailurePolicy) + mutatingWebhookSkeleton := c.getMutatingWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1beta1.Fail, *mutatingWebhookSkeleton.FailurePolicy) + } } func TestAdmissionControllerReinvocationPolicyV1beta1(t *testing.T) { mockConfig := configmock.New(t) f := newFixtureV1beta1(t) c, _ := f.createController() - c.config = NewConfig(true, false) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "IfNeeded") - c.config = NewConfig(true, false) - webhook := c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "ifneeded") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "Never") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.NeverReinvocationPolicy, *webhook.ReinvocationPolicy) - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "never") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.NeverReinvocationPolicy, *webhook.ReinvocationPolicy) + for _, value := range []string{"IfNeeded", "ifneeded", "Never", "never", "wrong", ""} { + mockConfig.SetWithoutSource("admission_controller.reinvocationpolicy", value) + c.config = NewConfig(true, false) - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "wrong") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) - - mockConfig.SetWithoutSource("admission_controller.reinvocation_policy", "") - c.config = NewConfig(true, false) - webhook = c.getWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) - assert.Equal(t, admiv1beta1.IfNeededReinvocationPolicy, *webhook.ReinvocationPolicy) + mutatingWebhookSkeleton := c.getMutatingWebhookSkeleton("foo", "/bar", []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nil, nil) + assert.Equal(t, admiv1beta1.IfNeededReinvocationPolicy, *mutatingWebhookSkeleton.ReinvocationPolicy) + } } func TestGenerateTemplatesV1beta1(t *testing.T) { @@ -955,7 +935,7 @@ func TestGenerateTemplatesV1beta1(t *testing.T) { c := &ControllerV1beta1{} c.config = tt.configFunc() - c.mutatingWebhooks = mutatingWebhooks(wmeta, nil) + c.webhooks = generateWebhooks(wmeta, nil) c.generateTemplates() assert.EqualValues(t, tt.want(), c.mutatingWebhookTemplates) @@ -1063,7 +1043,7 @@ func TestGetWebhookSkeletonV1beta1(t *testing.T) { nsSelector, objSelector := common.DefaultLabelSelectors(tt.namespaceSelector) - assert.EqualValues(t, tt.want, c.getWebhookSkeleton(tt.args.nameSuffix, tt.args.path, []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nsSelector, objSelector)) + assert.EqualValues(t, tt.want, c.getMutatingWebhookSkeleton(tt.args.nameSuffix, tt.args.path, []admiv1beta1.OperationType{admiv1beta1.Create}, []string{"pods"}, nsSelector, objSelector)) }) } } @@ -1085,6 +1065,7 @@ func (f *fixtureV1beta1) createController() (*ControllerV1beta1, informers.Share return NewControllerV1beta1( f.client, factory.Core().V1().Secrets(), + factory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations(), factory.Admissionregistration().V1beta1().MutatingWebhookConfigurations(), func() bool { return true }, make(chan struct{}), @@ -1103,19 +1084,36 @@ func (f *fixtureV1beta1) run(stopCh chan struct{}) *ControllerV1beta1 { return c } -func (f *fixtureV1beta1) populateWebhooksCache(webhooks ...*admiv1beta1.MutatingWebhookConfiguration) { +func (f *fixtureV1beta1) populateValidatingWebhooksCache(webhooks ...*admiv1beta1.ValidatingWebhookConfiguration) { + for _, w := range webhooks { + _, _ = f.client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(context.TODO(), w, metav1.CreateOptions{}) + } +} + +func (f *fixtureV1beta1) populateMutatingWebhooksCache(webhooks ...*admiv1beta1.MutatingWebhookConfiguration) { for _, w := range webhooks { _, _ = f.client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(context.TODO(), w, metav1.CreateOptions{}) } } -func validateV1beta1(w *admiv1beta1.MutatingWebhookConfiguration, s *corev1.Secret) error { - if len(w.Webhooks) != 3 { - return fmt.Errorf("Webhooks should contain 3 entries, got %d", len(w.Webhooks)) +func validateV1beta1(validatingWebhooks *admiv1beta1.ValidatingWebhookConfiguration, mutatingWebhooks *admiv1beta1.MutatingWebhookConfiguration, s *corev1.Secret) error { + // Validate the number of webhooks. + if len(validatingWebhooks.Webhooks) != 1 { // TODO (wassim): set validatingWebhooks to 0 once testing is done and we remove the always-admit webhook. + return fmt.Errorf("validatingWebhooks should contain 1 entries, got %d", len(validatingWebhooks.Webhooks)) + } + if len(mutatingWebhooks.Webhooks) != 3 { + return fmt.Errorf("mutatingWebhooks should contain 3 entries, got %d", len(validatingWebhooks.Webhooks)) + } + + // Validate the CA bundle for webhooks. + for i := 0; i < len(validatingWebhooks.Webhooks); i++ { + if !reflect.DeepEqual(validatingWebhooks.Webhooks[i].ClientConfig.CABundle, certificate.GetCABundle(s.Data)) { + return fmt.Errorf("the webhook CA bundle doesn't match the secret. CA bundle: %v, Secret: %v", validatingWebhooks.Webhooks[i].ClientConfig.CABundle, s) + } } - for i := 0; i < len(w.Webhooks); i++ { - if !reflect.DeepEqual(w.Webhooks[i].ClientConfig.CABundle, certificate.GetCABundle(s.Data)) { - return fmt.Errorf("The Webhook CABundle doesn't match the Secret: CABundle: %v, Secret: %v", w.Webhooks[i].ClientConfig.CABundle, s) + for i := 0; i < len(mutatingWebhooks.Webhooks); i++ { + if !reflect.DeepEqual(mutatingWebhooks.Webhooks[i].ClientConfig.CABundle, certificate.GetCABundle(s.Data)) { + return fmt.Errorf("the webhook CA bundle doesn't match the secret. CA bundle: %v, Secret: %v", mutatingWebhooks.Webhooks[i].ClientConfig.CABundle, s) } } return nil diff --git a/pkg/clusteragent/admission/metrics/metrics.go b/pkg/clusteragent/admission/metrics/metrics.go index c7f93e9789a1c3..3ce4d5fee19848 100644 --- a/pkg/clusteragent/admission/metrics/metrics.go +++ b/pkg/clusteragent/admission/metrics/metrics.go @@ -44,9 +44,15 @@ var ( CertificateDuration = telemetry.NewGaugeWithOpts("admission_webhooks", "certificate_expiry", []string{}, "Time left before the certificate expires in hours.", telemetry.Options{NoDoubleUnderscoreSep: true}) + ValidationAttempts = telemetry.NewGaugeWithOpts("admission_webhooks", "validation_attempts", + []string{"validation_type", "status", "validated", "error"}, "Number of pod validation attempts by validation type", + telemetry.Options{NoDoubleUnderscoreSep: true}) MutationAttempts = telemetry.NewGaugeWithOpts("admission_webhooks", "mutation_attempts", []string{"mutation_type", "status", "injected", "error"}, "Number of pod mutation attempts by mutation type", telemetry.Options{NoDoubleUnderscoreSep: true}) + ValidatingWebhooksReceived = telemetry.NewCounterWithOpts("admission_webhooks", "webhooks_received", + []string{"validation_type"}, "Number of webhook requests received.", + telemetry.Options{NoDoubleUnderscoreSep: true}) MutatingWebhooksReceived = telemetry.NewCounterWithOpts("admission_webhooks", "mutating_webhooks_received", []string{"mutation_type"}, "Number of webhook requests received.", telemetry.Options{NoDoubleUnderscoreSep: true}) @@ -56,6 +62,14 @@ var ( GetOwnerCacheMiss = telemetry.NewGaugeWithOpts("admission_webhooks", "owner_cache_miss", []string{"resource"}, "Number of cache misses while getting pod's owner object.", telemetry.Options{NoDoubleUnderscoreSep: true}) + ValidatingWebhooksResponseDuration = telemetry.NewHistogramWithOpts( + "admission_webhooks", + "response_duration", + []string{"validation_type"}, + "Webhook response duration distribution (in seconds).", + prometheus.DefBuckets, // The default prometheus buckets are adapted to measure response time + telemetry.Options{NoDoubleUnderscoreSep: true}, + ) MutatingWebhooksResponseDuration = telemetry.NewHistogramWithOpts( "admission_webhooks", "mutating_response_duration", diff --git a/pkg/clusteragent/admission/mutate/agent_sidecar/agent_sidecar.go b/pkg/clusteragent/admission/mutate/agent_sidecar/agent_sidecar.go index 155d9395dde78a..04da72729490ae 100644 --- a/pkg/clusteragent/admission/mutate/agent_sidecar/agent_sidecar.go +++ b/pkg/clusteragent/admission/mutate/agent_sidecar/agent_sidecar.go @@ -13,19 +13,21 @@ import ( "encoding/json" "errors" "fmt" + admiv1 "k8s.io/api/admission/v1" "os" "slices" "strconv" - admiv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" "github.com/DataDog/datadog-agent/pkg/config" apiCommon "github.com/DataDog/datadog-agent/pkg/util/kubernetes/apiserver/common" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/clustername" @@ -43,10 +45,11 @@ type Selector struct { // Webhook is the webhook that injects a Datadog Agent sidecar type Webhook struct { name string + webhookType common.WebhookType isEnabled bool endpoint string resources []string - operations []admiv1.OperationType + operations []admissionregistrationv1.OperationType namespaceSelector *metav1.LabelSelector objectSelector *metav1.LabelSelector containerRegistry string @@ -56,14 +59,15 @@ type Webhook struct { func NewWebhook() *Webhook { nsSelector, objSelector := labelSelectors() - containerRegistry := common.ContainerRegistry("admission_controller.agent_sidecar.container_registry") + containerRegistry := mutatecommon.ContainerRegistry("admission_controller.agent_sidecar.container_registry") return &Webhook{ name: webhookName, + webhookType: common.MutatingWebhook, isEnabled: config.Datadog().GetBool("admission_controller.agent_sidecar.enabled"), endpoint: config.Datadog().GetString("admission_controller.agent_sidecar.endpoint"), resources: []string{"pods"}, - operations: []admiv1.OperationType{admiv1.Create}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, namespaceSelector: nsSelector, objectSelector: objSelector, containerRegistry: containerRegistry, @@ -75,6 +79,11 @@ func (w *Webhook) Name() string { return w.name } +// WebhookType returns the type of the webhook +func (w *Webhook) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *Webhook) IsEnabled() bool { return w.isEnabled && (w.namespaceSelector != nil || w.objectSelector != nil) @@ -93,7 +102,7 @@ func (w *Webhook) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *Webhook) Operations() []admiv1.OperationType { +func (w *Webhook) Operations() []admissionregistrationv1.OperationType { return w.operations } @@ -103,14 +112,11 @@ func (w *Webhook) LabelSelectors(_ bool) (namespaceSelector *metav1.LabelSelecto return w.namespaceSelector, w.objectSelector } -// MutateFunc returns the function that mutates the resources -func (w *Webhook) MutateFunc() admission.MutatingWebhookFunc { - return w.mutate -} - -// mutate handles mutating pod requests for the agentsidecar webhook -func (w *Webhook) mutate(request *admission.MutateRequest) ([]byte, error) { - return common.Mutate(request.Raw, request.Namespace, w.Name(), w.injectAgentSidecar, request.DynamicClient) +// WebhookFunc returns the function that mutates the resources +func (w *Webhook) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { + return func(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatecommon.Mutate(request.Raw, request.Namespace, w.Name(), w.injectAgentSidecar, request.DynamicClient)) + } } func (w *Webhook) injectAgentSidecar(pod *corev1.Pod, _ string, _ dynamic.Interface) (bool, error) { diff --git a/pkg/clusteragent/admission/mutate/autoinstrumentation/auto_instrumentation.go b/pkg/clusteragent/admission/mutate/autoinstrumentation/auto_instrumentation.go index a2b2725993c0c5..dd38c5b9fc8ae5 100644 --- a/pkg/clusteragent/admission/mutate/autoinstrumentation/auto_instrumentation.go +++ b/pkg/clusteragent/admission/mutate/autoinstrumentation/auto_instrumentation.go @@ -13,12 +13,13 @@ import ( "encoding/json" "errors" "fmt" + admiv1 "k8s.io/api/admission/v1" "os" "strconv" "strings" "time" - admiv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,10 +50,11 @@ const ( // Webhook is the auto instrumentation webhook type Webhook struct { name string + webhookType common.WebhookType isEnabled bool endpoint string resources []string - operations []admiv1.OperationType + operations []admissionregistrationv1.OperationType initSecurityContext *corev1.SecurityContext initResourceRequirements corev1.ResourceRequirements containerRegistry string @@ -101,10 +103,11 @@ func NewWebhook(wmeta workloadmeta.Component, filter mutatecommon.InjectionFilte return &Webhook{ name: webhookName, + webhookType: common.MutatingWebhook, isEnabled: isEnabled, endpoint: config.Datadog().GetString("admission_controller.auto_instrumentation.endpoint"), resources: []string{"pods"}, - operations: []admiv1.OperationType{admiv1.Create}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, initSecurityContext: initSecurityContext, initResourceRequirements: initResourceRequirements, injectionFilter: filter, @@ -121,6 +124,11 @@ func (w *Webhook) Name() string { return w.name } +// WebhookType returns the type of the webhook +func (w *Webhook) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *Webhook) IsEnabled() bool { return w.isEnabled @@ -139,24 +147,21 @@ func (w *Webhook) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *Webhook) Operations() []admiv1.OperationType { +func (w *Webhook) Operations() []admissionregistrationv1.OperationType { return w.operations } // LabelSelectors returns the label selectors that specify when the webhook // should be invoked func (w *Webhook) LabelSelectors(useNamespaceSelector bool) (namespaceSelector *metav1.LabelSelector, objectSelector *metav1.LabelSelector) { - return mutatecommon.DefaultLabelSelectors(useNamespaceSelector) + return common.DefaultLabelSelectors(useNamespaceSelector) } -// MutateFunc returns the function that mutates the resources -func (w *Webhook) MutateFunc() admission.MutatingWebhookFunc { - return w.injectAutoInstrumentation -} - -// injectAutoInstrumentation injects APM libraries into pods -func (w *Webhook) injectAutoInstrumentation(request *admission.MutateRequest) ([]byte, error) { - return mutatecommon.Mutate(request.Raw, request.Namespace, w.Name(), w.inject, request.DynamicClient) +// WebhookFunc returns the function that mutates the resources +func (w *Webhook) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { + return func(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatecommon.Mutate(request.Raw, request.Namespace, w.Name(), w.inject, request.DynamicClient)) + } } func initContainerName(lang language) string { diff --git a/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go b/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go index ebdfee3031d4b3..7e728158ba1df4 100644 --- a/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go +++ b/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go @@ -10,11 +10,13 @@ package autoscaling import ( "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" + mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/config" - admiv1 "k8s.io/api/admissionregistration/v1" + admiv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" @@ -27,23 +29,25 @@ const ( // Webhook implements the MutatingWebhook interface type Webhook struct { - name string - isEnabled bool - endpoint string - resources []string - operations []admiv1.OperationType - patcher workload.PodPatcher + name string + webhookType common.WebhookType + isEnabled bool + endpoint string + resources []string + operations []admissionregistrationv1.OperationType + patcher workload.PodPatcher } // NewWebhook returns a new Webhook func NewWebhook(patcher workload.PodPatcher) *Webhook { return &Webhook{ - name: webhookName, - isEnabled: config.Datadog().GetBool("autoscaling.workload.enabled"), - endpoint: webhookEndpoint, - resources: []string{"pods"}, - operations: []admiv1.OperationType{admiv1.Create}, - patcher: patcher, + name: webhookName, + webhookType: common.MutatingWebhook, + isEnabled: config.Datadog().GetBool("autoscaling.workload.enabled"), + endpoint: webhookEndpoint, + resources: []string{"pods"}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, + patcher: patcher, } } @@ -52,6 +56,11 @@ func (w *Webhook) Name() string { return w.name } +// WebhookType returns the type of the webhook +func (w *Webhook) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *Webhook) IsEnabled() bool { return w.isEnabled @@ -70,7 +79,7 @@ func (w *Webhook) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *Webhook) Operations() []admiv1.OperationType { +func (w *Webhook) Operations() []admissionregistrationv1.OperationType { return w.operations } @@ -82,18 +91,14 @@ func (w *Webhook) LabelSelectors(_ bool) (namespaceSelector *metav1.LabelSelecto return nil, nil } -// MutateFunc returns the function that mutates the resources -func (w *Webhook) MutateFunc() admission.MutatingWebhookFunc { - return w.mutate -} - -// mutate adds the DD_AGENT_HOST and DD_ENTITY_ID env vars to the pod template if they don't exist -func (w *Webhook) mutate(request *admission.MutateRequest) ([]byte, error) { - return common.Mutate(request.Raw, request.Namespace, w.Name(), w.updateResources, request.DynamicClient) +// WebhookFunc returns the function that mutates the resources +func (w *Webhook) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { + return func(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatecommon.Mutate(request.Raw, request.Namespace, w.Name(), w.updateResources, request.DynamicClient)) + } } -// updateResource finds the owner of a pod, calls the recommender to retrieve the recommended CPU and Memory -// requests +// updateResources finds the owner of a pod, calls the recommender to retrieve the recommended CPU and Memory requests func (w *Webhook) updateResources(pod *corev1.Pod, _ string, _ dynamic.Interface) (bool, error) { return w.patcher.ApplyRecommendations(pod) } diff --git a/pkg/clusteragent/admission/mutate/config/config.go b/pkg/clusteragent/admission/mutate/config/config.go index b6e0099d3a0259..b7f2d46c85e1bd 100644 --- a/pkg/clusteragent/admission/mutate/config/config.go +++ b/pkg/clusteragent/admission/mutate/config/config.go @@ -14,16 +14,17 @@ import ( "fmt" "strings" - admiv1 "k8s.io/api/admissionregistration/v1" + admiv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" - admCommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" "github.com/DataDog/datadog-agent/pkg/config" apiCommon "github.com/DataDog/datadog-agent/pkg/util/kubernetes/apiserver/common" "github.com/DataDog/datadog-agent/pkg/util/log" @@ -96,23 +97,25 @@ var ( // Webhook is the webhook that injects DD_AGENT_HOST and DD_ENTITY_ID into a pod type Webhook struct { name string + webhookType common.WebhookType isEnabled bool endpoint string resources []string - operations []admiv1.OperationType + operations []admissionregistrationv1.OperationType mode string wmeta workloadmeta.Component - injectionFilter common.InjectionFilter + injectionFilter mutatecommon.InjectionFilter } // NewWebhook returns a new Webhook -func NewWebhook(wmeta workloadmeta.Component, injectionFilter common.InjectionFilter) *Webhook { +func NewWebhook(wmeta workloadmeta.Component, injectionFilter mutatecommon.InjectionFilter) *Webhook { return &Webhook{ name: webhookName, + webhookType: common.MutatingWebhook, isEnabled: config.Datadog().GetBool("admission_controller.inject_config.enabled"), endpoint: config.Datadog().GetString("admission_controller.inject_config.endpoint"), resources: []string{"pods"}, - operations: []admiv1.OperationType{admiv1.Create}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, mode: config.Datadog().GetString("admission_controller.inject_config.mode"), wmeta: wmeta, injectionFilter: injectionFilter, @@ -124,6 +127,11 @@ func (w *Webhook) Name() string { return w.name } +// WebhookType returns the type of the webhook +func (w *Webhook) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *Webhook) IsEnabled() bool { return w.isEnabled @@ -142,7 +150,7 @@ func (w *Webhook) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *Webhook) Operations() []admiv1.OperationType { +func (w *Webhook) Operations() []admissionregistrationv1.OperationType { return w.operations } @@ -152,14 +160,11 @@ func (w *Webhook) LabelSelectors(useNamespaceSelector bool) (namespaceSelector * return common.DefaultLabelSelectors(useNamespaceSelector) } -// MutateFunc returns the function that mutates the resources -func (w *Webhook) MutateFunc() admission.MutatingWebhookFunc { - return w.mutate -} - -// mutate adds the DD_AGENT_HOST and DD_ENTITY_ID env vars to the pod template if they don't exist -func (w *Webhook) mutate(request *admission.MutateRequest) ([]byte, error) { - return common.Mutate(request.Raw, request.Namespace, w.Name(), w.inject, request.DynamicClient) +// WebhookFunc returns the function that mutates the resources +func (w *Webhook) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { + return func(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatecommon.Mutate(request.Raw, request.Namespace, w.Name(), w.inject, request.DynamicClient)) + } } // inject injects the following environment variables into the pod template: @@ -180,21 +185,21 @@ func (w *Webhook) inject(pod *corev1.Pod, _ string, _ dynamic.Interface) (bool, // Inject DD_AGENT_HOST switch injectionMode(pod, w.mode) { case hostIP: - injectedConfig = common.InjectEnv(pod, agentHostIPEnvVar) + injectedConfig = mutatecommon.InjectEnv(pod, agentHostIPEnvVar) case service: - injectedConfig = common.InjectEnv(pod, agentHostServiceEnvVar) + injectedConfig = mutatecommon.InjectEnv(pod, agentHostServiceEnvVar) case socket: volume, volumeMount := buildVolume(DatadogVolumeName, config.Datadog().GetString("admission_controller.inject_config.socket_path"), true) - injectedVol := common.InjectVolume(pod, volume, volumeMount) - injectedEnv := common.InjectEnv(pod, traceURLSocketEnvVar) - injectedEnv = common.InjectEnv(pod, dogstatsdURLSocketEnvVar) || injectedEnv + injectedVol := mutatecommon.InjectVolume(pod, volume, volumeMount) + injectedEnv := mutatecommon.InjectEnv(pod, traceURLSocketEnvVar) + injectedEnv = mutatecommon.InjectEnv(pod, dogstatsdURLSocketEnvVar) || injectedEnv injectedConfig = injectedEnv || injectedVol default: log.Errorf("invalid injection mode %q", w.mode) return false, errors.New(metrics.InvalidInput) } - injectedEntity = common.InjectEnv(pod, defaultDdEntityIDEnvVar) + injectedEntity = mutatecommon.InjectEnv(pod, defaultDdEntityIDEnvVar) // Inject External Data Environment Variable injectedExternalEnv = injectExternalDataEnvVar(pod) @@ -204,13 +209,13 @@ func (w *Webhook) inject(pod *corev1.Pod, _ string, _ dynamic.Interface) (bool, // injectionMode returns the injection mode based on the global mode and pod labels func injectionMode(pod *corev1.Pod, globalMode string) string { - if val, found := pod.GetLabels()[admCommon.InjectionModeLabelKey]; found { + if val, found := pod.GetLabels()[common.InjectionModeLabelKey]; found { mode := strings.ToLower(val) switch mode { case hostIP, service, socket: return mode default: - log.Warnf("Invalid label value '%s=%s' on pod %s should be either 'hostip', 'service' or 'socket', defaulting to %q", admCommon.InjectionModeLabelKey, val, common.PodString(pod), globalMode) + log.Warnf("Invalid label value '%s=%s' on pod %s should be either 'hostip', 'service' or 'socket', defaulting to %q", common.InjectionModeLabelKey, val, mutatecommon.PodString(pod), globalMode) return globalMode } } diff --git a/pkg/clusteragent/admission/mutate/config/config_test.go b/pkg/clusteragent/admission/mutate/config/config_test.go index 4fade296b0ca1b..03f168174db546 100644 --- a/pkg/clusteragent/admission/mutate/config/config_test.go +++ b/pkg/clusteragent/admission/mutate/config/config_test.go @@ -400,16 +400,15 @@ func TestJSONPatchCorrectness(t *testing.T) { fx.Replace(config.MockParams{Overrides: tt.overrides}), ) webhook := NewWebhook(wmeta, autoinstrumentation.GetInjectionFilter()) - request := admission.MutateRequest{ + request := admission.Request{ Raw: podJSON, Namespace: "bar", } - jsonPatch, err := webhook.MutateFunc()(&request) - assert.NoError(t, err) + admissionResponse := webhook.WebhookFunc()(&request) expected, err := os.ReadFile(tt.file) assert.NoError(t, err) - assert.JSONEq(t, string(expected), string(jsonPatch)) + assert.JSONEq(t, string(expected), string(admissionResponse.Patch)) }) } } @@ -435,11 +434,12 @@ func BenchmarkJSONPatch(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - request := admission.MutateRequest{ + request := admission.Request{ Raw: podJSON, Namespace: "bar", } - jsonPatch, err := webhook.MutateFunc()(&request) + admissionResponse := webhook.WebhookFunc()(&request) + jsonPatch, err := json.Marshal(admissionResponse.Patch) if err != nil { b.Fatal(err) } diff --git a/pkg/clusteragent/admission/mutate/cwsinstrumentation/cws_instrumentation.go b/pkg/clusteragent/admission/mutate/cwsinstrumentation/cws_instrumentation.go index d611e769f661d4..86ffdc882f1312 100644 --- a/pkg/clusteragent/admission/mutate/cwsinstrumentation/cws_instrumentation.go +++ b/pkg/clusteragent/admission/mutate/cwsinstrumentation/cws_instrumentation.go @@ -14,6 +14,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" + admiv1 "k8s.io/api/admission/v1" "path/filepath" "strconv" @@ -21,7 +23,7 @@ import ( "k8s.io/utils/strings/slices" "github.com/wI2L/jsondiff" - admiv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -33,7 +35,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors/util" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation/k8scp" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation/k8sexec" "github.com/DataDog/datadog-agent/pkg/config" @@ -86,21 +88,23 @@ type mutatePodExecFunc func(*corev1.PodExecOptions, string, string, *authenticat // WebhookForPods is the webhook that injects CWS pod instrumentation type WebhookForPods struct { name string + webhookType common.WebhookType isEnabled bool endpoint string resources []string - operations []admiv1.OperationType - admissionFunc admission.MutatingWebhookFunc + operations []admissionregistrationv1.OperationType + admissionFunc admission.WebhookFunc } -func newWebhookForPods(admissionFunc admission.MutatingWebhookFunc) *WebhookForPods { +func newWebhookForPods(admissionFunc admission.WebhookFunc) *WebhookForPods { return &WebhookForPods{ - name: webhookForPodsName, + name: webhookForPodsName, + webhookType: common.MutatingWebhook, isEnabled: config.Datadog().GetBool("admission_controller.cws_instrumentation.enabled") && len(config.Datadog().GetString("admission_controller.cws_instrumentation.image_name")) > 0, endpoint: config.Datadog().GetString("admission_controller.cws_instrumentation.pod_endpoint"), resources: []string{"pods"}, - operations: []admiv1.OperationType{admiv1.Create}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, admissionFunc: admissionFunc, } } @@ -110,6 +114,11 @@ func (w *WebhookForPods) Name() string { return w.name } +// WebhookType returns the type of the webhook +func (w *WebhookForPods) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *WebhookForPods) IsEnabled() bool { return w.isEnabled @@ -128,7 +137,7 @@ func (w *WebhookForPods) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *WebhookForPods) Operations() []admiv1.OperationType { +func (w *WebhookForPods) Operations() []admissionregistrationv1.OperationType { return w.operations } @@ -138,29 +147,31 @@ func (w *WebhookForPods) LabelSelectors(useNamespaceSelector bool) (namespaceSel return labelSelectors(useNamespaceSelector) } -// MutateFunc returns the function that mutates the resources -func (w *WebhookForPods) MutateFunc() admission.MutatingWebhookFunc { +// WebhookFunc returns the function that mutates the resources +func (w *WebhookForPods) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { return w.admissionFunc } // WebhookForCommands is the webhook that injects CWS pods/exec instrumentation type WebhookForCommands struct { name string + webhookType common.WebhookType isEnabled bool endpoint string resources []string - operations []admiv1.OperationType - admissionFunc admission.MutatingWebhookFunc + operations []admissionregistrationv1.OperationType + admissionFunc admission.WebhookFunc } -func newWebhookForCommands(admissionFunc admission.MutatingWebhookFunc) *WebhookForCommands { +func newWebhookForCommands(admissionFunc admission.WebhookFunc) *WebhookForCommands { return &WebhookForCommands{ - name: webhookForCommandsName, + name: webhookForCommandsName, + webhookType: common.MutatingWebhook, isEnabled: config.Datadog().GetBool("admission_controller.cws_instrumentation.enabled") && len(config.Datadog().GetString("admission_controller.cws_instrumentation.image_name")) > 0, endpoint: config.Datadog().GetString("admission_controller.cws_instrumentation.command_endpoint"), resources: []string{"pods/exec"}, - operations: []admiv1.OperationType{admiv1.Connect}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Connect}, admissionFunc: admissionFunc, } } @@ -170,6 +181,11 @@ func (w *WebhookForCommands) Name() string { return w.name } +// WebhookType returns the name of the webhook +func (w *WebhookForCommands) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *WebhookForCommands) IsEnabled() bool { return w.isEnabled @@ -188,7 +204,7 @@ func (w *WebhookForCommands) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *WebhookForCommands) Operations() []admiv1.OperationType { +func (w *WebhookForCommands) Operations() []admissionregistrationv1.OperationType { return w.operations } @@ -198,8 +214,8 @@ func (w *WebhookForCommands) LabelSelectors(_ bool) (namespaceSelector *metav1.L return nil, nil } -// MutateFunc returns the function that mutates the resources -func (w *WebhookForCommands) MutateFunc() admission.MutatingWebhookFunc { +// WebhookFunc MutateFunc returns the function that mutates the resources +func (w *WebhookForCommands) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { return w.admissionFunc } @@ -302,7 +318,7 @@ func NewCWSInstrumentation(wmeta workloadmeta.Component) (*CWSInstrumentation, e cwsInjectorImageName := config.Datadog().GetString("admission_controller.cws_instrumentation.image_name") cwsInjectorImageTag := config.Datadog().GetString("admission_controller.cws_instrumentation.image_tag") - cwsInjectorContainerRegistry := common.ContainerRegistry("admission_controller.cws_instrumentation.container_registry") + cwsInjectorContainerRegistry := mutatecommon.ContainerRegistry("admission_controller.cws_instrumentation.container_registry") if len(cwsInjectorImageName) == 0 { return nil, fmt.Errorf("can't initialize CWS Instrumentation without an image_name") @@ -357,8 +373,8 @@ func (ci *CWSInstrumentation) WebhookForCommands() *WebhookForCommands { return ci.webhookForCommands } -func (ci *CWSInstrumentation) injectForCommand(request *admission.MutateRequest) ([]byte, error) { - return mutatePodExecOptions(request.Raw, request.Name, request.Namespace, ci.webhookForCommands.Name(), request.UserInfo, ci.injectCWSCommandInstrumentation, request.DynamicClient, request.APIClient) +func (ci *CWSInstrumentation) injectForCommand(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatePodExecOptions(request.Raw, request.Name, request.Namespace, ci.webhookForCommands.Name(), request.UserInfo, ci.injectCWSCommandInstrumentation, request.DynamicClient, request.APIClient)) } func (ci *CWSInstrumentation) resolveNodeArch(nodeName string, apiClient kubernetes.Interface) (string, error) { @@ -476,7 +492,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx // is the pod instrumentation ready ? (i.e. has the CWS Instrumentation init container been added ?) if !isPodCWSInstrumentationReady(pod.Annotations) { // pod isn't instrumented, do not attempt to override the pod exec command - log.Debugf("Ignoring exec request into %s, pod not instrumented yet", common.PodString(pod)) + log.Debugf("Ignoring exec request into %s, pod not instrumented yet", mutatecommon.PodString(pod)) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsPodNotInstrumentedReason) return false, nil } @@ -487,7 +503,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx if ci.mountVolumeForRemoteCopy { if !isPodCWSInstrumentationReady(pod.Annotations) { // pod isn't instrumented, do not attempt to override the pod exec command - log.Debugf("Ignoring exec request into %s, pod not instrumented yet", common.PodString(pod)) + log.Debugf("Ignoring exec request into %s, pod not instrumented yet", mutatecommon.PodString(pod)) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsPodNotInstrumentedReason) return false, nil } @@ -496,7 +512,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx // check if the target pod has a read only filesystem if readOnly := ci.hasReadonlyRootfs(pod, exec.Container); readOnly { // readonly rootfs containers can't be instrumented - log.Errorf("Ignoring exec request into %s, container %s has read only rootfs. Try enabling admission_controller.cws_instrumentation.remote_copy.mount_volume", common.PodString(pod), exec.Container) + log.Errorf("Ignoring exec request into %s, container %s has read only rootfs. Try enabling admission_controller.cws_instrumentation.remote_copy.mount_volume", mutatecommon.PodString(pod), exec.Container) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsReadonlyFilesystemReason) return false, errors.New(metrics.InvalidInput) } @@ -512,7 +528,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx arch, err := ci.resolveNodeArch(pod.Spec.NodeName, apiClient) if err != nil { - log.Errorf("Ignoring exec request into %s: %v", common.PodString(pod), err) + log.Errorf("Ignoring exec request into %s: %v", mutatecommon.PodString(pod), err) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsMissingArchReason) return false, errors.New(metrics.InternalError) } @@ -520,7 +536,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx // check if the pod is ready to be exec-ed into if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { - log.Errorf("Ignoring exec request into %s: cannot exec into a container in a completed pod; current phase is %s", common.PodString(pod), pod.Status.Phase) + log.Errorf("Ignoring exec request into %s: cannot exec into a container in a completed pod; current phase is %s", mutatecommon.PodString(pod), pod.Status.Phase) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsCompletedPodReason) return false, errors.New(metrics.InvalidInput) } @@ -528,19 +544,19 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx // check if the input container exists, or select the default one to which the user will be redirected container, err := podcmd.FindOrDefaultContainerByName(pod, exec.Container, true, nil) if err != nil { - log.Errorf("Ignoring exec request into %s, invalid container: %v", common.PodString(pod), err) + log.Errorf("Ignoring exec request into %s, invalid container: %v", mutatecommon.PodString(pod), err) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsInvalidInputContainerReason) return false, errors.New(metrics.InvalidInput) } // copy CWS instrumentation directly to the target container if err := ci.injectCWSCommandInstrumentationRemoteCopy(pod, container.Name, cwsInstrumentationLocalPath, cwsInstrumentationRemotePath); err != nil { - log.Warnf("Ignoring exec request into %s, remote copy failed: %v", common.PodString(pod), err) + log.Warnf("Ignoring exec request into %s, remote copy failed: %v", mutatecommon.PodString(pod), err) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsRemoteCopyFailedReason) return false, errors.New(metrics.InternalError) } default: - log.Errorf("Ignoring exec request into %s, unknown CWS Instrumentation mode %v", common.PodString(pod), ci.mode) + log.Errorf("Ignoring exec request into %s, unknown CWS Instrumentation mode %v", mutatecommon.PodString(pod), ci.mode) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsUnknownModeReason) return false, errors.New(metrics.InvalidInput) } @@ -548,7 +564,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx // prepare the user session context userSessionCtx, err := usersessions.PrepareK8SUserSessionContext(userInfo, cwsUserSessionDataMaxSize) if err != nil { - log.Debugf("ignoring instrumentation of %s: %v", common.PodString(pod), err) + log.Debugf("ignoring instrumentation of %s: %v", mutatecommon.PodString(pod), err) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsCredentialsSerializationErrorReason) return false, errors.New(metrics.InternalError) } @@ -563,7 +579,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx exec.Command[6] == "--" { if exec.Command[5] == string(userSessionCtx) { - log.Debugf("Exec request into %s is already instrumented, ignoring", common.PodString(pod)) + log.Debugf("Exec request into %s is already instrumented, ignoring", mutatecommon.PodString(pod)) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsAlreadyInstrumentedReason) return true, nil } @@ -581,7 +597,7 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentation(exec *corev1.PodEx "--", }, exec.Command...) - log.Debugf("Pod exec request to %s by %s is now instrumented for CWS", common.PodString(pod), userInfo.Username) + log.Debugf("Pod exec request to %s by %s is now instrumented for CWS", mutatecommon.PodString(pod), userInfo.Username) metrics.CWSExecInstrumentationAttempts.Observe(1, ci.mode.String(), "true", "") injected = true @@ -604,8 +620,8 @@ func (ci *CWSInstrumentation) injectCWSCommandInstrumentationRemoteCopy(pod *cor return health.Run(cwsInstrumentationRemotePath, pod, container) } -func (ci *CWSInstrumentation) injectForPod(request *admission.MutateRequest) ([]byte, error) { - return common.Mutate(request.Raw, request.Namespace, ci.webhookForPods.Name(), ci.injectCWSPodInstrumentation, request.DynamicClient) +func (ci *CWSInstrumentation) injectForPod(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatecommon.Mutate(request.Raw, request.Namespace, ci.webhookForPods.Name(), ci.injectCWSPodInstrumentation, request.DynamicClient)) } func (ci *CWSInstrumentation) injectCWSPodInstrumentation(pod *corev1.Pod, ns string, _ dynamic.Interface) (bool, error) { @@ -637,7 +653,7 @@ func (ci *CWSInstrumentation) injectCWSPodInstrumentation(pod *corev1.Pod, ns st case RemoteCopy: instrumented = ci.injectCWSPodInstrumentationRemoteCopy(pod) default: - log.Errorf("Ignoring Pod %s admission request: unknown CWS Instrumentation mode %v", common.PodString(pod), ci.mode) + log.Errorf("Ignoring Pod %s admission request: unknown CWS Instrumentation mode %v", mutatecommon.PodString(pod), ci.mode) metrics.CWSPodInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsUnknownModeReason) return false, errors.New(metrics.InvalidInput) } @@ -648,7 +664,7 @@ func (ci *CWSInstrumentation) injectCWSPodInstrumentation(pod *corev1.Pod, ns st pod.Annotations = make(map[string]string) } pod.Annotations[cwsInstrumentationPodAnotationStatus] = cwsInstrumentationPodAnotationReady - log.Debugf("Pod %s is now instrumented for CWS", common.PodString(pod)) + log.Debugf("Pod %s is now instrumented for CWS", mutatecommon.PodString(pod)) metrics.CWSPodInstrumentationAttempts.Observe(1, ci.mode.String(), "true", "") } else { metrics.CWSPodInstrumentationAttempts.Observe(1, ci.mode.String(), "false", cwsNoInstrumentationNeededReason) diff --git a/pkg/clusteragent/admission/mutate/doc.go b/pkg/clusteragent/admission/mutate/doc.go index 931e1e30f1e01f..77f78813b1afd4 100644 --- a/pkg/clusteragent/admission/mutate/doc.go +++ b/pkg/clusteragent/admission/mutate/doc.go @@ -25,10 +25,12 @@ // grouped in the same package. For example, the CWS webhooks are all in the // same package. // -// Each mutating webhook needs to implement the "MutatingWebhook" interface. +// Each mutating webhook needs to implement the "Webhook" interface. // Here's a brief description of each function and what they are used for: // - Name: it's the name of the webhook. It's used to identify it. The name // appears in some telemetry tags. +// - WebhookType: it's the type of the webhook. It can be either "mutating" or +// "validating". // - IsEnabled: returns whether the webhook is enabled or not. In general, the // recommendation is to disable the webhook by default unless it's needed by a // core feature that should be enabled for everyone that deploys the Datadog @@ -44,7 +46,7 @@ // minimize the number of requests that the webhook receives. The label // selectors help us with that. There are some default label selectors defined // in the "common" package. -// - MutateFunc: the function that mutates the Kubernetes object. +// - WebhookFunc: the function that runs the logic of the webhook and returns the admission response. // // As any other feature, mutating webhooks can be configured using the Datadog // configuration. When adding new configuration parameters, please try to follow @@ -58,7 +60,7 @@ // We should try to avoid depending on the order in which webhooks are executed. // When this cannot be avoided, keep in mind that the order in which the // webhooks are executed is the order in which they are returned by the -// "mutatingWebhooks" function in the "webhook" package. +// "generateWebhooks" function in the "webhook" package. // // Mutating webhooks emit telemetry metrics. Each webhook can define its own // metrics as needed but some metrics like "mutation_attempts" or diff --git a/pkg/clusteragent/admission/mutate/tagsfromlabels/tags.go b/pkg/clusteragent/admission/mutate/tagsfromlabels/tags.go index 72609e3dc55387..9e3846fb100967 100644 --- a/pkg/clusteragent/admission/mutate/tagsfromlabels/tags.go +++ b/pkg/clusteragent/admission/mutate/tagsfromlabels/tags.go @@ -13,10 +13,12 @@ import ( "context" "errors" "fmt" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" + admiv1 "k8s.io/api/admission/v1" "strings" "time" - admiv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -25,7 +27,7 @@ import ( "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" - "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/cache" "github.com/DataDog/datadog-agent/pkg/util/kubernetes" @@ -43,23 +45,25 @@ const webhookName = "standard_tags" // Webhook is the webhook that injects DD_ENV, DD_VERSION, DD_SERVICE env vars type Webhook struct { name string + webhookType common.WebhookType isEnabled bool endpoint string resources []string - operations []admiv1.OperationType + operations []admissionregistrationv1.OperationType ownerCacheTTL time.Duration wmeta workloadmeta.Component - injectionFilter common.InjectionFilter + injectionFilter mutatecommon.InjectionFilter } // NewWebhook returns a new Webhook -func NewWebhook(wmeta workloadmeta.Component, injectionFilter common.InjectionFilter) *Webhook { +func NewWebhook(wmeta workloadmeta.Component, injectionFilter mutatecommon.InjectionFilter) *Webhook { return &Webhook{ name: webhookName, + webhookType: common.MutatingWebhook, isEnabled: config.Datadog().GetBool("admission_controller.inject_tags.enabled"), endpoint: config.Datadog().GetString("admission_controller.inject_tags.endpoint"), resources: []string{"pods"}, - operations: []admiv1.OperationType{admiv1.Create}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, ownerCacheTTL: ownerCacheTTL(), wmeta: wmeta, injectionFilter: injectionFilter, @@ -71,6 +75,11 @@ func (w *Webhook) Name() string { return w.name } +// WebhookType returns the type of the webhook +func (w *Webhook) WebhookType() common.WebhookType { + return w.webhookType +} + // IsEnabled returns whether the webhook is enabled func (w *Webhook) IsEnabled() bool { return w.isEnabled @@ -89,7 +98,7 @@ func (w *Webhook) Resources() []string { // Operations returns the operations on the resources specified for which // the webhook should be invoked -func (w *Webhook) Operations() []admiv1.OperationType { +func (w *Webhook) Operations() []admissionregistrationv1.OperationType { return w.operations } @@ -99,11 +108,6 @@ func (w *Webhook) LabelSelectors(useNamespaceSelector bool) (namespaceSelector * return common.DefaultLabelSelectors(useNamespaceSelector) } -// MutateFunc returns the function that mutates the resources -func (w *Webhook) MutateFunc() admission.MutatingWebhookFunc { - return w.mutate -} - type owner struct { name string namespace string @@ -123,12 +127,14 @@ func (o *ownerInfo) buildID(ns string) string { return fmt.Sprintf("%s/%s/%s", ns, o.name, o.gvr.String()) } -// mutate adds the DD_ENV, DD_VERSION, DD_SERVICE env vars to -// the pod template from pod and higher-level resource labels -func (w *Webhook) mutate(request *admission.MutateRequest) ([]byte, error) { - return common.Mutate(request.Raw, request.Namespace, w.Name(), func(pod *corev1.Pod, ns string, dc dynamic.Interface) (bool, error) { - return w.injectTags(pod, ns, dc) - }, request.DynamicClient) +// WebhookFunc returns the function that mutates the resources +func (w *Webhook) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { + return func(request *admission.Request) *admiv1.AdmissionResponse { + return common.MutationResponse(mutatecommon.Mutate(request.Raw, request.Namespace, w.Name(), func(pod *corev1.Pod, ns string, dc dynamic.Interface) (bool, error) { + // Adds the DD_ENV, DD_VERSION, DD_SERVICE env vars to the pod template from pod and higher-level resource labels. + return w.injectTags(pod, ns, dc) + }, request.DynamicClient)) + } } // injectTags injects DD_ENV, DD_VERSION, DD_SERVICE @@ -173,7 +179,7 @@ func (w *Webhook) injectTags(pod *corev1.Pod, ns string, dc dynamic.Interface) ( return false, errors.New(metrics.InternalError) } - log.Debugf("Looking for standard labels on '%s/%s' - kind '%s' owner of pod %s", owner.namespace, owner.name, owner.kind, common.PodString(pod)) + log.Debugf("Looking for standard labels on '%s/%s' - kind '%s' owner of pod %s", owner.namespace, owner.name, owner.kind, mutatecommon.PodString(pod)) _, injected = injectTagsFromLabels(owner.labels, pod) return injected, nil @@ -190,7 +196,7 @@ func injectTagsFromLabels(labels map[string]string, pod *corev1.Pod) (bool, bool Name: envName, Value: tagValue, } - if injected := common.InjectEnv(pod, env); injected { + if injected := mutatecommon.InjectEnv(pod, env); injected { injectedAtLeastOnce = true } found = true diff --git a/pkg/clusteragent/admission/start.go b/pkg/clusteragent/admission/start.go index 5d9fe1e866ff13..bccb14bb0f771d 100644 --- a/pkg/clusteragent/admission/start.go +++ b/pkg/clusteragent/admission/start.go @@ -36,10 +36,12 @@ type ControllerContext struct { } // StartControllers starts the secret and webhook controllers -func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component, pa workload.PodPatcher) ([]webhook.MutatingWebhook, error) { +func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component, pa workload.PodPatcher) ([]webhook.Webhook, error) { + var webhooks []webhook.Webhook + if !config.Datadog().GetBool("admission_controller.enabled") { log.Info("Admission controller is disabled") - return nil, nil + return webhooks, nil } certConfig := secret.NewCertConfig( @@ -60,28 +62,28 @@ func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component, pa wo nsSelectorEnabled, err := useNamespaceSelector(ctx.Client.Discovery()) if err != nil { - return nil, err + return webhooks, err } v1Enabled, err := UseAdmissionV1(ctx.Client.Discovery()) if err != nil { - return nil, err + return webhooks, err } - mutatingWebhookConfig := webhook.NewConfig(v1Enabled, nsSelectorEnabled) - mutatingWebhookController := webhook.NewController( + webhookConfig := webhook.NewConfig(v1Enabled, nsSelectorEnabled) + webhookController := webhook.NewController( ctx.Client, ctx.SecretInformers.Core().V1().Secrets(), ctx.WebhookInformers.Admissionregistration(), ctx.IsLeaderFunc, ctx.LeaderSubscribeFunc(), - mutatingWebhookConfig, + webhookConfig, wmeta, pa, ) go secretController.Run(ctx.StopCh) - go mutatingWebhookController.Run(ctx.StopCh) + go webhookController.Run(ctx.StopCh) ctx.SecretInformers.Start(ctx.StopCh) ctx.WebhookInformers.Start(ctx.StopCh) @@ -91,12 +93,16 @@ func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component, pa wo } if v1Enabled { + informers[apiserver.ValidatingWebhooksInformer] = ctx.WebhookInformers.Admissionregistration().V1().ValidatingWebhookConfigurations().Informer() informers[apiserver.MutatingWebhooksInformer] = ctx.WebhookInformers.Admissionregistration().V1().MutatingWebhookConfigurations().Informer() getWebhookStatus = getWebhookStatusV1 } else { + informers[apiserver.ValidatingWebhooksInformer] = ctx.WebhookInformers.Admissionregistration().V1beta1().ValidatingWebhookConfigurations().Informer() informers[apiserver.MutatingWebhooksInformer] = ctx.WebhookInformers.Admissionregistration().V1beta1().MutatingWebhookConfigurations().Informer() getWebhookStatus = getWebhookStatusV1beta1 } - return mutatingWebhookController.EnabledMutatingWebhooks(), apiserver.SyncInformers(informers, 0) + webhooks = append(webhooks, webhookController.EnabledWebhooks()...) + + return webhooks, apiserver.SyncInformers(informers, 0) } diff --git a/pkg/clusteragent/admission/status.go b/pkg/clusteragent/admission/status.go index 3492640df251bb..4551413421c2be 100644 --- a/pkg/clusteragent/admission/status.go +++ b/pkg/clusteragent/admission/status.go @@ -100,7 +100,8 @@ func getWebhookStatusV1beta1(name string, apiCl kubernetes.Interface) (map[strin func getWebhookStatusV1(name string, apiCl kubernetes.Interface) (map[string]interface{}, error) { webhookStatus := make(map[string]interface{}) - webhook, err := apiCl.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), name, metav1.GetOptions{}) + // TODO Redo status to also get ValidationWebhookConfigurations + webhook, err := apiCl.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return webhookStatus, err } diff --git a/pkg/clusteragent/admission/validate/alwaysadmit/alwaysadmit.go b/pkg/clusteragent/admission/validate/alwaysadmit/alwaysadmit.go new file mode 100644 index 00000000000000..6bb028ff570466 --- /dev/null +++ b/pkg/clusteragent/admission/validate/alwaysadmit/alwaysadmit.go @@ -0,0 +1,99 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build kubeapiserver + +// Package alwaysadmit is a validation webhook that allows all pods into the cluster. +// Its behavior is the same as if there were no validation at all. +package alwaysadmit + +import ( + "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common" + validatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/validate/common" + admiv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/dynamic" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + webhookName = "always_admit" + webhookEndpoint = "/always-admit" +) + +// Webhook is a validation webhook that allows all pods into the cluster. +type Webhook struct { + name string + webhookType common.WebhookType + isEnabled bool + endpoint string + resources []string + operations []admissionregistrationv1.OperationType +} + +// NewWebhook returns a new webhook +func NewWebhook() *Webhook { + return &Webhook{ + name: webhookName, + webhookType: common.ValidatingWebhook, + isEnabled: true, + endpoint: webhookEndpoint, + resources: []string{"pods"}, + operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, + } +} + +// Name returns the name of the webhook +func (w *Webhook) Name() string { + return w.name +} + +// WebhookType returns the type of the webhook +func (w *Webhook) WebhookType() common.WebhookType { + return w.webhookType +} + +// IsEnabled returns whether the webhook is enabled +func (w *Webhook) IsEnabled() bool { + return w.isEnabled +} + +// Endpoint returns the endpoint of the webhook +func (w *Webhook) Endpoint() string { + return w.endpoint +} + +// Resources returns the kubernetes resources for which the webhook should +// be invoked +func (w *Webhook) Resources() []string { + return w.resources +} + +// Operations returns the operations on the resources specified for which +// the webhook should be invoked +func (w *Webhook) Operations() []admissionregistrationv1.OperationType { + return w.operations +} + +// LabelSelectors returns the label selectors that specify when the webhook +// should be invoked +func (w *Webhook) LabelSelectors(useNamespaceSelector bool) (namespaceSelector *metav1.LabelSelector, objectSelector *metav1.LabelSelector) { + return common.DefaultLabelSelectors(useNamespaceSelector) +} + +// WebhookFunc returns the function that validate the resources +func (w *Webhook) WebhookFunc() func(request *admission.Request) *admiv1.AdmissionResponse { + return func(request *admission.Request) *admiv1.AdmissionResponse { + return common.ValidationResponse(validatecommon.Validate(request.Raw, request.Namespace, w.Name(), w.alwaysAdmit, request.DynamicClient)) + } +} + +// alwaysAdmit is a function that always admits the pod. +func (w *Webhook) alwaysAdmit(_ *corev1.Pod, _ string, _ dynamic.Interface) (bool, error) { + return true, nil +} diff --git a/pkg/clusteragent/admission/validate/common/common.go b/pkg/clusteragent/admission/validate/common/common.go new file mode 100644 index 00000000000000..f5cdc764bcb774 --- /dev/null +++ b/pkg/clusteragent/admission/validate/common/common.go @@ -0,0 +1,43 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build kubeapiserver + +// Package common provides functions used by several mutating webhooks +package common + +import ( + "encoding/json" + "fmt" + "strconv" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/dynamic" + + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" +) + +// ValidationFunc is a function that validates a pod +type ValidationFunc func(pod *corev1.Pod, ns string, cl dynamic.Interface) (bool, error) + +// Validate handles validating pods and encoding and decoding admission +// requests and responses for the public validate functions +func Validate(rawPod []byte, ns string, validationType string, v ValidationFunc, dc dynamic.Interface) (bool, error) { + var pod corev1.Pod + if err := json.Unmarshal(rawPod, &pod); err != nil { + return false, fmt.Errorf("failed to decode raw object: %v", err) + } + + validated, err := v(&pod, ns, dc) + if err != nil { + // TODO (wassim): Check telemetry + metrics.ValidationAttempts.Inc(validationType, metrics.StatusError, strconv.FormatBool(false), err.Error()) + return false, fmt.Errorf("failed to validate pod: %v", err) + } + + // TODO (wassim): Check telemetry + metrics.ValidationAttempts.Inc(validationType, metrics.StatusSuccess, strconv.FormatBool(validated), "") + return validated, nil +} diff --git a/pkg/clusteragent/admission/validate/doc.go b/pkg/clusteragent/admission/validate/doc.go new file mode 100644 index 00000000000000..e6ceedd10a235a --- /dev/null +++ b/pkg/clusteragent/admission/validate/doc.go @@ -0,0 +1,73 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build kubeapiserver + +// Package validate contains validating webhooks registered in the admission +// controller. +// +// The general idea of validating webhooks is to intercept requests to the +// Kubernetes API server and either validate or refuse the Kubernetes objects before the operation +// specified in the request is applied. For example, a validating webhook can be +// configured to receive all the requests about creating or updating a pod, and refuse pod creation +// if they don't respect specific A typical example is to intercept +// requests to create a pod and add some environment variables or volumes to the +// pod to enable some functionality automatically. This saves the user from +// having to add environment variables or volumes manually on each pod in their +// cluster. +// To learn more about validating webhooks, see the official Kubernetes documentation: +// https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/ +// +// In general, each validating webhook should be implemented in its own Go +// package. If there are some related webhooks that share some code, they can be +// grouped in the same package. For example, the CWS webhooks are all in the +// same package. +// +// Each validating webhook needs to implement the "Webhook" interface. +// Here's a brief description of each function and what they are used for: +// - Name: it's the name of the webhook. It's used to identify it. The name +// appears in some telemetry tags. +// - WebhookType: it's the type of the webhook. It can be either "mutating" or +// "validating". +// - IsEnabled: returns whether the webhook is enabled or not. In general, the +// recommendation is to disable the webhook by default unless it's needed by a +// core feature that should be enabled for everyone that deploys the Datadog +// Agent on Kubernetes. +// - Endpoint: the endpoint where the webhook is registered. +// - Resources: the Kubernetes resources that the webhook is interested in. For +// example, pods, deployments, etc. +// - Operations: the operations applied to the resources that the webhook is +// interested in. For example: create, update, delete, etc. +// - LabelSelectors: allow us to filter the requests that the webhook receives. +// For example, we can configure the webhook to only receive requests about pods +// that have a specific label. For performance reasons, we should try to +// minimize the number of requests that the webhook receives. The label +// selectors help us with that. There are some default label selectors defined +// in the "common" package. +// - WebhookFunc: the function that runs the logic of the webhook and returns the admission response. +// +// As any other feature, validating webhooks can be configured using the Datadog +// configuration. When adding new configuration parameters, please try to follow +// the convention of the other validating webhooks. The configuration parameters +// for a webhook should be under the "admission_controller.name_of_the_webhook" +// key. +// +// Dependencies between webhooks should be avoided. If there's a dependency +// between webhooks, consider grouping them in the same webhook instead. +// +// We should try to avoid depending on the order in which webhooks are executed. +// When this cannot be avoided, keep in mind that the order in which the +// webhooks are executed is the order in which they are returned by the +// "generateWebhooks" function in the "webhook" package. +// +// Validating webhooks emit telemetry metrics. Each webhook can define its own +// metrics as needed but some metrics like "validation_attempts" or +// "webhooks_received" are common to all webhooks and defined in common code, so +// new webhooks can use them without having to define them again. +// +// When implementing a new webhook keep performance in mind. For instance, if +// the webhook reacts upon the creation of a new pod, it could slow down the pod +// creation process. +package validate diff --git a/pkg/util/kubernetes/apiserver/types.go b/pkg/util/kubernetes/apiserver/types.go index 3bad498238a06f..177817cc8fd65b 100644 --- a/pkg/util/kubernetes/apiserver/types.go +++ b/pkg/util/kubernetes/apiserver/types.go @@ -13,6 +13,8 @@ type InformerName string const ( // SecretsInformer holds the name of the informer SecretsInformer InformerName = "v1/secrets" + // ValidatingWebhooksInformer holds the name of the validating webhook informer + ValidatingWebhooksInformer InformerName = "admissionregistration.k8s.io/v1/validatingwebhookconfigurations" // MutatingWebhooksInformer holds the name of the mutating webhook informer MutatingWebhooksInformer InformerName = "admissionregistration.k8s.io/v1/mutatingwebhookconfigurations" )