Skip to content

Commit

Permalink
fix(ssi): create injector interface and refactor webhooks
Browse files Browse the repository at this point in the history
This commit adds an injector interface and refactors the tagsfromlabels
and the config webhooks to use it. This will allow us to use the
injector logic without needing to instantiate a webhook.
  • Loading branch information
betterengineering committed Jan 31, 2025
1 parent 748d0b8 commit 5ed5b03
Showing 8 changed files with 706 additions and 558 deletions.
21 changes: 21 additions & 0 deletions pkg/clusteragent/admission/mutate/common/injector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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

import (
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/dynamic"
)

// Injector provides a common interface for building components capable of
// mutating pods so that individual webhooks can share injectors.
type Injector interface {
// InjectPod will optionally inject a pod, returning true if injection occurs and an error if there is a problem.
// This must match the MutateFunc signature.
InjectPod(pod *corev1.Pod, ns string, dc dynamic.Interface) (bool, error)
}
219 changes: 8 additions & 211 deletions pkg/clusteragent/admission/mutate/config/config.go
Original file line number Diff line number Diff line change
@@ -10,10 +10,6 @@
package config

import (
"errors"
"fmt"
"strings"

admiv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
@@ -24,10 +20,7 @@ import (
"github.com/DataDog/datadog-agent/comp/core/config"
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"
mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common"
apiCommon "github.com/DataDog/datadog-agent/pkg/util/kubernetes/apiserver/common"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

const (
@@ -72,17 +65,7 @@ type Webhook struct {
operations []admissionregistrationv1.OperationType
matchConditions []admissionregistrationv1.MatchCondition
wmeta workloadmeta.Component
injectionFilter mutatecommon.InjectionFilter

// These fields store datadog agent config parameters
// to avoid calling the config resolution each time the webhook
// receives requests because the resolution is CPU expensive.
mode string
localServiceName string
traceAgentSocket string
dogStatsDSocket string
socketPath string
typeSocketVolumes bool
injector mutatecommon.Injector
}

// NewWebhook returns a new Webhook
@@ -92,6 +75,8 @@ func NewWebhook(wmeta workloadmeta.Component, datadogConfig config.Component) *W
disabledNamespaces := datadogConfig.GetStringSlice("admission_controller.inject_config.disabled_namespaces")
injectionFilter, _ := mutatecommon.NewInjectionFilter(enabled, enabledNamespaces, disabledNamespaces)

injector := NewConfigInjector(NewConfigInjectorConfig(datadogConfig), injectionFilter)

return &Webhook{
name: webhookName,
isEnabled: enabled,
@@ -100,14 +85,7 @@ func NewWebhook(wmeta workloadmeta.Component, datadogConfig config.Component) *W
operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
matchConditions: []admissionregistrationv1.MatchCondition{},
wmeta: wmeta,
injectionFilter: injectionFilter,

mode: datadogConfig.GetString("admission_controller.inject_config.mode"),
localServiceName: datadogConfig.GetString("admission_controller.inject_config.local_service_name"),
traceAgentSocket: datadogConfig.GetString("admission_controller.inject_config.trace_agent_socket"),
dogStatsDSocket: datadogConfig.GetString("admission_controller.inject_config.dogstatsd_socket"),
socketPath: datadogConfig.GetString("admission_controller.inject_config.socket_path"),
typeSocketVolumes: datadogConfig.GetBool("admission_controller.inject_config.type_socket_volumes"),
injector: injector,
}
}

@@ -162,189 +140,8 @@ func (w *Webhook) WebhookFunc() admission.WebhookFunc {
}
}

// inject injects the following environment variables into the pod template:
// - DD_AGENT_HOST: the host IP of the node
// - DD_ENTITY_ID: the entity ID of the pod
// - DD_EXTERNAL_ENV: the External Data Environment Variable
func (w *Webhook) inject(pod *corev1.Pod, _ string, _ dynamic.Interface) (bool, error) {
var injectedConfig, injectedEntity, injectedExternalEnv bool
var (
agentHostIPEnvVar = corev1.EnvVar{
Name: agentHostEnvVarName,
Value: "",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "status.hostIP",
},
},
}

agentHostServiceEnvVar = corev1.EnvVar{
Name: agentHostEnvVarName,
Value: w.localServiceName + "." + apiCommon.GetMyNamespace() + ".svc.cluster.local",
}

defaultDdEntityIDEnvVar = corev1.EnvVar{
Name: ddEntityIDEnvVarName,
Value: "",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.uid",
},
},
}

traceURLSocketEnvVar = corev1.EnvVar{
Name: traceURLEnvVarName,
Value: w.traceAgentSocket,
}

dogstatsdURLSocketEnvVar = corev1.EnvVar{
Name: dogstatsdURLEnvVarName,
Value: w.dogStatsDSocket,
}
)

