Skip to content

Feat: Emit lineage.self.namespace; require namespace (contract v1.7) - #918

Merged
huang195 merged 1 commit into
rossoctl:mainfrom
s-and-p-team:lane/lineage-namespace
Sep 9, 2026
Merged

Feat: Emit lineage.self.namespace; require namespace (contract v1.7)#918
huang195 merged 1 commit into
rossoctl:mainfrom
s-and-p-team:lane/lineage-namespace

Conversation

@JoshSag

@JoshSag JoshSag commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #917. Producer half of rossoctl/lab-data-governance#201; consumer: rossoctl/lab-data-governance#230.

lineage-telemetry emits lineage.self.namespace on both spans, from a new required namespace key, or namespace_file for the kubelet-projected file in ConfigMaps shared across namespaces. Resolved first in Init; absent, blank or not an RFC 1123 DNS label refuses at start, nothing left behind; not capped; never parsed out of the SPIFFE path. Wire contract v1.7.0, vendored byte-identical with lab-data-governance.

Unchanged: lineage.self.id, its reduction, the §4 clause and the "collides by design" row in TestServiceLabel (#761 rounds 5–6) — that was the documentation half; this adds the identity half. Closed alongside: #761 round 6's open question — a separators-only self_id (/) now refuses at start like a blank one.

Compatibility. Additive on the wire; breaking in configuration both ways (an older sidecar rejects the key), so image and ConfigMap flip together per pod — one sidecar-patch.sh re-run with a matching SIDECAR_IMAGE. The kit renders NAMESPACE and warns on a no-op re-run; README covers the upgrade, the enrolled route via namespace_file, and three troubleshooting rows. Schema marks the key required; Capabilities cite → v1.7.

Tests. Both spans, both directions, through decode → Init → emit; never capped; refusals (absent, blank, /, uppercase, spaces, dots, 64 chars; 63 and padded pass) before the identity poll; namespace_file; schema flag; / and // in the identity table. gofmt, vet, go test -race ./... clean.

Live. Same weather pair under identical names in team1 and team2: four entity rows, each trace on its own rows; a team1 agent calling the team2 tool attributes to tool:team2/weather-tool; bad or missing keys crashloop the sidecar with the message naming the key; namespace_file resolves team1; the #853 demo passes end to end on this image.

Summary by CodeRabbit

  • New Features
    • Lineage spans now include the workload’s Kubernetes namespace as lineage.self.namespace.
    • Namespace can be supplied directly or read from a file and is included in lineage identity and storage keys.
  • Bug Fixes
    • Startup now rejects missing, blank, or invalid namespaces and identity values.
  • Documentation
    • Updated the wire contract, plugin catalog, deployment guides, recipes, and troubleshooting guidance for the new required namespace configuration.
  • Chores
    • Updated attachment and patching scripts to configure and verify the namespace.
    • Advanced the lineage wire contract to v1.7.0.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 2b8cea25-118e-4798-9592-f871bf0a1cd3

📥 Commits

Reviewing files that changed from the base of the PR and between 9e3ef03 and 3fda5ce.

📒 Files selected for processing (4)
  • authbridge/authlib/plugins/lineage/plugin.go
  • authbridge/authlib/plugins/lineage/plugin_test.go
  • authbridge/docs/lineage-wire-contract.md
  • authbridge/docs/plugin-catalog.md

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: e0d8c855-f55c-4a1d-9130-2d37e74786ca

📥 Commits

Reviewing files that changed from the base of the PR and between 76e6db2 and 9e3ef03.

📒 Files selected for processing (9)
  • authbridge/authlib/plugins/lineage/config.go
  • authbridge/authlib/plugins/lineage/plugin.go
  • authbridge/authlib/plugins/lineage/plugin_test.go
  • authbridge/docs/lineage-wire-contract.md
  • authbridge/docs/plugin-catalog.md
  • authbridge/lineage-attach/README.md
  • authbridge/lineage-attach/RECIPE.md
  • authbridge/lineage-attach/attach-lineage.sh
  • authbridge/lineage-attach/sidecar-patch.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The lineage plugin now requires a validated namespace from inline configuration or a startup-read file. It emits the namespace on both spans, rejects separator-only identities, updates the v1.7.0 contract, and updates sidecar attachment tooling and documentation.

Changes

Lineage namespace identity

Layer / File(s) Summary
Namespace configuration and startup resolution
authbridge/authlib/plugins/lineage/config.go, authbridge/authlib/plugins/lineage/plugin.go
Adds Namespace and NamespaceFile. Startup resolves and validates the namespace, then includes it in initialization logs.
Identity validation and span emission
authbridge/authlib/plugins/lineage/plugin.go, authbridge/authlib/plugins/lineage/plugin_test.go
Rejects separator-only identities and emits lineage.self.namespace on request and response spans without truncation. Tests cover validation, file resolution, precedence, schema decoding, and span output.
Wire contract and plugin configuration documentation
authbridge/docs/lineage-wire-contract.md, authbridge/docs/plugin-catalog.md
Documents contract v1.7.0, namespace-aware entity keys, startup validation, and legacy unnamespaced spans.
Sidecar configuration and rollout guidance
authbridge/lineage-attach/README.md, authbridge/lineage-attach/RECIPE.md, authbridge/lineage-attach/attach-lineage.sh, authbridge/lineage-attach/sidecar-patch.sh
Adds namespace configuration to generated sidecar entries and documents rollout, hot-reload, logging, and troubleshooting behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~30 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 9e3ef

The namespace configuration, validation, emission, contract, and attachment tooling are aligned with no merge-blocking issue identified.

Sequence Diagram(s)

sequenceDiagram
  participant SidecarConfig
  participant LineageTelemetry
  participant NamespaceFile
  participant RequestSpan
  participant ResponseSpan
  SidecarConfig->>LineageTelemetry: provide namespace or namespace_file
  LineageTelemetry->>NamespaceFile: read namespace once when needed
  NamespaceFile-->>LineageTelemetry: validated namespace
  LineageTelemetry->>RequestSpan: emit lineage.self.namespace
  LineageTelemetry->>ResponseSpan: emit lineage.self.namespace
Loading

Suggested reviewers: huang195, evaline-ju

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #917 by adding namespace and namespace_file configuration, validating the namespace at startup, emitting lineage.self.namespace on both spans, preserving self.id reduction, a…
Out of Scope Changes check ✅ Passed The configuration, validation, identity handling, documentation, tests, attach scripts, and migration guidance support the namespace-based identity change and contract update. No unrelated code change…
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 5 files. (4 skipped: 4 …
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: emitting lineage.self.namespace, requiring namespace configuration, and updating the contract to v1.7.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@abigailgold abigailgold added the ready-for-ai-review Request automated AI code review from clawgenti label Sep 9, 2026
Two workloads with the same name in two namespaces derived as ONE entity at
the consumer: lineage.self.id is the last segment of the SPIFFE ID, and the
data-governance sidecar algorithm keyed an entity on kind:self.id. A
team1/weather-service and a team2/weather-service therefore shared one row,
and every interaction of both pods pointed at it. That collision was made
normative in rossoctl#761 (round 5: contract v1.6.1 §4; round 6: the TestServiceLabel
row "collides by design") as the documentation half — the reduction is
deliberate and stays. This is the identity half: self.id alone was never the
whole identity of a pod, so the missing fact is added rather than the
shipped reduction changed.

The plugin now emits lineage.self.namespace on both spans, from a new
required `namespace` config key (or `namespace_file`, read once at start —
for the file the kubelet projects from the pod's own metadata, the one
source that is right in every copy of a ConfigMap shared across
namespaces). The value is resolved before anything else in Init and must be
an RFC 1123 DNS label: absent, blank, a "/" (which would make the consumer's
{kind}:{namespace}/{self.id} key ambiguous), or any other shape refuses to
start and leaves nothing behind — the treatment a blank self_id gets,
because a name without a namespace is half an identity. It is never parsed
out of the SPIFFE path (a registrar convention; a kit-attached pod has no
SPIFFE ID). Neither identity fact is capped by max_attr_bytes any more —
self.id was, and a truncated identity keys the pod on a name that is not
its own; both are operator configuration, not caller input. lineage.self.id,
its reduction, the §4 clause, the by-design test row and the span names are
unchanged. The rossoctl#761 round-6 question is closed alongside: a self_id made
only of separators, which the reduction emits as-is, now refuses at start
like a blank one, and a self_id_file carrying one keeps the plugin not-ready
like a blank file.

Wire contract v1.7.0 (vendored byte-identical with lab-data-governance):
the attribute (§4), the keys (§6), and the consumer commitment that a pod's
identity is the (namespace, self.id) pair, natural key
{kind}:{namespace}/{self.id}, read from the request span (§7). Additive on
the wire; breaking in configuration in both directions — a sidecar older
than the key rejects a config that carries it, so image and ConfigMap flip
together per pod (no published release carries the plugin yet, so no
deployed configuration is affected). The attach kit writes its NAMESPACE,
warns when a re-run patches nothing (no pod rolls; the old sidecar only
hot-reloads), and the README covers the upgrade, the enrolled-workload route
via namespace_file (a literal in the platform's shared ConfigMap would be
wrong in every namespace but one), and three troubleshooting rows. The
schema marks the key required for abctl and /v1/plugins; the Capabilities
cite moves to v1.7.

Tests: the namespace on both spans in both directions and through the real
decode → Init → emit path (trimmed, otherwise verbatim); never capped;
refusals for absent, blank, "/", uppercase, spaces, dots and 64 chars with
63 and padded accepted, and nothing left behind; refusal precedes the
identity poll; namespace_file (projected file, inline wins; absent, blank —
its own message — and non-label refuse); the schema's required flag; every
Init fixture carries the key; the identity table gains the "/" and " // "
rows and the file test a separators-only file. Tests that run a full Init
shut the plugin down (provider and gRPC client), not only the provider.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJpNC6yQgZAdfiLAp3RksZ
Signed-off-by: YehoshuaSagron <ysagron@gmail.com>
@JoshSag
JoshSag force-pushed the lane/lineage-namespace branch from 9e3ef03 to 3fda5ce Compare September 9, 2026 13:16

@huang195 huang195 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Careful, well-argued change. The namespace is resolved before anything else Init builds (no tracer provider, no poller, no connection behind a refusal), the fact is never derived from the SPIFFE path, and the docs, the wire-contract ladder, the kit scripts and the troubleshooting rows all move together. The breaking half is owned honestly in both the PR body and the contract, and it strands no in-repo config: lineage-telemetry is rendered nowhere in this repo's charts or fixtures, and nowhere at all in rossoctl. Test coverage for the new key is thorough — decode → Init → both spans, plus the file source and the shape refusals.

Claims I verified against source at head rather than taking on faith:

Claim Result
"the same check the attach kit applies to NAMESPACE" attach-lineage.sh:139-140 uses the byte-identical regex
ReadCredentialFile "already trims; zero-length is its error" authlib/config/resolve.go:25-38 — so the raw == "" branch is reachable for whitespace-only files
required:"true" won't reject a namespace_file-only config pipeline/schema.go:64-66 — "just metadata"
p.cfg.Namespace = ns is race-free reloader.go builds fresh pipelines per reload (Configure+Init both run) — which also makes sidecar-patch.sh's "old pipeline kept on a rejected reload" note accurate
RECIPE's … self_id=<x> namespace=<ns> pass line matches the slog.Info arg order
self.id uncapped, span name still capped code and both docs agree; the changelog is explicit that span names are unchanged

One I could not check: "vendored byte-identical with lab-data-governance" — that repo 404s for me, so it rests on the consumer PR.

Nothing blocking. Six inline items, all suggestions or nits; the two worth acting on are the untested self.id uncapping and the automountServiceAccountToken: false caveat on the recommended platform route.

Author: JoshSag (CONTRIBUTOR — returning external, from fork s-and-p-team/cortex)
Areas reviewed: Go (plugin + config + tests), Shell, Docs
Agent/IDE config (.claude/.vscode): none — grepped for both +++ b/ and rename to
Commits: 1, signed-off: yes
CI: passing (23 green, Spellcheck skipped)

Assisted-By: Claude Code

// hasIdentity is the one rule behind both identity sources: an identity is
// a string with at least one non-empty "/"-segment, so serviceLabel has a
// name to emit. Blank, and separator-only values such as "/", carry none.
func hasIdentity(id string) bool { return strings.Trim(id, "/") != "" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nithasIdentity still admits whitespace-only segments. I ran it: " / / " → TrimSpace → "/ /"true, and "// //" → true. Since serviceLabel returns non-SPIFFE input as-is, those emit a whitespace self.id — the exact "entity keyed on whitespace" the Init comment above says this prevents.

The motivating / case is caught, so this is cosmetic completeness rather than a live hole:

func hasIdentity(id string) bool { return strings.Trim(id, "/ \t\n\r") != "" }

// both of these are operator configuration (self_id / self_id_file,
// namespace / namespace_file), and the namespace is bounded to a DNS
// label by Init besides.
attribute.String("lineage.self.id", self),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — dropping p.capped() here changes behavior on a pre-existing fact, and nothing tests it. TestAttrBytesCapsCallerControlledValues only asserts url.path / lineage.peer.host / mcp.tool / span name, so there was no stale assertion to update — but there's also no new one, and lineage.self.namespace got TestNamespace_NeverCapped while lineage.self.id got nothing.

A self_id-flavored twin of that test (long self_id, MaxAttrBytes = 4, assert the whole value survives) would pin the half of v1.7.0 that is currently unguarded — and it's the half a future refactor is likelier to silently re-cap, since capped() is the default idiom everywhere else in baseAttrs.

fi
kubectl apply -f - <<<"$cm"
kubectl patch deploy "$DEPLOY" -n "$NAMESPACE" --type strategic --patch "$patch" || {
patched="$(kubectl patch deploy "$DEPLOY" -n "$NAMESPACE" --type strategic --patch "$patch")" || {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpatched isn't declared local, making it the only variable in apply() that leaks to global scope: cm patch undo restored_image cm_existed are declared on line 175 and dryrun_err gets its own local on line 198. Adding it to the line-175 declaration keeps the function's convention intact.

# kept. "attached" below would then be true of the objects and false of
# the spans. Say so, and name the check.
case "$patched" in
*"(no change)"*)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — the NOTE itself is genuinely valuable: it catches the case where "attached" is true of the objects and false of the spans, which is exactly the trap a re-run falls into. But it hinges on matching kubectl's human-readable (no change) prose, which isn't part of any API contract and has changed across kubectl versions.

.metadata.generation gives the same signal from a stable surface — read it before the patch, compare after (or -o jsonpath='{.metadata.generation}' on the patch itself); unchanged generation means no roll, regardless of what kubectl chose to print.


That file is projected by the kubelet from the pod's own metadata into every
container that mounts the service-account volume; if the injected sidecar
does not mount it, the plugin refuses to start (loudly, naming the path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — worth naming the one way this file is absent that isn't a wrong path or a missing mount: automountServiceAccountToken: false on the pod or the ServiceAccount, which suppresses the projection entirely.

The no-poller reasoning is sound — the kubelet projects before the container starts, so absence really is misconfiguration rather than a race. But on a cluster that disables automount, the route recommended here crashloops the whole sidecar and every plugin in its chain, which is the failure mode v1.6.3 deliberately walked back for self_id_file. A troubleshooting row pointing at automount would turn a confusing outage into a one-line fix.

// all. Empty, blank, or not an RFC 1123 DNS label (the only shape a
// namespace can have) refuses at start (see resolveNamespace); when empty,
// NamespaceFile is consulted instead.
Namespace string `json:"namespace" required:"true" description:"This workload's Kubernetes namespace (an RFC 1123 DNS label), emitted as lineage.self.namespace on every span; refused at start when empty or not a label. Alternatively namespace_file."`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitnamespace is the only required:"true" field in this Config, and the tag carries no "one-of" notion, so abctl's renderer (edit/templates.go:110, collectRequiredPaths) will emit # Required: namespace plus a [REQUIRED] annotation — contradicting the README's platform recipe, which sets only namespace_file.

I checked that this can't actually break that path: pipeline/schema.go:64-66 is explicit that Required is metadata and "boot semantics are the plugin's own concern." And the tag is defensible on its own terms — this is the one key with no default, unlike self_id, which falls back to self_id_file's default. The description does name the alternative. It's purely that an operator generating a template from the schema will be told they can't omit a key the docs tell them to omit.

@huang195
huang195 merged commit 25e612f into rossoctl:main Sep 9, 2026
23 checks passed
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ai-review Request automated AI code review from clawgenti

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

lineage-telemetry: same workload name in two namespaces emits one identity

4 participants