Skip to content

feat(e2e): real-ddev e2e suite with golden-image cache + ddev-as-user security inventory - #27

Merged
steffenmaechtel merged 26 commits into
masterfrom
feature/ddev-e2e-suite
Aug 22, 2026
Merged

feat(e2e): real-ddev e2e suite with golden-image cache + ddev-as-user security inventory#27
steffenmaechtel merged 26 commits into
masterfrom
feature/ddev-e2e-suite

Conversation

@steffenmaechtel

@steffenmaechtel steffenmaechtel commented Aug 22, 2026

Copy link
Copy Markdown
Owner

What

Two related pieces around ddev-as-opencode:

  1. docs/design/ddev-as-user-issues.md — security inventory for the
    recurring "run ddev as the developer again" idea. Verdict: not viable.
    The .ddev/commands/host/ gap (proof-3 H3) is one of four file
    carriers of the same class; exec-host hooks fire on the developer's
    own 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.

  2. make e2e-ddev — a third e2e suite running the REAL ddev against
    the real inner docker-rootless daemon (the burn-in class fake-ddev
    cannot catch). Implements docs/design/ddev-e2e-test.md:

    • Golden-image cache: the provisioned environment is snapshotted
      once via docker commit; repeat runs warm-start. The inner image
      store is cached host-side as a docker save tarball — committing
      the store corrupts overlay whiteouts (verified: layer deletions
      void, every cached db image died on an inaccessible unix socket).
    • Camino fixture (tests/e2e/fixtures/camino/): real TYPO3 v14.3
      • theme, 85 KB demo dump, harvested from the 2026-08-22 manual
        burn-in; make-bare-origin.sh generates the per-run git origin
        (main + 3 collision branches).
    • 15 sections (DD0–DD14): cache gates, upgrade-style kit install,
      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.
    • Diagnosed along the way (design doc §7.3): Finding 2 root cause —
      ddev composer create-project moves via rsync -rltgopD; -o/-g
      chowns the composer root, so dev-owned mode × create-project fails
      with exit 23 (deterministic now, asserted both ways in DD14).
    • CI: e2e-ddev.yml manual dispatch (site_tier + ddev_version
      inputs), 60 min timeout, no image cache in CI; weekly schedule as
      follow-up. NOT part of e2e-all / the merge gate.

Test plan

  • make test green (incl. extended test-workflows.sh over all 3 workflows)
  • make e2e-ddev — skeleton tier: 52/52 PASS
  • E2E_DDEV_SITE=camino make e2e-ddev — full tier: 71/71 PASS
    (warm start reuses the golden image + image tarball)
  • Manual e2e-ddev workflow dispatch on GitHub (runtime + runner check)

Notes for reviewers

  • run-ddev.sh shares lib.sh with the other suites (new knobs:
    E2E_SKIP_BUILD, E2E_KEEP for fast iteration).
  • Known incompatibility documented, not fixed here: dev-owned mode ×
    ddev composer create-project (Finding 2) — kit-side workaround vs.
    upstream ddev report is tracked as its own issue.

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.
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.

@steffenmaechtel steffenmaechtel left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

@steffenmaechtel
steffenmaechtel merged commit 33baa3f into master Aug 22, 2026
6 of 7 checks passed
@steffenmaechtel
steffenmaechtel deleted the feature/ddev-e2e-suite branch August 22, 2026 21:19
@steffenmaechtel

Copy link
Copy Markdown
Owner Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant