Skip to content

Commit

Permalink
feat(admission server): implement validating webhooks
Browse files Browse the repository at this point in the history
Signed-off-by: Wassim DHIF <wassim.dhif@datadoghq.com>
  • Loading branch information
wdhif committed Aug 20, 2024
1 parent cbc8c98 commit 29a988a
Show file tree
Hide file tree
Showing 25 changed files with 1,152 additions and 503 deletions.
99 changes: 53 additions & 46 deletions cmd/cluster-agent/admission/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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 <container-integrations>
type Server struct {
Expand Down Expand Up @@ -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)
})
}

Expand Down Expand Up @@ -134,43 +133,54 @@ 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)
log.Warnf("Could not read request body: %v", err)
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)
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"):
Expand All @@ -180,16 +190,26 @@ 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,
UserInfo: &admissionReviewReq.Request.UserInfo,
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"):
Expand All @@ -199,16 +219,26 @@ 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,
UserInfo: &admissionReviewReq.Request.UserInfo,
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:
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions cmd/cluster-agent/subcommands/start/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion pkg/clusteragent/admission/common/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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"

"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,
}
}
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"
"github.com/DataDog/datadog-agent/pkg/config"
)

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
Loading

0 comments on commit 29a988a

Please sign in to comment.