Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <container>` | 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 |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <container>` | 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 |
Expand Down
76 changes: 76 additions & 0 deletions commands/proxy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <container>"
echo " easy proxy detach <container>"
echo " easy proxy networks [prune]"
Expand Down Expand Up @@ -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 $?
Expand Down Expand Up @@ -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}"
Expand Down
54 changes: 54 additions & 0 deletions test/recover.bats
Original file line number Diff line number Diff line change
@@ -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"
}
17 changes: 17 additions & 0 deletions test/test_helper.bash
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,20 @@ esac
MOCK
chmod +x "$MOCK_BIN/docker"
}

# Mock `docker` for `recover`: `inspect <container>` returns the networks listed
# for that container in the $DOCKER_TOPOLOGY file ("<name> <net> [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"
}
Loading