From 7b53a86a70ce23812c5f9eb7dd4140b209282448 Mon Sep 17 00:00:00 2001 From: Montoya Edu Date: Mon, 18 May 2026 19:49:05 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20easy=20proxy=20doctor=20=E2=80=94=20pre?= =?UTF-8?q?-flight=20config=20&=20network=20diagnostic=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A live outage was caused by a hand-written vhost using a static `upstream {}` block: nginx resolves such hosts at startup and aborts if one fails — taking down every site. Nothing surfaced the risk before a container recreate. Add `easy proxy doctor`, a read-only diagnostic: - static vhost scan (host-side): flags `upstream {}` blocks and the deprecated `listen ... http2` directive - nginx config test (`nginx -t`) when the proxy is running - lists the proxy's Docker networks - exits non-zero only when `nginx -t` fails (a definite startup blocker) - commands/proxy.sh: `doctor` subcommand + help entry - test/doctor.bats: 7 tests; test_helper: mock_docker_nginx_invalid - CLAUDE.md, README.md: command tables; CHANGELOG.md: Unreleased entry `npm run lint` exits 0; the bats suite passes 29/29. Closes #22 Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 9 ++++++ CLAUDE.md | 1 + README.md | 1 + commands/proxy.sh | 69 +++++++++++++++++++++++++++++++++++++++++++ test/doctor.bats | 65 ++++++++++++++++++++++++++++++++++++++++ test/test_helper.bash | 13 ++++++++ 6 files changed, 158 insertions(+) create mode 100644 test/doctor.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index d192582..fdca8a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +- `easy proxy doctor` — read-only pre-flight diagnostic. Flags non-standard + vhost configs (`upstream {}` blocks, deprecated `listen ... http2`), runs the + nginx config test when the proxy is running, and lists the proxy's Docker + networks. + ## [2.0.0] — 2026-05-18 A major release: automatic Let's Encrypt SSL with multi-DNS-provider support. diff --git a/CLAUDE.md b/CLAUDE.md index 01bdbf2..70aa958 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,6 +142,7 @@ easy proxy status # → container ID se running | `easy proxy reload` | Ricarica nginx (dopo new o modifica conf) | | `easy proxy status` | Container ID se running, vuoto se fermo | | `easy proxy id` | Container ID (`docker ps` per nome `easy-proxy`, anche se fermo) | +| `easy proxy doctor` | Diagnosi read-only: vhost non-standard, `nginx -t`, reti del proxy | | `easy proxy start/stop/restart` | Ciclo container | | `easy proxy sh` | Shell interattiva nel container | | `easy proxy log` | `docker logs -f` del container | diff --git a/README.md b/README.md index c8d4af0..40e5a2c 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Run `easy proxy help` for the full list. | `easy proxy reload` | Reload nginx after adding or changing vhosts | | `easy proxy status` | Container id if running, empty if stopped | | `easy proxy id` | Container id (running or stopped) | +| `easy proxy doctor` | Read-only diagnostic: non-standard vhosts, `nginx -t`, proxy networks | | `easy proxy start` / `stop` / `restart` | Container lifecycle | | `easy proxy sh` | Interactive shell in the container | | `easy proxy log` | Follow the container logs | diff --git a/commands/proxy.sh b/commands/proxy.sh index 94f4ef2..3a7d5d4 100644 --- a/commands/proxy.sh +++ b/commands/proxy.sh @@ -45,6 +45,7 @@ function __easy_command_proxy_help { echo " easy proxy new" echo " easy proxy id" echo " easy proxy status" + echo " easy proxy doctor" echo " easy proxy start" echo " easy proxy stop" echo " easy proxy destroy" @@ -222,6 +223,10 @@ chmod 600 /etc/letsencrypt/ionos.ini" docker ps -q -f "name=^${EASY_PROXY_NAME}$" 2>/dev/null return $? fi + if [[ "doctor" == "$2" ]]; then + __easy_command_proxy_doctor + return $? + fi if [[ -z "$2" ]]; then __easy_command_proxy_default return $? @@ -251,6 +256,70 @@ function __easy_command_proxy_create { return $? } +# Read-only pre-flight diagnostic: static vhost analysis (host-side) plus, +# when the proxy is running, the nginx config test and the network list. +# Exits non-zero only when nginx -t fails (a definite startup blocker). +function __easy_command_proxy_doctor { + echo "easy proxy doctor — pre-flight check" + echo + local warnings=0 + + # vhost configs — static analysis, host-side (no Docker needed) + echo "vhost configs (${EASY_DOMAINS_DIR}):" + local confs + confs=$(find "${EASY_DOMAINS_DIR}" -type f -name '*.conf' 2>/dev/null | sort) + if [[ -z "${confs}" ]]; then + echo " no vhost files found" + else + echo " $(printf '%s\n' "${confs}" | wc -l | tr -d ' ') vhost file(s)" + local conf + while IFS= read -r conf; do + if grep -qE '^[[:space:]]*upstream[[:space:]]' "${conf}"; then + echo " WARN ${conf}" + echo " static 'upstream {}' block — resolved at nginx startup; one" + echo " unresolvable host blocks every site. Convert to a variable:" + echo " set \$u ; proxy_pass http://\$u;" + warnings=$((warnings + 1)) + fi + if grep -qE 'listen[^;]*http2' "${conf}"; then + echo " WARN ${conf}" + echo " deprecated 'listen ... http2' — use the 'http2 on;' directive" + warnings=$((warnings + 1)) + fi + done <<< "${confs}" + fi + echo + + # runtime checks — need the running container + local proxy_running + proxy_running=$(easy proxy status) + + echo "nginx config test:" + local nginx_failed=0 + if [[ -z "${proxy_running}" ]]; then + echo " skipped — proxy not running ('easy proxy create' first)" + elif docker exec "${EASY_PROXY_NAME}" nginx -t -c /usr/local/share/easy/nginx.conf >/dev/null 2>&1; then + echo " PASS" + else + echo " FAIL — details: docker exec ${EASY_PROXY_NAME} nginx -t" + nginx_failed=1 + fi + echo + + echo "proxy networks:" + if [[ -z "${proxy_running}" ]]; then + echo " skipped — proxy not running" + else + local nets + nets=$(docker inspect "${EASY_PROXY_NAME}" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null) + echo " ${nets:-(none)}" + fi + echo + + echo "summary: ${warnings} warning(s)" + return "${nginx_failed}" +} + function __easy_command_proxy_default { __easy_command_proxy_help return 1 diff --git a/test/doctor.bats b/test/doctor.bats new file mode 100644 index 0000000..2e23135 --- /dev/null +++ b/test/doctor.bats @@ -0,0 +1,65 @@ +#!/usr/bin/env bats +# Tests for `easy proxy doctor` — the read-only pre-flight diagnostic. + +load test_helper + +setup() { easy_setup; } + +@test "easy proxy help lists the doctor command" { + run easy proxy help + [ "$status" -eq 0 ] + [[ "$output" == *"easy proxy doctor"* ]] +} + +@test "easy proxy doctor reports no vhost files on a clean setup" { + mock_docker_stopped + run easy proxy doctor + [ "$status" -eq 0 ] + [[ "$output" == *"no vhost files"* ]] +} + +@test "easy proxy doctor flags a static upstream block" { + mock_docker_stopped + mkdir -p "$EASY_DOMAINS_DIR/legacy.example.com" + cat > "$EASY_DOMAINS_DIR/legacy.example.com/site.conf" <<'CONF' +upstream backend { server some_container; } +server { listen 80; location / { proxy_pass http://backend; } } +CONF + run easy proxy doctor + [[ "$output" == *"site.conf"* ]] + [[ "$output" == *"upstream"* ]] + [[ "$output" == *"1 warning"* ]] +} + +@test "easy proxy doctor flags a deprecated listen ... http2 directive" { + mock_docker_stopped + mkdir -p "$EASY_DOMAINS_DIR/old.example.com" + echo 'server { listen 443 ssl http2; }' > "$EASY_DOMAINS_DIR/old.example.com/site.conf" + run easy proxy doctor + [[ "$output" == *"http2"* ]] +} + +@test "easy proxy doctor reports zero warnings for a template-style vhost" { + mock_docker_stopped + mkdir -p "$EASY_DOMAINS_DIR/good.example.com" + cat > "$EASY_DOMAINS_DIR/good.example.com/site.conf" <<'CONF' +server { listen 80; location / { set $u http://app:8080; proxy_pass $u; } } +CONF + run easy proxy doctor + [ "$status" -eq 0 ] + [[ "$output" == *"0 warning(s)"* ]] +} + +@test "easy proxy doctor runs the nginx config test when the proxy is running" { + mock_docker_running + run easy proxy doctor + [ "$status" -eq 0 ] + [[ "$output" == *"PASS"* ]] +} + +@test "easy proxy doctor exits non-zero when the nginx config test fails" { + mock_docker_nginx_invalid + run easy proxy doctor + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"* ]] +} diff --git a/test/test_helper.bash b/test/test_helper.bash index da1be5f..f972567 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -55,3 +55,16 @@ mock_docker_stopped() { printf '#!/usr/bin/env bash\nexit 0\n' > "$MOCK_BIN/docker" chmod +x "$MOCK_BIN/docker" } + +# Mock `docker`: the proxy is running, but `docker exec` (e.g. `nginx -t`) fails. +mock_docker_nginx_invalid() { + cat > "$MOCK_BIN/docker" <<'MOCK' +#!/usr/bin/env bash +case "$1" in + ps) echo "deadbeefcafe1234" ;; + exec) exit 1 ;; + *) exit 0 ;; +esac +MOCK + chmod +x "$MOCK_BIN/docker" +}