Repository navigation
Description
Summary
The cilium-ipam-migration post-upgrade hook added by #4778 (backported to v1.6.5 in #4918) fails with /usr/bin/jq: Argument list too long on clusters whose CiliumNode list is larger than 128 KiB. It fails before any node is moved, so the migration never happens. The cilium HelmRelease then loops on UpgradeFailed, and every package that depends on networking stays DependencyNotReady: the platform upgrade stalls behind Cilium.
Where
packages/system/cilium/files/ipam-migration.sh, line 244 on main (unchanged since dc362fa):
jq -r --argjson cn "$ciliumnodes" '
([$cn.items[] | select(.spec.ingress.ipv4) | {key: .spec.ingress.ipv4, value: .metadata.name}] | from_entries) as $ingress
| .[] | select($ingress[.ip]) | "\(.name) \(.ip) \($ingress[.ip])"' <<<"$ipobjects"$ciliumnodes is the full kubectl get ciliumnodes -o json. Passed as a single argument, it is bounded by Linux MAX_ARG_STRLEN (128 KiB per argument), not by ARG_MAX.
Why the list gets that large
Cilium copies the Node labels onto each CiliumNode. On our nodes those labels weigh about 30 KB per node (Node Feature Discovery and GPU operator labels), so the list reaches 408 KB for 12 nodes. A 3-node lab cluster stays under the limit, which is why the hook passed there.
Observed on v1.6.0 → v1.6.5 (isp-full, 12 nodes)
2026-10-19T18:05:07Z subnet ovn-default already excludes the pool and the current Ingress IPs
/bin/bash: line 244: /usr/bin/jq: Argument list too long
- The Job fails (
backoffLimit: 0), Helm reportspost-upgrade hooks failed, and withRetryOnFailurethe hook reruns and fails every 30 s. - The agents already run with
ipam.mode: cluster-pool, but everyCiliumNodekeeps its old/24(allocator not configured for the requested CIDR). Networking keeps working on the old addresses, so nothing breaks, but nothing moves either. - About 130 HelmReleases sat in
DependencyNotReadyuntil theciliumrelease succeeded. - Setting
spec.upgrade.disableHooks: trueon the HelmRelease does not help on its own:cozystack-operatorrewritesspec.upgradewithin a second.
Workaround we used
- Run the script by hand, unchanged except for that call, which reads the list from a file:
It moved the 12 nodes one by one in about 3 minutes.
cnfile=$(mktemp); printf '%s' "$ciliumnodes" > "$cnfile" jq -r --slurpfile cnw "$cnfile" '($cnw[0]) as $cn | ...' <<<"$ipobjects"
- Scale
cozystack-operatorto 0, setdisableHooks: trueon theciliumHelmRelease, wait forUpgradeSucceeded, then scale the operator back up. The operator removesdisableHooksagain, and no new upgrade is triggered because chart and values are unchanged.
Suggested fix
Never pass the CiliumNode list as an argument. Use --slurpfile or --rawfile from a temp file (the hook already mounts a writable /tmp), or fold both inputs into a single jq call fed through stdin. A test with a CiliumNode list over 128 KiB would catch a regression. The other jq calls in the script take their input through herestrings, which have no size limit; only this --argjson is affected.
As long as this is not fixed, any later upgrade that reruns the hook on a large cluster will fail the same way.
Activity
zhangxiaopeng-zgci commentedon Oct 22, 2026
Independently hit this on a 30-node v1.6.3 → v1.6.5 (isp-full) cluster — same failure at the same
jq --argjsoncall, but with a different amplifier that may be worth documenting alongside NFD/GPU labels: Talos. Each of our Talos v1.13.9 nodes carries ~405 labels (extension modules, CPU timer / TSC, kubevirt node-labeller, etc., ~21.5 KB of labels per Node), and Cilium copies them all onto the CiliumNode. The fullkubectl get ciliumnodes -o json(managedFields stripped) came to 879,680 bytes for 30 nodes — workers ~22 KB each, so the limit is hit around node 6, long before any reasonable cluster size.Confirming the rest of the report: agents ran
ipam.mode: cluster-poolwith every CiliumNode still holding its old /24, networking stayed healthy on the old addresses the whole time, and ~84 HelmReleases sat in DependencyNotReady overnight while the hook job retried every ~30 s. Also confirmingspec.upgrade.disableHooks: trueis rewritten by cozystack-operator within a second — patching it directly is not viable.Full workaround that worked for us (cluster is now fully upgraded, 106/106 HelmReleases Ready):
mktemp+printf > $cnfile+jq --slurpfile). 30/30 nodes moved into the new /21 pool in ~7 minutes (~13 s per node), agents restarted one at a time, zero failures. Note: manually running it while the hook job keeps retrying is safe — the hook dies at thejqline before touching anything, so there is no concurrent-writer risk.kubectl -n cozy-system scale deploy cozystack-operator --replicas=0→ patchspec.upgrade.disableHooks: trueon the cilium HelmRelease → annotatereconcile.fluxcd.io/requestedAtto force a reconcile. With the operator paused, the patch sticks; the release upgrades without hooks and wentUpgradeSucceededin ~15 s. Scale the operator back to 1 afterwards. We leftdisableHooks: truein place — harmless as long as the chart/values don't change (no further upgrade is triggered), and may also be the safer state for clusters that already migrated manually.UpgradeFaileduntil the DS rollout caught up — self-healing via retries, just noisy on large clusters.One more data point for the fix: of all
jqinvocations in the script only this--argjsonis affected (the rest read from herestrings), so a--slurpfile/--rawfile(or stdin) variant plus a unit test feeding a >128 KiB list should close it completely. Happy to share our patched script if useful.hjkbnm911 commentedon Oct 25, 2026
Adding a data point from a small cluster, because it shows the threshold is crossed even
without label inflation.
Environment: Cozystack v1.6.5, Talos 1.13, Kubernetes 1.33, 6 nodes (3 control-plane +
3 workers), plain nodes — no NFD/GPU labels involved.
So ~28 KiB per CiliumNode is already enough on its own — the copied labels are an amplifier,
not the trigger.
How we hit it: we changed an unrelated Cilium value (
gatewayAPI.externalTrafficPolicy)through
Package/cozystack.networking. Any values change re-runs the upgrade, so the hookfires and fails.
State of the migration: already applied and correct on our side — every
CiliumNodehasa
/29from the pool10.244.248.0/21, kube-ovn already excludes the pool (thesubnet ovn-default already excludes the pool and the current Ingress IPsline prints beforethe failure). Only the advisory step — listing pods that still hold pre-migration Ingress IPs
— dies, which is why the release looks broken while nothing is actually left to do.
Impact with helm-controller:
upgradeFailureswent 3 → 6, one attempt every ~5 min, andeach attempt re-applies the chart, so with
rollOutCiliumPods: truethe Cilium DaemonSet rolledagain on a live cluster every 5 minutes. We parked it with:
After suspending: nodes Ready, Cilium agents 6/6, operator 1/1, no traffic impact (all
manifests were already applied). Not a fix, but a safe parking position — and clearly better
than the repeated CNI rollouts.
For whichever fix gets picked: the projection below is 217 bytes for the same 6 nodes and
works, so the advisory step can stay
--argjson-based:alexey8312 commentedon Oct 31, 2026
Another data point from a v1.6.5 cluster (9 nodes, CiliumNode list ~232 KB, labels ~28 KB per node).
Here the migration itself had already completed on the 1.6.5 upgrade (ipam
cluster-pool, every CiliumNodeholds a /29 from the pool,
ovn-defaultalready excludes the pool). The hook still runs on every laterhelm upgradeof cilium (it is rendered wheneveripam.mode == cluster-pool), so any change that touches thecilium values afterwards — in our case
publishing.apiServerEndpoint, which lands in_clusterof everypackage — loops the HelmRelease on
UpgradeFailedand leaves ~80 dependent HelmReleasesDependencyNotReady.It started failing once the cluster grew from 6 to 9 nodes.
Workaround that worked for us (safe only because the migration is done): scale
cozystack-operatorto 0(it resets the HelmRelease spec immediately), set
spec.upgrade.disableHooks: trueoncozy-cilium/cilium,reconcile, wait for
UpgradeSucceeded, remove the flag, scale the operator back.Two suggestions besides the
--argjsonfix (--slurpfile/ reading$ciliumnodesfrom stdin or a file):spec.ipam.podCIDRs(check that first,before the expensive checks), or
fix(kubernetes): move the OIDC users out of the bootstrap Job's script (
fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook (…
fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook (…
fix(kubernetes): move the OIDC users out of the bootstrap Job's script (
fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook (…
fix(kubernetes): move the OIDC users out of the bootstrap Job's script (