Skip to content

Commit

Permalink
feat(ssi): add target based filtering (#33561)
Browse files Browse the repository at this point in the history
Co-authored-by: Adel Haj Hassan <41540817+adel121@users.noreply.github.com>
  • Loading branch information
betterengineering and adel121 authored Jan 31, 2025
1 parent c6f586b commit 2d57cea
Showing 10 changed files with 538 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -311,13 +311,13 @@ func (w *Webhook) getLibrariesLanguageDetection(pod *corev1.Pod) *libInfoLanguag

// getAllLatestDefaultLibraries returns all supported by APM Instrumentation tracing libraries
// that should be enabled by default
func (w *Webhook) getAllLatestDefaultLibraries() []libInfo {
func getAllLatestDefaultLibraries(containerRegistry string) []libInfo {
var libsToInject []libInfo
for _, lang := range supportedLanguages {
if !lang.isEnabledByDefault() {
continue
}
libsToInject = append(libsToInject, lang.defaultLibInfo(w.config.containerRegistry, ""))
libsToInject = append(libsToInject, lang.defaultLibInfo(containerRegistry, ""))
}

return libsToInject
@@ -444,7 +444,7 @@ func (w *Webhook) extractLibInfo(pod *corev1.Pod) extractedPodLibInfo {
}

if extracted.source.isSingleStep() {
return extracted.withLibs(w.getAllLatestDefaultLibraries())
return extracted.withLibs(getAllLatestDefaultLibraries(w.config.containerRegistry))
}

// Get libraries to inject for Remote Instrumentation
@@ -458,7 +458,7 @@ func (w *Webhook) extractLibInfo(pod *corev1.Pod) extractedPodLibInfo {
log.Warnf("Ignoring version %q. To inject all libs, the only supported version is latest for now", version)
}

return extracted.withLibs(w.getAllLatestDefaultLibraries())
return extracted.withLibs(getAllLatestDefaultLibraries(w.config.containerRegistry))
}

return extractedPodLibInfo{}
89 changes: 89 additions & 0 deletions pkg/clusteragent/admission/mutate/autoinstrumentation/config.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"

"github.com/DataDog/datadog-agent/comp/core/config"
mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common"
@@ -44,6 +46,83 @@ type InstrumentationConfig struct {
// InjectorImageTag is the tag of the image to use for the auto instrumentation injector library. Full config key:
// apm_config.instrumentation.injector_image_tag
InjectorImageTag string `mapstructure:"injector_image_tag"`
// Targets is a list of targets to apply the auto instrumentation to. The first target that matches the pod will be
// used. If no target matches, the auto instrumentation will not be applied. Full config key:
// apm_config.instrumentation.targets
Targets []Target `mapstructure:"targets"`
}

// Target is a rule to apply the auto instrumentation to a specific workload using the pod and namespace selectors.
// Full config key: apm_config.instrumentation.targets to get the list of targets.
type Target struct {
// Name is the name of the target. It will be appended to the pod annotations to identify the target that was used.
// Full config key: apm_config.instrumentation.targets[].name
Name string `mapstructure:"name"`
// PodSelector is the pod selector to match the pods to apply the auto instrumentation to. It will be used in
// conjunction with the NamespaceSelector to match the pods. Full config key:
// apm_config.instrumentation.targets[].selector
PodSelector PodSelector `mapstructure:"podSelector"`
// NamespaceSelector is the namespace selector to match the namespaces to apply the auto instrumentation to. It will
// be used in conjunction with the Selector to match the pods. Full config key:
// apm_config.instrumentation.targets[].namespaceSelector
NamespaceSelector NamespaceSelector `mapstructure:"namespaceSelector"`
// TracerVersions is a map of tracer versions to inject for workloads that match the target. The key is the tracer
// name and the value is the version to inject. Full config key:
// apm_config.instrumentation.targets[].ddTraceVersions
TracerVersions map[string]string `mapstructure:"ddTraceVersions"`
}

// PodSelector is a reconstruction of the metav1.LabelSelector struct to be able to unmarshal the configuration. It
// can be converted to a metav1.LabelSelector using the AsLabelSelector method. Full config key:
// apm_config.instrumentation.targets[].selector
type PodSelector struct {
// MatchLabels is a map of key-value pairs to match the labels of the pod. The labels and expressions are ANDed.
// Full config key: apm_config.instrumentation.targets[].selector.matchLabels
MatchLabels map[string]string `mapstructure:"matchLabels"`
// MatchExpressions is a list of label selector requirements to match the labels of the pod. The labels and
// expressions are ANDed. Full config key: apm_config.instrumentation.targets[].selector.matchExpressions
MatchExpressions []PodSelectorMatchExpression `mapstructure:"matchExpressions"`
}

// AsLabelSelector converts the PodSelector to a labels.Selector. It returns an error if the conversion fails.
func (p PodSelector) AsLabelSelector() (labels.Selector, error) {
labelSelector := &metav1.LabelSelector{
MatchLabels: p.MatchLabels,
MatchExpressions: make([]metav1.LabelSelectorRequirement, len(p.MatchExpressions)),
}
for i, expr := range p.MatchExpressions {
labelSelector.MatchExpressions[i] = metav1.LabelSelectorRequirement{
Key: expr.Key,
Operator: expr.Operator,
Values: expr.Values,
}
}

return metav1.LabelSelectorAsSelector(labelSelector)
}

// PodSelectorMatchExpression is a reconstruction of the metav1.LabelSelectorRequirement struct to be able to unmarshal
// the configuration. Full config key: apm_config.instrumentation.targets[].selector.matchExpressions
type PodSelectorMatchExpression struct {
// Key is the key of the label to match. Full config key:
// apm_config.instrumentation.targets[].selector.matchExpressions[].key
Key string `mapstructure:"key"`
// Operator is the operator to use to match the label. Valid values are In, NotIn, Exists, DoesNotExist. Full config
// key: apm_config.instrumentation.targets[].selector.matchExpressions[].operator
Operator metav1.LabelSelectorOperator `mapstructure:"operator"`
// Values is a list of values to match the label against. If the operator is Exists or DoesNotExist, the values
// should be empty. If the operator is In or NotIn, the values should be non-empty. Full config key:
// apm_config.instrumentation.targets[].selector.matchExpressions[].values
Values []string `mapstructure:"values"`
}

// NamespaceSelector is a struct to store the configuration for the namespace selector. It can be used to match the
// namespaces to apply the auto instrumentation to. Full config key:
// apm_config.instrumentation.targets[].namespaceSelector
type NamespaceSelector struct {
// MatchNames is a list of namespace names to match. If empty, all namespaces are matched. Full config key:
// apm_config.instrumentation.targets[].namespaceSelector.matchNames
MatchNames []string `mapstructure:"matchNames"`
}

// NewInstrumentationConfig creates a new InstrumentationConfig from the datadog config. It returns an error if the
@@ -60,6 +139,16 @@ func NewInstrumentationConfig(datadogConfig config.Component) (*InstrumentationC
return nil, fmt.Errorf("apm.instrumentation.enabled_namespaces and apm.instrumentation.disabled_namespaces are mutually exclusive and cannot be set together")
}

// Ensure both enabled namespaces and targets are not set together.
if len(cfg.EnabledNamespaces) > 0 && len(cfg.Targets) > 0 {
return nil, fmt.Errorf("apm.instrumentation.enabled_namespaces and apm.instrumentation.targets are mutually exclusive and cannot be set together")
}

// Ensure both library versions and targets are not set together.
if len(cfg.LibVersions) > 0 && len(cfg.Targets) > 0 {
return nil, fmt.Errorf("apm.instrumentation.lib_versions and apm.instrumentation.targets are mutually exclusive and cannot be set together")
}

return cfg, nil
}

Original file line number Diff line number Diff line change
@@ -52,11 +52,59 @@ func TestNewInstrumentationConfig(t *testing.T) {
InjectorImageTag: "foo",
},
},
{
name: "valid targets based config",
configPath: "testdata/targets.yaml",
shouldErr: false,
expected: &InstrumentationConfig{
Enabled: true,
EnabledNamespaces: []string{},
InjectorImageTag: "0",
LibVersions: map[string]string{},
Version: "v2",
DisabledNamespaces: []string{
"hacks",
},
Targets: []Target{
{
Name: "Billing Service",
PodSelector: PodSelector{
MatchLabels: map[string]string{
"app": "billing-service",
},
MatchExpressions: []PodSelectorMatchExpression{
{
Key: "env",
Operator: "In",
Values: []string{"prod"},
},
},
},
NamespaceSelector: NamespaceSelector{
MatchNames: []string{"billing"},
},
TracerVersions: map[string]string{
"java": "default",
},
},
},
},
},
{
name: "both enabled and disabled namespaces",
configPath: "testdata/both_enabled_and_disabled.yaml",
shouldErr: true,
},
{
name: "both enabled namespaces and targets",
configPath: "testdata/both_enabled_and_targets.yaml",
shouldErr: true,
},
{
name: "both library versions and targets",
configPath: "testdata/both_versions_and_targets.yaml",
shouldErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
115 changes: 115 additions & 0 deletions pkg/clusteragent/admission/mutate/autoinstrumentation/target_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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 autoinstrumentation

import (
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
)

// TargetFilter filters pods based on a set of targeting rules.
type TargetFilter struct {
targets []targetInternal
disabledNamespaces map[string]bool
}

// targetInternal is the struct we use to convert the config based target into
// something more performant to check against.
type targetInternal struct {
name string
podSelector labels.Selector
enabledNamespaces map[string]bool
libVersions []libInfo
}

// NewTargetFilter creates a new TargetFilter from a list of targets and disabled namespaces. We convert the targets
// to a more efficient internal format for quick lookups.
func NewTargetFilter(targets []Target, disabledNamespaces []string, containerRegistry string) (*TargetFilter, error) {
// Create a map of disabled namespaces for quick lookups.
disabledNamespacesMap := make(map[string]bool, len(disabledNamespaces))
for _, ns := range disabledNamespaces {
disabledNamespacesMap[ns] = true
}

// Convert the targets to internal format.
internalTargets := make([]targetInternal, len(targets))
for i, t := range targets {
// Convert the pod selector to a label selector.
podSelector, err := t.PodSelector.AsLabelSelector()
if err != nil {
return nil, fmt.Errorf("could not convert selector to label selector: %w", err)
}

// Create a map of enabled namespaces for quick lookups.
enabledNamespaces := make(map[string]bool, len(t.NamespaceSelector.MatchNames))
for _, ns := range t.NamespaceSelector.MatchNames {
enabledNamespaces[ns] = true
}

// Get the library versions to inject. If no versions are specified, we inject all libraries.
var libVersions []libInfo
if len(t.TracerVersions) == 0 {
libVersions = getAllLatestDefaultLibraries(containerRegistry)
} else {
libVersions = getPinnedLibraries(t.TracerVersions, containerRegistry)
}

// Store the target in the internal format.
internalTargets[i] = targetInternal{
name: t.Name,
podSelector: podSelector,
enabledNamespaces: enabledNamespaces,
libVersions: libVersions,
}
}

return &TargetFilter{
targets: internalTargets,
disabledNamespaces: disabledNamespacesMap,
}, nil
}

// filter filters a pod based on the targets. It returns the list of libraries to inject.
func (f *TargetFilter) filter(pod *corev1.Pod) []libInfo {
// If the namespace is disabled, we don't need to check the targets.
if _, ok := f.disabledNamespaces[pod.Namespace]; ok {
return nil
}

// Check if the pod matches any of the targets. The first match wins.
for _, target := range f.targets {
// Check the pod namespace against the namespace selector.
if !matchesNamespaceSelector(pod, target.enabledNamespaces) {
continue
}

// Check the pod labels against the pod selector.
if !target.podSelector.Matches(labels.Set(pod.Labels)) {
continue
}

// If the namespace and pod selector match, return the libraries to inject.
return target.libVersions
}

// No target matched.
return nil
}

func matchesNamespaceSelector(pod *corev1.Pod, enabledNamespaces map[string]bool) bool {
// If there are no match names, the selector matches all namespaces.
if len(enabledNamespaces) == 0 {
return true
}

// Check if the pod namespace is in the match names.
_, ok := enabledNamespaces[pod.Namespace]
return ok
}
Loading
Oops, something went wrong.

0 comments on commit 2d57cea

Please sign in to comment.