feat: implémentation initiale du reverse proxy de maintenance
Reverse proxy Nginx (image stable-alpine) qui sert l'app upstream ou une page de maintenance 503 selon l'IP du client. - Modes whitelist/blacklist commutables via MAINTENANCE_MODE - Liste IPv4 via MAINTENANCE_IP_LIST (séparée par virgules, validée au boot) - Logique en directives Nginx natives (geo + map), zéro Lua/njs - Page statique sobre HTML+CSS inline, zéro JS, zéro réseau sortant - Log dédié /var/log/nginx/maintenance.log pour les requêtes bloquées - Validation des env vars dans /docker-entrypoint.d/10-init.sh - Stack Docker + docker-compose pour dev local et tests - 6 cas de tests d'intégration (whitelist/blacklist x autorisée/bloquée + log + nginx -t) - Lint shellcheck propre sur tous les scripts shell
This commit is contained in:
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mode blacklist + IP du client présente → page de maintenance (503).
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=../lib.sh
|
||||
source "$(dirname "$0")/../lib.sh"
|
||||
|
||||
restart_proxy blacklist "172.28.5.50"
|
||||
|
||||
status="$(curl_status)"
|
||||
assert_eq "503" "$status" "code HTTP"
|
||||
|
||||
body="$(curl_body)"
|
||||
assert_contains "Site en maintenance" "$body" "corps de la page de maintenance"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mode blacklist + IP du client absente → l'app upstream est servie (200).
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=../lib.sh
|
||||
source "$(dirname "$0")/../lib.sh"
|
||||
|
||||
restart_proxy blacklist "10.99.99.99"
|
||||
|
||||
status="$(curl_status)"
|
||||
assert_eq "200" "$status" "code HTTP"
|
||||
|
||||
body="$(curl_body)"
|
||||
assert_contains "UPSTREAM_OK" "$body" "corps de la réponse"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Une requête bloquée doit apparaître dans /var/log/nginx/maintenance.log
|
||||
# avec l'IP du client, et NE DOIT PAS y apparaître en mode passant.
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=../lib.sh
|
||||
source "$(dirname "$0")/../lib.sh"
|
||||
|
||||
# Scénario bloqué (whitelist sans l'IP du client)
|
||||
restart_proxy whitelist "10.99.99.99"
|
||||
|
||||
# Vide le log avant le test pour partir d'un état connu
|
||||
proxy_exec sh -c ': > /var/log/nginx/maintenance.log'
|
||||
|
||||
# Génère une requête bloquée
|
||||
status="$(curl_status)"
|
||||
assert_eq "503" "$status" "code HTTP attendu"
|
||||
|
||||
# Petit délai pour laisser Nginx flusher le log
|
||||
sleep 1
|
||||
|
||||
log_content="$(proxy_exec cat /var/log/nginx/maintenance.log)"
|
||||
assert_contains "172.28.5.50" "$log_content" "IP du client dans maintenance.log"
|
||||
|
||||
# Vérifie qu'une requête passante (mode blacklist sans IP du client) ne logge PAS
|
||||
restart_proxy blacklist "10.99.99.99"
|
||||
proxy_exec sh -c ': > /var/log/nginx/maintenance.log'
|
||||
|
||||
status="$(curl_status)"
|
||||
assert_eq "200" "$status" "code HTTP en mode passant"
|
||||
|
||||
sleep 1
|
||||
log_after_pass="$(proxy_exec cat /var/log/nginx/maintenance.log)"
|
||||
if [[ -n "$log_after_pass" ]]; then
|
||||
t_fail "maintenance.log devrait être vide après une requête non bloquée, contient : $log_after_pass"
|
||||
fi
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Vérifie que la configuration Nginx complète passe `nginx -t` une fois
|
||||
# templatée et le snippet geo généré. Ne dépend pas du proxy "live" :
|
||||
# lance un conteneur jetable.
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=../lib.sh
|
||||
source "$(dirname "$0")/../lib.sh"
|
||||
|
||||
docker run --rm \
|
||||
-e MAINTENANCE_MODE=whitelist \
|
||||
-e MAINTENANCE_IP_LIST="172.28.5.50,10.0.0.42" \
|
||||
-e UPSTREAM_HOST="upstream:80" \
|
||||
-e LISTEN_PORT=8080 \
|
||||
-e SERVER_NAME=_ \
|
||||
--entrypoint /bin/sh \
|
||||
maintenance-proxy:test \
|
||||
-c '/docker-entrypoint.sh nginx -t' >/dev/null 2>&1 \
|
||||
|| t_fail "nginx -t a échoué sur la configuration générée"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mode whitelist + IP du client présente → l'app upstream est servie (200).
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=../lib.sh
|
||||
source "$(dirname "$0")/../lib.sh"
|
||||
|
||||
restart_proxy whitelist "172.28.5.50"
|
||||
|
||||
status="$(curl_status)"
|
||||
assert_eq "200" "$status" "code HTTP"
|
||||
|
||||
body="$(curl_body)"
|
||||
assert_contains "UPSTREAM_OK" "$body" "corps de la réponse"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mode whitelist + IP du client absente → page de maintenance (503).
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=../lib.sh
|
||||
source "$(dirname "$0")/../lib.sh"
|
||||
|
||||
restart_proxy whitelist "10.99.99.99"
|
||||
|
||||
status="$(curl_status)"
|
||||
assert_eq "503" "$status" "code HTTP"
|
||||
|
||||
body="$(curl_body)"
|
||||
assert_contains "Site en maintenance" "$body" "corps de la page de maintenance"
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head><meta charset="UTF-8"><title>Upstream OK</title></head>
|
||||
<body><h1 id="upstream-marker">UPSTREAM_OK</h1></body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Helpers partagés par tests/run.sh et tests/cases/*.sh.
|
||||
# Ce fichier est sourcé, pas exécuté directement.
|
||||
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.test.yml"
|
||||
COMPOSE=(docker compose -f "$COMPOSE_FILE" -p maintenance_proxy_tests)
|
||||
|
||||
PROXY_URL="http://172.28.5.10:8080/"
|
||||
|
||||
t_log() { echo "[test] $*"; }
|
||||
t_fail() { echo "[test] FAIL: $*" >&2; return 1; }
|
||||
|
||||
# Recrée le conteneur proxy avec les env vars passées, puis attend qu'il
|
||||
# réponde sur PROXY_URL (n'importe quel code HTTP suffit, on veut juste
|
||||
# qu'Nginx ait fini de booter).
|
||||
restart_proxy() {
|
||||
local mode="$1"
|
||||
local ip_list="$2"
|
||||
|
||||
MAINTENANCE_MODE="$mode" \
|
||||
MAINTENANCE_IP_LIST="$ip_list" \
|
||||
"${COMPOSE[@]}" up -d --force-recreate --no-deps proxy >/dev/null
|
||||
|
||||
local _
|
||||
for _ in $(seq 1 30); do
|
||||
if "${COMPOSE[@]}" exec -T client \
|
||||
curl -s -o /dev/null -w '%{http_code}' --max-time 2 "$PROXY_URL" \
|
||||
| grep -qE '^(200|503)$'; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
t_fail "le proxy n'a pas démarré dans les 30s (mode=$mode, list=$ip_list)"
|
||||
}
|
||||
|
||||
curl_status() {
|
||||
"${COMPOSE[@]}" exec -T client \
|
||||
curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$PROXY_URL"
|
||||
}
|
||||
|
||||
curl_body() {
|
||||
"${COMPOSE[@]}" exec -T client curl -s --max-time 5 "$PROXY_URL"
|
||||
}
|
||||
|
||||
proxy_exec() {
|
||||
"${COMPOSE[@]}" exec -T proxy "$@"
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1"
|
||||
local actual="$2"
|
||||
local label="${3:-valeur}"
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
t_fail "$label : attendu '$expected', obtenu '$actual'"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local needle="$1"
|
||||
local haystack="$2"
|
||||
local label="${3:-contenu}"
|
||||
if [[ "$haystack" != *"$needle"* ]]; then
|
||||
t_fail "$label : '$needle' introuvable dans la sortie"
|
||||
fi
|
||||
}
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Orchestrateur des tests d'intégration :
|
||||
# 1. Build l'image proxy.
|
||||
# 2. Démarre la stack docker-compose.test.yml (upstream + client + proxy initial).
|
||||
# 3. Exécute chaque tests/cases/*.sh dans un sous-shell.
|
||||
# 4. Tear down systématique en fin (succès ou échec).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=lib.sh
|
||||
source "$PROJECT_ROOT/tests/lib.sh"
|
||||
|
||||
cleanup() {
|
||||
t_log "Tear down de la stack de tests..."
|
||||
"${COMPOSE[@]}" down --remove-orphans --volumes >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
t_log "Build de l'image maintenance-proxy:test..."
|
||||
"${COMPOSE[@]}" build --quiet proxy
|
||||
|
||||
t_log "Démarrage initial de la stack..."
|
||||
MAINTENANCE_MODE="whitelist" \
|
||||
MAINTENANCE_IP_LIST="172.28.5.50" \
|
||||
"${COMPOSE[@]}" up -d >/dev/null
|
||||
|
||||
# Attente que le client ait fini d'installer curl (entrypoint contient apk add).
|
||||
for _ in $(seq 1 30); do
|
||||
if "${COMPOSE[@]}" exec -T client sh -c 'command -v curl' >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! "${COMPOSE[@]}" exec -T client sh -c 'command -v curl' >/dev/null 2>&1; then
|
||||
echo "[test] FAIL: curl indisponible dans le conteneur client" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
cases=("$PROJECT_ROOT/tests/cases/"*.sh)
|
||||
if [[ ${#cases[@]} -eq 0 ]]; then
|
||||
echo "[test] aucun cas trouvé dans tests/cases/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
failed_names=()
|
||||
|
||||
for case_path in "${cases[@]}"; do
|
||||
case_name="$(basename "$case_path" .sh)"
|
||||
t_log "▶ $case_name"
|
||||
if bash "$case_path"; then
|
||||
t_log " ✓ OK"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
t_log " ✗ FAIL"
|
||||
failed=$((failed + 1))
|
||||
failed_names+=("$case_name")
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "==============================================="
|
||||
echo " Résultats : $passed OK / $failed FAIL / $((passed + failed)) total"
|
||||
echo "==============================================="
|
||||
|
||||
if [[ $failed -gt 0 ]]; then
|
||||
echo "Cas échoués :" >&2
|
||||
for n in "${failed_names[@]}"; do
|
||||
echo " - $n" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user