diff --git a/VERSION b/VERSION index 2678ff8..c4475d3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.25 +0.0.26 diff --git a/docs/concepts/ddev-integration.md b/docs/concepts/ddev-integration.md index 4a9214a..7374ced 100644 --- a/docs/concepts/ddev-integration.md +++ b/docs/concepts/ddev-integration.md @@ -24,30 +24,43 @@ is a different world entirely. ### Browser-opening commands (issue #20) -Commands that open a browser — `ddev launch` itself and every wrapper -whose internals spawn `ddev launch ...` (`mailpit` → `launch -m`, the -phpmyadmin/adminer add-ons and custom project host commands → -`launch :`, `xhgui` bare) — cannot run as `opencode`: opening the -browser on WSL2 needs `explorer.exe` / `xdg-open` → `wslview`, i.e. -**Windows interop**, exactly what the `opencode` user must not have (an -`.exe` would run as your Windows user, outside every soft rule). The -`ddev()` function routes the whole class through a split: - -1. The command runs **as `opencode`** with `DDEV_DEBUG=true`: whatever - internal `ddev launch` child it spawns (bash host command or Go exec) - inherits the flag, prints `FULLURL ` and exits instead of opening - anything. Running as `opencode` is what makes this correct — as - *you*, ddev cannot see the rootless daemon, would decide "not - running" and run its internal `ddev start` on **every** call; the - https/mkcert detection needs the `opencode`-owned CAROOT. A stopped - project is started by that same run. -2. The URL is opened **as you** (`explorer.exe`, falling back to - `xdg-open`) — your interop, your browser. +Commands that open a browser — `ddev launch` itself and its wrappers +(`mailpit`, the phpmyadmin/adminer add-ons, `xhgui`) — cannot run as +`opencode`: opening the browser on WSL2 needs `explorer.exe` / +`xdg-open` → `wslview`, i.e. **Windows interop**, exactly what the +`opencode` user must not have (an `.exe` would run as your Windows +user, outside every soft rule). The `ddev()` function routes the whole +class through a split: + +1. The URL is read from **`ddev describe -j`** run **as `opencode`** + via the sudoers helper — ddev maintains every URL in that document + from the project config, even while the project is stopped (the + upstream-recommended scripting interface, + [ddev/ddev#8771](https://github.com/ddev/ddev/issues/8771)). + `launch` maps to `raw.primary_url` (with `launch`'s argument forms: + path, `:`, full URL), `mailpit` to `raw.mailpit_https_url`, + `xhgui` to `raw.xhgui_https_url` (when enabled), and add-on + commands like `phpmyadmin` to `raw.services..https_url`. + Running as `opencode` is what makes this correct — as *you*, ddev + cannot see the rootless daemon and would run its internal + `ddev start` on **every** call. The JSON is parsed with python3 (a + kit prerequisite). A stopped project is started first, exactly like + ddev's own launch script — but plainly, without the `DDEV_DEBUG` + log flooding the old URL transport needed. +2. Commands whose URL describe does not carry — the built-in + `phpmyadmin` installer prompt (add-on not yet installed), custom + project commands without a matching service — **plain-run as + `opencode`**: output, prompts and exit code pass through + unchanged; only the browser open is skipped. +3. The URL is printed and opened **as you** (`explorer.exe`, falling + back to `xdg-open`) — your interop, your browser. Project-specific browser commands can be added to -`/etc/opencode-permissions-kit/ddev-browser-cmds.conf` (one command name -per line, `#` comments) — any ddev host command that internally calls -`ddev launch` fits the same mechanism. +`/etc/opencode-permissions-kit/ddev-browser-cmds.conf` (one command +name per line, `#` comments) — they open their browser when +`ddev describe` carries a service of the same name +(`raw.services..https_url`); otherwise they plain-run as +`opencode`. Net effect: browser commands in your terminal open the browser without a restart detour; agent-side they still fail interop-blocked (by design — diff --git a/files/opencode-permissions-kit-lib/ddev-as-opencode.sh b/files/opencode-permissions-kit-lib/ddev-as-opencode.sh index 7f3ecbc..ca106e5 100755 --- a/files/opencode-permissions-kit-lib/ddev-as-opencode.sh +++ b/files/opencode-permissions-kit-lib/ddev-as-opencode.sh @@ -33,36 +33,158 @@ _opk_browser_open() { # _opk_ddev_browser : issue #20 + follow-up. Covers every # ddev command that opens a browser — `launch` itself and the wrappers -# whose internals spawn `ddev launch ...` (mailpit: "launch -m", the -# phpmyadmin/adminer add-ons: "launch :", xhgui: "launch "). -# The command runs AS OPENCODE with DDEV_DEBUG=true: whatever internal -# `ddev launch` child it spawns (bash host command or Go exec) inherits -# the flag, prints "FULLURL " and exits instead of opening a browser -# (as opencode it could not — no interop). Output streams to stderr live -# (prompts like the phpmyadmin install question stay interactive) but -# WITHOUT the FULLURL transport lines — they go to the capture file only, -# the clean URL is printed on stdout by this function. DDEV_DEBUG -# survives sudo via the kit's sudoers env_keep. ddev without the FULLURL -# debug contract simply shows its output; the browser then stays closed -# (upgrade ddev). +# that would spawn it (mailpit, the phpmyadmin/adminer add-ons, xhgui). +# The URL is read from `ddev describe -j` run AS OPENCODE (via the +# sudoers helper): running as opencode is what makes it correct — as +# *you*, ddev cannot see the rootless daemon and would decide "not +# running" and run its internal `ddev start` on EVERY call. ddev +# maintains every URL in the describe document from the project config, +# even while the project is stopped (upstream-recommended scripting +# interface, ddev/ddev#8771) — no DDEV_DEBUG log flooding needed +# anymore. A stopped project is started first, exactly like ddev's own +# launch script (whose `ddev start` used to arrive wrapped in debug +# logging). Commands whose URL describe does not carry — the built-in +# phpmyadmin installer prompt (add-on not installed yet), custom +# project commands without a matching service — fall back to a plain +# run AS OPENCODE: output and prompts stay interactive, only the +# browser open is skipped (as opencode there is no interop anyway). +# The URL is printed on stdout and opened AS THE DEVELOPER +# (_opk_browser_open — WSL interop, which the opencode user must not +# have). +_opk_ddev_describe() { + /usr/bin/sudo -u opencode /usr/local/lib/opencode-permissions-kit/bin/ddev-as-opencode describe -j 2>/dev/null || true +} + +# _opk_json_get ...: the string value at .raw..... +# of a `ddev describe -j` document (single line); empty when the path is +# absent or not a string. Keys may be passed as separate arguments or +# dot-joined. python3 is a kit prerequisite (the installer already +# depends on it for its jsonc parsing). +_opk_json_get() { + _opk_gj="$1" + shift + printf '%s' "$_opk_gj" | python3 -c ' +import json, sys +try: + d = json.load(sys.stdin)["raw"] + for k in ".".join(sys.argv[1:]).split("."): + d = d[k] + if isinstance(d, str): + print(d) +except Exception: + pass' "$@" 2>/dev/null +} + +# _opk_scheme_url : the URL variant matching +# the primary URL's scheme (ddev's launch script picks the mailpit port +# the same way), the other variant as fallback — empty when describe +# carries neither. +_opk_scheme_url() { + case "$(_opk_json_get "$1" primary_url)" in + https*) _opk_su="$(_opk_json_get "$1" "$2")" ;; + *) _opk_su="$(_opk_json_get "$1" "$3")" ;; + esac + [ -n "$_opk_su" ] || _opk_su="$(_opk_json_get "$1" "$2")" + [ -n "$_opk_su" ] || _opk_su="$(_opk_json_get "$1" "$3")" + printf '%s\n' "$_opk_su" +} + +# _opk_browser_url : the URL this browser command would +# open, computed from the describe document — empty when the command +# must fall back to a plain run. The `launch` argument handling mirrors +# ddev's launch script: -m switches to the Mailpit URL, -p must run ddev +# itself (ddev prints its phpMyAdmin add-on hint there), `--` ends the +# flags, then one positional — a full URL as-is, : replaces the +# primary URL's port (keeping its scheme), anything else is appended as +# a path. +_opk_browser_url() { + _opk_bj="$1" + shift + _opk_bc="$1" + case "$_opk_bc" in + launch) + shift + _opk_bl_base="$(_opk_json_get "$_opk_bj" primary_url)" + while :; do + case "${1:-}" in + -m|--mailpit|--mailhog) + _opk_bl_base="$(_opk_scheme_url "$_opk_bj" mailpit_https_url mailpit_url)" + ;; + -p|--phpmyadmin) return 0 ;; + --) shift; break ;; + -*) ;; + *) break ;; + esac + shift + done + case "${1:-}" in + "") printf '%s\n' "$_opk_bl_base" ;; + http://*|https://*) printf '%s\n' "$1" ;; + :*) printf '%s\n' "${_opk_bl_base%:[0-9]*}$1" ;; + *) printf '%s\n' "${_opk_bl_base%/}/${1#/}" ;; + esac + return 0 + ;; + mailpit) + _opk_scheme_url "$_opk_bj" mailpit_https_url mailpit_url + return 0 + ;; + xhgui) + if [ "$(_opk_json_get "$_opk_bj" xhgui_status)" = "enabled" ]; then + _opk_scheme_url "$_opk_bj" xhgui_https_url xhgui_url + fi + return 0 + ;; + esac + # phpmyadmin/adminer (+ conf-registered commands): a ddev service of + # the same name carries the URL in describe + _opk_scheme_url "$_opk_bj" "services.$_opk_bc.https_url" "services.$_opk_bc.http_url" +} + _opk_ddev_browser() { - _opk_tmp="${TMPDIR:-/tmp}/opk-ddev-browser.$$" - DDEV_DEBUG=true /usr/bin/sudo -u opencode /usr/local/lib/opencode-permissions-kit/bin/ddev-as-opencode "$@" 2>&1 \ - | tee "$_opk_tmp" | grep -v '^FULLURL ' >&2 - _opk_url="$(sed -n 's/^FULLURL //p' "$_opk_tmp" 2>/dev/null | tail -1)" - rm -f "$_opk_tmp" - if [ -n "$_opk_url" ]; then - printf '%s\n' "$_opk_url" - _opk_browser_open "$_opk_url" + _opk_desc="$(_opk_ddev_describe)" + _opk_url="" + if [ -n "$_opk_desc" ]; then + _opk_url="$(_opk_browser_url "$_opk_desc" "$@")" + fi + if [ -z "$_opk_url" ]; then + # No URL from describe (not a project dir, unknown command, + # add-on not installed): plain run — ddev's own output, prompts + # and exit code pass through. + /usr/bin/sudo -u opencode /usr/local/lib/opencode-permissions-kit/bin/ddev-as-opencode "$@" + return $? + fi + if [ "$(_opk_json_get "$_opk_desc" status)" != "running" ]; then + # Launch-script parity: a stopped project is started first — with + # the same hints a direct `ddev start` prints (bootstrap hint + # before, hosts-file hint after): the developer must not lose the + # "opk ddev-hosts-add" bridge just because OUR arm ran the start. + _opk_bootstrap_hint 2>/dev/null || true + /usr/bin/sudo -u opencode /usr/local/lib/opencode-permissions-kit/bin/ddev-as-opencode start + _opk_brc=$? + _opk_hosts_hint 2>/dev/null || true + if [ "$_opk_brc" -ne 0 ]; then + return "$_opk_brc" + fi + _opk_desc="$(_opk_ddev_describe)" + _opk_url2="$(_opk_browser_url "$_opk_desc" "$@")" + if [ -n "$_opk_url2" ]; then + _opk_url="$_opk_url2" + fi fi + printf '%s\n' "$_opk_url" + _opk_browser_open "$_opk_url" return 0 } # _opk_browser_cmds: command names whose ddev run opens a browser — -# `launch` itself plus the wrappers whose internals spawn -# `ddev launch ...` (mailpit: "launch -m", phpmyadmin/adminer add-ons: -# "launch :" — also matches CUSTOM project host commands of the -# same name, e.g. a phpmyadmin with own ports). Extendable via +# `launch` itself plus the wrappers (mailpit, the phpmyadmin/adminer +# add-ons, xhgui). Their URL comes from `ddev describe -j`: the +# built-ins carry theirs in dedicated fields, add-on commands appear as +# raw.services. — CUSTOM project host commands of the same name +# only open their browser when describe carries a service of that name +# too (otherwise they plain-run as opencode and the browser stays +# closed). Extendable via # /etc/opencode-permissions-kit/ddev-browser-cmds.conf (one name per # line, '#' comments allowed) for project-specific browser commands. _opk_browser_cmds() { diff --git a/files/sudoers.template b/files/sudoers.template index 525a7ca..8ed7848 100644 --- a/files/sudoers.template +++ b/files/sudoers.template @@ -16,9 +16,9 @@ # (third-party UIs like OpenChamber, which pass the password they generate # from --ui-password) does not silently lose its authentication — the # username is the Basic-auth counterpart (server/auth.ts, default -# "opencode"). DDEV_DEBUG rides along for the `ddev launch` URL transport -# (ddev's launch script prints "FULLURL " instead of opening a -# browser — the developer side opens it, see issue #20). +# "opencode"). Browser commands (`ddev launch` & co.) need no extra +# variable anymore: their URLs are read from `ddev describe -j` +# (ddev/ddev#8771). # # Deliberately NOT kept: every other OPENCODE_* variable. Agent-side # config (OPENCODE_CONFIG*, OPENCODE_AUTH_CONTENT, OPENCODE_API_KEY, @@ -26,7 +26,7 @@ # belongs to the opencode user's own environment — env_reset is the # protection here, keeping any of them would let the calling shell # override agent config/permissions per invocation. -Defaults env_keep += "DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME DDEV_DEBUG" +Defaults env_keep += "DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME" # Base RunAs: the wrapper always execs opencode as the opencode user. DEFAULT_USER ALL=(opencode) NOPASSWD: /usr/local/lib/opencode-permissions-kit/bin/opencode * diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index c036bcb..b24f6dc 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -243,28 +243,45 @@ check "4b: ddev() survives the vendor #!/bin/sh wrapper chain into the bash targ E 'mkdir -p /tmp/opk-fakebin && printf "#!/bin/sh\ncase \"\$*\" in \"-u\") echo 4242;; \"-u opencode\") echo 9999;; *) echo 0;; esac\n" > /tmp/opk-fakebin/id && printf "#!/bin/sh\necho \"REAL_DDEV_RAN:\$*\"\n" > /tmp/opk-fakebin/ddev && chmod 755 /tmp/opk-fakebin/id /tmp/opk-fakebin/ddev' check "4b: ddev start still routes through the sudoers helper as opencode" \ E 'sudo -u dev -H env PATH=/tmp/opk-fakebin:/usr/bin:/bin sh -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev start" 2>&1 | grep -q "ddev is not installed"' -# Issue #20 full chain: the browser-command arm computes the URL AS -# OPENCODE via the sudoers helper with DDEV_DEBUG=true (ddev's launch -# prints "FULLURL " instead of opening a browser — inherited by -# INTERNAL `ddev launch` children of wrapper commands like mailpit or -# the phpmyadmin/adminer add-ons) and only the browser open runs as the -# developer. Fake ddev honors the FULLURL contract for the whole class -# and records the caller; no explorer.exe/xdg-open in the container, so -# the function prints the URL. -E 'printf "#!/bin/sh\ncase \"\$1\" in launch|mailpit|phpmyadmin|adminer) echo \"FULLURL https://fake-project.ddev.site as \$(id -un)\";; *) exit 0;; esac\n" | sudo tee /usr/local/bin/ddev >/dev/null && sudo chmod 755 /usr/local/bin/ddev' +# Issue #20 full chain: the browser-command arm reads the URL from +# `ddev describe -j` AS OPENCODE via the sudoers helper (upstream +# guidance, ddev/ddev#8771 — describe carries the URLs even for a +# stopped project) and only the browser open runs as the developer. +# The fake ddev implements the describe contract, records every caller +# into /tmp/fake-ddev-calls, and models the launch-script behavior of +# starting a stopped project (state file flips stopped -> running). +# No explorer.exe/xdg-open in the container, so the arm prints the URL. +E 'printf "#!/bin/sh\nid -un >> /tmp/fake-ddev-calls\ncase \"\$1\" in\n start)\n touch /tmp/fake-ddev-running\n echo FAKE_DDEV_START_RAN\n ;;\n describe)\n if [ -f /tmp/fake-ddev-running ]; then s=running; else s=stopped; fi\n printf \"{\\\"raw\\\":{\\\"status\\\":\\\"%%s\\\",\\\"primary_url\\\":\\\"https://fake-project.ddev.site\\\",\\\"mailpit_https_url\\\":\\\"https://fake-project.ddev.site:8026\\\",\\\"xhgui_status\\\":\\\"disabled\\\",\\\"services\\\":{\\\"phpmyadmin\\\":{\\\"https_url\\\":\\\"https://pma-fake-project.ddev.site\\\"}}},\\\"level\\\":\\\"info\\\",\\\"msg\\\":\\\"fake\\\"}\n\" \"\$s\"\n ;;\n *)\n echo \"FAKE_DDEV_RAN:\$*\"\n ;;\nesac\nexit 0\n" | sudo tee /usr/local/bin/ddev >/dev/null && sudo chmod 755 /usr/local/bin/ddev && rm -f /tmp/fake-ddev-running /tmp/fake-ddev-calls' +check "4b: ddev launch starts a stopped project first (launch-script parity)" \ + E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch /typo3" 2>&1 | grep -q FAKE_DDEV_START_RAN' check "4b: ddev launch computes the URL as opencode and hands it to the developer (issue #20)" \ - E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch /typo3" | grep -qx "https://fake-project.ddev.site as opencode"' + E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch /typo3" 2>/dev/null | grep -qx "https://fake-project.ddev.site/typo3"' +check "4b: describe/start invocations ran as opencode" \ + E 'grep -qx opencode /tmp/fake-ddev-calls' +check_fail "4b: no ddev invocation ran as the developer" \ + E 'grep -qx dev /tmp/fake-ddev-calls' +# Direct-start parity: the arm's internal `ddev start` must print the +# same hosts-file hint a direct `ddev start` prints. A planted (empty) +# /mnt/c Windows hosts file + a custom-tld project fixture make +# _opk_hosts_hint fire; the stopped state file forces the internal start. +E 'sudo mkdir -p /mnt/c/Windows/System32/drivers/etc && sudo touch /mnt/c/Windows/System32/drivers/etc/hosts && mkdir -p /tmp/opk-hint-proj/.ddev && printf "name: hint-proj\nproject_tld: local\n" > /tmp/opk-hint-proj/.ddev/config.yaml && sudo rm -f /tmp/fake-ddev-running' +E 'sudo -u dev -H bash -c "cd /tmp/opk-hint-proj && . /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch" > /tmp/opk-hint-out 2>&1' +check "4b: the internal start prints the hosts-file hint (direct-start parity)" \ + E 'grep -q "hint: these hostnames are missing" /tmp/opk-hint-out' +check "4b: the hint offers the ready-made opk ddev-hosts-add command" \ + E 'grep -q "opk ddev-hosts-add hint-proj.local" /tmp/opk-hint-out' +E 'sudo rm -rf /mnt/c /tmp/opk-hint-proj /tmp/opk-hint-out' check "4b: ddev mailpit routes through the browser arm (issue #20 follow-up)" \ - E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev mailpit" | grep -qx "https://fake-project.ddev.site as opencode"' -check "4b: ddev phpmyadmin routes through the browser arm (issue #20 follow-up)" \ - E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev phpmyadmin" | grep -qx "https://fake-project.ddev.site as opencode"' -check "4b: launch does not run ddev as the developer (no spurious internal start)" \ - E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch" | grep -qv "as dev"' -check_fail "4b: FULLURL transport lines stay off the visible output (stdout AND stderr)" \ - E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch" 2>&1 | grep -q "^FULLURL"' -check "4b: stdout carries exactly the clean URL line" \ - E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch" 2>/dev/null | grep -qx "https://fake-project.ddev.site as opencode"' -E 'sudo rm -f /usr/local/bin/ddev' + E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev mailpit" 2>/dev/null | grep -qx "https://fake-project.ddev.site:8026"' +check "4b: ddev phpmyadmin opens the describe service URL (issue #20 follow-up)" \ + E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev phpmyadmin" 2>/dev/null | grep -qx "https://pma-fake-project.ddev.site"' +check "4b: xhgui without a describe URL plain-runs as opencode (own output)" \ + E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev xhgui" 2>/dev/null | grep -qx "FAKE_DDEV_RAN:xhgui"' +check_fail "4b: the launch arm never executes ddev launch itself (URL from describe)" \ + E 'sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch" 2>&1 | grep -q FAKE_DDEV_RAN' +check "4b: stdout carries exactly the clean URL line (running project, no start output)" \ + E 'test "$(sudo -u dev -H bash -c ". /usr/local/lib/opencode-permissions-kit/ddev-as-opencode.sh; ddev launch" 2>/dev/null | wc -l)" = 1' +E 'sudo rm -f /usr/local/bin/ddev /tmp/fake-ddev-running /tmp/fake-ddev-calls' echo "" echo "--- 4c. .ddev handover to the opencode user (ddev-working) ---" diff --git a/tests/test-container-backend.sh b/tests/test-container-backend.sh index 157621b..7b07cee 100755 --- a/tests/test-container-backend.sh +++ b/tests/test-container-backend.sh @@ -99,8 +99,10 @@ echo "======================================" echo "" echo "-- sudoers.template structure --" -check "template keeps DOCKER_HOST + XDG_RUNTIME_DIR + OPENCODE_SERVER_PASSWORD + USERNAME + DDEV_DEBUG env_keep" \ - grep -Fq 'env_keep += "DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME DDEV_DEBUG"' "$SUDOERS" +check "template keeps DOCKER_HOST + XDG_RUNTIME_DIR + OPENCODE_SERVER_PASSWORD + USERNAME env_keep" \ + grep -Fq 'env_keep += "DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME"' "$SUDOERS" +check "template has NO DDEV_DEBUG env_keep (describe URL transport needs none)" \ + grep_absent -Fq 'DDEV_DEBUG' "$SUDOERS" check "template has NO OPENCODE_LAUNCH_CWD env_keep" \ grep_absent -Fq 'OPENCODE_LAUNCH_CWD' "$SUDOERS" check "template has NO docker-group block sentinels" \ diff --git a/tests/test-ddev-as-opencode.sh b/tests/test-ddev-as-opencode.sh index 85ba34e..311dde5 100755 --- a/tests/test-ddev-as-opencode.sh +++ b/tests/test-ddev-as-opencode.sh @@ -150,24 +150,79 @@ check "function execs the kit's sudoers helper for the developer" \ check "function uses 'command ddev' in the opencode branch" \ sh -c "grep -q 'command ddev' \"\$1\"" _ "$FUNC" -# --- 3a. launch special case (issue #20) --------------------------------------- -# `ddev launch` computes the URL AS OPENCODE via the sudoers helper with -# DDEV_DEBUG=true (ddev's launch script then prints "FULLURL " instead -# of opening a browser) and only the browser open runs as the developer — -# as the developer ddev cannot see the rootless daemon and would run its -# internal `ddev start` on every launch. Static checks (the arm calls the -# absolute /usr/bin/sudo — not interceptable in unit tests; the e2e suite -# covers the full chain). -check "launch arm computes the URL as opencode via the helper (issue #20)" \ - sh -c "grep -qF 'DDEV_DEBUG=true /usr/bin/sudo -u opencode /usr/local/lib/opencode-permissions-kit/bin/ddev-as-opencode' \"\$1\"" _ "$FUNC" -check "launch arm extracts the FULLURL line (ddev debug contract)" \ - sh -c "grep -qF \"s/^FULLURL //p\" \"\$1\"" _ "$FUNC" -check "FULLURL transport lines are filtered from the visible output" \ - sh -c "grep -qF \"grep -v '^FULLURL ' >&2\" \"\$1\"" _ "$FUNC" -check "launch arm opens the URL with the developer's interop (explorer.exe/xdg-open)" \ +# --- 3a. browser commands (issue #20) ------------------------------------------ +# `ddev launch` & co read their URL from `ddev describe -j` AS OPENCODE +# via the sudoers helper (upstream guidance, ddev/ddev#8771 — describe +# carries the URLs even for a stopped project) and only the browser open +# runs as the developer — as the developer ddev cannot see the rootless +# daemon and would run its internal `ddev start` on every launch. The +# describe→URL mapping is pure shell (tested functionally below); the +# sudo arms are static checks (the arm calls the absolute /usr/bin/sudo +# — not interceptable in unit tests; the e2e suite covers the full +# chain). +check "browser arm describes the project as opencode via the helper (issue #20)" \ + sh -c "grep -qF 'ddev-as-opencode describe -j' \"\$1\"" _ "$FUNC" +check "browser arm starts a stopped project first (launch-script parity)" \ + sh -c "grep -qF 'ddev-as-opencode start' \"\$1\"" _ "$FUNC" +check "browser arm prints the bootstrap hint before its internal start" \ + sh -c "grep -qF '_opk_bootstrap_hint' \"\$1\"" _ "$FUNC" +check "browser arm prints the hosts-file hint after its internal start (direct-start parity)" \ + sh -c "grep -qF '_opk_hosts_hint' \"\$1\"" _ "$FUNC" +check "browser arm opens the URL with the developer's interop (explorer.exe/xdg-open)" \ sh -c "grep -q 'explorer.exe' \"\$1\" && grep -q 'xdg-open' \"\$1\"" _ "$FUNC" -check "sudoers env_keep includes DDEV_DEBUG (launch URL transport)" \ - sh -c "grep -q 'env_keep += \"DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME DDEV_DEBUG\"' \"\$1\"" _ "$SUDOERS" +check "browser arm plain-runs commands without a describe URL (prompts stay interactive)" \ + sh -c "grep -qF 'browser open is skipped' \"\$1\"" _ "$FUNC" +check "browser arm parses the describe JSON with python3 (kit prerequisite)" \ + sh -c "grep -q 'python3' \"\$1\"" _ "$FUNC" +check "sudoers env_keep keeps the transport vars" \ + sh -c "grep -q 'env_keep += \"DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME\"' \"\$1\"" _ "$SUDOERS" +check_fail "sudoers no longer keep DDEV_DEBUG (describe transport needs none)" \ + sh -c "grep -q 'DDEV_DEBUG' \"\$1\"" _ "$SUDOERS" +check_fail "function file no longer uses the FULLURL debug contract" \ + sh -c "grep -q 'FULLURL' \"\$1\"" _ "$FUNC" + +# describe→URL mapping, functional: the fixture mirrors the fields ddev +# emits for a running project (config-derived fields exist even when +# stopped). BU [arg] runs _opk_browser_url on the fixture. +DJ='{"raw":{"status":"running","primary_url":"https://fake-project.ddev.site","mailpit_https_url":"https://fake-project.ddev.site:8026","mailpit_url":"http://fake-project.ddev.site:8025","xhgui_status":"enabled","xhgui_https_url":"https://fake-project.ddev.site:8443","xhgui_url":"http://fake-project.ddev.site:8442","services":{"phpmyadmin":{"https_url":"https://fake-project.ddev.site:8036"}}},"level":"info","msg":"fake"}' +DJ_HTTP='{"raw":{"status":"running","primary_url":"http://fake-project.ddev.site:8080","mailpit_https_url":"https://fake-project.ddev.site:8026","mailpit_url":"http://fake-project.ddev.site:8025"},"level":"info","msg":"fake"}' +DJ_NOX='{"raw":{"status":"running","primary_url":"https://fake-project.ddev.site","xhgui_status":"disabled"},"level":"info","msg":"fake"}' +BU() { sh -c '. "$1" && _opk_browser_url "$2" "$3" "$4"' _ "$FUNC" "$1" "$2" "${3:-}"; } +assert_eq "url: bare launch is the primary URL" \ + "https://fake-project.ddev.site" "$(BU "$DJ" launch)" +assert_eq "url: launch appends the path" \ + "https://fake-project.ddev.site/typo3" "$(BU "$DJ" launch /typo3)" +assert_eq "url: launch inserts the slash" \ + "https://fake-project.ddev.site/phpinfo.php" "$(BU "$DJ" launch phpinfo.php)" +assert_eq "url: launch passes it through" \ + "https://elsewhere.example/sub" "$(BU "$DJ" launch https://elsewhere.example/sub)" +assert_eq "url: launch : replaces the primary port" \ + "https://fake-project.ddev.site:8031" "$(BU "$DJ" launch :8031)" +assert_eq "url: launch : on a ported primary strips the old port" \ + "http://fake-project.ddev.site:3000" "$(BU "$DJ_HTTP" launch :3000)" +assert_eq "url: launch -m is the Mailpit URL" \ + "https://fake-project.ddev.site:8026" "$(BU "$DJ" launch -m)" +assert_eq "url: launch --mailpit works too" \ + "https://fake-project.ddev.site:8026" "$(BU "$DJ" launch --mailpit)" +assert_eq "url: launch -m falls back to plain run for -p (ddev prints its hint)" \ + "" "$(BU "$DJ" launch -p)" +assert_eq "url: mailpit is the https variant (https primary)" \ + "https://fake-project.ddev.site:8026" "$(BU "$DJ" mailpit)" +assert_eq "url: mailpit is the http variant (http primary)" \ + "http://fake-project.ddev.site:8025" "$(BU "$DJ_HTTP" mailpit)" +assert_eq "url: xhgui is the xhgui URL when enabled" \ + "https://fake-project.ddev.site:8443" "$(BU "$DJ" xhgui)" +assert_eq "url: xhgui disabled falls back to plain run" \ + "" "$(BU "$DJ_NOX" xhgui)" +assert_eq "url: phpmyadmin comes from the describe service" \ + "https://fake-project.ddev.site:8036" "$(BU "$DJ" phpmyadmin)" +assert_eq "url: adminer without a describe service falls back to plain run" \ + "" "$(BU "$DJ" adminer)" +assert_eq "url: json field extraction of a nested path" \ + "https://fake-project.ddev.site:8036" \ + "$(sh -c '. "$1" && _opk_json_get "$2" services phpmyadmin https_url' _ "$FUNC" "$DJ")" +assert_eq "url: json field extraction of a missing path is empty" \ + "" "$(sh -c '. "$1" && _opk_json_get "$2" services adminer https_url' _ "$FUNC" "$DJ")" # --- 3a-2. browser-command routing (issue #20 follow-up: mailpit/phpmyadmin) --- # Functional against _opk_is_browser_cmd: the default list, the xhgui diff --git a/tests/test-wrapper-validation.sh b/tests/test-wrapper-validation.sh index 9eef4a7..244c35c 100755 --- a/tests/test-wrapper-validation.sh +++ b/tests/test-wrapper-validation.sh @@ -396,10 +396,11 @@ for marker in '#@docker-group-begin' '#@ddev-delegated-begin' '#@ddev-sandbox-be fi done -if grep -q 'env_keep += "DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME DDEV_DEBUG"' "$SUDOERS_FILE" \ +if grep -q 'env_keep += "DOCKER_HOST XDG_RUNTIME_DIR OPENCODE_SERVER_PASSWORD OPENCODE_SERVER_USERNAME"' "$SUDOERS_FILE" \ + && ! grep -q 'DDEV_DEBUG' "$SUDOERS_FILE" \ && grep -q '(opencode) NOPASSWD: /usr/local/lib/opencode-permissions-kit/bin/opencode' "$SUDOERS_FILE" \ && grep -q 'socket-check.sh' "$SUDOERS_FILE"; then - echo " ${GREEN}PASS${NC} sudoers.template keeps base RunAs + socket-check + env_keep" + echo " ${GREEN}PASS${NC} sudoers.template keeps base RunAs + socket-check + env_keep (no DDEV_DEBUG)" passed=$((passed + 1)) else echo " ${RED}FAIL${NC} sudoers.template lost a required rule"