diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f155d2..d7a4505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/). surfaces the startup error if it crashed. `easy proxy create` now runs this check automatically, so it no longer reports success when nginx failed to start. +- `easy proxy recover` — break-glass network recovery. Scans the vhost configs + for backend hostnames, tallies the Docker networks they live on, connects the + proxy to them and restarts. Reports the de-facto edge network (the one most + backends share) and the outliers; `--consolidate` attaches the backends to + the edge network instead. ## [2.1.0] — 2026-05-18 diff --git a/CLAUDE.md b/CLAUDE.md index add31a9..ce21b96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,6 +148,7 @@ easy proxy status # → container ID se running | `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 verify` | Verifica che il proxy sia davvero up; `create` la esegue da solo | +| `easy proxy recover [--consolidate]` | Break-glass: trova le reti dei backend, collega il proxy, riavvia | | `easy proxy attach\|detach ` | Collega/scollega un container alla rete edge `EASY_PROXY_NETWORK` | | `easy proxy networks [prune]` | Mostra le reti del proxy; `prune` scollega quelle non-edge | | `easy proxy start/stop/restart` | Ciclo container | diff --git a/README.md b/README.md index d752506..8998bef 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Run `easy proxy help` for the full list. | `easy proxy id` | Container id (running or stopped) | | `easy proxy doctor` | Read-only diagnostic: non-standard vhosts, `nginx -t`, proxy networks | | `easy proxy verify` | Check the proxy is actually running; `create` runs it automatically | +| `easy proxy recover [--consolidate]` | Break-glass: find the backends' networks, connect the proxy, restart | | `easy proxy attach` / `detach ` | Connect/disconnect a site container to the edge network | | `easy proxy networks [prune]` | Show the proxy's networks; `prune` disconnects non-edge ones | | `easy proxy start` / `stop` / `restart` | Container lifecycle | diff --git a/commands/proxy.sh b/commands/proxy.sh index 1360616..02dae5e 100644 --- a/commands/proxy.sh +++ b/commands/proxy.sh @@ -47,6 +47,7 @@ function __easy_command_proxy_help { echo " easy proxy status" echo " easy proxy doctor" echo " easy proxy verify" + echo " easy proxy recover [--consolidate]" echo " easy proxy attach " echo " easy proxy detach " echo " easy proxy networks [prune]" @@ -235,6 +236,10 @@ chmod 600 /etc/letsencrypt/ionos.ini" __easy_command_proxy_verify return $? fi + if [[ "recover" == "$2" ]]; then + __easy_command_proxy_recover "$3" + return $? + fi if [[ "attach" == "$2" ]]; then __easy_command_proxy_attach "$3" return $? @@ -273,6 +278,77 @@ function __easy_command_proxy_verify { return 1 } +# Break-glass recovery: find the Docker networks the backends live on, connect +# the proxy to them, restart and verify. Reports the de-facto edge network (the +# one most backends share). With --consolidate it attaches the backends to the +# edge network instead of joining the proxy to every backend network. +function __easy_command_proxy_recover { + local consolidate=0 b net line + [[ "$1" == "--consolidate" ]] && consolidate=1 + + if [[ -z "$(easy proxy id)" ]]; then + echo "No ${EASY_PROXY_NAME} container — run 'easy proxy create' first." + return 1 + fi + + echo "easy proxy recover" + + # Backend hostnames referenced statically by the vhost configs. + local backends + backends=$( { + grep -rhoE 'proxy_pass[[:space:]]+https?://[A-Za-z0-9._-]+' "${EASY_DOMAINS_DIR}" 2>/dev/null | sed -E 's#.*//##' + grep -rhoE 'server[[:space:]]+[A-Za-z0-9._-]+' "${EASY_DOMAINS_DIR}" 2>/dev/null | sed -E 's#server[[:space:]]+##' + } | sed -E 's#:[0-9]+$##' | sort -u ) + if [[ -z "${backends}" ]]; then + echo " no backend hostnames found in ${EASY_DOMAINS_DIR}" + return 1 + fi + + # Tally the user-defined networks the backend containers live on. + local netcounts + netcounts=$( + while IFS= read -r b; do + docker inspect "${b}" --format '{{range $k, $v := .NetworkSettings.Networks}}{{println $k}}{{end}}' 2>/dev/null + done <<< "${backends}" | grep -vE '^(bridge|host|none)?$' | sort | uniq -c | sort -rn ) + if [[ -z "${netcounts}" ]]; then + echo " no running backend containers found — nothing to connect." + return 1 + fi + + echo "backend networks (backends sharing each):" + while IFS= read -r line; do echo " ${line}"; done <<< "${netcounts}" + + local edge + if [[ -n "${EASY_PROXY_NETWORK}" ]]; then + edge="${EASY_PROXY_NETWORK}" + echo "edge network: ${edge} (from EASY_PROXY_NETWORK)" + else + edge=$(echo "${netcounts}" | head -1 | awk '{print $2}') + echo "edge network: ${edge} (detected — shared by the most backends)" + echo " tip: export EASY_PROXY_NETWORK=${edge}" + fi + + if [[ ${consolidate} -eq 1 ]]; then + echo "consolidating — attaching every backend to ${edge}:" + while IFS= read -r b; do + docker network connect "${edge}" "${b}" >/dev/null 2>&1 && echo " attached ${b}" + done <<< "${backends}" + docker network connect "${edge}" "${EASY_PROXY_NAME}" >/dev/null 2>&1 + else + echo "connecting ${EASY_PROXY_NAME} to the backend networks:" + local all_nets + all_nets=$(echo "${netcounts}" | awk '{print $2}') + while IFS= read -r net; do + docker network connect "${net}" "${EASY_PROXY_NAME}" >/dev/null 2>&1 && echo " + ${net}" + done <<< "${all_nets}" + fi + + echo "restarting ${EASY_PROXY_NAME}..." + docker restart "${EASY_PROXY_NAME}" >/dev/null 2>&1 + sleep "${EASY_VERIFY_DELAY:-2}" + __easy_command_proxy_verify +} + function __easy_command_proxy_create { if [[ -n "$(easy proxy id)" ]]; then echo "There is already an easy proxy instance named ${EASY_PROXY_NAME}" diff --git a/test/recover.bats b/test/recover.bats new file mode 100644 index 0000000..df15465 --- /dev/null +++ b/test/recover.bats @@ -0,0 +1,54 @@ +#!/usr/bin/env bats +# Tests for `easy proxy recover` — break-glass network recovery. + +load test_helper + +setup() { easy_setup; } + +@test "easy proxy help lists recover" { + run easy proxy help + [ "$status" -eq 0 ] + [[ "$output" == *"easy proxy recover"* ]] +} + +@test "easy proxy recover fails when there is no proxy container" { + mock_docker_stopped + run easy proxy recover + [ "$status" -ne 0 ] +} + +@test "easy proxy recover reports when no backends are found" { + mock_docker_topology + run easy proxy recover + [ "$status" -ne 0 ] + [[ "$output" == *"no backend"* ]] +} + +@test "easy proxy recover detects the edge network and connects the proxy" { + mock_docker_topology + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + export DOCKER_TOPOLOGY="$BATS_TEST_TMPDIR/topology" + printf 'wp ethicnet\napi ethicnet\nlegacy oldnet\n' > "$DOCKER_TOPOLOGY" + mkdir -p "$EASY_DOMAINS_DIR/site-a" "$EASY_DOMAINS_DIR/site-b" + echo 'upstream a { server wp; }' > "$EASY_DOMAINS_DIR/site-a/app.conf" + echo 'upstream b { server api; }' > "$EASY_DOMAINS_DIR/site-a/api.conf" + echo 'upstream c { server legacy; }' > "$EASY_DOMAINS_DIR/site-b/legacy.conf" + run easy proxy recover + [[ "$output" == *"ethicnet"* ]] + grep -q "network connect ethicnet easy-proxy" "$DOCKER_LOG" + grep -q "network connect oldnet easy-proxy" "$DOCKER_LOG" +} + +@test "easy proxy recover --consolidate attaches backends to the edge network" { + mock_docker_topology + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + export DOCKER_TOPOLOGY="$BATS_TEST_TMPDIR/topology" + export EASY_PROXY_NETWORK=ethicnet + printf 'wp ethicnet\nlegacy oldnet\n' > "$DOCKER_TOPOLOGY" + mkdir -p "$EASY_DOMAINS_DIR/s" + echo 'upstream a { server wp; }' > "$EASY_DOMAINS_DIR/s/a.conf" + echo 'upstream c { server legacy; }' > "$EASY_DOMAINS_DIR/s/c.conf" + run easy proxy recover --consolidate + [ "$status" -eq 0 ] + grep -q "network connect ethicnet legacy" "$DOCKER_LOG" +} diff --git a/test/test_helper.bash b/test/test_helper.bash index a7a93a7..c0357f6 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -120,3 +120,20 @@ esac MOCK chmod +x "$MOCK_BIN/docker" } + +# Mock `docker` for `recover`: `inspect ` returns the networks listed +# for that container in the $DOCKER_TOPOLOGY file (" [net...]"). +mock_docker_topology() { + cat > "$MOCK_BIN/docker" <<'MOCK' +#!/usr/bin/env bash +echo "$*" >> "${DOCKER_LOG:-/dev/null}" +case "$1" in + ps) echo "deadbeefcafe1234" ;; + inspect) + line=$(grep "^$2 " "$DOCKER_TOPOLOGY" 2>/dev/null) || exit 1 + for n in ${line#* }; do echo "$n"; done ;; + *) exit 0 ;; +esac +MOCK + chmod +x "$MOCK_BIN/docker" +}