feat(go): native Go policy evaluator for Kubernetes SSI targeting#71
feat(go): native Go policy evaluator for Kubernetes SSI targeting#71iamluc wants to merge 4 commits into
Conversation
Add NAMESPACE_NAME, NAMESPACE_LABEL, POD_LABEL and POD_ANNOTATION to the StringEvaluators enum (append-only) so policies can target Kubernetes workloads, shared with the C engine. Regenerated the Go schema and JSON schema with flatc 25.2.10. The committed policy.schema.json had been produced by an older flatc and lagged the .fbs, so the regeneration also resyncs it: the *_UNKNOWN zero sentinels, the evaluator ids 34-51 (IIS_APPLICATION_POOL..CONTAINER_LABEL) and the dd_wls_UUID / Policy.version definitions, on top of the four new ids.
Add a self-contained, dependency-free Go package that evaluates policies natively (no CGO), mirroring the dd-policy-engine C evaluator: tri-state TRUE/FALSE/ABSTAIN over an AND/OR/NOT tree of leaf evaluators, plus a dd-wls JSON decoder (the JSON projection of the FlatBuffers policy schema). This is the Go evaluator the Cluster Agent will use for Kubernetes SSI targeting; the target->policy compiler and the wiring stay in datadog-agent and land in follow-up PRs. Tests reuse what is common with the C implementation: - the wildcard and tri-state conformance vectors are ported from c/src/test/test_evaluator.c to guarantee semantic parity with libpolicies; - TestDecoderIdentifiersMatchSchema pins every identifier the decoder accepts to the .fbs source shared with the C engine, so the two cannot drift apart silently.
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 37fc60f | Docs | Datadog PR Page | Give us feedback! |
…licies Add the shared github.com/DataDog/dd-policy-engine/go evaluator as a dependency and lower the static "targets" SSI configuration into its policy model (policiesFromTargets). This is the static-configuration counterpart of the remote-config dd-wls parser: both produce policies.Policy, the single shape the matcher understands, so Kubernetes SSI targeting shares one rule semantics with the host injector. The dependency is pinned to a pseudo-version of the dd-policy-engine PR branch (DataDog/dd-policy-engine#71); repin to the released version once that PR merges. golang.org/x/sys is bumped 0.45.0 -> 0.46.0 as required by the dependency's go.mod. Only the target->policy compiler lands here; the mutator wiring and the RC subscription follow in a separate PR.
…licies Add the shared github.com/DataDog/dd-policy-engine/go evaluator as a dependency and lower the static "targets" SSI configuration into its policy model (policiesFromTargets). This is the static-configuration counterpart of the remote-config dd-wls parser: both produce policies.Policy, the single shape the matcher understands, so Kubernetes SSI targeting shares one rule semantics with the host injector. The dependency is pinned to a pseudo-version of the dd-policy-engine PR branch (DataDog/dd-policy-engine#71); repin to the released version once that PR merges. golang.org/x/sys is bumped 0.45.0 -> 0.46.0 as required by the dependency's go.mod. Only the target->policy compiler lands here; the mutator wiring and the RC subscription follow in a separate PR.
Add a single `go test` (make -C go conformance-cross) that runs the shared vectors.json corpus through both engines in one process and asserts they agree: the Go engine via the public ParsePolicies + Evaluate path, and the real C engine (libpolicies, linked via cgo) fed the same vector serialized to a FlatBuffers NodeTypeWrapper buffer. - go/policies/conformance_cgo.go: cgo bridge to the C engine plus host evaluators that mirror the Go engine's baked-in semantics (constant ALWAYS_* leaves, "key=value"/"key=" label evaluator, numeric evaluators that abstain on an unset source). Serializes vectors to FlatBuffers. - go/policies/conformance_cross_test.go: the cross-engine test, gated behind the conformance_cgo build tag so default CGO_ENABLED=0 builds and the portable Go-only corpus test are unaffected. - go/policies/conformance_test.go + testdata/conformance/vectors.json: the shared conformance corpus and its Go-only runner. - c: extend the public string-evaluator enum with the Kubernetes SSI ids (NAMESPACE_NAME/NAMESPACE_LABEL/POD_LABEL/POD_ANNOTATION) to reach schema parity; regenerate evaluator_ids_reader.h. - go engine alignment (model/evaluator/dd_wls): full tri-state parity with the C engine (boolean folding, numeric + label semantics, depth limit). - go/Makefile: conformance-cross target. - .github/workflows/dev.yml: run the cross-engine suite on Linux amd64+arm64.
d5bde87 to
d83e17d
Compare
Move the cross-engine conformance suite (the cgo bridge to libpolicies, the cross-engine test, and the Go-only corpus runner) out of the importable "policies" package into a dedicated "policies/conformance" package. The cgo bridge must live in a regular, non-test source file (cgo is not allowed in _test.go files), so its FlatBuffers and go/schema imports were part of the "policies" package's dependency set. `go mod tidy` resolves imports across all build tags, so even though the bridge is gated behind the "conformance_cgo" tag, those imports leaked into every consumer's module graph (e.g. the Cluster Agent pulled github.com/google/flatbuffers and go/schema, plus the matching LICENSE-3rdparty entries) despite never being linked into any binary. The bridge no longer reuses the unexported dd-wls decoder types; it parses the dd-wls JSON with its own local structs, so the conformance package depends only on the public policies API. The "policies" package thus has no non-test dependency on FlatBuffers/schema and consumers stop pulling them.
| @CGO_ENABLED=1 \ | ||
| CGO_CFLAGS="-I$(C_ROOT)/include -I$(C_ROOT)/src -I$(C_SCHEMA_OUTPUT_DIR)" \ | ||
| CGO_LDFLAGS="$(LIBPOLICIES) -lflatccrt" \ | ||
| go test -tags conformance_cgo ./policies/conformance/ -run TestConformanceCrossEngine -v |
There was a problem hiding this comment.
Looks like this never runs TestConformanceCorpus and won't run any other tests under conformance if they're added.
Is that intentional? Should we just do:
| go test -tags conformance_cgo ./policies/conformance/ -run TestConformanceCrossEngine -v | |
| go test -tags conformance_cgo ./policies/conformance/ -run . -v |
and run everything under the conformance directory?
| test: | ||
| go test ./internal/... | ||
| go test ./cmd/... |
There was a problem hiding this comment.
There are tests in policies that aren't exercised, it looks like. We probably want those to run in CI, yes?
| out.InjectSet = true | ||
| case actionEnableSDK: | ||
| for _, v := range a.Values { | ||
| lang, version, _ := strings.Cut(v, "=") |
There was a problem hiding this comment.
strings.Cut's found bool is discarded here, so a malformed ENABLE_SDK value missing =version (e.g. "java" instead of "java=1.2.3") is silently accepted as TracerVersions["java"] = "". decodeLabel (below, for POD_LABEL/NAMESPACE_LABEL values) hard-errors on the identical missing-= shape — this should too.
Regression test (fails today, run against origin/luc/wls-go-policy-evaluator):
func TestDecodeActionsEnableSDKRejectsMissingEquals(t *testing.T) {
raw := `{
"policies": [{
"description": "malformed enable_sdk value",
"rules": {"node_type": "EvaluatorNode", "node": {"eval_type": "StrEvaluator",
"eval": {"id": "ALWAYS_TRUE"}}},
"actions": [{"action": "ENABLE_SDK", "values": ["java"]}]
}]
}`
if _, err := ParsePolicies([]byte(raw)); err == nil {
t.Fatalf("expected ParsePolicies to reject malformed ENABLE_SDK value %q, got nil error", "java")
}
}--- FAIL: TestDecodeActionsEnableSDKRejectsMissingEquals (0.00s)
dd_wls_test.go:200: expected ParsePolicies to reject malformed ENABLE_SDK value "java", got nil error
| func decodeActions(actions []wlsAction) Outcome { | ||
| out := Outcome{} | ||
| for _, a := range actions { | ||
| switch a.Action { |
There was a problem hiding this comment.
This switch has no default case, so any action id not in {INJECT_ALLOW, INJECT_DENY, ENABLE_SDK, ENABLE_PROFILER} — e.g. SET_ENVAR or REEXEC, both of which exist in the generated ActionId schema (go/schema/dd/wls/ActionId.go) — is silently dropped, indistinguishable from a policy with no actions at all.
Regression test (fails today):
func TestDecodeActionsRejectsUnhandledActionIDs(t *testing.T) {
raw := `{
"policies": [{
"description": "set_envar action",
"rules": {"node_type": "EvaluatorNode", "node": {"eval_type": "StrEvaluator",
"eval": {"id": "ALWAYS_TRUE"}}},
"actions": [{"action": "SET_ENVAR", "values": ["DD_FOO=bar"]}]
}]
}`
if _, err := ParsePolicies([]byte(raw)); err == nil {
t.Fatalf("expected ParsePolicies to reject unhandled action id %q, got nil error", "SET_ENVAR")
}
}--- FAIL: TestDecodeActionsRejectsUnhandledActionIDs (0.00s)
dd_wls_test.go:218: expected ParsePolicies to reject unhandled action id "SET_ENVAR", got nil error
There was a problem hiding this comment.
For forward compatibility, we do want to ignore unknown action (like on the C side)
| if err := json.Unmarshal(e.Eval, &se); err != nil { | ||
| return 0, err | ||
| } | ||
| evalOff = schema.StrEvaluatorCreate(b, wls.EnumValuesStringEvaluators[se.ID], se.Value, wls.EnumValuesCmpTypeSTR[se.Cmp]) |
There was a problem hiding this comment.
wls.EnumValuesCmpTypeSTR[se.Cmp] (and the analogous EnumValuesStringEvaluators/EnumValuesNumericEvaluators/EnumValuesCmpTypeNUM lookups on this line and lines 355/362) is a plain map index with no , ok check. An unknown/typo'd string (e.g. "CMP_BOGUS" in testdata/vectors.json) silently resolves to the map's zero value (CmpTypeSTRCMP_STR_UNKNOWN == 0) and gets built into the buffer instead of failing loudly. The Go engine's own decoder (policies.ParsePolicies → decodeStrCmp) hard-errors on the identical shape — see the "unknown string cmp" case in dd_wls_test.go's TestParseErrors. buildRulesBuffer should do the same, otherwise a typo in the shared corpus could silently mask a real cross-engine divergence rather than failing the conformance test with a clear error.
Regression test (fails today, requires the cgo/C toolchain — make -C go conformance-cross build environment):
func TestBuildRulesBufferRejectsUnknownComparator(t *testing.T) {
rules := []byte(`{
"node_type": "EvaluatorNode",
"node": {
"eval_type": "StrEvaluator",
"eval": {"id": "NAMESPACE_NAME", "cmp": "CMP_BOGUS", "value": "x"}
}
}`)
if _, err := buildRulesBuffer(rules); err == nil {
t.Fatalf("expected buildRulesBuffer to reject unknown comparator %q, got nil error", "CMP_BOGUS")
}
}--- FAIL: TestBuildRulesBufferRejectsUnknownComparator (0.00s)
buildrules_bug_test.go:30: expected buildRulesBuffer to reject unknown comparator "CMP_BOGUS", got nil error
Verified by building libpolicies.a + flatcc from source in this repo and running it with CGO_ENABLED=1 against the real static lib.
Description
Adds a self-contained, dependency-free Go policy evaluator to this repo
(
go/policies), mirroring the semantics of the C engine: tri-stateTRUE/FALSE/ABSTAIN over an AND/OR/NOT tree of leaf evaluators, plus a
dd-wlsJSON decoder (the JSON projection of the FlatBuffers policy schema).
This is the Go evaluator the Cluster Agent will use to target Kubernetes
Single Step Instrumentation (SSI) workloads natively (no CGO at runtime).
To make "shares the same test suite" real, this PR also adds a cross-engine
conformance suite: a single
go testruns a shared corpus through both theGo engine and the real C engine (
libpolicies, linked via cgo) in oneprocess and asserts they agree. The K8s string evaluators are added to the
schema (and to the C engine's public enum) so policies can target
pods/namespaces on both sides.
Scope is deliberately limited to the engine + schema + conformance. The
target -> policycompiler and the Cluster Agent wiring (RC subscription,mutator) stay in
datadog-agentand land in follow-up PRs.What's in here
Go engine (
go/policies)(boolean folding, signed/unsigned numeric leaves,
key=value/key=labelleaves, depth limit mirroring
PLCS_MAX_EVAL_DEPTH);dd-wlsJSON decoder (ParsePolicies);Evaluate(rule tree -> tri-stateResult) andDecide(folds the outcomesof all matching policies in document order, matching the C engine's
no-first-match-short-circuit behavior — not first-match-wins).
Cross-engine conformance
go/policies/testdata/conformance/vectors.json: shared corpus (booleancomposition, string comparators, labels, numeric, ABSTAIN propagation).
conformance_test.go: portable Go-only runner (no cgo, runs everywhere).conformance_cgo.go+conformance_cross_test.go: cgo bridge to the real Cengine plus host evaluators that mirror what the Go engine bakes in (constant
ALWAYS_*leaves, thekey=value/key=label evaluator, and numericevaluators that abstain on an unset source). Each vector is serialized to a
FlatBuffers
NodeTypeWrapperbuffer and run throughlibpolicies; the resultmust equal both the Go engine's result and the expected value. Gated behind
the
conformance_cgobuild tag, so the defaultCGO_ENABLED=0builds areunaffected. Run with
make -C go conformance-cross.Schema + C engine parity
fbs-schema/evaluator_ids.fbs:NAMESPACE_NAME,NAMESPACE_LABEL,POD_LABEL,POD_ANNOTATION(append-only); regenerated Go schema.c/include/dd/policies/evaluator_types.h: extend the publicstring-evaluator enum with the same 4 K8s ids so the engine reaches schema
parity (the
_Static_assertagainst the schema count now matches);regenerate
evaluator_ids_reader.h.CI
.github/workflows/dev.yml: newconformance-cross-enginejob running thecross-engine suite in the build container on Linux amd64 + arm64.
Related Issue
Type of Change
Checklist
How Has This Been Tested?
In the build container:
CGO_ENABLED=0 go vet ./policies/...and the Go-only conformance test aregreen; the cgo-gated file is correctly excluded from default builds.
make -C go conformance-cross: all corpus vectors pass through both theGo engine and the real C engine in one process (semantic parity with
libpoliciesproven on the same inputs, not just ported expectations).make -C c test: the full C unit-test suite still passes after the publicenum extension.
TestDecoderIdentifiersMatchSchemapins every identifier the decoder acceptsto the
.fbssource shared with the C engine, so the two cannot drift apart.Additional Notes
missing fact against the
NOT_SETsentinel (FALSE), whereas the Go modelabstains. Rather than change the engine (and break existing C tests), the
conformance host registers numeric evaluators that abstain on
NOT_SET,reproducing the Go semantics at the host level — where the generic C engine
expects such decisions to live.
policy.schema.jsonwas regenerated withflatc 25.2.10; the committed file lagged the.fbs, so the regen alsoresyncs it in addition to the 4 new K8s ids.
datadog-agent):target -> policycompiler + goldentests; Cluster Agent wiring (facts, mutator, RC
apm-wlssubscription) with adifferential test vs the current
TargetMutator.