Skip to content

Migrate sibling config controllers to MutateAndPatchStatus/Spec#5509

Open
ChrisJBurns wants to merge 6 commits into
mainfrom
cburns/migrate-sibling-controllers-mutatepatchstatus
Open

Migrate sibling config controllers to MutateAndPatchStatus/Spec#5509
ChrisJBurns wants to merge 6 commits into
mainfrom
cburns/migrate-sibling-controllers-mutatepatchstatus

Conversation

@ChrisJBurns

@ChrisJBurns ChrisJBurns commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

MCPAuthzConfigReconciler was migrated in #4777 to write status through ctrlutil.MutateAndPatchStatus and finalizers through ctrlutil.MutateAndPatchSpec, because a raw r.Status().Update sends a full PUT that clobbers Status.Conditions entries owned by any disjoint writer (JSON merge-patch is the only form that honors the +listType=map marker), and a raw r.Update on finalizers has no optimistic-lock guard. The two sibling controllers still used the older anti-pattern. This brings them to parity per .claude/rules/operator.md §"Status Writes" and §"Spec / metadata patching".

Closes #5497

Medium level
  • MCPOIDCConfig controller: every r.Status().UpdateMutateAndPatchStatus and every finalizer r.UpdateMutateAndPatchSpec. Condition/field mutations move inside the helper closures (the helper snapshots the object on entry, so pre-mutate changes are dropped from the diff). Added a setOIDCConfigValidTrueCondition helper to factor out the Valid=True transition.
  • MCPExternalAuthConfig controller: same migration. The IdentitySynthesized advisory was previously computed once in memory before validation; since that pre-mutate change would be dropped by MutateAndPatchStatus, it is now recomputed inside each status-write closure via a new setValidTrueAndSynthesized helper (and on the validation-failure and setInvalid paths). applyIdentitySynthesizedCondition is idempotent on the same spec, so the advisory transition is preserved on every path. setInvalid already used the helper correctly; its doc comment was updated to drop a stale reference to the removed upfront mutation.
  • Added a PreservesForeignConditions regression test for each sibling, mirroring the MCPAuthzConfig guard: a foreign-owned condition must survive a reconcile alongside the controller-owned Valid condition.
Low level
File Change
cmd/thv-operator/controllers/mcpoidcconfig_controller.go Finalizer add/remove via MutateAndPatchSpec; all status writes via MutateAndPatchStatus; new setOIDCConfigValidTrueCondition helper
cmd/thv-operator/controllers/mcpexternalauthconfig_controller.go Same migration; remove upfront in-memory advisory call; new setValidTrueAndSynthesized helper; fold advisory into every status closure; update stale setInvalid doc comment
cmd/thv-operator/controllers/mcpoidcconfig_controller_test.go Add TestMCPOIDCConfigReconciler_PreservesForeignConditions
cmd/thv-operator/controllers/mcpexternalauthconfig_controller_test.go Add TestMCPExternalAuthConfigReconciler_PreservesForeignConditions

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Documentation
  • Other

