Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CONTP-356] feat(admission server): implement ValidatingAdmissionWebhook #28512

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
103 changes: 46 additions & 57 deletions cmd/cluster-agent/admission/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,28 @@ import (
"net/http"
"time"

authenticationv1 "k8s.io/api/authentication/v1"

"github.com/cihub/seelog"
admiv1 "k8s.io/api/admission/v1"
admiv1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"

admicommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics"
pkgconfigsetup "github.com/DataDog/datadog-agent/pkg/config/setup"
"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"
pkglogsetup "github.com/DataDog/datadog-agent/pkg/util/log/setup"

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"
"k8s.io/client-go/kubernetes"
)

const jsonContentType = "application/json"

// MutateRequest contains the information of a mutation request
type MutateRequest struct {
// Request contains the information of an admission request
type Request struct {
// Raw is the raw request object
Raw []byte
// Name is the name of the object
Expand All @@ -56,8 +54,9 @@ type MutateRequest struct {
APIClient kubernetes.Interface
}

// WebhookFunc is the function that runs the webhook logic
type WebhookFunc func(request *MutateRequest) ([]byte, error)
// WebhookFunc is the function that runs the webhook logic.
// We always return an `admissionv1.AdmissionResponse` as it will be converted to the appropriate version if needed.
type WebhookFunc func(request *Request) *admiv1.AdmissionResponse

// Server TODO <container-integrations>
type Server struct {
Expand Down Expand Up @@ -94,9 +93,9 @@ func (s *Server) initDecoder() {

// 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 WebhookFunc, dc dynamic.Interface, apiClient kubernetes.Interface) {
func (s *Server) Register(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)
})
}

Expand Down Expand Up @@ -136,16 +135,21 @@ 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 WebhookFunc, dc dynamic.Interface, apiClient kubernetes.Interface) {
metrics.WebhooksReceived.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.
// We send the webhook name twice to keep the backward compatibility with `mutation_type` tag.
metrics.WebhooksReceived.Inc(webhookName, webhookName, webhookType.String())

// Measure the time it takes to process the request.
start := time.Now()
defer func() {
metrics.WebhooksResponseDuration.Observe(time.Since(start).Seconds(), webhookName)
// We send the webhook name twice to keep the backward compatibility with `mutation_type` tag.
metrics.WebhooksResponseDuration.Observe(time.Since(start).Seconds(), webhookName, webhookName, webhookType.String())
}()

// Validate admission request.
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
log.Warnf("Invalid method %s, only POST requests are allowed", r.Method)
Expand All @@ -166,53 +170,61 @@ func (s *Server) mutateHandler(w http.ResponseWriter, r *http.Request, webhookNa
return
}

// Deserialize admission request.
obj, gvk, err := s.decoder.Decode(body, nil, nil)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Warnf("Could not deserialize request: %v", err)
return
}

