Skip to content

fix(attribution): resolve the audit route from one ClusterProvider read#264

Merged
sunib merged 2 commits into
mainfrom
fix/audit-route-no-fallback-guess
Jul 23, 2026
Merged

fix(attribution): resolve the audit route from one ClusterProvider read#264
sunib merged 2 commits into
mainfrom
fix/audit-route-no-fallback-guess

Conversation

@sunib

@sunib sunib commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The bug

auditRouteFor re-read the GitTarget's ClusterProvider and, when that read failed, returned the provider name as the audit route. Its comment claimed this is "never a route that silently matches no facts" — that holds only for a provider that declares no spec.attribution.auditRoute.

A provider declaring auditRoute: shared-route was instead declared under prod-eu-1, filing that target's streams against a partition the audit webhook never writes to. Attribution then resolves nobody — or, if some other provider happens to be named shared-route, resolves that cluster's facts and writes a wrong author into Git.

It self-heals on the next reconcile, so this is not a release blocker; the safety claim in the comment was simply wrong as written.

The fix

One reconcile read the same ClusterProvider three times, with three different policies for the same failure:

site on read failure
authz.GitTargetAdmitted (Validated gate) error → requeue
auditRouteFor guess the provider name
clusterProviderReadiness Unknown

That inconsistency is what let the wrong policy in. The provider is now read once, after the Validated gate has already proved it exists and admits the namespace, and handed to both consumers:

  • auditRouteFor is deleted. The call site uses AuditRoute() on the object that was read. A read failure returns an error and requeues — the same policy the gate 100 lines earlier already applies to the identical failure. A route is never guessed.
  • clusterProviderReadiness takes the provider instead of reading its own, so it becomes a pure function: no ctx, no client, no "cannot observe" branch. That branch was unreachable in the real flow anyway — a missing provider is the Validated gate's hard denial, not a readiness axis. Its stale comment and the now-impossible absent provider test case are removed.

Net: one read instead of three, one function gone, one fallback gone, one failure policy.

Deliberately unchanged

  • authz.GitTargetAdmitted's own read. It is the shared gate for three packages; threading its object out would ripple into watchrule_compile.go and source_namespace.go for no behavioural gain — ClusterProvider is watched by this controller, so it is a cached read.
  • watch.auditRouteForCluster's fallback. Different situation: no client there by design, and DeclareForGitTarget populates the map before any watch opens, so a miss means no stream exists yet.

Validation

task lint, task test (coverage 77.8%, baseline held), and task test-e2e (69 passed / 0 failed) all green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved Git target reconciliation by using a consistent ClusterProvider snapshot for both audit routing and readiness reporting.
    • Audit attribution now honors the declared audit route when available, otherwise falls back to the provider name.
    • If a ClusterProvider can’t be read, reconciliation now errors clearly instead of guessing.
  • Tests
    • Updated and added coverage for source ClusterProvider resolution behavior and reconciliation timing around secret recreation.

auditRouteFor re-read the GitTarget's ClusterProvider and, when that read
failed, returned the provider NAME as the audit route. Its comment claimed
this "never a route that silently matches no facts" — true only for a
provider that declares no spec.attribution.auditRoute. A provider that
declares `shared-route` was declared under its own name instead, filing this
target's streams against a partition the audit webhook never writes to:
attribution silently resolves nobody, or resolves another cluster's facts if
some provider happens to be NAMED like this one's route, writing a wrong
author into Git.

One reconcile read the same ClusterProvider three times with three different
failure policies (error, guess, Unknown), which is what let the wrong one in.
It is now read once, after the Validated gate that already proved it exists
and admits the namespace, and handed to both consumers:

- auditRouteFor is gone; the call site uses AuditRoute() on that object. A
  read failure returns an error and requeues, the same policy the gate
  already applies to the identical failure — a route is never guessed.
- clusterProviderReadiness takes the provider instead of reading its own, so
  it is a pure function with no ctx, client, or "cannot observe" branch. That
  branch was unreachable anyway: a missing provider is the gate's hard denial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f77e6074-5105-4104-80e4-62a48c1453e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4e5cb3c and 4c0d328.

📒 Files selected for processing (1)
  • internal/controller/gittarget_controller_test.go

📝 Walkthrough

Walkthrough

Reconciliation now resolves the source ClusterProvider once and reuses it for audit routing and readiness projection. Provider resolution errors return immediately, and tests now validate direct readiness projection, attribution, unreadable-provider behavior, and periodic secret recreation timing.