Test plan

  • task build passes
  • task test passes for cmd/thv-operator/... unit packages (envtest integration suites excluded — they require the local etcd envtest binary)
  • New PreservesForeignConditions regression test added and passing for each sibling controller
  • Existing controller behavior preserved — the IdentitySynthesized transition guards (...TransitionsOnValidationFailure, ...OBO_ClearsStaleIdentitySynthesized) still pass
  • Verified grep ACs: no r.Status().Update / client.Status().Update and no spec/finalizer r.Update(ctx remain in the three config controllers
  • Local task lint blocked by a known golangci-lint go1.24-vs-go1.26 toolchain mismatch; CI runs lint

Special notes for reviewers

The externalauth migration carries the only non-mechanical subtlety: the IdentitySynthesized advisory must be recomputed from the current spec inside each status patch (not before it), or MutateAndPatchStatus would snapshot the pre-mutate object and silently drop the transition. The existing IdentitySynthesizedTransitionsOnValidationFailure test pins this contract and continues to pass.

Residual gap (out of scope, tracked in #5511): three workload controllers (MCPServer, VirtualMCPServer, MCPRemoteProxy) still do a full r.Status().Update against these config objects to append ReferencingWorkloads. A full PUT carries the whole Status.Conditions array, so until those writers are migrated too, the config controllers are not yet the sole owners of their own status — the protection this PR adds is necessary but not by itself sufficient.

Generated with Claude Code

ChrisJBurns and others added 2 commits June 12, 2026 18:09
Status writes now flow through controllerutil.MutateAndPatchStatus and
finalizer writes through controllerutil.MutateAndPatchSpec, matching the
MCPAuthzConfig controller migration in #4777. The previous r.Status().Update
calls sent full PUT bodies that would clobber conditions written by any
disjoint owner of Status.Conditions on this CRD; the previous r.Update calls
had no optimistic-lock guard around the finalizer array.

Condition and field mutations move inside the helper closures so the
pre-mutate snapshot reflects the live state rather than already containing the
change — a MutateAndPatchStatus prerequisite. A small helper,
setOIDCConfigValidTrueCondition, factors out the Valid=True transition.

Add a PreservesForeignConditions regression test mirroring the MCPAuthzConfig
guard, asserting a foreign-owned condition survives a reconcile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Status writes now flow through controllerutil.MutateAndPatchStatus and
finalizer writes through controllerutil.MutateAndPatchSpec, matching the
MCPAuthzConfig and MCPOIDCConfig controllers. The previous r.Status().Update
calls sent full PUT bodies that would clobber conditions owned by any disjoint
writer of Status.Conditions on this CRD; the previous r.Update calls had no
optimistic-lock guard around the finalizer array.

The IdentitySynthesized advisory was previously computed once in memory before
validation. Because MutateAndPatchStatus snapshots the object on entry and any
pre-mutate change is dropped from the merge patch, that upfront mutation is
removed; the advisory is now recomputed inside each status-write closure via a
new setValidTrueAndSynthesized helper (and directly on the validation-failure
and setInvalid paths). applyIdentitySynthesizedCondition is idempotent on the
same spec, so this preserves the advisory transition on every path. The
setInvalid doc comment is updated to drop the stale upfront-mutation reference.

Add a PreservesForeignConditions regression test mirroring the sibling guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label Jun 12, 2026
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.48276% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.71%. Comparing base (b672d82) to head (03199e0).

Files with missing lines Patch % Lines
...v-operator/controllers/mcpoidcconfig_controller.go 80.76% 4 Missing and 6 partials ⚠️
...or/controllers/mcpexternalauthconfig_controller.go 87.50% 3 Missing and 5 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5509   +/-   ##
=======================================
  Coverage   69.71%   69.71%           
=======================================
  Files         645      645           
  Lines       65598    65616   +18     
=======================================
+ Hits        45729    45745   +16     
- Misses      16523    16526    +3     
+ Partials     3346     3345    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The advisory helper's bool return was consumed only by the upfront in-memory
call removed in the MutateAndPatchStatus migration; every remaining caller
ignores it inside a status-write closure. Drop the return value to satisfy the
unparam linter and refresh the doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 12, 2026
Address review feedback on the status-write migration:

- Gate the OIDC steady-state reference refresh on a captured findErr == nil
  rather than the referencingWorkloads != nil sentinel, decoupling the guard
  from findReferencingWorkloads' nil-on-error / non-nil-empty-on-success
  contract.
- Assert the migrated DeletionBlocked condition (and reference bookkeeping) is
  persisted in the OIDC blocking-deletion test, which previously only checked
  the requeue and finalizer retention.
- Use meta.FindStatusCondition in the externalauth PreservesForeignConditions
  test for parity with the OIDC and MCPAuthzConfig sibling guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 12, 2026
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 12, 2026
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate MCPOIDCConfig and MCPExternalAuthConfig controllers to MutateAndPatchStatus/Spec

1 participant