// Handle the request based on `GroupVersionKind`.
var response runtime.Object
switch *gvk {
case admiv1.SchemeGroupVersion.WithKind("AdmissionReview"):
admissionReviewReq, ok := obj.(*admiv1.AdmissionReview)
if !ok {
log.Errorf("Expected v1.AdmissionReview, got type %T", obj)
}
admissionReviewResp := &admiv1.AdmissionReview{}
admissionReviewResp.SetGroupVersionKind(*gvk)
mutateRequest := MutateRequest{

admissionReview := &admiv1.AdmissionReview{}
admissionReview.SetGroupVersionKind(*gvk)
admissionRequest := Request{
Raw: admissionReviewReq.Request.Object.Raw,
Name: admissionReviewReq.Request.Name,
Namespace: admissionReviewReq.Request.Namespace,
UserInfo: &admissionReviewReq.Request.UserInfo,
DynamicClient: dc,
APIClient: apiClient,
}
jsonPatch, err := mutateFunc(&mutateRequest)
admissionReviewResp.Response = mutationResponse(jsonPatch, err)
admissionReviewResp.Response.UID = admissionReviewReq.Request.UID
response = admissionReviewResp

// Generate admission response
admissionResponse := webhookFunc(&admissionRequest)
admissionReview.Response = admissionResponse
admissionReview.Response.UID = admissionReviewReq.Request.UID
response = admissionReview
case admiv1beta1.SchemeGroupVersion.WithKind("AdmissionReview"):
admissionReviewReq, ok := obj.(*admiv1beta1.AdmissionReview)
if !ok {
log.Errorf("Expected v1beta1.AdmissionReview, got type %T", obj)
}
admissionReviewResp := &admiv1beta1.AdmissionReview{}
admissionReviewResp.SetGroupVersionKind(*gvk)
mutateRequest := MutateRequest{

admissionReview := &admiv1beta1.AdmissionReview{}
admissionReview.SetGroupVersionKind(*gvk)
admissionRequest := Request{
Raw: admissionReviewReq.Request.Object.Raw,
Name: admissionReviewReq.Request.Name,
Namespace: admissionReviewReq.Request.Namespace,
UserInfo: &admissionReviewReq.Request.UserInfo,
DynamicClient: dc,
APIClient: apiClient,
}
jsonPatch, err := mutateFunc(&mutateRequest)
admissionReviewResp.Response = responseV1ToV1beta1(mutationResponse(jsonPatch, err))
admissionReviewResp.Response.UID = admissionReviewReq.Request.UID
response = admissionReviewResp

// Generate admission response
admissionResponse := webhookFunc(&admissionRequest)
admissionReview.Response = responseV1ToV1beta1(admissionResponse)
admissionReview.Response.UID = admissionReviewReq.Request.UID
response = admissionReview
default:
log.Errorf("Group version kind %v is not supported", gvk)
w.WriteHeader(http.StatusBadRequest)
Expand All @@ -228,29 +240,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
Expand Down
17 changes: 13 additions & 4 deletions cmd/cluster-agent/subcommands/start/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package start

import (
"context"
"errors"
"fmt"
"net/http"
"os"
Expand Down Expand Up @@ -464,21 +465,29 @@ func start(log log.Component,
IsLeaderFunc: le.IsLeader,
LeaderSubscribeFunc: le.Subscribe,
SecretInformers: apiCl.CertificateSecretInformerFactory,
WebhookInformers: apiCl.WebhookConfigInformerFactory,
ValidatingInformers: apiCl.WebhookConfigInformerFactory,
MutatingInformers: apiCl.WebhookConfigInformerFactory,
Client: apiCl.Cl,
StopCh: stopCh,
ValidatingStopCh: make(chan struct{}),
}

webhooks, err := admissionpkg.StartControllers(admissionCtx, wmeta, pa)
if err != nil {
// Ignore the error if it's related to the validatingwebhookconfigurations.
var syncInformerError *apiserver.SyncInformersError
if err != nil && !(errors.As(err, &syncInformerError) && syncInformerError.Name == apiserver.ValidatingWebhooksInformer) {
pkglog.Errorf("Could not start admission controller: %v", err)
} else {
if err != nil {
pkglog.Warnf("Admission controller started with errors: %v", err)
close(admissionCtx.ValidatingStopCh)
}
wdhif marked this conversation as resolved.
Show resolved Hide resolved
// 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 webhooks {
server.Register(webhookConf.Endpoint(), webhookConf.Name(), webhookConf.MutateFunc(), apiCl.DynamicCl, apiCl.Cl)
server.Register(webhookConf.Endpoint(), webhookConf.Name(), webhookConf.WebhookType(), webhookConf.WebhookFunc(), apiCl.DynamicCl, apiCl.Cl)
}

// Start the k8s admission webhook server
Expand Down
17 changes: 16 additions & 1 deletion pkg/clusteragent/admission/common/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@
// Package common defines constants and types used by the Admission Controller.
package common

// WebhookType is the type of the webhook.
type WebhookType string

// String returns the string representation of the WebhookType.
func (t WebhookType) String() string {
return string(t)
}

const (
// ValidatingWebhook is type for Validating Webhooks.
ValidatingWebhook = "validating"
// MutatingWebhook is type for Mutating Webhooks.
MutatingWebhook = "mutating"
)

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
Expand Down
45 changes: 45 additions & 0 deletions pkg/clusteragent/admission/common/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,54 @@ package common
import (
"strconv"
"time"

admiv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/DataDog/datadog-agent/pkg/util/log"
)

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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to make this configurable.

}
}

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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package common
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/common"
pkgconfigsetup "github.com/DataDog/datadog-agent/pkg/config/setup"
)

Expand All @@ -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"},
},
Expand All @@ -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",
},
}
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/clusteragent/admission/controllers/webhook/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
type Config struct {
webhookName string
secretName string
validationEnabled bool
mutationEnabled bool
namespace string
admissionV1Enabled bool
namespaceSelectorEnabled bool
Expand All @@ -37,6 +39,8 @@ func NewConfig(admissionV1Enabled, namespaceSelectorEnabled bool) Config {
return Config{
webhookName: pkgconfigsetup.Datadog().GetString("admission_controller.webhook_name"),
secretName: pkgconfigsetup.Datadog().GetString("admission_controller.certificate.secret_name"),
validationEnabled: pkgconfigsetup.Datadog().GetBool("admission_controller.validation.enabled"),
mutationEnabled: pkgconfigsetup.Datadog().GetBool("admission_controller.mutation.enabled"),
namespace: common.GetResourcesNamespace(),
admissionV1Enabled: admissionV1Enabled,
namespaceSelectorEnabled: namespaceSelectorEnabled,
Expand All @@ -50,6 +54,8 @@ func NewConfig(admissionV1Enabled, namespaceSelectorEnabled bool) Config {

func (w *Config) getWebhookName() string { return w.webhookName }
func (w *Config) getSecretName() string { return w.secretName }
func (w *Config) isValidationEnabled() bool { return w.validationEnabled }
func (w *Config) isMutationEnabled() bool { return w.mutationEnabled }
func (w *Config) getSecretNs() string { return w.namespace }
func (w *Config) useAdmissionV1() bool { return w.admissionV1Enabled }
func (w *Config) useNamespaceSelector() bool { return w.namespaceSelectorEnabled }
Expand Down
Loading
Loading