diff --git a/scripts/ci-deploy.sh b/scripts/ci-deploy.sh index 6eccb73d..5ce7ceee 100644 --- a/scripts/ci-deploy.sh +++ b/scripts/ci-deploy.sh @@ -151,7 +151,12 @@ read_active_port() { assert_port_free() { local port="$1" name="$2" local holders="" - if command -v ss >/dev/null 2>&1; then + # `type`, niet `command -v`: de deploy-simulatietests leveren `ss` als + # shell-functie via BASH_ENV, en `command -v` herkent die wel op Bash maar de + # functie is niet geëxporteerd naar de subshell van start_candidate. Met `type` + # blijft de stub ook daar zichtbaar, zodat de test geen echte hostpoorten + # hoeft te zien. + if type ss >/dev/null 2>&1; then # `ss` drukt altijd een kolomkop af, ook als er geen listener is. Filter op # LISTEN, anders zou elke vrije poort als bezet gemeld worden. holders="$(ss -ltnp "sport = :$port" 2>/dev/null | grep -F 'LISTEN' || true)" diff --git a/src/lib/ci-deploy.test.ts b/src/lib/ci-deploy.test.ts index 8aa1b582..d1b010b8 100644 --- a/src/lib/ci-deploy.test.ts +++ b/src/lib/ci-deploy.test.ts @@ -299,6 +299,23 @@ describe("blue/green cutover", () => { expect(r.upstream).not.toContain("127.0.0.1:3003"); }); + // Regressie: een poort die al in gebruik is liet `docker run` stilletjes op + // EADDRINUSE sterven. De health-probe beantwoordde daarna vanaf de + // reeds draaiende container op diezelfde poort, waardoor de + // release-vergelijking 30 keer op een verkeerde release faalde in plaats + // van op de echte oorzaak te wijzen. + it("refuses to start the candidate on a port that is already in use", () => { + const r = simulateBlueGreen("port-taken"); + expect(r.status, r.output).not.toBe(0); + expect(r.output).toContain("already in use"); + // Er is geen kandidaat gestart, dus er is ook niets om te verwijderen. + expect(r.calls).not.toContain("docker run"); + // De live release draait ongestoord door en nginx wijst nog steeds + // naar de oude poort: geen halve cutover. + expect(r.upstream).toContain("127.0.0.1:3002"); + expect(r.upstream).not.toContain("127.0.0.1:3003"); + }); + it.each([ "run-failure", "health-failure", diff --git a/src/test/ci-deploy-harness.sh b/src/test/ci-deploy-harness.sh index e7ccb682..5cba57a9 100644 --- a/src/test/ci-deploy-harness.sh +++ b/src/test/ci-deploy-harness.sh @@ -38,6 +38,28 @@ curl() { echo '{"database":true}' } sleep() { :; } +# De poortconflict-check (assert_port_free in scripts/ci-deploy.sh) leest de +# echte luisterende sockets. Zonder deze stub zou de simulatie de containers van +# de productiehost zien — de test draait immers op dezelfde machine — en elke +# blauwe/groene scenario laten falen op een poort die de simulatie zelf net +# virtueel heeft toegewezen. Standaard is geen enkele poort bezet; het scenario +# 'port-taken' reserveert expliciet de kandidaatpoort. +ss() { + local requested="" arg + for arg in "$@"; do + case "$arg" in + sport=*) requested="${arg#sport=}" ;; + 'sport'|'='|':') ;; + *:*) [ -z "$requested" ] && requested="${arg##*:}" ;; + esac + done + if [ "$SCENARIO" = port-taken ] && [ -n "$requested" ]; then + printf 'State Recv-Q Send-Q Local Address:Port Peer Address:Port\n' + printf 'LISTEN 0 511 0.0.0.0:%s 0.0.0.0:*\n' "$requested" + return 0 + fi + printf 'State Recv-Q Send-Q Local Address:Port Peer Address:Port\n' +} docker() { echo "docker $*" >> "$TEST_DIR/calls" local name="${@: -1}" @@ -70,7 +92,7 @@ docker() { *) return 0 ;; esac } -export -f git flock pnpm curl sleep docker nginx candidate_is_new +export -f git flock pnpm curl sleep ss docker nginx candidate_is_new node() { echo "node $*" >> "$TEST_DIR/calls"