diff --git a/blocks/slot_decide.sh b/blocks/slot_decide.sh index 3035a4f..50769a6 100755 --- a/blocks/slot_decide.sh +++ b/blocks/slot_decide.sh @@ -58,6 +58,12 @@ parse_slot() { case "$PARSED_PORT" in *[!0-9]*) echo "$arg_name port must be numeric (got: $value)" >&2; exit 2;; esac + # 출력이 eval 로 소비되는 계약이라 이름 charset 을 허용목록으로 닫는다 - 지금 호출부는 고정 + # 리터럴(blue/green)이지만, 훗날 슬롯 이름이 외부 입력을 타는 호출부가 생겨도 eval 이 + # 명령 주입 표면이 되지 않게 블록이 스스로 막는다. + case "$PARSED_NAME" in + *[!a-zA-Z0-9_-]*) echo "$arg_name name must match [A-Za-z0-9_-] (got: $value)" >&2; exit 2;; + esac } parse_slot "--slot-a" "$SLOT_A" diff --git a/blocks/slot_decide.test.sh b/blocks/slot_decide.test.sh index a584bd5..feebdc7 100755 --- a/blocks/slot_decide.test.sh +++ b/blocks/slot_decide.test.sh @@ -44,6 +44,8 @@ check "알 수 없는 인자 -> exit 2" 2 "$?" check "콜론 없는 슬롯 -> exit 2" 2 "$?" "$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:1809x --slot-b green:18091 >/dev/null 2>&1 check "비숫자 포트 -> exit 2" 2 "$?" +"$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a 'bl;ue:18090' --slot-b green:18091 >/dev/null 2>&1 +check "허용목록 밖 슬롯 이름(eval 주입 표면) -> exit 2" 2 "$?" # 4. 부트스트랩: 상태 파일 없음 -> INACTIVE = slot-a OUT=$("$DECIDE" --state-file "$WORKDIR/none.conf" --slot-a blue:18090 --slot-b green:18091) diff --git a/blocks/slot_switch.sh b/blocks/slot_switch.sh index 382cff2..0f9317e 100755 --- a/blocks/slot_switch.sh +++ b/blocks/slot_switch.sh @@ -59,7 +59,13 @@ restore() { return 0 fi printf '%s\n' "$PREV" | run_priv tee "$STATE_FILE" >/dev/null - run_priv systemctl reload nginx 2>/dev/null || true + # 원복 반영도 전환과 같은 폴백(reload -> restart)을 쓴다. reload 만 두고 실패를 삼키면 + # 상태 파일은 이전 슬롯인데 nginx 런타임은 새 슬롯을 계속 가리키는 창이 남고, 그 사이 + # 호출자가 새 슬롯 컨테이너를 정리하면 서빙이 끊긴다. restart 폴백이면 그 창이 사라진다: + # restart 마저 실패 = nginx 정지이므로, 수동 기동만으로 상태 파일(이전 슬롯) 그대로 복귀한다. + if ! { run_priv systemctl reload nginx 2>/dev/null || run_priv systemctl restart nginx 2>/dev/null; }; then + echo "restore reload/restart FAILED - nginx 가 정지 상태일 수 있다. 상태 파일은 이전 슬롯으로 원복돼 있어 nginx 기동만으로 복귀된다" >&2 + fi echo "switch FAILED at $stage - restored previous upstream" >&2 } diff --git a/blocks/slot_switch.test.sh b/blocks/slot_switch.test.sh index 6f87091..bc0ebd7 100755 --- a/blocks/slot_switch.test.sh +++ b/blocks/slot_switch.test.sh @@ -33,6 +33,7 @@ cat >"$WORKDIR/bin/systemctl" <<'EOF' #!/usr/bin/env bash echo "$*" >>"$FAKE_DIR/systemctl.log" [ -f "$FAKE_DIR/systemctl_fail" ] && exit 1 +[ "$1" = "reload" ] && [ -f "$FAKE_DIR/systemctl_reload_fail" ] && exit 1 exit 0 EOF chmod +x "$WORKDIR/bin/sudo" "$WORKDIR/bin/nginx" "$WORKDIR/bin/systemctl" @@ -53,7 +54,8 @@ check() { } reset_fakes() { - rm -f "$FAKE_DIR/nginx_t_fail" "$FAKE_DIR/systemctl_fail" "$FAKE_DIR/systemctl.log" + rm -f "$FAKE_DIR/nginx_t_fail" "$FAKE_DIR/systemctl_fail" "$FAKE_DIR/systemctl_reload_fail" \ + "$FAKE_DIR/systemctl.log" } # 1. 필수 인자 누락 / 알 수 없는 인자 @@ -97,6 +99,16 @@ check "verify 실패 -> exit 1" 1 "$?" check "verify 실패 -> 상태 파일 원복" "server 127.0.0.1:18090;" "$(cat "$STATE")" check "verify 실패 -> reload 2회(전환+원복)" 2 "$(grep -c "reload nginx" "$FAKE_DIR/systemctl.log")" +# 5b. verify 실패 + 원복 reload 실패 -> restart 폴백으로 원복 반영 (상태·런타임 괴리 창 차단) +reset_fakes +echo "server 127.0.0.1:18090;" >"$STATE" +touch "$FAKE_DIR/systemctl_reload_fail" +"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18091 --verify-cmd "false" >/dev/null 2>&1 +check "verify 실패 + reload 불가 -> exit 1" 1 "$?" +check "verify 실패 + reload 불가 -> 상태 파일 원복" "server 127.0.0.1:18090;" "$(cat "$STATE")" +# 전환(reload 실패 -> restart)과 원복(reload 실패 -> restart)에서 각각 restart 가 불렸다 +check "verify 실패 + reload 불가 -> restart 폴백 2회(전환+원복)" 2 "$(grep -c "restart nginx" "$FAKE_DIR/systemctl.log")" + # 6. verify 성공 -> 전환 유지 reset_fakes echo "server 127.0.0.1:18090;" >"$STATE"