fix(flux-shard-operator): repair sharded helm-controller crashloop behind an HTTP proxy - #3546
Conversation
A sharded helm-controller inherits HTTP_PROXY/HTTPS_PROXY/NO_PROXY from the flux-aio helm-controller container. A standalone shard needs no external egress (source-controller fetches artifacts), so behind an unreachable corporate proxy it makes a blocking startup HTTPS call that never completes, the manager never serves /healthz, and the liveness probe crashloops the pod forever, freezing every HelmRelease carrying a sharding key. flux-aio itself survives only because it starts once and never restarts. Drop the proxy env (and the now-pointless NO_PROXY) when cloning, alongside the existing localhost/KubePrism sanitisation, so the shard talks to the in-cluster apiserver directly. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
The cloned shard Deployment inherits only a liveness probe (~30s window, no startupProbe), so a controller still syncing caches on startup is killed before it serves /healthz and never recovers. Derive a startupProbe from the liveness handler with a generous budget so a slow start is not fatal, while liveness still catches a wedged running pod. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesShard deployment hardening
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant BuildShardDeployment
participant HelmControllerContainer
participant KubernetesProbe
BuildShardDeployment->>HelmControllerContainer: clone container and remove proxy environment variables
BuildShardDeployment->>KubernetesProbe: copy liveness probe when startup probe is absent
KubernetesProbe-->>HelmControllerContainer: apply inherited handler with normalized startup budget
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Review follow-ups: - Do not force the cloned startupProbe TimeoutSeconds to 1: keep the value inherited from the liveness handler, otherwise a future flux-aio with a larger liveness timeout would get a stricter startup probe and could recreate the crashloop. Covered by a regression guard (liveness timeout 5s must be inherited). - Soften the proxy-strip rationale to the A/B-confirmed mechanism (blocking external HTTPS call at startup) and note the remote spec.kubeConfig exception that does not apply to cozystack in-cluster guest apiservers. - Correct the test fixture comment: the proxy env is operator/patch-level, not installer-injected (the installer injects only KUBERNETES_SERVICE_*). Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/fluxshardoperator/provisioner_test.go (1)
238-240: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the complete startup timing contract.
FailureThreshold >= 10does not ensure the required startup budget. IfPeriodSecondschanges to1, this test still passes while restoring a short crash-loop window. Assert the normalized delay, period, success threshold, and failure threshold.Proposed test update
- if hc.StartupProbe.FailureThreshold < 10 { - t.Fatalf("startupProbe budget too small to cover a slow cache sync: %d", hc.StartupProbe.FailureThreshold) + if hc.StartupProbe.InitialDelaySeconds != 0 || + hc.StartupProbe.PeriodSeconds != 10 || + hc.StartupProbe.SuccessThreshold != 1 || + hc.StartupProbe.FailureThreshold != 30 { + t.Fatalf("unexpected startupProbe timing: %+v", hc.StartupProbe) }🤖 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/fluxshardoperator/provisioner_test.go` around lines 238 - 240, Update the startup probe assertion in the relevant provisioner test to validate the complete normalized timing contract: delay, period, success threshold, and failure threshold. Replace the isolated StartupProbe.FailureThreshold check with assertions against the expected values for all four probe fields, preserving the existing failure message style where appropriate.
🤖 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.
Nitpick comments:
In `@internal/fluxshardoperator/provisioner_test.go`:
- Around line 238-240: Update the startup probe assertion in the relevant
provisioner test to validate the complete normalized timing contract: delay,
period, success threshold, and failure threshold. Replace the isolated
StartupProbe.FailureThreshold check with assertions against the expected values
for all four probe fields, preserving the existing failure message style where
appropriate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1dbcf43b-3634-4d86-a99e-c55774d705c3
📒 Files selected for processing (2)
internal/fluxshardoperator/provisioner.gointernal/fluxshardoperator/provisioner_test.go
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Approving — the design is right and it lands in the right place, as one more case in the existing sanitisation switch rather than a second mechanism alongside it.
I checked the load-bearing assumption that a standalone shard needs no external egress, and it holds for how this platform wires Flux. Chart delivery is chartRef: {kind: ExternalArtifact} throughout, and the artifact URLs advertise as flux.$(RUNTIME_NAMESPACE).svc (--storage-adv-addr); guest-cluster targets are in-cluster apiservers reached through the super-admin.svc kubeconfig key. Both were already NO_PROXY-bypassed, so dropping the proxy removes nothing that was working. Source-controller keeps its proxy env and keeps doing the fetching. The probe derivation is a genuine DeepCopy, so the 300s startup budget gates liveness without weakening it — a wedged running pod is still killed on the inherited 30s window.
One property worth stating in the PR description, because it decides whether anyone has to do anything by hand: already-crashlooping shards repair themselves on upgrade. containers[].env is listType=map keyed by name, the operator is the sole field manager applying with force ownership, and it previously owned the proxy keys — so dropping them from the apply set deletes them from the live object, and Recreate with one replica rolls the pod. No manual deletion needed.
Follow-ups, none of them blocking:
- The sanitisation list in the
BuildShardDeploymentdoc comment enumerates every step (hostNetwork, single container,--events-addr, selector,--concurrent,SOURCE_*_LOCALHOST,KUBERNETES_SERVICE_*, tolerations, podAntiAffinity) and the README carries a shorter version of the same claim. Both should gain the two new entries — that list is the only record anywhere of how far the clone has diverged from flux-aio, so a stale one invites the next contributor to re-add what was deliberately removed. - The root-cause wording is worth confirming before it settles as the reference explanation. Everything the shard dials advertises
.svc; the one endpointNO_PROXY=.svcnever matched is the management apiserver, because this same function dropsKUBERNETES_SERVICE_HOSTand the shard falls back to the kubelet-injected ClusterIP, while flux-aio's hostNetwork KubePrism value is loopback and Go bypasses loopback regardless ofNO_PROXY. If that is the mechanism, the fix here is unchanged, but "flux-aio is a latent risk if it ever restarts" would not be, and the residual exposure would sit inNO_PROXYat install time instead. An A/B withNO_PROXYset to the literalKUBERNETES_SERVICE_HOSTvalue the pod actually has would settle it; the reported run with cluster CIDRs points the other way, which is exactly why it is worth pinning down. - Two cheap test gaps: the assertion that the startup probe inherits
TimeoutSeconds: 5pins the fixture's value, but the real manifest probe is a barehttpGetwith no timing fields at all, so a hardcodedsp.TimeoutSeconds = 5would still pass — asserting against the source probe's own value would hold. And nothing currently fails if theDeepCopyregresses to an alias, which would putfailureThreshold: 30on liveness and give exactly the never-failing probe this change exists to avoid. - Separate issue, and platform-level rather than this operator's: the freeze was invisible because a frozen HelmRelease is not an unready one. With nothing reconciling it, nothing writes status, so
Readykeeps its last value indefinitely andHelmReleaseNotReadycannot see it. The exported metric set has noobservedGeneration, nolastHandledReconcileAt, no timestamp of any kind, so nothing in the stack distinguishes a HelmRelease reconciled thirty seconds ago from one reconciled eight days ago — even though every one of them declares its owninterval. That gap will hide the next reconciler stall just as completely, whatever causes it.
Broader note, explicitly not for this PR. Shards are currently derived by deep-copying the live flux-aio Deployment and subtracting what is flux-aio-specific, which buys free version-sync — and that matters more than it first appears, since flux-aio's own helm-controller is a peer bucket (--watch-label-selector=!sharding.fluxcd.io/key) and skew between it and the shards would mean two helm-controller versions reconciling one CRD version in one cluster. The cost is that the shard spec is defined by negation against a hostNetwork pod, so any field whose correctness depended on host networking is inherited until someone finds it in production: SOURCE_*_LOCALHOST, then KUBERNETES_SERVICE_*, now the proxy env. I would like the sharding controller to be able to stand on its own rather than being inseparable from the flux-aio topology. The version-sync requirement is real and should be kept, but it could be met by rendering the unsharded controller and the shards from one definition, instead of one being a mutated copy of the other's runtime object. Happy to open that as its own issue if there is appetite for it.
…sanitisation list The BuildShardDeployment doc comment and the operator README are the only record of how far the cloned shard container has diverged from flux-aio. Add the two steps this fix introduced (dropping the inherited corporate-proxy env and deriving a startupProbe) so the list stays the authoritative account and does not invite re-adding what was deliberately removed. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
…ct alias regression Model the source liveness probe as flux-aio actually ships it (a bare httpGet with no timing fields) and tighten the startupProbe assertions: - assert the complete normalised budget (delay/period/success/failure) instead of only FailureThreshold, so a later PeriodSeconds regression cannot silently restore a short crashloop window; - assert TimeoutSeconds is inherited from the source probe rather than a hardcoded value, so forcing it stricter is caught; - assert the startupProbe is a DeepCopy of liveness, not an alias, which would otherwise stamp the 30-failure startup budget onto liveness and produce a never-failing liveness probe. The alias assertion is non-vacuous: it fails when the DeepCopy is regressed to a plain alias. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Two wording suggestions on the comments, nothing on the code — the implementation and the new tests both look right to me.
I checked the three new guards by introducing the regressions they describe: aliasing the liveness probe instead of deep-copying it, hardcoding TimeoutSeconds, and eroding PeriodSeconds to 1. All three fail the package, with the intended message each time. Rebuilding the fixture as a bare httpGet so it matches what flux-aio actually ships is a better fix than asserting around the old one.
Both suggestions below are about explanatory comments hardening around a mechanism that has not been confirmed yet. Neither affects behaviour, and neither is a reason to hold the change.
Name all six proxy env spellings in the sanitisation list, and replace the unestablished 'external HTTPS call' / 'flux-aio does not restart' claims with the grounded in-cluster mechanism: after the KUBERNETES_SERVICE_HOST drop above, the shard talks to the management apiserver over the kubelet-injected ClusterIP, which NO_PROXY=.svc does not match, so that startup call is what stalls behind an unreachable proxy. Comment-only; no behaviour change. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
|
Successfully created backport PR for |
|
Successfully created backport PR for |
What this PR does
Fixes #3544.
flux-shard-operatorclones the flux-aiohelm-controllercontainer into eachhelm-controller-shard<i>Deployment. It already sanitises the all-in-one localhost wiring, but keeps two things that stop the standalone shard from starting behind a corporate HTTP proxy:Inherited
HTTP_PROXY/HTTPS_PROXY/NO_PROXY. A standalone shard needs no external egress (source-controller fetches artifacts). Behind an unreachable proxy the controller makes a blocking startup HTTPS call that never completes, the manager never serves/healthz, and the liveness probe crashloops the pod forever. flux-aio survives only because it starts once and never restarts. On an affected cluster every HelmRelease carrying asharding.fluxcd.io/key(150 of ~240) was frozen for days, silently.No
startupProbe. The clone inherits only a ~30s liveness window, so any slow start (large cache sync) is fatal and unrecoverable.This PR extends the sanitisation to drop the proxy env, and derives a
startupProbefrom the liveness handler with a generous budget so a slow-but-progressing start is not killed. Verified by an A/B on a live cluster (throwaway helm-controller with a non-matching selector): with the proxy env it hangs; without it, it starts cleanly (Starting workers).Downstream repositories
Internal operator behavior only; no API/CRD/values contract change.
Release note
Summary by CodeRabbit
Bug Fixes
NO_PROXY, from affecting Flux shard deployments.Documentation