Changes

ClusterProvider resolution

Layer / File(s) Summary
Provider resolution and projections
internal/controller/gittarget_source_cluster.go, internal/controller/gittarget_source_cluster_test.go
A shared resolver returns the source ClusterProvider or an error, readiness projection consumes the resolved object directly, and tests cover attribution, unreadable providers, and readiness scenarios.
Reconcile provider reuse
internal/controller/gittarget_controller.go
Reconcile resolves the provider early, returns lookup errors immediately, and reuses its audit route and readiness data downstream.
Secret recreation timing validation
internal/controller/gittarget_controller_test.go
The encryption-secret recreation test documents periodic requeue behavior and waits beyond the stream-settle interval before asserting recreation.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately summarizes the main change: resolving audit route attribution from a single ClusterProvider read.
Description check ✅ Passed It clearly explains the bug, fix, unchanged behavior, and validation, though it doesn't use the template's checkbox-style sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-route-no-fallback-guess

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/controller/gittarget_source_cluster.go`:
- Around line 74-77: Update the Godoc comment immediately above
GitTargetConditionClusterProviderReady so its first words begin with the exact
exported identifier, while preserving the existing readiness semantics and
explanatory details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 190b58c9-93cf-4802-94f9-ebc7b4974a64

📥 Commits

Reviewing files that changed from the base of the PR and between 20db735 and 4e5cb3c.

📒 Files selected for processing (3)
  • internal/controller/gittarget_controller.go
  • internal/controller/gittarget_source_cluster.go
  • internal/controller/gittarget_source_cluster_test.go

Comment on lines +74 to 77
// EXPLICIT Ready=False downgrades the GitTarget; a provider that has not reported readiness yet
// is Unknown and does not. A MISSING provider never reaches this projection at all — that is the
// Validated gate's hard denial (GitTargetReasonClusterProviderNotFound), not a readiness axis.
GitTargetConditionClusterProviderReady = "ClusterProviderReady"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the exported condition’s Godoc identifier-led.

Line 74 documents GitTargetConditionClusterProviderReady, but its comment does not begin with the identifier.

Proposed fix
-	// EXPLICIT Ready=False downgrades the GitTarget; a provider that has not reported readiness yet
-	// is Unknown and does not. A MISSING provider never reaches this projection at all — that is the
-	// Validated gate's hard denial (GitTargetReasonClusterProviderNotFound), not a readiness axis.
+	// GitTargetConditionClusterProviderReady reports the referenced provider's readiness.
+	// Explicit Ready=False downgrades the GitTarget; missing providers are denied by Validated.
 	GitTargetConditionClusterProviderReady = "ClusterProviderReady"

As per coding guidelines, “add godoc comments for all exported Go identifiers.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// EXPLICIT Ready=False downgrades the GitTarget; a provider that has not reported readiness yet
// is Unknown and does not. A MISSING provider never reaches this projection at all — that is the
// Validated gate's hard denial (GitTargetReasonClusterProviderNotFound), not a readiness axis.
GitTargetConditionClusterProviderReady = "ClusterProviderReady"
// GitTargetConditionClusterProviderReady reports the referenced provider's readiness.
// Explicit Ready=False downgrades the GitTarget; missing providers are denied by Validated.
GitTargetConditionClusterProviderReady = "ClusterProviderReady"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/gittarget_source_cluster.go` around lines 74 - 77, Update
the Godoc comment immediately above GitTargetConditionClusterProviderReady so
its first words begin with the exact exported identifier, while preserving the
existing readiness semantics and explanatory details.

Source: Coding guidelines

…reated

The recreate-on-deletion spec waited the shared 10s `timeout` for a recovery
that only the periodic requeue can deliver: this controller runs no
control-plane Secret watch, so nothing enqueues the GitTarget when its age-key
Secret is deleted, and the next reconcile is one RequeueStreamSettleInterval
away — also 10s. The wait therefore equalled the interval it was waiting for,
and a deletion landing just after a reconcile lost by milliseconds. It failed
that way on this branch's CI run and passed on a plain re-run.

Wait RequeueStreamSettleInterval + timeout instead, so one full requeue plus
reconcile time fits inside the window, and say in the comment what actually
drives the recovery — the old one credited a "secret watch" that does not exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sunib
sunib merged commit 686fb00 into main Jul 23, 2026
18 checks passed
@sunib
sunib deleted the fix/audit-route-no-fallback-guess branch July 23, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant