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

feat(ssi): add namespace support for labels/expressions #33796

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions pkg/clusteragent/admission/mutate/autoinstrumentation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ type Target struct {
// 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
// Full config key: apm_config.instrumentation.targets[].podSelector.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"`
// expressions are ANDed. Full config key: apm_config.instrumentation.targets[].podSelector.matchExpressions
MatchExpressions []SelectorMatchExpression `mapstructure:"matchExpressions"`
}

// AsLabelSelector converts the PodSelector to a labels.Selector. It returns an error if the conversion fails.
Expand All @@ -100,18 +100,15 @@ func (p PodSelector) AsLabelSelector() (labels.Selector, error) {
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
// SelectorMatchExpression is a reconstruction of the metav1.LabelSelectorRequirement struct to be able to unmarshal
// the configuration.
type SelectorMatchExpression struct {
// Key is the key of the label to match.
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 is the operator to use to match the label. Valid values are In, NotIn, Exists, DoesNotExist.
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
// should be empty. If the operator is In or NotIn, the values should be non-empty.
Values []string `mapstructure:"values"`
}

Expand All @@ -122,6 +119,31 @@ 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"`
// MatchLabels is a map of key-value pairs to match the labels of the namespace. The labels and expressions are
// ANDed. This cannot be used with MatchNames. Full config key:
// apm_config.instrumentation.targets[].namespaceSelector.matchLabels
MatchLabels map[string]string `mapstructure:"matchLabels"`
// MatchExpressions is a list of label selector requirements to match the labels of the namespace. The labels and
// expressions are ANDed. This cannot be used with MatchNames. Full config key:
// apm_config.instrumentation.targets[].selector.matchExpressions
MatchExpressions []SelectorMatchExpression `mapstructure:"matchExpressions"`
}

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

return metav1.LabelSelectorAsSelector(labelSelector)
}

// NewInstrumentationConfig creates a new InstrumentationConfig from the datadog config. It returns an error if the
Expand All @@ -148,6 +170,13 @@ func NewInstrumentationConfig(datadogConfig config.Component) (*InstrumentationC
return nil, fmt.Errorf("apm.instrumentation.lib_versions and apm.instrumentation.targets are mutually exclusive and cannot be set together")
}

// Ensure both namespace names and labels are not set together.
for _, target := range cfg.Targets {
if len(target.NamespaceSelector.MatchNames) > 0 && (len(target.NamespaceSelector.MatchLabels) > 0 || len(target.NamespaceSelector.MatchExpressions) > 0) {
return nil, fmt.Errorf("apm.instrumentation.targets[].namespaceSelector.matchNames and apm.instrumentation.targets[].namespaceSelector.matchLabels/matchExpressions are mutually exclusive and cannot be set together")
}
}

return cfg, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestNewInstrumentationConfig(t *testing.T) {
MatchLabels: map[string]string{
"app": "billing-service",
},
MatchExpressions: []PodSelectorMatchExpression{
MatchExpressions: []SelectorMatchExpression{
{
Key: "env",
Operator: "In",
Expand All @@ -90,11 +90,63 @@ func TestNewInstrumentationConfig(t *testing.T) {
},
},
},
{
name: "valid targets based config with namespace label selector",
configPath: "testdata/targets_namespace_labels.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: []SelectorMatchExpression{
{
Key: "env",
Operator: "In",
Values: []string{"prod"},
},
},
},
NamespaceSelector: NamespaceSelector{
MatchLabels: map[string]string{
"app": "billing",
},
MatchExpressions: []SelectorMatchExpression{
{
Key: "env",
Operator: "In",
Values: []string{"prod"},
},
},
},
TracerVersions: map[string]string{
"java": "default",
},
},
},
},
},
{
name: "both enabled and disabled namespaces",
configPath: "testdata/both_enabled_and_disabled.yaml",
shouldErr: true,
},
{
name: "both labels and names for a namespace",
configPath: "testdata/both_labels_and_names.yaml",
shouldErr: true,
},
{
name: "both enabled namespaces and targets",
configPath: "testdata/both_enabled_and_targets.yaml",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (

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

"github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors/util"
workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def"
)

// TargetFilter filters pods based on a set of targeting rules.
Expand All @@ -23,15 +26,18 @@ type TargetFilter struct {
// 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
name string
podSelector labels.Selector
nameSpaceSelector labels.Selector
useNamespaceSelector bool
enabledNamespaces map[string]bool
libVersions []libInfo
wmeta workloadmeta.Component
}

// 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) {
func NewTargetFilter(targets []Target, wmeta workloadmeta.Component, 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 {
Expand All @@ -47,10 +53,25 @@ func NewTargetFilter(targets []Target, disabledNamespaces []string, containerReg
return nil, fmt.Errorf("could not convert selector to label selector: %w", err)
}

// Determine if we should use the namespace selector or if we should use enabledNamespaces.
useNamespaceSelector := len(t.NamespaceSelector.MatchLabels)+len(t.NamespaceSelector.MatchExpressions) > 0

// Convert the namespace selector to a label selector.
var namespaceSelector labels.Selector
if useNamespaceSelector {
namespaceSelector, err = t.NamespaceSelector.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
var enabledNamespaces map[string]bool
if !useNamespaceSelector {
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.
Expand All @@ -63,10 +84,13 @@ func NewTargetFilter(targets []Target, disabledNamespaces []string, containerReg

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

Expand All @@ -86,12 +110,12 @@ func (f *TargetFilter) filter(pod *corev1.Pod) []libInfo {
// 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) {
if !target.matchesNamespaceSelector(pod.Namespace) {
continue
}

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

Expand All @@ -103,13 +127,31 @@ func (f *TargetFilter) filter(pod *corev1.Pod) []libInfo {
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 {
func (t targetInternal) matchesNamespaceSelector(namespace string) bool {
// If we are using the namespace selector, check if the namespace matches the selector.
if t.useNamespaceSelector {
// Get the namespace metadata. At the time of writing, this method will only return an error if the namespace
// does not exist, in which case we return false for this selector.
id := util.GenerateKubeMetadataEntityID("", "namespaces", "", namespace)
meta, _ := t.wmeta.GetKubernetesMetadata(id)
Copy link
Member Author

Choose a reason for hiding this comment

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

@davidor is this the best way to do this? How does this store work? Is it safe to assume the namespace will be there by the time we get a mutating webhook?

if meta == nil {
return false
}

// Check if the namespace labels match the selector.
return t.nameSpaceSelector.Matches(labels.Set(meta.EntityMeta.Labels))
}

// If there are no match names, we match all namespaces.
if len(t.enabledNamespaces) == 0 {
return true
}

// Check if the pod namespace is in the match names.
_, ok := enabledNamespaces[pod.Namespace]
_, ok := t.enabledNamespaces[namespace]
return ok
}

func (t targetInternal) matchesPodSelector(podLabels map[string]string) bool {
return t.podSelector.Matches(labels.Set(podLabels))
}
Loading
Loading