diff --git a/CHANGELOG.md b/CHANGELOG.md index fdca8a7..442df6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/). vhost configs (`upstream {}` blocks, deprecated `listen ... http2`), runs the nginx config test when the proxy is running, and lists the proxy's Docker networks. +- `EASY_PROXY_NETWORK` — when set, `easy proxy create` joins that Docker network + (created if missing), so recreating the proxy keeps its connectivity to the + backends. New commands: `easy proxy attach`/`detach ` to connect a + site to the edge network, and `easy proxy networks [prune]` to audit and clean + up the proxy's network membership. ## [2.0.0] — 2026-05-18 diff --git a/CLAUDE.md b/CLAUDE.md index 70aa958..ca2db3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,6 +110,10 @@ mkdir -p "$EASY_LETSENCRYPT_DIR" "$EASY_DOMAINS_DIR" Aggiungi queste righe a `~/.zshrc` o `~/.bashrc` per averle sempre disponibili. +Opzionale — `EASY_PROXY_NETWORK`: se impostata, `easy proxy create` aggancia il +container a quella rete Docker (creata se non esiste) e ricrearlo non perde la +connettività verso i backend. I siti si collegano con `easy proxy attach`. + ### Build immagine locale Il `Dockerfile` è self-contained (`FROM certbot/certbot:latest`): un solo build, @@ -143,6 +147,8 @@ easy proxy status # → container ID se running | `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 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 | | `easy proxy sh` | Shell interattiva nel container | | `easy proxy log` | `docker logs -f` del container | diff --git a/README.md b/README.md index 40e5a2c..0a75601 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ Run `easy proxy help` for the full list. | `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 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 | | `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 3a7d5d4..0c98835 100644 --- a/commands/proxy.sh +++ b/commands/proxy.sh @@ -46,6 +46,9 @@ function __easy_command_proxy_help { echo " easy proxy id" echo " easy proxy status" echo " easy proxy doctor" + echo " easy proxy attach " + echo " easy proxy detach " + echo " easy proxy networks [prune]" echo " easy proxy start" echo " easy proxy stop" echo " easy proxy destroy" @@ -227,6 +230,18 @@ chmod 600 /etc/letsencrypt/ionos.ini" __easy_command_proxy_doctor return $? fi + if [[ "attach" == "$2" ]]; then + __easy_command_proxy_attach "$3" + return $? + fi + if [[ "detach" == "$2" ]]; then + __easy_command_proxy_detach "$3" + return $? + fi + if [[ "networks" == "$2" ]]; then + __easy_command_proxy_networks "$3" + return $? + fi if [[ -z "$2" ]]; then __easy_command_proxy_default return $? @@ -245,8 +260,19 @@ function __easy_command_proxy_create { echo "Invalid EASY_LETSENCRYPT_DIR" return 1 fi + # Join the edge network when EASY_PROXY_NETWORK is set, so recreating the + # container keeps its connectivity to the backends (see #23). Unset = default. + local network_args=() + if [[ -n "${EASY_PROXY_NETWORK}" ]]; then + if ! docker network inspect "${EASY_PROXY_NETWORK}" >/dev/null 2>&1; then + echo "Creating Docker network ${EASY_PROXY_NETWORK}" + docker network create "${EASY_PROXY_NETWORK}" >/dev/null || return 1 + fi + network_args=(--network "${EASY_PROXY_NETWORK}") + fi docker run -d \ --name "${EASY_PROXY_NAME}" \ + "${network_args[@]}" \ -v "${EASY_DOMAINS_DIR}:/domains" \ -v "${EASY_LETSENCRYPT_DIR}:/etc/letsencrypt" \ -v "${EASY_DIR}/easyhome:/usr/local/share/easy" \ @@ -320,6 +346,69 @@ function __easy_command_proxy_doctor { return "${nginx_failed}" } +# Connect a site container to the edge network so the proxy can reach it. +function __easy_command_proxy_attach { + if [[ -z "$1" ]]; then + echo "Usage: easy proxy attach " + return 1 + fi + if [[ -z "${EASY_PROXY_NETWORK}" ]]; then + echo "EASY_PROXY_NETWORK is not set — set it to the proxy's edge network first." + return 1 + fi + docker network connect "${EASY_PROXY_NETWORK}" "$1" + return $? +} + +# Disconnect a site container from the edge network. +function __easy_command_proxy_detach { + if [[ -z "$1" ]]; then + echo "Usage: easy proxy detach " + return 1 + fi + if [[ -z "${EASY_PROXY_NETWORK}" ]]; then + echo "EASY_PROXY_NETWORK is not set." + return 1 + fi + docker network disconnect "${EASY_PROXY_NETWORK}" "$1" + return $? +} + +# Audit the networks the proxy is joined to; `prune` disconnects it from every +# network other than EASY_PROXY_NETWORK. +function __easy_command_proxy_networks { + if [[ -z "$(easy proxy id)" ]]; then + echo "Proxy container not found — run 'easy proxy create' first." + return 1 + fi + local nets + nets=$(docker inspect "${EASY_PROXY_NAME}" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null) + local -a netlist + read -ra netlist <<< "${nets}" + local net + if [[ "$1" == "prune" ]]; then + if [[ -z "${EASY_PROXY_NETWORK}" ]]; then + echo "EASY_PROXY_NETWORK is not set — refusing to prune (no edge network to keep)." + return 1 + fi + for net in "${netlist[@]}"; do + if [[ "${net}" != "${EASY_PROXY_NETWORK}" ]]; then + echo "disconnecting ${EASY_PROXY_NAME} from ${net}" + docker network disconnect "${net}" "${EASY_PROXY_NAME}" + fi + done + else + echo "${EASY_PROXY_NAME} is connected to:" + for net in "${netlist[@]}"; do + if [[ "${net}" == "${EASY_PROXY_NETWORK}" ]]; then + echo " ${net} (edge)" + else + echo " ${net} (extra — 'easy proxy networks prune' to disconnect)" + fi + done + fi +} + function __easy_command_proxy_default { __easy_command_proxy_help return 1 diff --git a/test/network.bats b/test/network.bats new file mode 100644 index 0000000..44eca8a --- /dev/null +++ b/test/network.bats @@ -0,0 +1,86 @@ +#!/usr/bin/env bats +# Tests for `easy proxy` edge-network management — EASY_PROXY_NETWORK, +# attach/detach, and the proxy network audit/prune. + +load test_helper + +setup() { easy_setup; } + +@test "easy proxy help lists the network commands" { + run easy proxy help + [ "$status" -eq 0 ] + [[ "$output" == *"easy proxy attach"* ]] + [[ "$output" == *"easy proxy networks"* ]] +} + +@test "easy proxy create joins EASY_PROXY_NETWORK and auto-creates it" { + export EASY_PROXY_NETWORK=ethicnet + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + mock_docker_record + run easy proxy create + [ "$status" -eq 0 ] + grep -q "network create ethicnet" "$DOCKER_LOG" + grep -q -- "--network ethicnet" "$DOCKER_LOG" +} + +@test "easy proxy create stays on the default network when EASY_PROXY_NETWORK is unset" { + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + mock_docker_record + run easy proxy create + [ "$status" -eq 0 ] + ! grep -q -- "--network" "$DOCKER_LOG" +} + +@test "easy proxy attach without a container prints usage" { + mock_docker_stopped + export EASY_PROXY_NETWORK=ethicnet + run easy proxy attach + [ "$status" -ne 0 ] + [[ "$output" == *"Usage"* ]] +} + +@test "easy proxy attach without EASY_PROXY_NETWORK fails" { + mock_docker_stopped + run easy proxy attach mysite + [ "$status" -ne 0 ] + [[ "$output" == *"EASY_PROXY_NETWORK"* ]] +} + +@test "easy proxy attach connects a container to the edge network" { + export EASY_PROXY_NETWORK=ethicnet + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + mock_docker_record + run easy proxy attach mysite + [ "$status" -eq 0 ] + grep -q "network connect ethicnet mysite" "$DOCKER_LOG" +} + +@test "easy proxy detach disconnects a container from the edge network" { + export EASY_PROXY_NETWORK=ethicnet + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + mock_docker_record + run easy proxy detach mysite + [ "$status" -eq 0 ] + grep -q "network disconnect ethicnet mysite" "$DOCKER_LOG" +} + +@test "easy proxy networks lists the proxy networks and flags the extras" { + export EASY_PROXY_NETWORK=ethicnet + mock_docker_multinet + run easy proxy networks + [ "$status" -eq 0 ] + [[ "$output" == *"ethicnet"* ]] + [[ "$output" == *"edge"* ]] + [[ "$output" == *"extra"* ]] +} + +@test "easy proxy networks prune disconnects the proxy from non-edge networks" { + export EASY_PROXY_NETWORK=ethicnet + export DOCKER_LOG="$BATS_TEST_TMPDIR/docker.log" + mock_docker_multinet + run easy proxy networks prune + [ "$status" -eq 0 ] + grep -q "network disconnect bridge easy-proxy" "$DOCKER_LOG" + grep -q "network disconnect legacynet easy-proxy" "$DOCKER_LOG" + ! grep -q "network disconnect ethicnet" "$DOCKER_LOG" +} diff --git a/test/test_helper.bash b/test/test_helper.bash index f972567..a714c54 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -27,7 +27,7 @@ easy_setup() { export PATH="$MOCK_BIN:$EASY_CLI_DIR:$PATH" # Deterministic: never inherit real credentials/config from the host shell. - unset IONOS_API_KEY IONOS_API_SECRET EASY_LETSENCRYPT_EMAIL EASY_LETSENCRYPT_DOMAIN + unset IONOS_API_KEY IONOS_API_SECRET EASY_LETSENCRYPT_EMAIL EASY_LETSENCRYPT_DOMAIN EASY_PROXY_NETWORK } # Mock `docker` so `docker ps` reports a fake running easy-proxy container. @@ -68,3 +68,31 @@ esac MOCK chmod +x "$MOCK_BIN/docker" } + +# Mock `docker` that records every invocation to $DOCKER_LOG. `ps` returns +# nothing (proxy absent); `network inspect` fails (network absent). +mock_docker_record() { + cat > "$MOCK_BIN/docker" <<'MOCK' +#!/usr/bin/env bash +echo "$*" >> "${DOCKER_LOG:-/dev/null}" +case "$1 $2" in + "network inspect"*) exit 1 ;; + *) exit 0 ;; +esac +MOCK + chmod +x "$MOCK_BIN/docker" +} + +# Mock `docker`: proxy running and joined to several networks (for `networks`). +mock_docker_multinet() { + cat > "$MOCK_BIN/docker" <<'MOCK' +#!/usr/bin/env bash +echo "$*" >> "${DOCKER_LOG:-/dev/null}" +case "$1 $2" in + "ps "*) echo "deadbeefcafe1234" ;; + "inspect "*) echo "ethicnet bridge legacynet " ;; + *) exit 0 ;; +esac +MOCK + chmod +x "$MOCK_BIN/docker" +}