Skip to content
Draft
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
1 change: 1 addition & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ core,github.com/DataDog/datadog-traceroute/tcp,Apache-2.0,"Copyright 2016-presen
core,github.com/DataDog/datadog-traceroute/traceroute,Apache-2.0,"Copyright 2016-present Datadog, Inc"
core,github.com/DataDog/datadog-traceroute/udp,Apache-2.0,"Copyright 2016-present Datadog, Inc"
core,github.com/DataDog/datadog-traceroute/winconn,Apache-2.0,"Copyright 2016-present Datadog, Inc"
core,github.com/DataDog/dd-policy-engine/go/policies,Apache-2.0,Copyright 2022 Datadog Inc
core,github.com/DataDog/dd-trace-go/v2/appsec/events,Apache-2.0,"Copyright (c) 2016-Present, Datadog <info@datadoghq.com> | Copyright 2016 Datadog, Inc | Copyright 2016-Present Datadog, Inc"
core,github.com/DataDog/dd-trace-go/v2/datastreams/options,Apache-2.0,"Copyright (c) 2016-Present, Datadog <info@datadoghq.com> | Copyright 2016 Datadog, Inc | Copyright 2016-Present Datadog, Inc"
core,github.com/DataDog/dd-trace-go/v2/ddtrace,Apache-2.0,"Copyright (c) 2016-Present, Datadog <info@datadoghq.com> | Copyright 2016 Datadog, Inc | Copyright 2016-Present Datadog, Inc"
Expand Down
1 change: 1 addition & 0 deletions deps/go.MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Commit the Bazel module lock update

bazel/codereview_guideline.md requires MODULE.bazel.lock to be committed whenever a module extension invocation changes. This adds a new go_deps repo exposure for the new Go module, but the commit leaves MODULE.bazel.lock untouched, so Bazel module resolution can run with stale lock state; please run bazel mod tidy/the repo’s Bazel dependency update flow and commit the lockfile change.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Commit the updated Bazel lockfile

Adding com_github_datadog_dd_policy_engine_go changes the go_deps Bzlmod extension output, but MODULE.bazel.lock was not updated and still contains no entry for this repo (checked with rg com_github_datadog_dd_policy_engine MODULE.bazel.lock). The Bazel review guideline in bazel/codereview_guideline.md requires committing the refreshed lockfile for module-extension changes; otherwise Bazel lockfile-mode checks/reproducible fetches can fail before this new dependency is available. Please run the Bazel dependency refresh and commit the resulting MODULE.bazel.lock update.

Useful? React with 👍 / 👎.

"com_github_datadog_dd_trace_go_v2",
"com_github_datadog_ddtrivy",
"com_github_datadog_ebpf_manager",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Regenerate the third-party license manifest

Adding this Go module also changes go.sum, but LICENSE-3rdparty.csv was not updated. The GitLab lint_licenses job runs dda inv -- -e lint-licenses, and tasks/go.py compares that CSV against the vendored dependency set from go.sum, so this dependency addition will leave the license manifest stale until dda inv generate-licenses is run and committed.

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
Expand Down
2 changes: 2 additions & 0 deletions go.sum

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"},
})
}
Loading
Loading