perf(deploy): layer-incremental publish path + image diet for make redeploy - #3101
Conversation
…deploy (#2999) make redeploy moved ~40 GB of disk I/O per code edit: every source change busted the sandbox image id, and k3s-import then docker-save'd + ctr-import'd the full ~9.3 GB image once per tag (latest + sha). That serialization churn is also the btrfs chunk-over-allocation engine behind the recurring DiskPressure wedges. Publish path (issue #2999): - New loopback-only registry flow: `make registry-setup` runs a registry:2 container bound to 127.0.0.1 and points k3s at it via registries.yaml; `make k3s-push` docker-pushes the registry-subset images and pre-pulls them via crictl. Both legs are layer-aware, so a code-only rebuild moves tens of MB instead of full images. Measured: 31 s cold gateway push, 0.03 s for a second tag of the same image. - EGG_REGISTRY_IMAGES defaults to gateway/orchestrator/litellm. The sandbox image is EXCLUDED by default: it bakes private repo content, so it never touches a registry unless the operator opts in. It publishes via k3s-import instead, which now serializes each image ONCE (single multi-tag tarball) instead of once per tag. push-egg-images.sh hard-refuses non-loopback registries, no override. - EGG_IMAGE_REGISTRY= (empty) falls back to the pure save+import flow; CI pins that (plus BUILD_JOBS=1) and keeps its inline import step. Image diet: - .dockerignore now excludes .claude/ worktrees, .egg-state/ and tool caches: the `COPY . /opt/egg-runtime/` layer drops 1.91 GB -> 23 MB, which is the entire per-edit delta in the new flow. - sandbox/Dockerfile restores persist_system_dirs to / with a COPY --from=repo-deps of the new /opt/egg-system-dirs base instead of cp -a'ing a tree the image already contains, removing a ~2 GB duplicate layer. Sandbox image: 9.29 GB -> 5.33 GB. - The four image builds now run under make -j (BUILD_JOBS, default 4). Disk hygiene (levers B + C of #2999): - reap-stale-egg-images.sh grows three scopes: stale-tag reap in the docker daemon store + BuildKit cache cap (EGG_BUILDKIT_CACHE_CAP, default 40GB), stale-manifest reap + garbage-collect in the local registry, and a btrfs chunk-allocation check that warns under 16 GiB unallocated and auto-balances under 4 GiB. Containerd reap understands per-image authoritative prefixes (registry-qualified vs bare) and treats non-authoritative leftovers as stale, digest-guarded as before. - New `make btrfs-reclaim` target (scripts/btrfs-reclaim.sh) as the manual runbook command for the over-allocation wedge. Fixes #2999
This comment has been minimized.
This comment has been minimized.
…invocation The script's containerd-reap awk block was refactored to receive its regex inputs via ENVIRON[] (KEEP_TAG, IMAGE_RE, PREFIX_ALT_RE, AUTH_REG_RE, AUTH_BARE_RE) instead of -v flags, because gawk escape-processes -v values and drops the backslash in 'docker\.io/...'. The TestReapAwkDigestGuard helpers still pattern-matched the old 'awk -v keep="$KEEP_TAG" -v image_re="$IMAGE_RE"' invocation and fed values via -v, so all five tests failed with 'could not find awk block'. Update _extract_awk_program to anchor on the AUTH_BARE_RE env prefix (specific enough to skip the later docker-store awk), and update _run_awk to pass the five env-driven regex fragments mirroring the no-registry case the synthetic listings exercise.
Autofix tracking{"Test/Unit Tests": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns. This PR is scoped to deployment infrastructure (Docker .dockerignore, parallel image builds, loopback registry publish path, btrfs reclaim, image reaping) and doesn't touch agent prompts, LLM call sites, model identifiers, or output-parsing pipelines.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review
Substantial perf/disk-hygiene refactor of the make redeploy publish path. I traced the diff end-to-end against the existing Makefile/scripts and the consuming code paths (deploy sed rewrites, pre-flight check, post-deploy reap, CI workflow). The implementation is careful and well-commented, the safety invariants from the prior reap script (full-four-image safety gate, digest guard) are preserved, and the privacy guard around off-host registries is strong. No security or correctness blockers.
Detail below — all non-blocking.
What I verified end-to-end
- Three configurations are consistent: default hybrid (registry-subset = gateway/orch/litellm; sandbox via import), full opt-in (
EGG_REGISTRY_IMAGESincludes egg-sandbox), and no-registry (EGG_IMAGE_REGISTRY="", CI). For each, theimage_tagsmacro, thedeploytarget's sed rewrites, thecheck-egg-images-present.shlookup arm, and thereap-stale-egg-images.shauthority logic all line up. - Containerd reap auth logic: a bare
docker.io/library/egg-gateway:KEEP_TAGref under hybrid mode (where egg-gateway is in the registry subset) is not treated as authoritative — it falls into the candidate list, but the digest guard then spares it because it shares the authoritativelocalhost:5000/egg-gateway:KEEP_TAGdigest. That's the right outcome and matches the comment. - Privacy guard:
push-egg-images.shvalidates${REGISTRY%%:*}againstlocalhost/127.0.0.1and refuses anything else with no override. Thesetup-local-registry.shscriptdocker run -p 127.0.0.1:${PORT}:5000so even host-internal IPs other than loopback can't reach the registry. - CI path:
EGG_IMAGE_REGISTRY=""correctly forcesEGG_IMAGE_PREFIX=""→reg_prefixreturns empty → bare tags everywhere →check-egg-images-present.shskips the registry HEAD check → matches the inlinedocker save | ctr importpipeline. CI's reap call also passes""as registry, so the script'sREGISTRY_SUBSETends up empty and the no-registry behavior is preserved bit-for-bit.
Non-blocking suggestions
1. Testing gap for the new reap paths
tests/scripts/test_reap_stale_egg_images.py was updated to track the awk extractor change (good — the AUTH_BARE_RE=...awk ' anchor is robust), but the existing test cases all exercise only the no-registry case (REGISTRY="", AUTH_REG_RE='^$'). The PR description acknowledges the hybrid path was exercised by a one-time manual dry-run.
The hybrid logic is the heart of the new behavior — registry-subset authoritative form, bare-prefix non-authoritative form on subset images, mixed authority across IMAGES[], the auth-aware safety-gate prefix selection (reap-stale-egg-images.sh:128-137), and the digest guard's interaction with refs that match match_re but neither auth_reg_re nor auth_bare_re. A regression in any of those would silently stop reaping rather than mis-reap (so it's not destructive), but a slow disk fill-up creeping back is the exact thing #2999 was about. Worth a TestReapHybridMode class with at least:
- registry-subset image kept via authoritative
localhost:5000/...ref, bare leftoverdocker.io/library/...:KEEP_TAGwith same digest → bare ref left alone (digest guard). - registry-subset image kept via authoritative ref, bare stale
docker.io/library/...:OLDwith different digest → bare ref reaped. - bare-subset image (egg-sandbox) authoritative as bare, registry-qualified leftover with same digest → registry ref left alone.
The docker-store reap and registry-side reap are likewise untested. Less critical (no destructive failure mode) but worth at least a smoke test that the DOCKER_REF_RE interpolation doesn't accidentally match unrelated images when REGISTRY contains a name like localhost:5000 — the regex anchors on ^ so it's safe, but a future tweak could regress.
2. EGG_REGISTRY_IMAGES="" + non-empty EGG_IMAGE_REGISTRY is a footgun
Makefile:722-730:
k3s-publish: sudo-keepalive
@if [ -n "$(EGG_IMAGE_REGISTRY)" ]; then \
$(MAKE) --no-print-directory k3s-push; \
if [ -n "$(strip $(EGG_IMPORT_IMAGES))" ]; then \
$(MAKE) --no-print-directory k3s-import K3S_IMPORT_IMAGES="$(EGG_IMPORT_IMAGES)"; \
fi; \
else \
$(MAKE) --no-print-directory k3s-import; \
fiIf an operator empties the registry-subset (EGG_REGISTRY_IMAGES="") but leaves EGG_IMAGE_REGISTRY set (thinking "I want all images via import"), k3s-push is still invoked, and push-egg-images.sh:28 : "${3:?$usage}" aborts with usage: — make redeploy fails. The canonical way to "import everything" is EGG_IMAGE_REGISTRY="", but the empty-subset variant fails non-obviously. Suggest:
@if [ -n "$(EGG_IMAGE_REGISTRY)" ] && [ -n "$(strip $(EGG_REGISTRY_IMAGES))" ]; then \
$(MAKE) --no-print-directory k3s-push; \
...and fall through to the all-import branch otherwise.
3. "Restart bounces the service, not the pods" — approximate at best
scripts/setup-local-registry.sh:119-120:
echo " (k3s only reads registries.yaml at startup; the restart bounces the"
echo " k3s service itself, not the running pods)"K3s's embedded containerd is a child of the k3s server process. systemctl restart k3s does take down containerd; running pod sandboxes survive (their network namespaces are not torn down), but kubelet briefly disconnects and individual container processes can be affected depending on probe configuration. The script is only ever called against a fresh k3s (no egg pods deployed yet), so this is academic in practice — but the operator-facing claim is more confident than the behavior warrants. Suggest softening to e.g. "the restart is brief; running pod containers reattach when containerd comes back" — or just remove the parenthetical so an operator who does run it against a populated cluster isn't surprised.
4. Parallel docker builds still ship repo-deps/ to non-sandbox contexts
The .dockerignore tightening is great (1.91 GB → 23 MB on the COPY . layer), but repo-deps/ (generated by prepare-sandbox-build-context.py, only used by sandbox/Dockerfile) is not in .dockerignore. Each of the four parallel docker build invocations sends it as part of the context. On a typical checkout with full webapp/mobile/etc. that's 50-200 MB of context the gateway/orchestrator/litellm builds don't read.
Two options:
- Add
repo-deps/to.dockerignoreand override per-image with asandbox/.dockerignore(Docker reads the dockerfile-adjacent one when-fpoints outside the context root — but only in BuildKit, and even then it's the combined effect). Simplest: ignorerepo-deps/in the root and useCOPY --from=from a separate prep stage in sandbox. - Or, easier, accept the cost — it's small relative to the ~5 GB sandbox base layers and the savings here would be a one-time optimization.
Not worth blocking the PR over; flagging because the diet effort is what motivated the rest of #2999.
5. Redundant :latest push when EGG_IMAGE_TAG == "latest"
scripts/push-egg-images.sh:71-77:
for image in "${IMAGES[@]}"; do
echo ">>> pushing ${REGISTRY}/${image}:${TAG}"
docker push "${REGISTRY}/${image}:${TAG}"
echo ">>> pushing ${REGISTRY}/${image}:latest"
docker push "${REGISTRY}/${image}:latest"
doneWhen EGG_IMAGE_TAG == "latest" (the git describe fallback outside a git checkout), the two pushes hit the same ref. The second is a manifest-only no-op so it's harmless, but logs are misleading. Mirror the Makefile's k3s-import pattern:
tags=("$TAG")
[ "$TAG" != "latest" ] && tags+=("latest")
for image in "${IMAGES[@]}"; do
for tag in "${tags[@]}"; do
docker push "${REGISTRY}/${image}:${tag}"
done
done6. Cross-script image-set duplication (pre-existing, not introduced here)
EGG_ALL_IMAGES in the Makefile, ALL_IMAGES in check-egg-images-present.sh, IMAGES in reap-stale-egg-images.sh — three copies of the same list, with comments asking to keep them in sync. The PR doesn't make this worse and explicitly calls out the contract in the comments, but adding a 5th image down the road still requires three coordinated edits. Out of scope for this PR; worth a follow-up issue.
Things I checked and found fine
- The
escape_recharacter class (s/[][\\.*^$+?(){}|]/\\&/g) is correct —]first,\\as literal backslash, no/(the comment justification holds). - The
(A && B) || Cexit gate atreap-stale-egg-images.sh:278works correctly for all four input combinations. - The awk programs use
ENVIRON[](not-v) for the regex fragments containing\.— the test extractor's anchor (AUTH_BARE_RE="$AUTH_BARE_RE"\s+awk\s+'(.+?)'\s*<<<) uniquely identifies the containerd reap block and won't drift onto the later docker-store awk. - The
mkdir -p /opt/prebuilt-deps /opt/egg-system-dirsinsandbox/Dockerfile:65makes the Stage 3COPY --from=repo-deps /opt/egg-system-dirs/ /safe even when nopersist_system_dirswere declared — empty source dir, no-op copy. entrypoint.py:858-863keeps the__egg_system_dirs__skip for backward compat with images built before this change. Correct: a current entrypoint running against an older image would otherwise restore system dirs into a repo mount.- The CI workflow's existing inline import is untouched; with
EGG_IMAGE_REGISTRY=""set as a job env, the rest of the make machinery (sed rewrites, pre-flight check, reap) degrades correctly to the pre-registry behavior. - The registry-side reap's
manifest_digestfunction's|| trueis load-bearing underset -e -o pipefail; the comment correctly identifies why.
Approving. The non-blocking items above (especially #1 and #2) are worth a follow-up but don't gate this merge.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- TestReapHybridMode: 7 tests pinning the auth-aware reap logic across the hybrid-mode prefix split (registry-subset authoritative ref form, bare-subset authoritative on the sandbox, digest guard on shared-ID non-authoritative leftovers, latest-via-authoritative-prefix protection). - k3s-publish: empty EGG_REGISTRY_IMAGES with a non-empty EGG_IMAGE_REGISTRY now falls through to the all-import branch instead of failing in push-egg-images.sh's positional-args guard. - push-egg-images.sh: skip the redundant :latest push when EGG_IMAGE_TAG == "latest" (git describe fallback outside a checkout). - .dockerignore: exclude repo-deps/ (only sandbox uses it). Mirror via sandbox/Dockerfile.dockerignore (BuildKit per-Dockerfile override) so the sandbox build keeps repo-deps/ in its context. Drops 50-200 MB of context transfer from each non-sandbox build. - setup-local-registry.sh: soften the "restart bounces the service, not the pods" claim — k3s restart takes containerd down with it; running pod containers reattach but aren't fully insulated.
|
Thanks for the thorough review. Addressing the non-blocking suggestions: 1. Testing gap for the new reap paths — 2. 3. "Restart bounces the service, not the pods" — 4. Parallel docker builds shipping 5. Redundant 6. Cross-script image-set duplication — — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns. The re-review delta (fe96364..6bcaf26, single commit) is scoped to deployment infrastructure: .dockerignore (plus a sandbox/Dockerfile.dockerignore BuildKit override), Makefile k3s-publish gating, push-egg-images.sh :latest-push dedup, setup-local-registry.sh wording softening, and new TestReapHybridMode pytest cases for the reap script. Nothing in the delta touches agent prompts, LLM call sites, model identifiers, the Agent SDK boundary, or output-parsing pipelines.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
Re-reviewed the delta fe96364..6bcaf26 (the "Address review feedback" commit). All five non-blocking suggestions from the previous review were addressed cleanly, and the deferral of #6 was filed as a follow-up. I traced each fix against the production code paths it touches; CI is green on every required check.
Verification of previous-review items
-
TestReapHybridMode(suggestion #1) — Added with 7 cases that pin the hybrid-mode reap logic against the script's auth-aware regex construction:- subset image, bare leftover sharing digest → spared (digest guard)
- subset image, bare stale with distinct digest → reaped
- bare-subset (sandbox), registry-qualified leftover sharing digest → spared
- bare-subset, registry-qualified leftover distinct digest → reaped
- mixed stale refs across all four images → all reaped
- authoritative-form stale refs with distinct digests → reaped
:latest-on-authoritative-prefix sharing-digest protection (a bonus case beyond what I asked for)
The
_run_awkhelper is generalized correctly:registry=""/registry_subset=()keeps the existing no-registry tests' behavior bit-for-bit (verified by reading the existing five test classes — their call sites are unchanged). With non-empty kwargs, thePREFIX_ALT_RE / AUTH_REG_RE / AUTH_BARE_REconstruction attests/scripts/test_reap_stale_egg_images.py:88-98mirrorsreap-stale-egg-images.sh:88-114line-for-line, and the docstring explicitly points at both line ranges to keep the test in sync on future tweaks. Docker-store and registry-side reap deferrals are correctly justified (no destructive failure mode, would need a different harness). -
Empty-subset footgun (suggestion #2) —
Makefile:723now requires bothEGG_IMAGE_REGISTRYAND non-emptyEGG_REGISTRY_IMAGESfor the push branch; otherwise falls through to barek3s-import(which defaultsK3S_IMPORT_IMAGES := $(EGG_ALL_IMAGES)at line 801). Traced the empty-subset case through the rest of the pipeline:reg_prefix(X)returns empty for every X →deploysed rewrites produce bareegg-X:$TAGtagscheck-egg-images-present.shputs every image intoCONTAINERD_IMAGES(no registry HEAD check)k3s-importimports asdocker.io/library/egg-X:$TAG, which k3s resolves bare names to
All three legs degrade consistently to the no-registry path. Good.
-
Registry restart wording (suggestion #3) —
setup-local-registry.sh:119-120softened to "the restart is brief and running pod containers reattach when containerd comes back." Accurate. -
Per-Dockerfile dockerignore for
repo-deps/(suggestion #4) —repo-deps/added to root.dockerignore, andsandbox/Dockerfile.dockerignoremirrors the root minus that one line. Verified withdiff— the only content differences are the documented header comments and the singlerepo-deps/exclusion. The KEEP-IN-SYNC comment is in both files. -
Redundant
:latestpush (suggestion #5) —push-egg-images.sh:74-82now uses thek3s-importarray-loop pattern. WhenTAG="latest"(git describefallback outside a checkout), the array stays("latest")and only one push fires. -
Cross-script image duplication (#6) — Deferred to #3102 as flagged.
One new non-blocking observation in the delta
Per-Dockerfile .dockerignore is a BuildKit-only feature; nothing enforces BuildKit.
The new sandbox/Dockerfile.dockerignore is honored only when BuildKit is the active builder. Docker made BuildKit the default in 23.0 (Feb 2023). On Docker 20.10-22.x with no DOCKER_BUILDKIT=1, the legacy builder ignores <Dockerfile>.dockerignore and falls back to the root .dockerignore — which now excludes repo-deps/. The sandbox build's Stage 1 COPY repo-deps/ /tmp/repo-deps/ would then fail with COPY failed: file not found in build context or excluded by .dockerignore.
skills/egg-setup/SKILL.md:33 still documents Docker 20.10+ as the minimum. This PR implicitly bumps that to 23.0+ for anyone not setting DOCKER_BUILDKIT=1. In practice everyone on 2026 Docker is on 23+ (Docker Desktop auto-updates, modern distro packages ship 24+), but the dependency is silent — the failure mode is a confusing COPY failed rather than "you need BuildKit."
Three ways to harden, pick whichever:
- Bump the documented minimum to Docker 23.0 in
skills/egg-setup/SKILL.md - Add
DOCKER_BUILDKIT=1tobuild-gateway / build-orchestrator / build-sandbox / build-litellmin the Makefile (also nudges legacy builds toward modern caching behavior) - Add an early check in
make buildthat errors with a clear "BuildKit required for per-Dockerfile dockerignore (since #2999)" message if the env doesn't have it
The comment at the top of sandbox/Dockerfile.dockerignore ("BuildKit feature") acknowledges awareness, but the runtime path doesn't gate on it.
Optional follow-up: drift test for the two dockerignore files
The KEEP-IN-SYNC contract between root .dockerignore and sandbox/Dockerfile.dockerignore is currently enforced by comments only. A 5-line test that diffs the two files modulo repo-deps/ would catch drift mechanically:
def test_sandbox_dockerignore_mirrors_root():
root = set(read_pattern_lines(".dockerignore"))
sandbox = set(read_pattern_lines("sandbox/Dockerfile.dockerignore"))
assert root - sandbox == {"repo-deps/"}, \
"Drift between root .dockerignore and sandbox/Dockerfile.dockerignore — see KEEP-IN-SYNC comment"Cheap, mechanically enforces the invariant, runs in milliseconds.
Things I checked and found fine
- Test extraction's
re.escape(f"{registry}/")vs script'sescape_reare output-identical for all plausible registry hostnames (localhost,127.0.0.1, hostnames with., hostnames with-) — both escape.and neither escapes:or/or-. Confirmed against the script's sed metachar class[][\\.*^$+?(){}|]and Python 3.7+re.escapesemantics. - The
AUTH_BARE_RE="$AUTH_BARE_RE"\s+awk\s+'(.+?)'\s*<<<anchor in_extract_awk_programstill uniquely identifies the containerd reap block; it doesn't drift onto the later docker-store awk (which usesDOCKER_REF_REinstead). - Existing five test classes (
TestReapAwkDigestGuardand below) still pass through the generalized_run_awkwith default kwargs — verified by reading their call sites; none passregistry=/registry_subset=.
Approving with the BuildKit-version note and the drift-test idea as non-blocking suggestions. The PR is ready to ship.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Pin DOCKER_BUILDKIT=1 on all four build-* targets so the per-Dockerfile sandbox/Dockerfile.dockerignore override is honored on Docker 18.09-22.x (which supports BuildKit but doesn't default to it until 23.0). - Add tests/scripts/test_dockerignore_drift.py: mechanically enforces the KEEP-IN-SYNC contract between root .dockerignore and the sandbox override by diffing pattern sets modulo the single allowed delta (repo-deps/). - Cross-reference the test from the comment headers in both .dockerignore files so future editors find the guard rail. Both addressing non-blocking suggestions in the re-review of PR #3101.
|
Thanks for the re-review. Addressing both non-blocking suggestions: 1. BuildKit dependency for per-Dockerfile Went with option 2 of the three you offered (pin
Applied to all four build sub-targets ( I considered also bumping 2. Drift test for the two dockerignore files — Added
Cross-referenced the test from the comment headers in both — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns. The re-review delta (6bcaf26..f03d895, single commit) is scoped to deployment infrastructure: pinning DOCKER_BUILDKIT=1 on the four build-* Makefile targets, a new tests/scripts/test_dockerignore_drift.py mechanical drift guard, and cross-reference comment updates in both .dockerignore files. Nothing in the delta touches agent prompts, LLM call sites, model identifiers, the Agent SDK boundary, or output-parsing pipelines.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review (delta 6bcaf26..f03d895)
Re-reviewed the single new commit f03d895 ("Address re-review: force BuildKit + drift guard for dockerignore"). Both non-blocking suggestions from my prior review are addressed cleanly; no new issues introduced.
Verification of previous-review items
1. BuildKit dependency (suggestion #1) — fixed. Makefile:535-545 now pins DOCKER_BUILDKIT=1 on build-gateway, build-orchestrator, build-sandbox, and build-litellm. The new comment block at Makefile:527-534 explains the constraint and points future editors at sandbox/Dockerfile.dockerignore. Choosing option 2 (env-var pin) over option 1 (docs bump) or option 3 (early check) is the right call — it makes the contract Makefile-enforced rather than docs-enforced.
Verified the pin reaches every path that builds these images:
- Local:
make build→make -j build-{gateway,orchestrator,sandbox,litellm}(all pinned) - CI:
.github/workflows/test-integration.yml:85invokesmake build, so it inherits the pin - No other
docker buildinvocations remain in the repo's build paths — only documentation examples (docs/guides/deploy-migration.md:63,docs/guides/deployment.md:85-88) that are not executed
2. Drift test (suggestion #2) — fixed. tests/scripts/test_dockerignore_drift.py parses pattern sets from both files (stripping blanks and comments) and asserts the symmetric-difference is exactly {"repo-deps/"}. The assertion structure is the right shape:
(root - sandbox) - ALLOWED_SANDBOX_DELTAempty → no new root exclusion missing from sandboxsandbox - rootempty → no sandbox-only exclusion sneaking inALLOWED_SANDBOX_DELTA.issubset(root)→ catches "someone deleted the override entirely but left the sandbox file"not ALLOWED_SANDBOX_DELTA & sandbox→ catches "someone added repo-deps/ to the sandbox override"
Manually verified the files are in sync today (only delta is repo-deps/). Cross-references from the comment headers of both .dockerignore files point future editors at the test, which closes the loop.
One minor inaccuracy in the new comment block (non-blocking)
Makefile:533-534 says "older Docker (18.09–22.x, which supports BuildKit but doesn't default to it) on the same path." That's true for invoking BuildKit as the builder, but the per-Dockerfile <Dockerfile>.dockerignore feature specifically requires BuildKit ≥ 0.10 (released March 2022). Docker bundles BuildKit, and 18.09–early-22.x shipped older BuildKit (0.3–0.9). So the real floor for this feature is roughly Docker 20.10.20+ / 22.x-late, not 18.09.
In practice this is academic — skills/egg-setup/SKILL.md:33 documents Docker 20.10+ as the minimum, and anyone in 2026 is on Docker 24+. But the comment overstates the support window; if you want to be precise, "older Docker (20.10.x late / 22.x, which ships BuildKit but doesn't default to it)" is more accurate. Not worth a follow-up commit on its own.
Things I checked and found fine
- Drift test handles edge cases correctly: blank lines skipped, comment lines skipped (
.lstrip().startswith("#")handles indented comments too, though there are none), set semantics make order-independence explicit. No false positives on the current files. - The pin doesn't introduce a cache-invalidation regression: BuildKit and legacy builder use separate caches, so first build under BuildKit pays a one-time cost. Subsequent rebuilds reuse the BuildKit cache as designed.
- CI is green on f03d895 (all required checks COMPLETED/SUCCESS).
- The
_read_patternshelper correctly handles!-negation patterns as opaque strings — if anyone adds negations to either file, the test still catches drift on string-equality.
The PR is ready to ship.
— Authored by egg
|
egg review completed. View run logs 11 previous review(s) hidden. |
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
…ault (#3109) * feat(deploy): include egg-sandbox in the registry publish path by default #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 * fix(deploy): restart registry after GC + propagate k3s-push failure 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 * test(reap): cover all-registry default reap behavior 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. * test(reap): end-to-end safety-gate coverage for all-registry mode 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. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Fixes #2999
Problem
make redeploymoved ~40 GB of disk I/O per code edit: any source change busts the sandbox image id (COPY . /opt/egg-runtime/), andk3s-importthendocker saved +ctr imported the full ~9.3 GB image once per tag (:latest+:<sha>). Beyond being slow, that serialization churn is the btrfs chunk-over-allocation engine behind the recurring DiskPressure wedges #2999 documents.Two compounding bugs made it far worse than it had to be:
.claude/(3.1 GB of worktrees),.mypy_cache,.pytest_cache,.egg-state,.hypothesis— the per-editCOPY .layer was 1.91 GB for a 65 MB repo;cp -a'd the persisted system dirs over a tree the image already contained, storing ~2 GB twice.What this PR does
Publish path
make registry-setup(one-time, also run bymake k3s-setup) starts aregistry:2container bound to 127.0.0.1 and points k3s at it viaregistries.yaml;make k3s-pushpushes + pre-pulls. Push and pull are both layer-aware — measured 31 s for the cold 442 MB gateway push, 0.03 s for a second tag of the same image. Nothing is ever published off-host, andpush-egg-images.shhard-refuses non-loopback registries with no override.EGG_REGISTRY_IMAGES = egg-gateway egg-orchestrator egg-litellm): it bakes in private repo content, so it stays on the store-to-storek3s-importpath unless the operator opts in.k3s-importnow serializes each image once (single multi-tag tarball) instead of once per tag, halving its I/O.EGG_IMAGE_REGISTRY=(empty) falls back to pure save+import everywhere; CI pins that (plusBUILD_JOBS=1) and keeps its inline import step unchanged.Image diet
.dockerignoreexclusions: per-editCOPY .layer 1.91 GB → 23 MB.persist_system_dirsmoves to/opt/egg-system-dirs+COPY --fromrestore: kills the ~2 GB duplicate layer. Sandbox image 9.29 GB → 5.33 GB.make -j(BUILD_JOBS, default 4).Disk hygiene (levers B + C of #2999)
reap-stale-egg-images.shgrows three scopes: docker-store stale-tag reap + BuildKit cache cap (EGG_BUILDKIT_CACHE_CAP, 40 GB default), registry stale-manifest reap +garbage-collect --delete-untagged, and a btrfs unallocated-chunk check (warn <16 GiB, auto-balance <4 GiB). The containerd reap understands per-image authoritative ref forms (registry-qualified vs bare) and treats non-authoritative leftovers as stale, digest-guarded as before.make btrfs-reclaim(the make redeploy recurringly wedges local k3s (DiskPressure / 'images not in k3s') — root cause: btrfs chunk over-allocation from image churn #2999 runbook command, first-class).Net effect
Default config (sandbox on import): per-edit redeploy I/O drops from ~40 GB to ~11 GB (one 5.33 GB save+import instead of two 9.29 GB ones) plus ~25 MB of registry traffic for the core images. Opting the sandbox into the registry drops it to ~tens of MB total. Either way the tarball/serialization churn that fragments btrfs is mostly or entirely gone, and the reap + balance hooks bound what remains.
Verification
make -nrenders verified in all three configs (default hybrid / sandbox opt-in / CI no-registry).make buildwith the new Dockerfile: green, sizes above; 194 targeted tests pass (test_docker_setup,test_entrypoint);make lintgreen.make registry-setup'sregistries.yamlwrite + k3s restart, and an end-to-endmake redeployagainst the live cluster.Operator notes
One-time:
make registry-setup(writes/etc/rancher/k3s/registries.yaml, restarts the k3s service — pods keep running). Thenmake redeployas usual. To opt the sandbox into the registry path:EGG_REGISTRY_IMAGES="egg-gateway egg-orchestrator egg-sandbox egg-litellm" make redeploy(or export it).