if pod == nil {
return false, errors.New(metrics.InvalidInput)
}

if !w.injectionFilter.ShouldMutatePod(pod) {
return false, nil
}

// Inject DD_AGENT_HOST
switch injectionMode(pod, w.mode) {
case hostIP:
injectedConfig = mutatecommon.InjectEnv(pod, agentHostIPEnvVar)
case service:
injectedConfig = mutatecommon.InjectEnv(pod, agentHostServiceEnvVar)
case socket:
injectedVolumes := w.injectSocketVolumes(pod)
injectedEnv := mutatecommon.InjectEnv(pod, traceURLSocketEnvVar)
injectedEnv = mutatecommon.InjectEnv(pod, dogstatsdURLSocketEnvVar) || injectedEnv
injectedConfig = injectedVolumes || injectedEnv
default:
log.Errorf("invalid injection mode %q", w.mode)
return false, errors.New(metrics.InvalidInput)
}

injectedEntity = mutatecommon.InjectEnv(pod, defaultDdEntityIDEnvVar)

// Inject External Data Environment Variable
injectedExternalEnv = injectExternalDataEnvVar(pod)

return injectedConfig || injectedEntity || injectedExternalEnv, nil
}

// 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()[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", common.InjectionModeLabelKey, val, mutatecommon.PodString(pod), globalMode)
return globalMode
}
}

return globalMode
}

// buildExternalEnv generate an External Data environment variable.
func buildExternalEnv(container *corev1.Container, init bool) (corev1.EnvVar, error) {
return corev1.EnvVar{
Name: ddExternalDataEnvVarName,
Value: fmt.Sprintf("%s%t,%s%s,%s$(%s)", externalDataInitPrefix, init, externalDataContainerNamePrefix, container.Name, externalDataPodUIDPrefix, podUIDEnvVarName),
}, nil
}

// injectExternalDataEnvVar injects the External Data environment variable.
// The format is: it-<init>,cn-<container_name>,pu-<pod_uid>
func injectExternalDataEnvVar(pod *corev1.Pod) (injected bool) {
// Inject External Data Environment Variable for the pod
injected = mutatecommon.InjectDynamicEnv(pod, buildExternalEnv)

// Inject Internal Pod UID
injected = mutatecommon.InjectEnv(pod, corev1.EnvVar{
Name: podUIDEnvVarName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.uid",
},
},
}) || injected

return
}

func buildVolume(volumeName, path string, hostpathType corev1.HostPathType, readOnly bool) (corev1.Volume, corev1.VolumeMount) {
volume := corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: path,
Type: &hostpathType,
},
},
}

volumeMount := corev1.VolumeMount{
Name: volumeName,
MountPath: path,
ReadOnly: readOnly,
}

return volume, volumeMount
}

// injectSocketVolumes injects the volumes for the dogstatsd and trace agent
// sockets.
//
// The type of the volume injected can be either a directory or a socket
// depending on the configuration. They offer different trade-offs. Using a
// socket ensures no lost traces or dogstatsd metrics but can cause the pod to
// wait if the agent has issues that prevent it from creating the sockets.
//
// This function returns true if at least one volume was injected.
func (w *Webhook) injectSocketVolumes(pod *corev1.Pod) bool {
var injectedVolNames []string

if w.typeSocketVolumes {
volumes := map[string]string{
DogstatsdSocketVolumeName: strings.TrimPrefix(
w.dogStatsDSocket, "unix://",
),
TraceAgentSocketVolumeName: strings.TrimPrefix(
w.traceAgentSocket, "unix://",
),
}

for volumeName, volumePath := range volumes {
volume, volumeMount := buildVolume(volumeName, volumePath, corev1.HostPathSocket, true)
injectedVol := mutatecommon.InjectVolume(pod, volume, volumeMount)
if injectedVol {
injectedVolNames = append(injectedVolNames, volumeName)
}
}
} else {
volume, volumeMount := buildVolume(
DatadogVolumeName,
w.socketPath,
corev1.HostPathDirectoryOrCreate,
true,
)
injectedVol := mutatecommon.InjectVolume(pod, volume, volumeMount)
if injectedVol {
injectedVolNames = append(injectedVolNames, DatadogVolumeName)
}
}

for _, volName := range injectedVolNames {
mutatecommon.MarkVolumeAsSafeToEvictForAutoscaler(pod, volName)
}

return len(injectedVolNames) > 0
// inject is a helper method to call the underlying injector directly from the webook. This is useful for testing. All
// the logic must be in the injector itself and we should consider refactoring this method out.
func (w *Webhook) inject(pod *corev1.Pod, ns string, dc dynamic.Interface) (bool, error) {
return w.injector.InjectPod(pod, ns, dc)
}
Loading

0 comments on commit 5ed5b03

Please sign in to comment.