Stop VirtualMCPServer hot-reconcile on cleared podTemplateSpec - #5846
Conversation
A stale podTemplateSpecHashAnnotation left over from a prior reconcile (when spec.podTemplateSpec was set) survived MergeAnnotations forever once the field was cleared, since the desired annotations map no longer carried a key to overwrite it with. That made podTemplateSpecNeedsUpdate report drift on every reconcile, hot-looping the Deployment update — same bug class as stacklok#5817, fixed here for the podTemplateSpec annotation: - Made podTemplateSpecNeedsUpdate a plain stored != expected hash comparison instead of the previous asymmetric presence check. - Extended mergeDeploymentAnnotations to prune podTemplateSpecHashAnnotation alongside imagePullRefsHashAnnotation when desired no longer wants it, collapsing the two prune checks into one loop. - Fixed the identical bug in the MCPRegistry registry-api deployment path (cmd/thv-operator/pkg/registryapi/deployment.go), where the same stale annotation could survive an additive-only annotation merge. - Added regression tests for both controllers, each reconciling twice from a stale-annotation fixture and asserting the second pass is a true no-op. Fixes stacklok#5818 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5846 +/- ##
==========================================
+ Coverage 71.06% 71.11% +0.05%
==========================================
Files 686 686
Lines 69909 69912 +3
==========================================
+ Hits 49680 49719 +39
+ Misses 16599 16543 -56
- Partials 3630 3650 +20 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
left a comment
There was a problem hiding this comment.
LGTM — correct fix, and it fully stops the hot-reconcile loop. ✅
Two independent lenses (K8s reconcile-correctness + Go/duplication/reuse) traced both halves on the real code: the symmetric stored != expected hash compare plus the write-path annotation prune converge to a true no-op on the second reconcile, and the regression tests assert exactly that via resourceVersion equality — the right shape for this bug class (a presence-only assertion would have missed it). The registry-api path is genuinely equivalent, not half-fixed (its deploymentNeedsUpdate was already symmetric; this PR adds the missing write-path prune). No remaining asymmetric-compare / no-op-write path in either deployment reconcile. CI is green.
One non-blocking finding (inline on registryapi/deployment.go): the merge-and-prune logic now exists in three diverging idioms across virtualmcpserver_controller, registryapi/deployment, and mcpremoteproxy_controller. Worth noting that mcpremoteproxy_controller.go:1767 still uses the old asymmetric hadPrevious presence check — safe today only because its write path deletes the annotation (line 412), but it's the exact #5818 anti-pattern kept alive. Extracting a shared ctrlutil helper (MergeAnnotations already lives at controllerutil/resources.go:155) and aligning mcpremoteproxy on it would collapse all three onto one correct path. Non-blocking — fine to track as follow-up.
Strengths: minimal, targeted, both halves matched per path; externally-managed annotations are preserved while only operator-owned keys are pruned (explicitly tested); good table-driven test consolidation covering both imagePullSecrets and podTemplateSpec; comments cite #5817/#5818.
Approving; recommend tracking the shared-helper extraction (and the mcpremoteproxy alignment) as a follow-up.
🤖 AI-assisted panel review via Claude Code (kubernetes-expert · Go/reuse lenses). Line numbers against 1783d87.
|
Thanks for this, I believe there was a similar community contribution for this that got auto closed due to staleness! |
Summary
Clearing
spec.podTemplateSpecon aVirtualMCPServer(or on anMCPRegistrywith a registry-api deployment) left the operator writing a no-op Deployment update on every reconcile, incrementingmetadata.generationindefinitely. On a kind cluster this saturates etcd write bandwidth and can starve leader-election lease renewal, causing the operator to lose leadership and restart. Same bug class as #5817 (imagePullSecrets), now fixed forpodTemplateSpec:podTemplateSpecNeedsUpdatea plainstored != expectedhash comparison instead of the previous asymmetric presence check that always reported drift once a hash annotation existed.mergeDeploymentAnnotationsto prunepodTemplateSpecHashAnnotationalongsideimagePullRefsHashAnnotationwhen desired no longer wants it, collapsing the two prune checks into one loop.cmd/thv-operator/pkg/registryapi/deployment.go), where the same stale annotation could survive an additive-only annotation merge.TestVirtualMCPServerDeploymentNeedsUpdate(previously only exercised 3 of the 5 branches it delegates to), and added a direct unit test formergeDeploymentAnnotations.Fixes #5818
Type of change
Test plan
task test)task test-e2e)task lint-fix)API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
cmd/thv-operator/controllers/virtualmcpserver_controller.gopodTemplateSpecNeedsUpdate;mergeDeploymentAnnotationsprunes both hash annotationscmd/thv-operator/controllers/virtualmcpserver_controller_test.gocmd/thv-operator/pkg/registryapi/deployment.gocmd/thv-operator/pkg/registryapi/deployment_test.goDoes this introduce a user-facing change?
Yes — clearing
spec.podTemplateSpec(VirtualMCPServer or MCPRegistry) no longer causes a runaway reconcile loop that repeatedly updates the managed Deployment.Special notes for reviewers
Both fixes are required together: the symmetric comparison alone doesn't stop the loop, since the write path would still never remove the stale annotation via
MergeAnnotations. Only pruning it explicitly at the write site converges to a steady state — same pattern as theimagePullSecretsfix in #5821.🤖 Generated with Claude Code