-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(autoinstrumentation): compile SSI targets to policies and wire the mutator to the policy engine #52489
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
base: main
Are you sure you want to change the base?
feat(autoinstrumentation): compile SSI targets to policies and wire the mutator to the policy engine #52489
Changes from all commits
d67991f
42b56d0
6e5d69b
91d7858
b92303a
5acd305
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,7 @@ use_repo( | |
| "com_github_datadog_datadog_go_v5", | ||
| "com_github_datadog_datadog_operator_api", | ||
| "com_github_datadog_datadog_traceroute", | ||
| "com_github_datadog_dd_policy_engine_go", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Adding Useful? React with 👍 / 👎. |
||
| "com_github_datadog_dd_trace_go_v2", | ||
| "com_github_datadog_ddtrivy", | ||
| "com_github_datadog_ebpf_manager", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,6 +166,7 @@ require ( | |
| github.com/DataDog/datadog-go/v5 v5.8.3 | ||
| github.com/DataDog/datadog-operator/api v0.0.0-20260515125012-8e158b708444 | ||
| github.com/DataDog/datadog-traceroute v1.0.17 | ||
| github.com/DataDog/dd-policy-engine/go v0.0.0-20260619124759-08b73482a87e | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Adding this Go module also changes Useful? React with 👍 / 👎. |
||
| github.com/DataDog/dd-trace-go/v2 v2.8.2 | ||
| github.com/DataDog/ebpf-manager v0.7.18 | ||
| github.com/DataDog/go-acl v1.0.1 | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| // 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 ( | ||
| corev1 "k8s.io/api/core/v1" | ||
|
|
||
| workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def" | ||
| "github.com/DataDog/datadog-agent/pkg/util/log" | ||
| "github.com/DataDog/dd-policy-engine/go/policies" | ||
| ) | ||
|
|
||
| // policyMatcher evaluates SSI policies against pods using the pure Go policy | ||
| // engine. It holds the effective ordered policy set (configuration policies, | ||
| // optionally augmented with remote-config ones) and resolves the first match. | ||
| type policyMatcher struct { | ||
| policies []policies.Policy | ||
| wmeta workloadmeta.Component | ||
| needsNamespaceLabels bool | ||
| } | ||
|
|
||
| // newPolicyMatcher builds a matcher over the given policies and records whether | ||
| // any rule reads namespace labels, so we only pay the workloadmeta lookup when a | ||
| // policy actually needs it. | ||
| func newPolicyMatcher(ps []policies.Policy, wmeta workloadmeta.Component) *policyMatcher { | ||
| return &policyMatcher{ | ||
| policies: ps, | ||
| wmeta: wmeta, | ||
| needsNamespaceLabels: usesNamespaceLabels(ps), | ||
| } | ||
| } | ||
|
|
||
| // Match returns the outcome of the first policy that matches the pod, mirroring | ||
| // the "first match wins" semantics of the target mutator. | ||
| func (m *policyMatcher) Match(pod *corev1.Pod) (policies.Outcome, bool) { | ||
| idx := m.matchIndex(pod) | ||
| if idx < 0 { | ||
| return policies.Outcome{}, false | ||
| } | ||
| return m.policies[idx].Outcome, true | ||
| } | ||
|
|
||
| // matchIndex returns the index of the first policy that matches the pod, or -1 | ||
| // if none match. Policies are evaluated in order (first match wins). | ||
| // | ||
| // A policy that reads namespace labels which could not be resolved (e.g. the | ||
| // pod's namespace is absent from the workloadmeta store) is skipped rather than | ||
| // fatal: it cannot be evaluated, so it is ignored, and the remaining policies | ||
| // are still evaluated in order. This guarantees that an unrelated unresolvable | ||
| // namespace rule never prevents an otherwise-matching rule from injecting. | ||
| func (m *policyMatcher) matchIndex(pod *corev1.Pod) int { | ||
| if m == nil || pod == nil { | ||
| return -1 | ||
| } | ||
| facts, namespaceLabelsResolved := m.factsForPod(pod) | ||
| for i := range m.policies { | ||
| if !namespaceLabelsResolved && nodeUsesNamespaceLabels(m.policies[i].Rules) { | ||
| // Cannot evaluate this rule without namespace labels; ignore it. | ||
| continue | ||
| } | ||
| if policies.Evaluate(m.policies[i].Rules, facts) == policies.ResultTrue { | ||
| return i | ||
| } | ||
| } | ||
| return -1 | ||
| } | ||
|
|
||
| // factsForPod builds the evaluation facts for a pod. The boolean reports whether | ||
| // namespace labels were resolved: when a policy needs them but they cannot be | ||
| // fetched, it is false and namespace-label rules are skipped by matchIndex. | ||
| func (m *policyMatcher) factsForPod(pod *corev1.Pod) (policies.Facts, bool) { | ||
| facts := policies.Facts{ | ||
| NamespaceName: pod.Namespace, | ||
| PodLabels: pod.Labels, | ||
| } | ||
| if m.needsNamespaceLabels && m.wmeta != nil { | ||
| nsLabels, err := getNamespaceLabels(m.wmeta, pod.Namespace) | ||
| if err != nil { | ||
| log.Debugf("policy matcher: namespace labels unavailable for namespace %q, namespace-label rules will be skipped: %v", pod.Namespace, err) | ||
| return facts, false | ||
| } | ||
| facts.NamespaceLabels = nsLabels | ||
| } | ||
| return facts, true | ||
| } | ||
|
|
||
| // usesNamespaceLabels reports whether any policy rule reads a namespace label, | ||
| // so the matcher can skip the workloadmeta lookup otherwise. | ||
| func usesNamespaceLabels(ps []policies.Policy) bool { | ||
| for i := range ps { | ||
| if nodeUsesNamespaceLabels(ps[i].Rules) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func nodeUsesNamespaceLabels(n *policies.Node) bool { | ||
| if n == nil { | ||
| return false | ||
| } | ||
| if n.Eval != nil { | ||
| return n.Eval.Source == policies.SourceNamespaceLabel | ||
| } | ||
| for _, c := range n.Children { | ||
| if nodeUsesNamespaceLabels(c) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // 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 ( | ||
| "testing" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| func podWith(ns string, labels map[string]string) *corev1.Pod { | ||
| return &corev1.Pod{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Namespace: ns, | ||
| Labels: labels, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func TestPolicyMatcherPodLabels(t *testing.T) { | ||
| targets := []Target{ | ||
| { | ||
| Name: "java", | ||
| PodSelector: &PodSelector{MatchLabels: map[string]string{"app": "db"}}, | ||
| TracerVersions: map[string]string{"java": "latest"}, | ||
| }, | ||
| { | ||
| Name: "catch-all", | ||
| TracerVersions: map[string]string{"php": "latest"}, | ||
| }, | ||
| } | ||
|
|
||
| m := newPolicyMatcher(policiesFromTargets(targets), nil) | ||
| if m.needsNamespaceLabels { | ||
| t.Errorf("matcher should not require namespace labels for pod-label targets") | ||
| } | ||
|
|
||
| out, ok := m.Match(podWith("any", map[string]string{"app": "db"})) | ||
| if !ok || out.TracerVersions["java"] != "latest" { | ||
| t.Fatalf("db pod: got %+v ok=%v", out, ok) | ||
| } | ||
|
|
||
| out, ok = m.Match(podWith("any", map[string]string{"app": "web"})) | ||
| if !ok || out.TracerVersions["php"] != "latest" { | ||
| t.Fatalf("web pod should hit catch-all: got %+v ok=%v", out, ok) | ||
| } | ||
| } | ||
|
|
||
| func TestPolicyMatcherDetectsNamespaceLabels(t *testing.T) { | ||
| targets := []Target{{ | ||
| Name: "by-ns-label", | ||
| NamespaceSelector: &NamespaceSelector{MatchLabels: map[string]string{"instrument": "true"}}, | ||
| }} | ||
| m := newPolicyMatcher(policiesFromTargets(targets), nil) | ||
| if !m.needsNamespaceLabels { | ||
| t.Errorf("matcher should require namespace labels when a policy reads them") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // 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 "testing" | ||
|
|
||
| // TestMatching_UnresolvableNamespaceRuleIsSkipped documents an intentional | ||
| // behavior change introduced by the policy-engine matcher. | ||
| // | ||
| // When a pod's namespace is absent from the workloadmeta store, a rule that | ||
| // reads namespace labels cannot be evaluated. The matcher now ignores only that | ||
| // rule and keeps evaluating the remaining rules in order, so an unresolvable | ||
| // namespace rule never blocks an otherwise-matching rule from injecting. | ||
| // | ||
| // The legacy label-selector matcher instead aborted all matching as soon as it | ||
| // reached a rule whose namespace could not be resolved (returning "no match"). | ||
| // It only injected in this situation by accident of ordering: if a matching | ||
| // pod-only rule happened to come first, it short-circuited before the | ||
| // unresolvable rule was reached. These cases pin the new, order-independent | ||
| // behavior; the "rule first" case is the one that changes versus the legacy | ||
| // matcher (it would previously have aborted). | ||
| func TestMatching_UnresolvableNamespaceRuleIsSkipped(t *testing.T) { | ||
| const podRuleFirst = ` | ||
| apm_config: | ||
| instrumentation: | ||
| enabled: true | ||
| targets: | ||
| - name: "pod-only" | ||
| podSelector: | ||
| matchLabels: | ||
| app: "web" | ||
| ddTraceVersions: | ||
| java: "default" | ||
| - name: "ns-label" | ||
| namespaceSelector: | ||
| matchLabels: | ||
| instrument: "true" | ||
| ddTraceVersions: | ||
| python: "default" | ||
| ` | ||
| const nsRuleFirst = ` | ||
| apm_config: | ||
| instrumentation: | ||
| enabled: true | ||
| targets: | ||
| - name: "ns-label" | ||
| namespaceSelector: | ||
| matchLabels: | ||
| instrument: "true" | ||
| ddTraceVersions: | ||
| python: "default" | ||
| - name: "pod-only" | ||
| podSelector: | ||
| matchLabels: | ||
| app: "web" | ||
| ddTraceVersions: | ||
| java: "default" | ||
| ` | ||
| // No namespace is registered in the store, so the namespace-label rule | ||
| // cannot be evaluated for the "ghost" namespace and is skipped. | ||
| runMatchCases(t, podRuleFirst, []matchCase{ | ||
| {name: "pod-only rule before the unresolvable rule still matches", ns: "ghost", podLabels: map[string]string{"app": "web"}, want: "pod-only"}, | ||
| }) | ||
| runMatchCases(t, nsRuleFirst, []matchCase{ | ||
| {name: "unresolvable rule first does not block the pod-only rule", ns: "ghost", podLabels: map[string]string{"app": "web"}, want: "pod-only"}, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bazel/codereview_guideline.mdrequiresMODULE.bazel.lockto be committed whenever a module extension invocation changes. This adds a newgo_depsrepo exposure for the new Go module, but the commit leavesMODULE.bazel.lockuntouched, so Bazel module resolution can run with stale lock state; please runbazel mod tidy/the repo’s Bazel dependency update flow and commit the lockfile change.Useful? React with 👍 / 👎.