feat(deploy): include egg-sandbox in the registry publish path by default - #3109
Conversation
…ault #3101 shipped the loopback-registry publish flow with the sandbox image excluded pending verification that nothing could be published off-host. That's confirmed: the registry is a 127.0.0.1-bound container on the dev host (setup-local-registry.sh refuses anything else) and push-egg-images.sh hard-refuses non-loopback registries with no override, so private repo content baked into the sandbox image cannot leave the machine. With that settled, the sandbox joins EGG_REGISTRY_IMAGES by default and gets the same layer-incremental publish as the core images — a code-only redeploy now moves tens of MB total instead of a full ~5.3 GB sandbox save+import. Operators can still exclude any image by removing it from EGG_REGISTRY_IMAGES (it then publishes via the save+import path), and EGG_IMAGE_REGISTRY= (empty) still disables the registry flow entirely (CI). Comment/doc updates only beyond the one-line default change. Refs #2999
Two bugs surfaced on the first live #3101 redeploy: 1. Registry blob corruption after garbage-collect. The stock registry:2 serves blob-existence (HEAD) from an in-memory descriptor cache, while `registry garbage-collect` runs as a separate process (docker exec) and deletes blob files behind the serving process's back. The next `docker push` is then told "Layer already exists" for a blob whose file is gone, never re-uploads it, and the subsequent containerd pull dies with "short read: expected N bytes but got 0: unexpected EOF". Fix: `docker restart egg-registry` (~1s) right after the post-deploy GC, dropping the stale cache so existence checks hit disk honestly. 2. k3s-publish swallowed a k3s-push failure: the recipe ran `$(MAKE) k3s-push; if ...` so the branch's exit status was whatever came last (the import leg), and a failed push could still let `deploy` repoint the cluster at unpullable images. Now `|| exit 1`. The crictl pre-pull in k3s-push is what caught the corruption before kubectl apply -- working as intended; these fixes remove the cause and the masking. Refs #2999
|
Pushed two fixes for the failure seen on the first live redeploy (
The host registry has been healed in place (restart + re-push of all core images; every blob of every tag verified servable at full size via the registry API). Note the pre-pull guard worked exactly as designed — it failed the publish before |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Summary
Approving. The default flip is sound: privacy guards in setup-local-registry.sh (loopback-only setup) and push-egg-images.sh (loopback-only push) defend the invariant the comments now correctly describe, and the rewrite path in deploy (Makefile:702-705) propagates the registry-qualified ref into EGG_SANDBOX_IMAGE so the orchestrator spawns sandbox pods at the right ref.
Two findings worth calling out — one a buried bug fix that this PR correctly addresses, one a doc/test-hygiene gap.
What this PR fixes that the description doesn't call out
|| exit 1 on k3s-push (Makefile:732) is a real correctness fix, not cosmetic. With the new default (EGG_REGISTRY_IMAGES = $(EGG_ALL_IMAGES)), EGG_IMPORT_IMAGES is empty by default. The shell recipe is:
if [ -n "$REGISTRY" ] && [ -n "$REGISTRY_IMAGES" ]; then \
$(MAKE) k3s-push || exit 1; \
if [ -n "$IMPORT_IMAGES" ]; then $(MAKE) k3s-import …; fi; \
else …; fi/bin/sh doesn't set -e. If k3s-push fails, execution continues. The inner if [ -n "" ] evaluates as false but the if-block as a whole exits 0, and that 0 overrides the earlier non-zero — so without || exit 1, a k3s-push failure is silently swallowed and make redeploy proceeds to deploy against an unpublished image set. This was masked in the old default by the k3s-import egg-sandbox call also running and propagating, but it was always a latent bug. Good catch making this explicit before flipping the default.
Non-blocking: stale test docstrings
tests/scripts/test_reap_stale_egg_images.py was not updated. After merge, the following are misleading:
-
Lines 231-233 (
TestReapHybridModedocstring):The default config in this PR has
EGG_REGISTRY_IMAGES = egg-gateway egg-orchestrator egg-litellm— egg-sandbox stays on the save+import path because it bakes in private repo content.This now describes a non-default operator opt-out, not the default. The tests in this class are still valid (they test the hybrid-with-sandbox-excluded mode which remains a supported operator configuration), but the prose is wrong post-merge.
-
Line 246:
# Default subset for this PR: sandbox stays on import (bare-authoritative).— same problem.
These are doc-only — the tests pass SUBSET explicitly and exercise valid code paths regardless. Worth a follow-up commit to either retitle the class to "operator opt-out" framing or add a sibling class for the new default (all four registry-authoritative). The new default's reap behavior — localhost:5000/egg-sandbox:<tag> authoritative, docker.io/library/egg-sandbox:<tag> non-authoritative leftover from pre-#3109 deploys, digest guard sparing same-digest cases — is uncovered by the existing tests and is exactly the migration path real operators will hit on first redeploy after this lands.
Other things I checked
docker restart egg-registryplacement (reap-stale-egg-images.sh:347-348): correctly runs unconditional-after-GC; theset -einteraction is fine because||provides the fallback. Cost ~1s per deploy, even when GC was a no-op — defensible because--delete-untaggedmay still strand cache entries from:latestoverwrites. The script's earlier early-exit at line 278 ([ -n "$REGISTRY" ] && [ "${#REGISTRY_SUBSET[@]}" -gt 0 ] || exit 0) keeps this from firing in CI mode (EGG_IMAGE_REGISTRY=empty), so CI behavior really is unchanged as claimed.- End-to-end ref correctness:
make deploy'ssedrewritesegg-sandbox:latest→localhost:5000/egg-sandbox:<tag>in the orchestrator-deployment.yaml'sEGG_SANDBOX_IMAGEenv var (Makefile:704).KubernetesSpawner.DEFAULT_SANDBOX_IMAGEreads this env at class-definition time, so post-redeploy the orchestrator spawns agents against the registry-qualified ref.imagePullPolicy=IfNotPresent+push-egg-images.sh's pre-pull (crictl pull) covers the cold-start case. The pieces line up. - Privacy invariant: both
setup-local-registry.sh(lines 38-42) andpush-egg-images.sh(lines 45-51) hard-refuse non-loopback hosts independently. Removing or weakening either one would be the regression to watch for — neither is touched here. - CI status: green at time of review.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Reviewer flagged that the existing TestReapHybridMode docstring still described the pre-flip default — sandbox excluded from EGG_REGISTRY_IMAGES, on the save+import path — and that the new default's reap behavior (every image authoritative as <registry>/<image>:<tag>, including the sandbox; bare docker.io/library/egg-sandbox:<tag> leftovers from pre-#3109 deploys non-authoritative; digest guard sparing same-digest cases) was uncovered. Reframe TestReapHybridMode as the operator-opt-out config it now is and add TestReapAllRegistryMode exercising the new default's reap behavior on the exact migration shape real operators will hit on first redeploy after this lands: bare leftovers sharing the kept digest (spared), distinct-digest bare leftovers across all four images (reaped), canonical authoritative-stale reap, and :latest digest protection crossing the prefix boundary.
|
Thanks for the review — addressed the non-blocking docstring/coverage gap in-PR.
All 19 tests in the file pass locally; CI will run the full suite on this push. The other items in the review (the — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Summary
Re-approving. The single new commit ed45aeb addresses both non-blocking items from the prior review and nothing else — the production code path (scripts/reap-stale-egg-images.sh, Makefile, setup-local-registry.sh, push-egg-images.sh) is unchanged since 394b95a.
Items from the prior review
1. Stale TestReapHybridMode docstring + SUBSET comment → fixed.
The class docstring (tests/scripts/test_reap_stale_egg_images.py:221-245) now correctly frames the hybrid EGG_REGISTRY_IMAGES = egg-gateway egg-orchestrator egg-litellm config as the operator-opt-out path, with an explicit cross-reference to TestReapAllRegistryMode for the new default. The inline # Operator-opt-out subset: … comment at line 248 matches. Reading the file end-to-end, the framing is now consistent — there is no remaining text that calls the hybrid split "the default."
2. New-default reap behavior uncovered → covered by TestReapAllRegistryMode.
tests/scripts/test_reap_stale_egg_images.py:387-507 adds the five cases that match the migration path real operators will hit. I traced each through the awk program (scripts/reap-stale-egg-images.sh:165-180) by hand with SUBSET = (egg-gateway, egg-orchestrator, egg-sandbox, egg-litellm):
test_sandbox_bare_leftover_sharing_digest_is_spared—keepdigpopulates withsha256:cccfrom the authlocalhost:5000/egg-sandbox:v2; the baredocker.io/library/egg-sandbox:v2matchesmatch_rebut neitherauth_reg_re(wrong prefix) norauth_bare_re(^$— emptybare_img_alt), falls to candidate, and is spared by the sharedsha256:ccc. ✓test_sandbox_bare_leftover_distinct_digest_is_reaped— same path, but the bare leftover carriessha256:oldsandwhich is not inkeepdig. Reaped. ✓test_bare_leftovers_across_all_images_distinct_digests_reaped— full migration shape across all four images; none ofold-g/old-o/old-s/old-lare inkeepdig, all four reaped. ✓test_authoritative_stale_refs_are_reaped— registry-qualified:v1candidates with distinct digests; matchesauth_reg_rebut tagv1 != keep && != latest, so they fall to candidates and are reaped. The case the auth-aware regex must NOT over-protect. ✓test_latest_authoritative_protects_bare_leftover_sharing_digest—localhost:5000/egg-sandbox:latestenterskeepdigwithsha256:zzz; the baredocker.io/library/egg-sandbox:v1with the same digest falls to candidate but is spared. The exact cross-prefix digest-guard invariant under the new default. ✓
All 5 cases verified locally:
tests/scripts/test_reap_stale_egg_images.py::TestReapAllRegistryMode::test_sandbox_bare_leftover_sharing_digest_is_spared PASSED
tests/scripts/test_reap_stale_egg_images.py::TestReapAllRegistryMode::test_sandbox_bare_leftover_distinct_digest_is_reaped PASSED
tests/scripts/test_reap_stale_egg_images.py::TestReapAllRegistryMode::test_bare_leftovers_across_all_images_distinct_digests_reaped PASSED
tests/scripts/test_reap_stale_egg_images.py::TestReapAllRegistryMode::test_authoritative_stale_refs_are_reaped PASSED
tests/scripts/test_reap_stale_egg_images.py::TestReapAllRegistryMode::test_latest_authoritative_protects_bare_leftover_sharing_digest PASSED
All 19 tests in the file pass; CI's Unit Tests / Integration Tests / Lint / Security Scan are all green on ed45aeb.
Coverage quality check
- Tests exercise the production awk:
_extract_awk_program()reads the awk block out ofscripts/reap-stale-egg-images.shon each run (anchored onAUTH_BARE_RE=…<<<, the containerd block specifically, not the later docker-store awk)._extract_images()readsIMAGES=(…)from the same script. So a future change to either flows into the test on the next run, not pinned by a stale copy. - Not self-seeding: assertions are hand-authored expected outputs (
reaped == [],reaped == ["docker.io/library/egg-sandbox:v1"], etc.), not regenerated from the implementation. - Naming matches assertions: every
test_*name describes what it asserts; no name-vs-behaviour contradictions.
Non-blocking observations (out of scope for this commit)
- Env-var construction (
PREFIX_ALT_RE/AUTH_REG_RE/AUTH_BARE_RE) is reconstructed in Python in_run_awkrather than extracted from the bash. A regression to the bash-side regex construction atscripts/reap-stale-egg-images.sh:88-114would not be caught here — the awk would still run, but on test-author-built env vars. This is the same trade-off the existingTestReapHybridModealready accepts;TestReapScriptSafetyGuardcovers the bash side via PATH shimming (no-registry mode only). Not introduced by this commit. TestReapScriptSafetyGuarddoesn't yet exercise the all-registry safety-gate path. The end-to-end safety-gate test uses the no-registry (bare) listing, so the per-image expected-prefix branch underis_registry_imagein the safety gate (scripts/reap-stale-egg-images.sh:129-138) is exercised only via the awk extractor, not end-to-end. Worth following up but explicitly outside what the prior review flagged.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The existing TestReapScriptSafetyGuard exercised the per-image expected-prefix branch (reap-stale-egg-images.sh:128-137) only on the no-registry path. Under the post-#3109 default every image is registry-authoritative — extend the end-to-end test to cover that branch with the registry prefix too. - Add an optional registry/registry_subset to _run_script that forwards to the script as positional args 2+, matching the Makefile call form. - test_all_registry_mode_proceeds_when_all_registry_kept_refs_present: all four images visible at localhost:5000/<image>:v2, safety gate passes, a bare leftover with a distinct digest is reaped end-to-end. - test_all_registry_mode_skips_reap_when_kept_ref_only_at_bare_prefix: sandbox KEEP_TAG only at docker.io/library/, registry-prefix form missing, safety gate fires — the canonical 'next pod cannot find an image' failure mode the gate exists to prevent.
|
Thanks for the re-approval. Addressed the second non-blocking observation in-PR; treating the first as pre-existing per the reviewer's own note that it is "not introduced by this commit."
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Summary
Re-approving. The single new commit 59fb516 addresses the second non-blocking observation from the prior review and nothing else. Production code (scripts/reap-stale-egg-images.sh, Makefile, setup-local-registry.sh, push-egg-images.sh) is unchanged since ed45aeb.
Item from the prior review
TestReapScriptSafetyGuard didn't yet exercise the all-registry safety-gate path → covered.
tests/scripts/test_reap_stale_egg_images.py:617-691 adds two end-to-end tests covering the per-image expected-prefix branch (scripts/reap-stale-egg-images.sh:128-137) under the post-#3109 default. I traced each through the bash by hand:
-
test_all_registry_mode_proceeds_when_all_registry_kept_refs_present—KEEP_TAG=v2,REGISTRY=localhost:5000,REGISTRY_SUBSET=(all four).is_registry_imagereturns true for every image, soexpect_prefix_re=REGISTRY_PREFIX_REfor the full loop. All fourlocalhost:5000/<img>:v2refs are visible,missing_keepis empty, gate passes. In the awk loop the four registry refs matchauth_reg_reand land inkeepdig; the baredocker.io/library/egg-sandbox:v1matchesmatch_rebut neitherauth_reg_re(wrong prefix) norauth_bare_re(^$), falls to candidate withsha256:oldsandnot inkeepdig→ reaped. Assertions onreaped docker.io/library/egg-sandbox:v1and absence ofcontainerd reap: not allare both correct. ✓ -
test_all_registry_mode_skips_reap_when_kept_ref_only_at_bare_prefix— same args; the listing has sandbox atdocker.io/library/egg-sandbox:v2only. For sandboxexpect_prefix_re=localhost:5000/, thegrep -qE "^localhost:5000/egg-sandbox:v2"check fails,missing_keep=(egg-sandbox:v2), script prints==> containerd reap: not all egg-*:v2 refs visible (egg-sandbox:v2); skipping (nothing reaped).and exits 0 before reachingcrictl rmi. Assertions onskipping+egg-sandbox:v2+ absence ofreapedare all correct. This is exactly the cross-prefix-leftover failure mode the gate exists to catch. ✓
The _run_script helper change is the right shape — registry + registry_subset are forwarded as positional args 2+, matching the Makefile call form (bash $SCRIPT $KEEP $REGISTRY $SUBSET...), so the production argument parsing at scripts/reap-stale-egg-images.sh:49-69 is exercised, not bypassed.
Coverage quality check
- End-to-end via PATH-shimmed
sudo k3s— same pattern as the existing safety-gate tests, so the bash-sideIMAGES=(...),is_registry_image, regex construction (escape_re,AUTH_REG_RE,AUTH_BARE_RE), the per-imageexpect_prefix_reselector, and the awk loop all run from the real script. - Registry-side reap doesn't contaminate — verified the
curl -fsS http://localhost:5000/v2/probe atscripts/reap-stale-egg-images.sh:281will fail in the test env (no real registry onlocalhost:5000), so the scriptexit 0s before any registry calls. Documented in the_run_scriptdocstring (tests/scripts/test_reap_stale_egg_images.py:541-546). - No self-seeding — assertions are hand-authored (
reaped docker.io/library/egg-sandbox:v1,skipping+egg-sandbox:v2), not regenerated from the implementation. - Names match assertions — both
test_*names describe exactly what is asserted; no contradictions. - Updated class docstring (
tests/scripts/test_reap_stale_egg_images.py:511-526) now correctly frames "authoritative prefix form" rather than implying every image must be visible regardless of prefix; the cross-reference to "both branches are exercised end-to-end" matches the new test names.
Items not addressed (acceptable for this PR)
- The producer noted the disagreement on the
_run_awkenv-var-reconstruction observation — a pre-existing trade-off inherited fromTestReapHybridModeframework, applies in lockstep to bash and Python sides, and the bash-side regex construction is now exercised end-to-end in both no-registry and all-registry modes byTestReapScriptSafetyGuard. Fair call; not worth widening the diff for.
CI status
All checks green on 59fb516 (Unit Tests / Integration Tests / Lint / Security Scan / Shell / Docker).
— Authored by egg
|
egg review completed. View run logs 7 previous review(s) hidden. |
Follow-up to #3101 (refs #2999).
#3101 shipped the loopback-registry publish flow with the sandbox image excluded from
EGG_REGISTRY_IMAGESpending verification that its private repo content could never be published off-host. That's now confirmed and accepted: the registry is aregistry:2container on the dev host bound to 127.0.0.1 only (setup-local-registry.shrefuses to set up anything else), andpush-egg-images.shhard-refuses any non-loopback registry with no override — exposure is identical to the docker daemon store and k3s's containerd.With that settled, this flips the default so
egg-sandboxrides the registry path too and gets the same layer-incremental publish as the core images. The practical effect is the rest of the #2999 win: a code-only redeploy moves the changed ~25 MB of layers instead of a full ~5.3 GB sandboxdocker save+ctr importper edit.EGG_REGISTRY_IMAGES ?= $(EGG_ALL_IMAGES); everything else is comments/docs catching up.EGG_REGISTRY_IMAGESand it publishes via save+import (store-to-store, no registry).EGG_IMAGE_REGISTRY=(empty) still disables the registry flow entirely (CI is pinned there).make -nrenders in default (all four pushed, import leg empty-guarded), exclusion, and no-registry modes; shellcheck clean; no behavior change for CI.