Fix fresh-cluster local k3s bring-up: Cilium datapath + namespace ordering - #2705
Conversation
…ering Two independent bugs both block bringing up a local k3s cluster from scratch (make k3s-teardown && make k3s-setup && make deploy): install-cilium.sh: the bare `cilium install` from #2704 enables kube-proxy replacement, BPF masquerade, and BPF host routing, all of which attach eBPF programs to the host's primary NIC. On hosts where that NIC is a wireless interface, this blackholes host connectivity entirely. Pass conservative datapath flags (kubeProxyReplacement=false, bpf.masquerade=false, bpf.hostLegacyRouting=true) so Cilium's eBPF stays on cilium_* interfaces and pod veths; full L3/L4 NetworkPolicy enforcement is unaffected. Makefile: k3s-secrets created the gateway-secrets secret in the egg-system namespace, but that namespace is created by deploy's manifest apply, which runs after k3s-secrets (deploy: k3s-secrets). On an existing cluster the namespace is already present; on a fresh cluster k3s-secrets fails with `namespaces "egg-system" not found`. Apply k8s/base/namespaces.yaml in k3s-secrets before creating the secret.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review
Both fixes are correct and address real bugs. The diff is small and well-scoped. Approving with non-blocking suggestions.
Makefile — namespace ordering fix
Makefile:503 correctly resolves the chicken-and-egg between k3s-secrets (needs egg-system to exist) and deploy (creates egg-system via kustomize). kubectl apply is idempotent, so the subsequent kubectl apply of the same Namespace resources via the kustomize bundle in the deploy target is a no-op — verified k8s/overlays/local/kustomization.yaml has no commonLabels / namePrefix / nameSuffix, so the names match. The CI workflow has its own pre-create of the egg-system namespace at test-integration.yml:149 that becomes redundant after this change; not blocking, but worth deleting in a follow-up since the workaround comment at test-integration.yml:144-147 now describes a fixed problem.
scripts/install-cilium.sh — conservative datapath flags
Flag names are correct for Cilium v1.19 (kubeProxyReplacement, bpf.masquerade, bpf.hostLegacyRouting). The bash array expansion ("${CILIUM_INSTALL_ARGS[@]}") is the right idiom and is safe with set -euo pipefail.
Non-blocking concerns
1. The cilium-cli appears to override --set kubeProxyReplacement=false — verify explicitly.
The most recent CI run on this PR (https://github.com/jwbron/egg/actions/runs/25883254559) shows:
[2026-05-14 20:19:59] Running 'cilium install --version v1.19.4 --set kubeProxyReplacement=false --set bpf.masquerade=false --set bpf.hostLegacyRouting=true'...
ℹ️ Using Cilium version 1.19.4
🔮 Auto-detected kube-proxy has not been installed
ℹ️ Cilium will fully replace all functionalities of kube-proxy
The 🔮 Auto-detected kube-proxy has not been installed message comes from cilium-cli's environment detection. k3s embeds kube-proxy in k3s-agent rather than running a standalone DaemonSet, so cilium-cli sees no kube-proxy and announces it will replace it — despite the explicit --set kubeProxyReplacement=false.
In principle --set overrides the auto-detected helm value during chart rendering, and your Asahi verification (cilium-config shows devices empty) is consistent with kube-proxy replacement actually being off (with KPR=true, Cilium has to attach to a device for L4 LB, so devices="" and KPR=true don't coexist). But "info message lies, actual config is correct" is a bad property to rely on without an explicit assertion in the script, because:
- The cilium-cli also sets non-helm values during auto-detection (e.g.
socketLB,nodePort) that may not be governed by the helm flag. - A future cilium-cli release could change the precedence.
Recommendation: extend the post-install verification block (lines 131–159) to read back cilium-config and assert the values you intended. Something like:
log "Verifying cilium-config matches expected conservative datapath..."
CFG=$(kubectl -n kube-system get cm cilium-config -o json)
for kv in \
'kube-proxy-replacement:false' \
'enable-bpf-masquerade:false' \
'enable-host-legacy-routing:true'; do
key="${kv%%:*}"; want="${kv##*:}"
got=$(echo "$CFG" | jq -r ".data[\"$key\"] // empty")
if [ "$got" != "$want" ]; then
error "cilium-config[$key] = '$got', expected '$want'"
error "The cilium-cli auto-detection may have silently overridden the --set flag."
exit 1
fi
done
log "cilium-config matches expected datapath."(Key names per Cilium docs: helm kubeProxyReplacement → configmap kube-proxy-replacement; helm bpf.masquerade → enable-bpf-masquerade; helm bpf.hostLegacyRouting → enable-host-legacy-routing. Worth double-checking against your live cluster before committing.) This would have caught the silent-override case on the very next run, without anyone having to remember to inspect cilium-config manually.
2. CI integration tests are red — pre-existing, but worth a follow-up issue.
The integration job exceeded its 30-min budget and was cancelled; the symptom was subprocess.TimeoutExpired: kubectl delete namespace egg-test-agents-27536 --ignore-not-found=true (60 s) during test_stack_lifecycle.py::TestSessionLifecycle::test_duplicate_container_id_handling teardown. The same failure mode is present on the merged #2704 (runs/25832935369), so this is a Cilium-migration regression that landed with #2704 and is not introduced by this PR. Not blocking here, but it's notable that the Cilium migration's CI tier has been red since it merged, and "namespace stuck terminating" is symptomatically close to #2580 (the very problem #2704 was meant to solve). File an issue.
3. PR description's "verify on Asahi" wording vs. what was actually verified.
The description says verified cilium-config shows devices empty. That's the right necessary check (it's the property that prevents the wireless-NIC blackhole), but it's not sufficient to claim all three flags took effect — bpf.masquerade=false and bpf.hostLegacyRouting=true should also be confirmed in the deployed config. Suggest checking those keys explicitly too.
Why this is still an ACK
The two changes are right. The Asahi blackhole fix matches the failure mode (eBPF on physical NIC), the chosen flags are the documented way to keep eBPF on virtual interfaces only, and the empty devices verification on the affected hardware is the load-bearing evidence. The namespace-ordering fix is mechanical and obviously correct. The CI red is pre-existing. Suggestions above are defense-in-depth and a follow-up issue, not blockers.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Post-install check reads the cilium-config ConfigMap and asserts that kube-proxy-replacement, enable-bpf-masquerade, and enable-host-legacy-routing match the values we passed via --set. The cilium-cli's auto-detection prints 'Cilium will fully replace all functionalities of kube-proxy' even when --set kubeProxyReplacement=false is passed (k3s embeds kube-proxy in k3s-agent, so cilium-cli sees no kube-proxy DaemonSet and announces it will replace it). The --set flag overrides during chart rendering, but the info-message-vs-real-config mismatch is a property we should not rely on silently. Fails fast if a future cilium-cli release ever changes override precedence.
|
Thanks for the thorough review. Per-item disposition below. Suggestion 1 — assert
|
This comment has been minimized.
This comment has been minimized.
Under the Cilium CNI (#2704), the k3s-bundled metrics-server pod cannot reach the kubelet on the node IP, so its readiness probe never passes and it stays out of its Service's endpoints. The v1beta1.metrics.k8s.io APIService is therefore permanently unavailable. The namespace controller runs API discovery across all groups before finalizing any namespace; a down APIService makes discovery fail (NamespaceDeletionDiscoveryFailure), so *every* namespace deletion hangs in Terminating forever. In CI this wedged integration-test fixture teardown (kubectl delete namespace timed out) and the job-cleanup step, blowing past the 30-minute job limit. egg does not use metrics-server (no HPA, no kubectl top, no metrics.k8s.io consumers), so disable it in both the Makefile k3s-setup target and the test-integration workflow's k3s install. This removes the unused addon and the cluster-wide failure mode it introduced.
orchestrator/Dockerfile hardcoded the linux_amd64 kustomize tarball. On an arm64 host `make build` produces an arm64 orchestrator image (the python:3.14-slim base is multi-arch) with an amd64 kustomize binary inside it; it runs under emulation and the amd64 Go runtime crashes. CI never caught it — CI runners are amd64. Resolve the arch at build time via `dpkg --print-architecture` (matching the gateway and sandbox Dockerfiles) and pin per-arch SHA256s. test_deployment_validation_logic.py's secret-leak check sliced the bearer at hardcoded offsets 0/16/32/48, assuming a 64-hex-char secret. A shorter lifecycle-secret yields empty windows, and `"" not in text` is always False — reporting a phantom leak for a response that contains no secret at all. Walk windows across the actual secret length, skip trailing short windows, and assert a minimum length up front with a clear "regenerate it" message.
There was a problem hiding this comment.
No agent-mode design concerns. This PR is scoped entirely to local k3s/Cilium bring-up plumbing (datapath flags, namespace ordering, arch-aware kustomize, metrics-server disable) and a test correctness fix — none of which interacts with agent prompts, output formats, the Agent SDK, or model selection.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
Three new commits since e3077289. All previous review items are addressed; new changes are correct. Approving with non-blocking suggestions.
Verification of prior review items
- cd8d83e —
cilium-configpost-install verification. Implements suggestion 1 from the prior review verbatim. Key names (kube-proxy-replacement,enable-bpf-masquerade,enable-host-legacy-routing) match the documented helm→configmap mapping. Logic is sound:jq -r '.data[$key] // empty'returns the string value because configmap data is always strings (so jq treats"false"as truthy and// emptyonly fires on absent keys). - #2706 filed. Suggestion 2 closed via #2706 with reproducer, linked CI runs, and the connection to #2580.
- Suggestion 3 (PR description "verify all three flags") obsoleted by the verification block. Every future run now self-asserts; future PR descriptions don't need to enumerate.
New changes (verified)
orchestrator/Dockerfile — arch-aware kustomize install. SHA256s verified against the published checksums.txt for kustomize v5.6.0:
linux_amd64:54e4031d…48f✓linux_arm64:ad8ab62d…660✓
dpkg --print-architecture is the right primitive here — python:3.14-slim is debian-based, and on cross-builds with --platform=linux/arm64 qemu reports the emulated arch correctly. Matches the pattern in gateway/Dockerfile:21 and sandbox/Dockerfile:105 (although those use dpkg --print-architecture only for apt source lists, not for per-arch binary fetching, so this is a slight extension rather than an exact mirror).
Makefile / test-integration.yml — --disable=metrics-server. The documented K8s failure mode (unavailable APIService → namespace controller's pre-finalize discovery cycle fails → namespace stuck in Terminating forever) is the right diagnosis for the symptoms in #2706. Confirmed egg has no consumers of the k8s metrics API (no HPA, no kubectl top, no v1beta1.metrics.k8s.io references anywhere in orchestrator/, gateway/, sandbox/, or k8s/). egg's /metrics and /metrics/prometheus routes are application-level Prometheus endpoints, unrelated to metrics-server.
test_deployment_validation_logic.py — windowed substring hardening. Correctly fixes the phantom-leak bug. Walkthrough for the standard 64-hex-char secret: range(0, 64, 16) = [0, 16, 32, 48], all windows 16 chars — identical to the prior hardcoded behavior. For non-standard lengths the len(window) < 8 guard skips windows too short to be meaningful signal. The upfront len(lifecycle_secret) >= 32 assert prevents the empty-window-always-False failure mode. Threshold of 32 (not 64) is sound: that's the minimum where range(0, len, 16) yields at least two full 16-char windows.
Non-blocking concerns
1. jq is now a runtime dependency of scripts/install-cilium.sh but not checked as a prerequisite.
Line 148 introduces jq for the first time in this script. The prereq block at lines 32–40 only checks kubectl and cluster-info. If jq is missing, the script will fail with jq: command not found at line 148 — after cilium install has already run and committed real cluster state. CI is fine (ubuntu-latest has jq preinstalled); Asahi dev machines typically have it via brew but it's not guaranteed.
Two ways to fix, either is fine:
# Option A: add a prereq check at the top
if ! command -v jq &>/dev/null; then
error "jq is not installed or not in PATH"
exit 1
fi
# Option B: drop the jq dependency by using kubectl jsonpath
got=$(kubectl -n kube-system get cm cilium-config \
-o "jsonpath={.data['$key']}")Option B is more consistent with the rest of egg's shell scripts (no other script in scripts/ uses jq today — grep -rn 'jq' scripts/ only finds the new lines) and avoids an unnecessary external dep.
2. Verification only runs on the fresh-install path; the idempotent skip bypasses it.
The early-exit at lines 62–69 returns 0 when Cilium is already installed and ready, before the new verification block at lines 131–159. So an operator coming from a pre-conservative-flags install can't run install-cilium.sh to check whether their existing config matches — they'd have to kubectl delete daemonset -n kube-system cilium first, or run make k3s-teardown && make k3s-setup. The documented migration path is the latter, so this is consistent — but consider either documenting the limitation in the script header or running the verification block unconditionally (before the install vs. skip branch) so operators can use it as a config audit on demand.
3. Integration Tests CI is still in progress on f1a9ee27.
The metrics-server theory is plausible and well-supported, but its effectiveness against the namespace stuck Terminating symptom is untested until the in-flight integration job (Run 25887017185, Integration Tests / Integration Tests) lands green. Worth waiting for that signal before merge — if it goes red the same way as before, the diagnosis needs another iteration (and the right move would be to land #4 and #5 separately while #1–#3 get re-investigated).
4. Redundant pre-create at test-integration.yml:152-153 still present.
Flagged in the prior review. Now that the Makefile k3s-secrets target applies k8s/base/namespaces.yaml first, the workflow's kubectl create namespace egg-system --dry-run=client -o yaml | kubectl apply -f - is a no-op, and the workaround comment at lines 143–151 describes a fixed problem. Not blocking; clean up in a follow-up.
Why this is still an ACK
Every change in the new commits is correct and addresses real bugs: SHA256s verified, key names match docs, test logic is tight, the metrics-server failure mode is documented. The cilium-config assertion implements the prior review's suggestion exactly. The remaining concerns are either UX polish (jq prereq) or unverified-in-CI (metrics-server effectiveness) — neither is grounds to block merge as long as the integration tests come back green.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…reate - install-cilium.sh: replace jq with kubectl jsonpath for cilium-config read-back. No other script in scripts/ depends on jq, and jq is not checked as a prereq in this script, so a missing jq would fail after 'cilium install' has already committed real cluster state. jsonpath returns the value directly (empty for absent keys), preserving the three-flag assertion verbatim. - install-cilium.sh: run the cilium-config verification block on both the fresh-install and idempotent-skip paths. Operators can now use this script as a live-cluster config audit without teardown + reinstall. The CNI on-host check also runs on the skip path; the final log line distinguishes "installed" vs "verified (skipped)". - test-integration.yml: drop the redundant 'kubectl create namespace egg-system' pre-create. The Makefile k3s-secrets target now applies k8s/base/namespaces.yaml before creating the Secret, so the workflow no longer needs the workaround. Comment updated to match.
|
Thanks for the re-review. All four items from the latest pass have a disposition below. Suggestion 1 —
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns. The delta since f1a9ee27 (commit 7b13903) is scoped to scripts/install-cilium.sh (drop the jq runtime dep, run the cilium-config verification block on the idempotent-skip path too) and .github/workflows/test-integration.yml (drop the redundant egg-system namespace pre-create now that make k3s-secrets applies the namespace manifest). None of it touches agent prompts, output formats, the Agent SDK, or model selection.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review (delta from f1a9ee27 → 7b13903)
Single new commit addresses all four non-blocking suggestions from the prior re-review. Both changed files are correct.
Verification of prior review items
-
Suggestion 1 (jq → kubectl jsonpath).
scripts/install-cilium.sh:160correctly uses bracket subscript syntax{.data['$key']}to access keys containing dashes (kube-proxy-replacement,enable-bpf-masquerade,enable-host-legacy-routing). The single-quote-inside-double-quote shell quoting is right:$keyinterpolates, the literal single quotes survive into the kubectl arg. kubectl's default--allow-missing-template-keys=truereturns empty string for absent keys, so the[ "$got" != "$want" ]check is functionally equivalent to the previousjq -r '.data[$key] // empty'. Three small kubectl calls instead of one + jq parse is a non-issue.grep -rn 'jq' scripts/now finds nothing, as the disposition note claims. -
Suggestion 2 (verification on skip path). The
SKIP_INSTALLrefactor atscripts/install-cilium.sh:64-89is sound: defaults to 0, set to 1 only when an existing cilium daemonset is fully ready, and the install block is correctly guarded. No resource leak — thetrap 'rm -rf "$TMPDIR"' EXITlives inside the install block alongside theTMPDIR=$(mktemp -d …), so the skip path neither creates nor traps. The CNI on-host check also runs on the skip path, which is the right call for an audit-mode invocation. The final log line correctly distinguishes "installed successfully" vs "verification passed (install skipped, cluster was already ready)". -
Suggestion 4 (redundant pre-create).
.github/workflows/test-integration.yml:144-150now relies entirely onmake k3s-secrets→kubectl apply -f k8s/base/namespaces.yaml(verified atMakefile:508, andk8s/base/namespaces.yamlcontainsegg-system). The updated comment accurately describes the new source of truth. -
Suggestion 3 (CI in progress). Acknowledged as not actionable; disposition is fine.
Edge cases traced
- If
cilium-configis absent (extreme edge case after a botched install),kubectl get cm cilium-config …returns non-zero, and underset -euo pipefailthe assignmentgot=$(…)propagates that to script exit. The kubectl stderr ("Error from server (NotFound): configmaps "cilium-config" not found") gives the operator a clear signal. Loud failure, no silent fallback. Good. - Audit mode on a pre-conservative cluster (e.g. operator who installed before this PR landed) correctly fails the verification with
cilium-config[kube-proxy-replacement] = 'true', expected 'false', which is the desired audit signal.
Non-blocking observations
None new. No suggestions on the delta itself.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
`make deploy` references egg-*:$(EGG_IMAGE_TAG), where EGG_IMAGE_TAG is
`git describe --always` and so changes on every commit, pull, rebase, or
checkout. `make redeploy` builds + imports + deploys in one invocation
so the tag stays self-consistent, but a bare `make deploy` after HEAD
has moved references images that were never imported into k3s — the
pods then sit in ImagePullBackOff.
Previously that surfaced only as a bare 120s `kubectl wait` timeout
("error: timed out waiting for the condition") with no hint at the
cause. Replace the two per-deployment waits with await-egg-deploy.sh,
which polls for Available and fails within seconds when it sees an
ImagePullBackOff, naming the tag and pointing at `make redeploy`.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review (delta from 7b13903 → e3ceeb39)
Single new commit (e3ceeb39): replaces the bare kubectl wait --for=condition=Available calls in the deploy Makefile target with scripts/await-egg-deploy.sh "$(EGG_IMAGE_TAG)", which polls deployment availability and fast-fails on ImagePullBackOff / ErrImagePull with an actionable "run make redeploy" message.
The change is correct and well-scoped. Approving.
Verified
-
Script semantics. Loop polls every 3s until either (a) all
DEPLOYMENTS=(orchestrator gateway)reportAvailable=True, (b) any pod inegg-systemshowsImagePullBackOff/ErrImagePull(fast-fail), or (c) 180s deadline expires (timeout-with-pod-state dump). Order of checks is right — success before fast-fail before timeout. -
Scoping.
kubectl get pods -n egg-systemis the right blast radius: sandbox pods land inegg-agents(orchestrator/kubernetes_client.py:72: DEFAULT_NAMESPACE = "egg-agents"), so the unfiltered pod list inegg-systemonly sees orchestrator + gateway. No risk of a stray sandbox pod's staleImagePullBackOfftriggering a false fast-fail. -
Race-window narrative for redeploys. I traced the "deployment briefly looks Available with old replicaset while the controller hasn't yet noticed the spec update" race.
orchestratorusesstrategy: Recreate(k8s/base/orchestrator-deployment.yaml:15-16), so during a redeploy with a broken tag, the orchestrator deployment goesAvailable=Falsefor the whole window — which means the script's all-deployments-available gate cannot prematurely succeed.gatewayuses the defaultRollingUpdateand could in principle stayAvailable=Truefrom the old pod while the new pod is inImagePullBackOff, but sinceEGG_IMAGE_TAGis a single value applied to both deployments, in practice both are stuck-or-both-pull. Same race semantics as the originalkubectl wait, so no regression. -
Bash + jsonpath correctness.
{range .items[*]}{range .status.containerStatuses[*]}{.state.waiting.reason}{"\n"}{end}{end}is valid kubectl jsonpath; non-waiting containers contribute empty lines, whichgrep -qE 'ImagePullBackOff|ErrImagePull'correctly does not match. The2>/dev/null || echo ""fallback on the deployment query keepsset -ehappy when a deployment object hasn't been observed yet. Pipefail interaction in theif kubectl ... | grep -q ...; thenis benign —ifsuspends errexit, and a kubectl failure simply means we skip the fast-fail and try again next tick. -
Makefile call site. The
&&chain atMakefile:530-531keeps the script gated behind a successfulkubectl apply -f -.scripts/await-egg-deploy.shischmod 0755per the new-file mode in the diff. Lint job (Shell) is green one3ceeb39. -
CI signal. Integration Tests are green on
e3ceeb39(run 25892787237 → "Integration Tests / Integration Tests" SUCCESS), so the metrics-server + Cilium stack from prior commits plus the new deploy gate work end-to-end on a fresh CI runner. The pre-existing concern from suggestion #3 of my prior re-review is now resolved by green CI.
Non-blocking observations
1. 2>/dev/null on both kubectl calls suppresses non-NotFound errors that would have been visible with kubectl wait.
If the operator's kubeconfig is broken or RBAC is wrong, the script just polls silently for 180s and then dumps an empty kubectl get pods (which itself fails silently via || true at line 56). The original kubectl wait would have surfaced "Unable to connect to the server: …" or "Forbidden: …" within the first tick. Consider distinguishing NotFound (transient, expected during early rollout) from auth/connection errors (immediate-fail, don't loop).
Quick patch:
err=$(kubectl -n "$NS" get deployment "$d" \
-o jsonpath='{.status.conditions[?(@.type=="Available")].status}' 2>&1) || rc=$?
# treat NotFound as "not yet" but surface other errors immediately
if [ "${rc:-0}" -ne 0 ] && ! grep -q 'NotFound' <<<"$err"; then
echo "ERROR: $err" >&2; exit 1
fi2. The ImagePullBackOff fast-fail message is unconditional about the cause.
Lines 46-49 always blame "tag drift" and recommend make redeploy. For egg's local-k3s + k3s ctr images import topology that's a near-100% accurate diagnosis (no external registry to be unreachable), but if a future deployment ever pulls from a remote registry (e.g. a CI variant, a staging cluster), the same fast-fail would mis-diagnose a registry outage as tag drift. Worth a one-line caveat ("most commonly tag drift; also check registry reachability if the cluster pulls from a remote registry") if you anticipate that path, otherwise non-issue.
3. TAG="${1:-unknown}" silently falls back when invoked without an argument.
The Makefile always passes $(EGG_IMAGE_TAG), so the fallback only fires if someone hand-runs the script. The error message then reads pods cannot pull image tag 'unknown', which is mildly confusing. Consider failing-loud on missing arg (: "${1:?usage: $0 <egg-image-tag> [timeout-seconds]}") to avoid the misleading-error-on-misuse footgun.
Why this is still an ACK
The new script does exactly what its commit message claims: turns a 120s opaque timeout into a sub-15-second actionable error for the most common bring-up failure mode. Scoping, jsonpath, pipefail interactions, file mode, and Makefile wiring are all correct. The Recreate strategy on orchestrator removes the redeploy-race concern. CI is green end-to-end on the head SHA. Non-blocking notes are UX polish — they're worth doing in a follow-up but don't gate this merge.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
Two non-blocking notes from the re-review on PR #2705: 1. Distinguish NotFound (transient, expected during early rollout) from real kubectl errors (auth, connection, RBAC). The previous `2>/dev/null || echo ""` swallowed everything, so a broken kubeconfig would poll silently for the full timeout before dumping an empty pod list. Now real errors surface within the first tick. 2. Replace the silent `TAG="${1:-unknown}"` fallback with `${1:?usage: ...}` so hand-running the script without an argument fails loud with a usage message, instead of producing a misleading "tag 'unknown'" diagnosis. Skipped: the third note (a caveat for hypothetical remote-registry topologies). Accurate for egg's local-k3s + `k3s ctr images import` setup today; revisit if/when that path actually lands.
There was a problem hiding this comment.
No agent-mode design concerns.
Delta from 7b13903 → d5dcfbc is two commits scoped entirely to the local k3s deploy workflow:
e3ceeb39— replaces the barekubectl wait --for=condition=Availablecalls inmake deploywithscripts/await-egg-deploy.sh, which polls availability and fast-fails onImagePullBackOffwith a pointer tomake redeploy.d5dcfbc2— hardens the script: distinguishesNotFound(expected during early rollout) from real kubectl errors (auth/connection/RBAC) so the latter surface immediately, and switches to${1:?usage: ...}so hand-running it without a tag fails loud rather than silently falling back tounknown.
Neither commit touches agent prompts, Agent SDK call sites, model identifiers, output formats, or any other agent-mode surface. Pure shell + Makefile plumbing.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review (delta from e3ceeb39 → d5dcfbc2)
Single new commit addresses two of three non-blocking suggestions from the prior re-review. The third is explicitly skipped with a documented rationale (accurate today for egg's local-k3s + k3s ctr images import topology; revisit when remote-registry pulls actually land). That's a reasonable disposition.
Approving with one new non-blocking observation.
Verification of prior review items
-
Suggestion 1 (distinguish NotFound from real kubectl errors).
scripts/await-egg-deploy.sh:32-43is correct. Capturing stderr via2>&1into$out, capturing rc via|| rc=$?, and gating the surface on[ "$rc" -ne 0 ] && ! grep -q 'NotFound' <<<"$out"matches kubectl's canonical not-found message (Error from server (NotFound): deployments.apps "X" not found). Connection-refused, Unauthorized, and Forbidden all fall through the negated grep correctly — they don't contain the literalNotFound.out=$(…) || rc=$?correctly suspends errexit underset -euo pipefail. The fall-through to[ "$out" = "True" ] || all_available=0is right: on a genuine NotFound (deployment not yet observed),$outcontains the error text rather than"True", soall_available=0and we loop. The only kubectl call that still uses2>/dev/nullis the pods query for fast-fail, which is fine — by the time it runs, the deployment query in the same iteration has already gated on auth/connection health. -
Suggestion 2 (fail loud on missing arg).
scripts/await-egg-deploy.sh:21-22uses: "${1:?usage: $0 <egg-image-tag> [timeout-seconds]}"followed byTAG="$1". The:?form (not?) errors on both unset and empty, soscript ""also exits loud. The Makefile always passes$(EGG_IMAGE_TAG), which falls back tolatestviagit describe --always --dirty 2>/dev/null || echo latestatMakefile:28, so it's never empty from the production call site — the change only affects hand-invocation, which is the intended scope. -
Suggestion 3 (remote-registry caveat). Explicitly skipped with rationale in the commit message. Reasonable — the script lives in
scripts/, the Makefile only ever pairs it withk3s ctr images import, and the diagnostic correctly describes the only failure mode it can actually see today. Revisit when a remote-pull path lands.
New non-blocking observation
1. 2>&1 merges kubectl stderr warnings into $out, which the equality check can't tolerate.
scripts/await-egg-deploy.sh:34-35 captures stderr into $out to enable the NotFound/non-NotFound distinction. The downstream comparison [ "$out" = "True" ] || all_available=0 then assumes $out is exactly the jsonpath value on success. That assumption holds for the typical local-k3s case (stable apps/v1 resource, no admission webhooks, no API deprecation warnings) but breaks if kubectl ever emits a stderr warning on a successful call — e.g. an HTTP Warning: header from the API server, an admission webhook injecting a deprecation notice, or a token-near-expiry advisory. In any of those cases $out becomes something like "Warning: …\nTrue", the equality fails, the deployment looks not-Available, and the script polls until the 180s timeout dumps the (actually-fine) pod state.
In current egg local-k3s this is theoretical — there are no admission webhooks in k8s/base/, apps/v1 deployments is stable, and k3s doesn't inject warnings. So the impact today is zero. But the stderr-on-success channel is shared by the warning surface in kubectl's design, and someone reading this code later may not see why the equality breaks.
If you want defense-in-depth, the cleanest separation is to keep stderr in a separate variable:
err_file=$(mktemp); trap 'rm -f "$err_file"' EXIT
rc=0
out=$(kubectl -n "$NS" get deployment "$d" \
-o jsonpath='...' 2>"$err_file") || rc=$?
err=$(<"$err_file")
if [ "$rc" -ne 0 ] && ! grep -q 'NotFound' <<<"$err"; then
echo "ERROR: kubectl get deployment $d failed: $err" >&2
exit 1
fi
[ "$out" = "True" ] || all_available=0This keeps the success-path $out strictly equal to the jsonpath value and surfaces the same NotFound-vs-real-error distinction from $err. Non-blocking — only land it if you anticipate webhooks/deprecation warnings on this cluster.
Why this is still an ACK
Both addressed suggestions land correctly: the rc/NotFound gate is the right shape, ${1:?…} is the right idiom, and the skipped suggestion has a coherent defer rationale. The new stderr-merge observation is a code-cleanliness concern with zero current production impact. No new bugs, no regressions, no security concerns; the script does exactly what its commit message claims.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Previously the deployment-availability check captured kubectl's stderr into the same variable as its stdout (`2>&1`) so the NotFound-vs-real- error gate could grep it. That made the downstream `[ "$out" = "True" ]` equality sensitive to any stderr line kubectl might emit on a successful call — admission webhook deprecation warnings, API-version advisories, near-expiry token notices, etc. None of those fire on egg's current local-k3s, but the channel is shared by design, so a future cluster could silently fail the equality and poll until the 180s timeout. Redirect stderr to a temp file instead, slurp it into $err for the gate, and keep $out strictly equal to the jsonpath value. trap rms the file on EXIT.
|
Thanks for the re-review. Single new observation from the latest pass; disposition below. Suggestion 1 —
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns.
Delta from d5dcfbc2 → f19e59bf is one commit, scoped to scripts/await-egg-deploy.sh: redirects kubectl stderr to a temp file (2>"$err_file") and slurps it into $err for the NotFound-vs-real-error gate, leaving $out strictly equal to the jsonpath value. Pure shell plumbing for the local k3s deploy workflow — no agent prompts, no Agent SDK call sites, no model identifiers, no output formats, no structured-output / post-processing / prompt-level-security surface touched.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review (delta from d5dcfbc → f19e59b)
Single new commit (f19e59b) addresses suggestion #1 from my prior re-review: separates kubectl stderr from stdout in scripts/await-egg-deploy.sh so the deployment-availability equality check is strict.
The change is correct and well-scoped. Approving with no new concerns.
Verified
- Mechanism is sound.
2>"$err_file"(truncating, not appending) ensures stale stderr does not leak across iterations. kubectl runs synchronously in the command substitution, soerr=$(<"$err_file")reads a complete file with no race against the producer.outis now strictly the jsonpath value — any future kubectl stderr warning on a successful call (admission webhook deprecation, API-version advisory, near-expiry token notice) can no longer break[ "$out" = "True" ]. - Error path unchanged in spirit. NotFound (transient, expected during early rollout) still falls through to the "not Available" branch; real kubectl errors (auth, RBAC, connection) still surface immediately within the first tick. The
[ "$rc" -ne 0 ] && ! grep -q 'NotFound' <<<"$err"short-circuits correctly onrc=0, so a benign stderr line on a successful call no longer triggers a false error. - Resource management.
mktemp+trap 'rm -f "$err_file"' EXITcleans up on every exit path (normal exit,exit 1, signal-driven exits — the bash EXIT trap fires for all of them).rm -fis no-op-safe if the file is already gone. No leak. - Asymmetry with the pods fast-fail block is fine. Line 65 still uses
2>/dev/nullon thekubectl get podsquery, but the deployments query above is the canary that already surfaces auth/connection failures loudly via the new$errgate. By the time we hit the pods query, we know kubectl is reachable — the2>/dev/nullthere only hides cosmetic noise, not blockers. - CI is green on the head SHA.
Integration Tests / Integration Tests: SUCCESS,Shell: SUCCESS,Unit Tests: SUCCESSonf19e59b, confirming the script still works end-to-end throughmake deployon a fresh CI runner.
Prior review items — all addressed across the PR
- ✓
jqruntime dep dropped in favor of kubectl jsonpath (7b13903). - ✓
cilium-configverification runs on both fresh-install and idempotent-skip paths (7b13903). - ✓ Redundant
egg-systemnamespace pre-create removed fromtest-integration.yml(7b13903). - ✓ NotFound vs. real kubectl error distinguished in
await-egg-deploy.sh(d5dcfbc). - ✓
${1:?usage: ...}replaces silent:-unknownfallback (d5dcfbc). - ✓ kubectl stderr captured separately from stdout (
f19e59b— this commit).
Non-blocking observations
None. The delta is a minimal, defensive fix for a hypothetical future failure mode that I flagged previously, and it's implemented correctly without overreach.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
|
Thanks for the re-review. Nothing actionable on the latest pass; disposition below for completeness. Latest pass at
|
|
egg feedback addressed. View run logs 18 previous review(s) hidden. |
* fix: chain portmap CNI so Cilium honors hostPort mappings PR #2705 installs Cilium with kubeProxyReplacement=false (deliberate — KPR attaches eBPF programs to the primary NIC and blackholes connectivity on hosts with unusual NICs like wireless). With KPR off, Cilium does not implement Kubernetes hostPort itself, and the install did not chain a CNI plugin that does — so hostPort mappings in the local overlay (orchestrator's 9849/9850) were silently dropped. Pods served fine inside the cluster but the mapped ports never bound on the node, leaving Claude Code's MCP client at http://localhost:9850/mcp with connection-refused. Add --set cni.chainingMode=portmap to the install args and a matching cni-chaining-mode=portmap assertion to the post-install verification loop so the same regression cannot land silently again. * Fix checks: install portmap CNI plugin alongside Cilium so chained hostPort works cni.chainingMode=portmap writes a CNI conflist that references the upstream portmap binary, but Cilium's install only ships cilium-cni — not portmap — into /opt/cni/bin. On the GitHub Actions runner there is no other source of standard CNI plugins, so kubelet's sandbox setup walks the chain and fails with 'failed to find plugin "portmap" in path [/opt/cni/bin]', leaving every pod with a hostPort (orchestrator's 9849/9850) stuck in ContainerCreating until the deploy times out. k3s ships portmap in /var/lib/rancher/k3s/data/current/bin; copy it into /opt/cni/bin when missing. Works on fresh CI runners and fresh local installs without adding a network dependency to the script. If neither location has portmap, fail with a remediation pointer to the containernetworking-plugins apt package. * Address review feedback on portmap install - Move portmap binary check + copy to before 'cilium install' so the conflist referencing portmap never lands on disk while the binary is missing — closes the pod-CNI outage window where coredns/traefik/ local-path-provisioner could enter sandbox-creation backoff. - Use 'sudo test -x' on both probes so a hardened parent-dir mode surfaces as a real permissions error instead of a misleading 'not found' (also distinguish exists-but-not-executable from absent). - Smoke-test the binary after copy by invoking it with no env vars and grepping for 'CNI' in its error output, so wrong-arch or truncated copies fail at install time instead of as cryptic CNI ADD failures at first pod schedule. - Add a remediation pointer to the verify-failed block: pre-#2713 Cilium installs missing the cni-chaining-mode key cannot be patched in place; the supported path is 'make k3s-teardown && make k3s-setup'. * Address non-blocking review feedback on portmap install - Add a fourth elif branch for the 'CNI_BIN_DIR/portmap exists but is not executable AND K3S source is absent' case so the error message matches the actual failure mode instead of saying 'not found'. - Rewrite the smoke-test comment to match modern portmap behavior: v1.5.1 (k3s + containernetworking-plugins) exits 0 with a banner on stdout; older versions print a CNI-spec error to stderr. The 2>&1 merge catches either path; the broken-binary failure mode is what the test actually guards against. - Track which key triggered verify_failed so the #2713 chainingMode remediation hint only fires when cni-chaining-mode mismatched, not when any of the four asserted keys mismatched. * fix: install pod-egress MASQUERADE rule that chained-CNI mode skips cni.chainingMode=portmap (set above to give us hostPort under kubeProxyReplacement=false) puts cilium-agent into a "chained" CNI mode where it treats itself as a secondary plugin behind a notional primary CNI, and defers iptables masquerade to that primary. But here Cilium IS the primary (it owns IPAM and the datapath); there is no other primary to install the rule. So agent's CILIUM_POST_nat chain stays empty even though cilium-config has enable-ipv4-masquerade=true, and pod traffic leaves the host with its pod-CIDR source IP intact. The internet routes responses to an unrouteable address, and any pod that needs external egress — gateway -> api.github.com for token refresh, sandbox agents -> Anthropic API — silently fails with connection timeouts. Observed locally: orchestrator pod could not reach api.github.com:443 or even 1.1.1.1:443; manually adding the missing MASQUERADE rule in POSTROUTING restored egress immediately and the gateway's token-refresher recovered from CrashLoopBackOff. Install the rule directly in POSTROUTING (not CILIUM_POST_nat, which the agent flushes on every config sync) so it survives cilium-agent restarts and config reloads. The match — `-s POD_POOL_CIDR ! -d POD_POOL_CIDR -j MASQUERADE` — is identical to what cilium-agent would install in non-chained mode. Read the pool CIDR from cilium-config's cluster-pool-ipv4-cidr key rather than hardcoding, so this still works if Cilium IPAM defaults change. Idempotent via `iptables -C ... || iptables -A ...` so re-runs (and the SKIP_INSTALL=1 audit path) are no-ops when the rule is already there. * fix(install-cilium): unmask MASQUERADE empty-CIDR error + reviewer suggestions The pod-egress MASQUERADE block silently swallowed the 'cluster-pool-ipv4-cidr missing from cilium-config' failure because 'set -euo pipefail' + grep's exit-1-on-no-match short-circuited the script before the empty-string check could fire. Add '|| true' to the grep pipeline so the assignment completes and the explicit diagnostic runs. Also addressed in this commit: - Bracket-notation jsonpath ({.data['cluster-pool-ipv4-cidr']}) for consistency with the verify loop and safety on older kubectl. - Documented the 10.0.0.0/8 default pod-pool footgun, lack of reboot persistence, IPv6/dual-stack TODO, and iptables backend skew. - Log 'update-alternatives --display iptables' so iptables-legacy hosts surface at install time rather than as silent packet loss. - Improved diagnostic enumerates likely causes (IPAM mode, key rename) and points operators at the right kubectl inspection command. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
Fixes for bringing up egg on a fresh local k3s cluster (notably on arm64 dev machines) and for the integration-CI timeout. All surfaced doing the #2704-mandated
make k3s-teardown && make k3s-setupmigration.1.
scripts/install-cilium.sh— conservative Cilium datapath flagsThe bare
cilium installfrom #2704 enables kube-proxy replacement, BPF masquerade, and BPF host routing, all of which attach eBPF to the host's primary NIC. On hosts where that NIC is a wireless interface, this blackholes host connectivity entirely. Install withkubeProxyReplacement=false,bpf.masquerade=false,bpf.hostLegacyRouting=true; a post-install check assertscilium-configreflects them. NetworkPolicy enforcement is unaffected.2.
Makefile—k3s-secretsnamespace orderingk3s-secretscreatedgateway-secretsinegg-systembefore that namespace existed (deploy: k3s-secrets, and the namespace is created bydeploy's manifest apply). Fresh-clustermake deployfailed withnamespaces "egg-system" not found. Now appliesk8s/base/namespaces.yamlfirst.3.
Makefile+test-integration.yml— disable metrics-server (fixes the integration-CI timeout)Under Cilium the k3s-bundled metrics-server pod can't reach the kubelet on the node IP, so
v1beta1.metrics.k8s.iostays unavailable. The namespace controller's pre-finalize API discovery then fails, so everykubectl delete namespacehangs inTerminatingforever — which wedged both integration-test fixture teardown and the CI job-cleanup step (run 25883254559:362 passed, 0 failed, then a 26-min cleanup hang → 30-min SIGKILL). egg doesn't use metrics-server, so it's disabled in the k3s install.4.
orchestrator/Dockerfile— arch-aware kustomize installThe kustomize download hardcoded
linux_amd64. On an arm64 hostmake buildproduces an arm64 orchestrator image with an amd64 kustomize binary inside it, which crashes under emulation. CI never caught it (amd64 runners). Now resolves arch viadpkg --print-architecturewith per-arch pinned SHA256s, matching the gateway/sandbox Dockerfiles.5.
test_deployment_validation_logic.py— secret-leak window checkThe leak check sliced the bearer at hardcoded offsets 0/16/32/48, assuming a 64-hex-char secret. A shorter
lifecycle-secretproduces empty windows, and"" not in textis always False — a phantom "leak" report. Windows are now derived from the actual secret length, with an upfront minimum-length assertion.Test plan
make k3s-teardown && make k3s-setupon an arm64 host with a wireless primary NIC — host network stays up.make buildon arm64 produces a working orchestrator image (kustomize runs, no Go-runtime crash).make deploysucceeds (nonamespaces "egg-system" not found).kubectl delete namespacereturns promptly.Follow-up to #2704.