feat(e2e): real-ddev e2e suite with golden-image cache + ddev-as-user security inventory - #27
Merged
Merged
Conversation
Planning record for the recurring idea of running ddev as the developer again. Verdict: not viable. The .ddev/commands/host/ gap (proof-3 H3) is one of four file carriers (host commands, exec-host hooks, add-on actions, compose overrides) that execute agent-authored input with the invoking user's authority; exec-host hooks fire on the developer's own runs, so no argv allowlist or agent-lockout closes the class. Maps the six closed ddev-labeled GitHub issues (#15 #17 #18 #20 #21 #25) to pain classes: 4/6 would vanish structurally, #17 (git dubious ownership) is UID-separation pain no ddev-user switch touches. In-model answer stays dev-owned projects (disable_settings_management).
e2e-ddev (make e2e-ddev): real ddev against the real docker-rootless daemon inside the systemd test container. Golden-image cache via docker commit (inner daemon + ddev + pulled images, kit and projects stay fresh); label-keyed on ddev version + format, TTL 14d, --fresh knob. Catalog DD0-DD14 mapped to the closed ddev issues (#18 #20 #21 #25), the sandbox 10a TODOs and the 2026-08-22 burn-in findings (ddev config g+w gap, composer create-project exit 23). Tiers: skeleton, real-site (camino fixture, 7.1), bare-origin git flow (7.2), burn-in findings (7.3). Not part of e2e-all/merge gate; CI weekly in phase 3.
Harvested from the 2026-08-22 burn-in install (local/TEST-PROJECT-INSTALL log): real TYPO3 v14.3.6 + typo3/theme-camino site, db dump 85 KB demo data only. site/ is the pristine master (burn-in tracked set, byte-ident except three documented deviations: no fixed ddev name:, !/public/ index.php gitignore exception so clones boot, dump as sidecar db.sql.gz). make-bare-origin.sh builds the per-run origin (main + top-level/settings /ddev-tree branches, incl. tracked .ddev/config.yaml churn and a custom host command) - ShellCheck clean, clone + branch switches verified. Implements ddev-e2e-test.md 7.1 fixture intake + 7.2 generator; phases 2b/2c warm-up integration and DD checks remain.
e2e_start_container can now run an existing image without building - the golden-image warm-start path of the ddev e2e suite. The setuid guard stays on the build path only (a golden image was guarded when it was built; the ddev suite re-asserts the bit on every boot in DD0).
Implements ddev-e2e-test.md phases 1+2: label-keyed golden image (kit.e2e.ddev.version/format/site/built; docker commit after quiesce), cold build via setup-container-backend.sh + cached ddev tarball + warm-up pulls, warm start from the image, kit install from the repo bind mount on every run. Catalog DD0-DD14: cache gates, upgrade-style install, handover + dev-owned bootstrap (dd2), transports, container identity, describe parity, db round-trip, lifecycle + EPERM sweep, camino site tier (DD10/11), bare-origin git flow incl. the handover EPERM tripwire (DD12), burn-in findings as tripwires (DD13) and create-project (DD14). Makefile/workflow wiring follows separately.
make e2e-ddev / make e2e-ddev-fresh (ARGS passthrough like e2e-rootless); run-ddev.sh added to the chmod lists of BOTH workflows and to the test-workflows required set (AGENTS.md rule). Not part of e2e-all - the suite builds a GB-sized golden image and must not gate every PR; the dedicated weekly workflow follows in phase 3.
$2 sat inside a single-quoted segment and reached the dev shell literally; docker exec sh -c has no positional params there, so the inner sh -c received 'cd <dir> && ' with an empty tail and died with 'Syntax error: end of file unexpected'. Expand $1/$2 while building the string (the dev shell must see the full inner command).
e2e_cleanup keeps the container only when failures>0; the cold-build error exits (provisioning, warm-up, kit install) bypassed the check() counter, so E2E_DEBUG=1 still destroyed the evidence. Increment the counter in every container-phase fail-exit path.
The e2e container's own uid_map tops out at 65536 host-side slots; the subuid window for the inner rootless daemon cannot cover uid 65534 (nobody) without colliding with the daemon user's own uid in the map. nginx-fpm chowns its dirs to exactly 65534 and dies with EINVAL, so every suite project (warm-up, dd2) now pins apache-fpm (www-data 33, well inside the window) like the camino fixture already does.
docker commit has no --label flag; labels are Dockerfile instructions passed through --change.
is-active can still read 'activating' when the socket already exists (dockerd Type=notify) - poll like the socket wait. 'ddev version' prints a table whose first line is the header - grep the version anywhere in the output.
docker commit silently drops character-device nodes; an overlayfs store is full of them (whiteouts = file deletions in image layers). The committed golden store had ZERO whiteouts, so layer deletions were void: /etc/mysql/mariadb.cnf (deleted by the ddev image build) reappeared, overrode the ddev socket config, and every project db died with 'Bind on unix socket: Permission denied' on /run/mysqld (mysql:999). Diagnosed via ddev debug test (setgroups 65534 noise) and mariadbd --print-defaults. The store now lives on a named outer-daemon volume (excluded from commit, persists across runs, no copy involved); fresh store on rebuild; new DD0 canary counts whiteout char devices.
Neither commit nor a named volume preserved the inner image store faithfully (layer deletions void - mariadb.cnf ghost, db dead on socket bind). v2: golden image ships system state only; after warm-up the inner images are exported with docker save to a host-side gitignored tarball (keyed ddev version + format) and the store is deleted from the container FS before commit. Warm starts docker load the tarball into a fresh store - the exact unpack path that produced working images in the cold build. New DD0 canary: mariadb.cnf must be absent from the db base image (the precise corruption symptom).
dash only recognizes VAR=x cmd assignments at parse time; through
check()'s "$@" the expanded word becomes a command ('OC_CWD=...:
not found') - the root cause behind the DD6/DD7/DD9 fails. Project
wrappers (OC_DD2 etc.) put the assignment in their own body. ddev
version prints a table (head -1 was the header); the EPERM sweep now
skips dd13-start1.log (the tripwire asserts that exact EPERM) and
covers dd9/dd12 logs.
Strip disable_settings_management after ddev config (ddev's generated yaml carries the commented default; the bootstrap checks must not depend on kit mode state), accept 'Restarted' (ddev restart wording) besides 'Successfully started', and keep expected-EPERM logs (dd13 tripwire, dd2-config burn-in warning) out of the DD7 sweep.
Finding 2 diagnosed: create-project moves via rsync -rltgopD; -o/-g chowns the composer root and only works when the ddev user owns it. Dev-owned mode (root=dev) -> 'chown /var/www/html/. failed: Operation not permitted' -> exit 23 (the burn-in symptom, now deterministic); handover mode (root=opencode) -> exit 0. DD14 asserts both sides; design doc 7.3 Finding 2 updated with cause + fix options.
An aborted/debug run leaves the named container behind; the next run died on the name conflict.
ddev create-project requires a near-empty project; the intentional dev-owned failure leaves the same partial files as the burn-in - clear them (keep .ddev) for the handover-mode retry.
typo3.go:24-27: with undetected TYPO3 the settings writer early-returns (paths flagged skipable) - no root chmod to 0755, no root AdditionalConfiguration.php on start. The old checks asserted pre-1.25 behavior. New checks pin what v1.25.3 actually does (tripwire on change); the #25 mechanics stay covered by DD13 + the camino tier. DD6 bind-read now targets the always-mounted .ddev/config.yaml.
E2E_KEEP keeps the container after ANY run (debug + green alike); the next run removes the leftover itself. Fixes the camino import-db failure: opencode cannot traverse /home/dev (750, dev-owned) to reach the repo bind mount - the fixtures dir is now additionally mounted world-readable at /e2e-fixtures and the two import-db paths use it.
ddev restart prints 'Restarted <proj>' (not 'Successfully started') - accept both in DD11/DD12. The DD12 tripwire sed-delete + handover re-append leaves the tracked .ddev/config.yaml dirty; restore the committed state (fixture commits the dev-owned flag) so the porcelain assertions and config.yaml-touching branch switches stay clean.
….3, composer 2.10.2)
e2e-ddev.yml (named after the make target): workflow_dispatch with site_tier + ddev_version inputs, 60min timeout, shared opencode-binary cache, no golden-image caching in CI (R6: runner disk is ephemeral, every run builds warm). Manual-only first - the weekly schedule follows once the maintainer has validated runtime on a real runner. test- workflows.sh now guards all three workflows incl. run-ddev.sh.
Owner
Author
|
green run in Github CI: https://github.com/steffenmaechtel/opencode-permissions-kit/actions/runs/32656153339 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two related pieces around ddev-as-opencode:
docs/design/ddev-as-user-issues.md— security inventory for therecurring "run ddev as the developer again" idea. Verdict: not viable.
The
.ddev/commands/host/gap (proof-3 H3) is one of four filecarriers of the same class;
exec-hosthooks fire on the developer'sown runs, so no argv allowlist closes it. Maps all six closed ddev
issues (First install move user ddev into opencode ddev #15 git log not accessible for Agent in project #17 vendor/bin/runTests.sh uses wrong ddev #18 ddev launch still error #20 ddev-hosts-add should ignore vendor folder #21 Write permission for group missing after ddev start for TYPO3 special file additional.php #25) to pain classes: 4/6 would vanish
structurally, git log not accessible for Agent in project #17 is UID-separation pain no ddev-user switch touches.
make e2e-ddev— a third e2e suite running the REAL ddev againstthe real inner docker-rootless daemon (the burn-in class fake-ddev
cannot catch). Implements
docs/design/ddev-e2e-test.md:once via
docker commit; repeat runs warm-start. The inner imagestore is cached host-side as a
docker savetarball — committingthe store corrupts overlay whiteouts (verified: layer deletions
void, every cached db image died on an inaccessible unix socket).
tests/e2e/fixtures/camino/): real TYPO3 v14.3burn-in;
make-bare-origin.shgenerates the per-run git origin(main + 3 collision branches).
bootstrap/handover/dev-owned flows, ddev() transports (vendor/bin/runTests.sh uses wrong ddev #18),
container identity (§9.1), describe parity (§10a), db round-trip,
real-site boot (first "site actually answers 200" assert), git-flow
incl. the checkout-EPERM tripwire, and the burn-in findings as
tripwires.
ddev composer create-projectmoves viarsync -rltgopD;-o/-gchowns the composer root, so dev-owned mode × create-project fails
with exit 23 (deterministic now, asserted both ways in DD14).
e2e-ddev.ymlmanual dispatch (site_tier + ddev_versioninputs), 60 min timeout, no image cache in CI; weekly schedule as
follow-up. NOT part of e2e-all / the merge gate.
Test plan
make testgreen (incl. extendedtest-workflows.shover all 3 workflows)make e2e-ddev— skeleton tier: 52/52 PASSE2E_DDEV_SITE=camino make e2e-ddev— full tier: 71/71 PASS(warm start reuses the golden image + image tarball)
e2e-ddevworkflow dispatch on GitHub (runtime + runner check)Notes for reviewers
run-ddev.shshareslib.shwith the other suites (new knobs:E2E_SKIP_BUILD,E2E_KEEPfor fast iteration).ddev composer create-project(Finding 2) — kit-side workaround vs.upstream ddev report is tracked as its own issue.