Skip to content

fix(flux-shard-operator): repair sharded helm-controller crashloop behind an HTTP proxy - #3546

Merged
Aleksei Sviridkin (lexfrei) merged 6 commits into
mainfrom
fix/flux-shard-operator-strip-proxy
Aug 14, 2026
Merged

fix(flux-shard-operator): repair sharded helm-controller crashloop behind an HTTP proxy#3546
Aleksei Sviridkin (lexfrei) merged 6 commits into
mainfrom
fix/flux-shard-operator-strip-proxy

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Fixes #3544.

flux-shard-operator clones the flux-aio helm-controller container into each helm-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:

  1. 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 a sharding.fluxcd.io/key (150 of ~240) was frozen for days, silently.

  2. 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 startupProbe from 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

  • No downstream repository is affected by this change

Internal operator behavior only; no API/CRD/values contract change.

Release note

fix(flux-shard-operator): sharded helm-controller no longer crashloops at startup behind an HTTP proxy; the cloned controller drops the inherited proxy env and gains a startupProbe, so HelmReleases assigned to shards keep reconciling in proxied environments.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented inherited proxy settings, including uppercase and lowercase variants of NO_PROXY, from affecting Flux shard deployments.
    • Automatically adds a startup health check when only a liveness check is configured.
    • Preserved the liveness check’s handler and timeout while applying appropriate startup timing and failure settings.
  • Documentation

    • Documented startup health checks for slow helm-controller startup.

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>
@IvanHunters IvanHunters added the backport Should change be backported on previous release label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 62715900-0d96-437a-8c81-f0224b5d1f3c

📥 Commits

Reviewing files that changed from the base of the PR and between a10fcb3 and 66df3b2.

📒 Files selected for processing (1)
  • internal/fluxshardoperator/provisioner.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/fluxshardoperator/provisioner.go

📝 Walkthrough

Walkthrough

BuildShardDeployment removes inherited proxy variables from standalone shard containers. When no startup probe exists, it derives one from the liveness probe with normalized timing and failure settings. Tests cover proxy removal and the generated /healthz startup probe.

Changes

Shard deployment hardening

Layer / File(s) Summary
Proxy environment sanitization
internal/fluxshardoperator/provisioner.go, internal/fluxshardoperator/provisioner_test.go
Standalone shard containers no longer inherit uppercase or lowercase HTTP, HTTPS, or NO_PROXY variables. Tests cover the removal.
Startup probe derivation
internal/fluxshardoperator/provisioner.go, internal/fluxshardoperator/provisioner_test.go, packages/system/flux-shard-operator/README.md
When no startup probe exists, the builder deep-copies the liveness probe, normalizes startup timing and thresholds, and preserves the handler and timeout. Tests verify the /healthz handler, failure budget, timeout, and object separation. The README documents the startup probe.

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
Loading

Possibly related PRs

Suggested reviewers: kvaps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the proxy-related crashloop fix for the flux-shard-operator.
Linked Issues check ✅ Passed The changes remove inherited proxy variables and add a startup probe, addressing both defects in issue #3544.
Out of Scope Changes check ✅ Passed The code, tests, and README update directly support the proxy and startup-probe fixes in issue #3544.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/flux-shard-operator-strip-proxy

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.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Aug 4, 2026
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>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/fluxshardoperator/provisioner_test.go (1)

238-240: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the complete startup timing contract.

FailureThreshold >= 10 does not ensure the required startup budget. If PeriodSeconds changes to 1, 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

📥 Commits

Reviewing files that changed from the base of the PR and between ee26a50 and affdd70.

📒 Files selected for processing (2)
  • internal/fluxshardoperator/provisioner.go
  • internal/fluxshardoperator/provisioner_test.go

@lllamnyp Timofei Larkin (lllamnyp) 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.

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 BuildShardDeployment doc 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 endpoint NO_PROXY=.svc never matched is the management apiserver, because this same function drops KUBERNETES_SERVICE_HOST and the shard falls back to the kubelet-injected ClusterIP, while flux-aio's hostNetwork KubePrism value is loopback and Go bypasses loopback regardless of NO_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 in NO_PROXY at install time instead. An A/B with NO_PROXY set to the literal KUBERNETES_SERVICE_HOST value 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: 5 pins the fixture's value, but the real manifest probe is a bare httpGet with no timing fields at all, so a hardcoded sp.TimeoutSeconds = 5 would still pass — asserting against the source probe's own value would hold. And nothing currently fails if the DeepCopy regresses to an alias, which would put failureThreshold: 30 on 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 Ready keeps its last value indefinitely and HelmReleaseNotReady cannot see it. The exported metric set has no observedGeneration, no lastHandledReconcileAt, 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 own interval. 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>
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size/M This PR changes 30-99 lines, ignoring generated files labels Aug 7, 2026

@lllamnyp Timofei Larkin (lllamnyp) 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.

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.

Comment thread internal/fluxshardoperator/provisioner.go Outdated
Comment thread internal/fluxshardoperator/provisioner.go Outdated
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>
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 22ce998 into main Aug 14, 2026
43 of 46 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/flux-shard-operator-strip-proxy branch August 14, 2026 10:28
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

myasnikovdaniil added a commit that referenced this pull request Aug 18, 2026
…controller crashloop behind an HTTP proxy (#3818)

# Description
Backport of #3546 to `release-1.6`.
myasnikovdaniil added a commit that referenced this pull request Aug 19, 2026
…controller crashloop behind an HTTP proxy (#3883)

# Description
Backport of #3546 to `release-1.5`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review backport Should change be backported on previous release backport-previous Backport target — previous release line kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(flux-shard-operator): sharded helm-controller crashloops behind an HTTP proxy, freezing all sharded HelmReleases

4 participants