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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ jobs:

- name: Run run_container.sh self-test
run: REQUIRE_DOCKER=1 ./blocks/run_container.test.sh

- name: Run slot_decide.sh self-test
run: ./blocks/slot_decide.test.sh

- name: Run slot_switch.sh self-test
run: ./blocks/slot_switch.test.sh
79 changes: 79 additions & 0 deletions blocks/slot_decide.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
#
# 공통 배포 블록: blue-green 슬롯 판정
#
# nginx upstream 상태 파일(현재 서빙 슬롯의 single source, 예: "server 127.0.0.1:18090;")을
# 읽어 ACTIVE(서빙 중)/INACTIVE(이번 배포 대상) 슬롯을 판정한다.
# 실행 위치 중립 - 순수 bash 만 쓴다. transport 는 호출자 소관 (conventions/blocks.md 1번).
#
# 포트 추출은 콜론 뒤 숫자만 문다 - "server 127.0.0.1:18090;" 처럼 host 에 숫자가 섞이면
# 무차별 숫자 추출은 "127" 을 먼저 물어 슬롯 판정이 조용히 어긋난다 (extractor 실측 함정.
# "server localhost:8080;" 류는 우연히 무사해서 복제 구현에 잠복해 있었다).
#
# 상태 파일이 없거나 포트가 두 슬롯 어느 쪽도 아니면 부트스트랩으로 본다:
# ACTIVE 는 빈 값, INACTIVE 는 --slot-a (첫 배포는 a 슬롯으로 간다).
#
# 출력: eval 가능한 한 줄 (stdout)
# ACTIVE=blue ACTIVE_PORT=18090 INACTIVE=green INACTIVE_PORT=18091
# 호출자는 2단계로 소비한다 - eval "$(...)" 직결은 치환 실패가 빈 문자열 eval(성공)로
# 위장되므로, 할당으로 실패를 먼저 드러낸 뒤 eval 한다:
# DECIDED=$(bash slot_decide.sh --state-file ... --slot-a blue:18090 --slot-b green:18091)
# eval "$DECIDED"
#
# 인자:
# --state-file (필수) upstream 상태 파일 경로
# --slot-a (필수) 첫 슬롯 NAME:PORT (예: blue:18090). 부트스트랩의 배포 대상
# --slot-b (필수) 둘째 슬롯 NAME:PORT (예: green:18091)
#
# 종료 코드: 성공 0, 인자 오류 2 (판정 자체는 실패하지 않는다 - 모르는 상태는 부트스트랩)

set -euo pipefail

STATE_FILE=""
SLOT_A=""
SLOT_B=""

while [ $# -gt 0 ]; do
case "$1" in
--state-file) STATE_FILE="${2:-}"; shift 2;;
--slot-a) SLOT_A="${2:-}"; shift 2;;
--slot-b) SLOT_B="${2:-}"; shift 2;;
Comment on lines +38 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

값이 없는 옵션의 종료 코드 계약을 두 스크립트에서 통일하십시오.

두 스크립트는 옵션 값이 없을 때 shift 2를 실행합니다. 이 동작은 set -e로 인해 문서화한 인자 오류 코드 2를 보장하지 않습니다.

  • blocks/slot_decide.sh#L38-L40: 각 옵션에서 shift 2 전에 인자 개수를 확인하고, 값이 없으면 오류 메시지와 함께 exit 2 하십시오.
  • blocks/slot_switch.sh#L38-L40: 동일한 검사를 적용하고, --verify-cmd가 마지막 인자인 경우도 종료 코드 2로 처리하십시오.
📍 Affects 2 files
  • blocks/slot_decide.sh#L38-L40 (this comment)
  • blocks/slot_switch.sh#L38-L40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blocks/slot_decide.sh` around lines 38 - 40, 통합된 옵션 인자 검사를
blocks/slot_decide.sh 38-40과 blocks/slot_switch.sh 38-40의 각 옵션 처리부에 추가하십시오.
`shift 2` 전에 필요한 값이 실제로 존재하는지 확인하고, 없으면 오류 메시지를 출력한 뒤 반드시 종료 코드 2로 종료하게 하며,
blocks/slot_switch.sh의 `--verify-cmd`가 마지막 인자인 경우도 동일하게 처리하십시오.

*) echo "unknown arg: $1" >&2; exit 2;;
esac
done

[ -n "$STATE_FILE" ] || { echo "--state-file is required" >&2; exit 2; }
[ -n "$SLOT_A" ] || { echo "--slot-a is required" >&2; exit 2; }
[ -n "$SLOT_B" ] || { echo "--slot-b is required" >&2; exit 2; }

parse_slot() {
# NAME:PORT 를 검증하며 분해한다. 전역 PARSED_NAME/PARSED_PORT 로 반환.
local arg_name="$1" value="$2"
PARSED_NAME="${value%%:*}"
PARSED_PORT="${value##*:}"
if [ "$value" = "$PARSED_NAME" ] || [ -z "$PARSED_NAME" ] || [ -z "$PARSED_PORT" ]; then
echo "$arg_name must be NAME:PORT (got: $value)" >&2
exit 2
fi
case "$PARSED_PORT" in
*[!0-9]*) echo "$arg_name port must be numeric (got: $value)" >&2; exit 2;;
esac
Comment on lines +52 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

eval 출력에 슬롯 이름을 안전하게 직렬화하십시오.

parse_slot은 슬롯 이름의 공백, 세미콜론, 명령 치환 문자를 허용합니다. Line 74~78은 그 값을 그대로 출력하고, blocks/slot_decide.test.sh Line 70은 출력을 eval합니다. 예를 들어 조작된 --slot-a 이름은 소비자 셸에서 명령으로 재해석될 수 있습니다.

모든 출력 값을 Bash 안전 형식으로 인코딩하십시오. 예를 들어 printf '%q'를 사용하십시오. 또는 슬롯 이름을 안전한 문자 집합으로 제한하십시오. 이 경우 eval 회귀 테스트에 악성 문자 입력도 추가하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blocks/slot_decide.sh` around lines 52 - 60, Update parse_slot and the output
path that serializes slot names so values consumed by eval in
blocks/slot_decide.test.sh are Bash-safe, preferably by encoding each emitted
value with printf '%q' while preserving valid names and ports. Add an eval
regression case covering malicious slot names containing spaces, semicolons, or
command-substitution characters, and verify they are not executed.

Source: Linters/SAST tools

}

parse_slot "--slot-a" "$SLOT_A"
NAME_A="$PARSED_NAME" PORT_A="$PARSED_PORT"
parse_slot "--slot-b" "$SLOT_B"
NAME_B="$PARSED_NAME" PORT_B="$PARSED_PORT"
Comment on lines +63 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

두 슬롯의 이름과 포트가 서로 다르도록 검증하십시오.

--slot-a blue:18090 --slot-b green:18090이 현재 통과합니다. 이 상태에서 INACTIVE_PORT도 활성 포트인 18090이 됩니다. 배포 대상이 현재 서빙 슬롯과 분리되지 않습니다.

NAME_A != NAME_BPORT_A != PORT_B를 확인하고, 중복이면 종료 코드 2를 반환하십시오. 동일 포트와 동일 이름의 회귀 테스트도 추가하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blocks/slot_decide.sh` around lines 63 - 66, After parsing both slots in the
slot-decision flow, validate that NAME_A differs from NAME_B and PORT_A differs
from PORT_B; when either value is duplicated, print an appropriate error and
exit with status 2. Add regression coverage for both identical names and
identical ports, while preserving valid distinct-slot behavior.


CURRENT_PORT=""
if [ -f "$STATE_FILE" ]; then
CURRENT_PORT=$(grep -oE ':[0-9]+' "$STATE_FILE" | head -1 | tr -d ':' || true)
fi

if [ "$CURRENT_PORT" = "$PORT_A" ]; then
echo "ACTIVE=$NAME_A ACTIVE_PORT=$PORT_A INACTIVE=$NAME_B INACTIVE_PORT=$PORT_B"
elif [ "$CURRENT_PORT" = "$PORT_B" ]; then
echo "ACTIVE=$NAME_B ACTIVE_PORT=$PORT_B INACTIVE=$NAME_A INACTIVE_PORT=$PORT_A"
else
echo "ACTIVE= ACTIVE_PORT= INACTIVE=$NAME_A INACTIVE_PORT=$PORT_A"
fi
79 changes: 79 additions & 0 deletions blocks/slot_decide.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
#
# blocks/slot_decide.sh 셀프 테스트
#
# 상태 파일 케이스(부트스트랩·a 활성·b 활성·모르는 포트)와 인자 오류, 그리고 이 블록의
# 존재 이유인 포트 추출 함정(host 의 127 이 먼저 잡히는 것) 회귀를 실측한다.
# conventions/blocks.md 5번 원칙(셀프 검증 가능)의 실행체.
#
# 실행: ./blocks/slot_decide.test.sh

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DECIDE="$SCRIPT_DIR/slot_decide.sh"

WORKDIR=$(mktemp -d)
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT

FAILURES=0

check() {
local desc="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
echo "PASS: $desc"
else
echo "FAIL: $desc (expected=[$expected] actual=[$actual])" >&2
FAILURES=$((FAILURES + 1))
fi
}

# 1. 필수 인자 누락
"$DECIDE" --slot-a blue:18090 --slot-b green:18091 >/dev/null 2>&1
check "state-file 없음 -> exit 2" 2 "$?"
"$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:18090 >/dev/null 2>&1
check "slot-b 없음 -> exit 2" 2 "$?"

# 2. 알 수 없는 인자
"$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:18090 --slot-b green:18091 --bogus x >/dev/null 2>&1
check "알 수 없는 인자 -> exit 2" 2 "$?"

# 3. 슬롯 형식 위반
"$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue18090 --slot-b green:18091 >/dev/null 2>&1
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 "$?"

# 4. 부트스트랩: 상태 파일 없음 -> INACTIVE = slot-a
OUT=$("$DECIDE" --state-file "$WORKDIR/none.conf" --slot-a blue:18090 --slot-b green:18091)
check "부트스트랩 판정" "ACTIVE= ACTIVE_PORT= INACTIVE=blue INACTIVE_PORT=18090" "$OUT"

# 5. 포트 추출 함정 회귀: host 에 숫자(127.0.0.1)가 있어도 콜론 뒤 포트만 문다
echo "server 127.0.0.1:18090;" >"$WORKDIR/up.conf"
OUT=$("$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:18090 --slot-b green:18091)
check "127.0.0.1 host 에서 a 활성 판정" "ACTIVE=blue ACTIVE_PORT=18090 INACTIVE=green INACTIVE_PORT=18091" "$OUT"

# 6. b 활성 (core 의 localhost 표기도 같은 추출 규칙으로 동작)
echo "server localhost:8081;" >"$WORKDIR/up.conf"
OUT=$("$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:8080 --slot-b green:8081)
check "localhost 표기에서 b 활성 판정" "ACTIVE=green ACTIVE_PORT=8081 INACTIVE=blue INACTIVE_PORT=8080" "$OUT"

# 7. 모르는 포트 -> 부트스트랩
echo "server 127.0.0.1:9999;" >"$WORKDIR/up.conf"
OUT=$("$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:18090 --slot-b green:18091)
check "모르는 포트 -> 부트스트랩" "ACTIVE= ACTIVE_PORT= INACTIVE=blue INACTIVE_PORT=18090" "$OUT"

# 8. 출력이 eval 가능한 계약인지 (문서의 2단계 소비 패턴 그대로)
echo "server 127.0.0.1:18091;" >"$WORKDIR/up.conf"
DECIDED=$("$DECIDE" --state-file "$WORKDIR/up.conf" --slot-a blue:18090 --slot-b green:18091)
eval "$DECIDED"
check "eval 소비: ACTIVE" "green" "${ACTIVE:-}"
check "eval 소비: INACTIVE_PORT" "18090" "${INACTIVE_PORT:-}"

if [ "$FAILURES" -gt 0 ]; then
echo "$FAILURES case(s) failed" >&2
exit 1
fi

echo "all slot_decide.sh cases passed"
82 changes: 82 additions & 0 deletions blocks/slot_switch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
#
# 공통 배포 블록: blue-green upstream 전환 (실패 시 원복)
#
# upstream 상태 파일을 새 슬롯으로 갱신하고 nginx 검증(-t)·reload 를 수행한다.
# 어느 단계가 실패해도(구성 검증, reload, 사후 검증) 이전 상태 파일을 원복해
# "상태 파일만 새 슬롯을 가리키고 실제 서빙은 옛 슬롯"인 괴리를 남기지 않는다.
# 이 괴리는 다음 배포의 슬롯 판정(slot_decide.sh)을 뒤집어, 실제 서빙 중인 슬롯을
# 비활성으로 오판·제거하게 만든다 (extractor 이행 배포 실사고의 일반화).
#
# 실행 위치 중립 - root 가 아니면 sudo 를 앞에 붙인다 (SSH runner 비루트 / SSM root 양쪽).
# reload 는 systemctl reload -> 실패 시 restart 폴백이라 nginx 가 정지 상태여도 전환된다.
#
# 사용 예:
# slot_switch.sh --state-file /etc/nginx/piki-extractor-upstream.conf --server 127.0.0.1:18091 \
# --verify-cmd "bash /tmp/piki-blocks/healthcheck.sh --url http://localhost:8090/actuator/health --interval 2 --attempts 10"
#
# 인자:
# --state-file (필수) upstream 상태 파일 경로. "server <값>;" 한 줄로 덮어쓴다
# --server (필수) 새 upstream server 값 (예: 127.0.0.1:18091)
# --verify-cmd (선택) reload 후 실행할 검증 명령(bash -c 로 실행). 비-0 이면 원복 후 실패.
# 전환 후 프론트 경유 헬스체크(healthcheck.sh 호출)가 대표 소비자 - 검증까지
# 원복 경계 안에 있어야 "전환됐지만 안 서빙되는" 상태가 남지 않는다
#
# 부트스트랩(이전 상태 파일 없음)은 원복 대상이 없다 - 실패해도 새 값이 남고 exit 1 로만
# 알린다 (되돌아갈 서빙 슬롯 자체가 없는 1회성 창이라 호출자·운영자가 수동 개입).
#
# 종료 코드: 성공 0, 전환 실패(가능하면 원복 수행) 1, 인자 오류 2

set -euo pipefail

STATE_FILE=""
SERVER=""
VERIFY_CMD=""

while [ $# -gt 0 ]; do
case "$1" in
--state-file) STATE_FILE="${2:-}"; shift 2;;
--server) SERVER="${2:-}"; shift 2;;
--verify-cmd) VERIFY_CMD="${2:-}"; shift 2;;
*) echo "unknown arg: $1" >&2; exit 2;;
esac
done

[ -n "$STATE_FILE" ] || { echo "--state-file is required" >&2; exit 2; }
[ -n "$SERVER" ] || { echo "--server is required" >&2; exit 2; }

run_priv() {
if [ "$(id -u)" = "0" ]; then "$@"; else sudo "$@"; fi
}

PREV=""
[ -f "$STATE_FILE" ] && PREV=$(cat "$STATE_FILE")

restore() {
local stage="$1"
if [ -z "$PREV" ]; then
echo "switch FAILED at $stage - no previous state to restore (bootstrap)" >&2
return 0
fi
printf '%s\n' "$PREV" | run_priv tee "$STATE_FILE" >/dev/null
run_priv systemctl reload nginx 2>/dev/null || true
echo "switch FAILED at $stage - restored previous upstream" >&2
Comment on lines +61 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

nginx 원복 실패를 성공으로 처리하지 마십시오.

verify 실패 경로에서는 새 upstream이 이미 Line 72에서 적용되었습니다. 이후 Line 62의 reload가 실패하면 상태 파일만 이전 값으로 돌아가고 nginx는 새 upstream을 계속 사용할 수 있습니다. 그러나 현재 코드는 실패를 무시하고 "restored previous upstream"을 출력합니다.

원복 상태 파일을 쓴 뒤 nginx -treload, 필요 시 restart를 수행하십시오. 원복 적용이 실패하면 별도 오류를 출력하고 성공 복원 로그를 출력하지 마십시오. blocks/slot_switch.test.sh에는 원복 reload 실패 시 성공 로그와 실제 적용 상태를 검증하는 케이스를 추가하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blocks/slot_switch.sh` around lines 61 - 63, Update the verify-failure
rollback path in slot_switch.sh so writing PREV to STATE_FILE is followed by
nginx -t and a reload, with a restart fallback when reload fails. Track rollback
application success, emit a separate error when restoration fails, and only
print “restored previous upstream” after nginx successfully applies it; add
coverage in the rollback tests for reload failure, success-log suppression, and
the actual nginx state.

}

printf 'server %s;\n' "$SERVER" | run_priv tee "$STATE_FILE" >/dev/null

if ! run_priv nginx -t; then
restore "nginx -t"
exit 1
fi
if ! { run_priv systemctl reload nginx || run_priv systemctl restart nginx; }; then
restore "reload/restart"
exit 1
fi
if [ -n "$VERIFY_CMD" ] && ! bash -c "$VERIFY_CMD"; then
restore "verify"
exit 1
fi

echo "switched upstream -> server $SERVER; ($STATE_FILE)"
exit 0
120 changes: 120 additions & 0 deletions blocks/slot_switch.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
#
# blocks/slot_switch.sh 셀프 테스트
#
# nginx·systemctl·sudo 를 PATH 스텁으로 대체해 전환·원복 계약(종료 코드 + 상태 파일 내용)을
# 실측한다. 이 블록의 존재 이유가 "실패 시 원복"이므로, 각 실패 지점(-t·reload·verify)마다
# 상태 파일이 이전 값으로 돌아오는지를 검사한다. conventions/blocks.md 5번 원칙의 실행체.
#
# 실행: ./blocks/slot_switch.test.sh

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SWITCH="$SCRIPT_DIR/slot_switch.sh"

WORKDIR=$(mktemp -d)
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT

# ── PATH 스텁: 실패 주입은 FAKE_DIR 의 마커 파일로 제어한다 ──
mkdir -p "$WORKDIR/bin"
export FAKE_DIR="$WORKDIR"
cat >"$WORKDIR/bin/sudo" <<'EOF'
#!/usr/bin/env bash
exec "$@"
EOF
cat >"$WORKDIR/bin/nginx" <<'EOF'
#!/usr/bin/env bash
[ -f "$FAKE_DIR/nginx_t_fail" ] && exit 1
exit 0
EOF
cat >"$WORKDIR/bin/systemctl" <<'EOF'
#!/usr/bin/env bash
echo "$*" >>"$FAKE_DIR/systemctl.log"
[ -f "$FAKE_DIR/systemctl_fail" ] && exit 1
exit 0
EOF
chmod +x "$WORKDIR/bin/sudo" "$WORKDIR/bin/nginx" "$WORKDIR/bin/systemctl"
export PATH="$WORKDIR/bin:$PATH"

STATE="$WORKDIR/upstream.conf"

FAILURES=0

check() {
local desc="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
echo "PASS: $desc"
else
echo "FAIL: $desc (expected=[$expected] actual=[$actual])" >&2
FAILURES=$((FAILURES + 1))
fi
}

reset_fakes() {
rm -f "$FAKE_DIR/nginx_t_fail" "$FAKE_DIR/systemctl_fail" "$FAKE_DIR/systemctl.log"
}

# 1. 필수 인자 누락 / 알 수 없는 인자
"$SWITCH" --server 127.0.0.1:18091 >/dev/null 2>&1
check "state-file 없음 -> exit 2" 2 "$?"
"$SWITCH" --state-file "$STATE" >/dev/null 2>&1
check "server 없음 -> exit 2" 2 "$?"
"$SWITCH" --state-file "$STATE" --server x --bogus y >/dev/null 2>&1
check "알 수 없는 인자 -> exit 2" 2 "$?"

# 2. 정상 전환: 상태 파일 갱신 + reload 수행
reset_fakes
echo "server 127.0.0.1:18090;" >"$STATE"
"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18091 >/dev/null 2>&1
check "정상 전환 -> exit 0" 0 "$?"
check "정상 전환 -> 상태 파일 갱신" "server 127.0.0.1:18091;" "$(cat "$STATE")"
grep -q "reload nginx" "$FAKE_DIR/systemctl.log"
check "정상 전환 -> reload 호출" 0 "$?"

# 3. nginx -t 실패 -> 원복
reset_fakes
echo "server 127.0.0.1:18090;" >"$STATE"
touch "$FAKE_DIR/nginx_t_fail"
"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18091 >/dev/null 2>&1
check "-t 실패 -> exit 1" 1 "$?"
check "-t 실패 -> 상태 파일 원복" "server 127.0.0.1:18090;" "$(cat "$STATE")"

# 4. reload·restart 모두 실패 -> 원복
reset_fakes
echo "server 127.0.0.1:18090;" >"$STATE"
touch "$FAKE_DIR/systemctl_fail"
"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18091 >/dev/null 2>&1
check "reload/restart 실패 -> exit 1" 1 "$?"
check "reload/restart 실패 -> 상태 파일 원복" "server 127.0.0.1:18090;" "$(cat "$STATE")"

# 5. verify 실패 -> 원복 + 원복 반영 reload
reset_fakes
echo "server 127.0.0.1:18090;" >"$STATE"
"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18091 --verify-cmd "false" >/dev/null 2>&1
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")"

# 6. verify 성공 -> 전환 유지
reset_fakes
echo "server 127.0.0.1:18090;" >"$STATE"
"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18091 --verify-cmd "true" >/dev/null 2>&1
check "verify 성공 -> exit 0" 0 "$?"
check "verify 성공 -> 상태 파일 유지" "server 127.0.0.1:18091;" "$(cat "$STATE")"

# 7. 부트스트랩(-t 실패, 이전 상태 없음) -> 원복 대상 없음, 새 값 유지 + exit 1
reset_fakes
rm -f "$STATE"
touch "$FAKE_DIR/nginx_t_fail"
"$SWITCH" --state-file "$STATE" --server 127.0.0.1:18090 >/dev/null 2>&1
check "부트스트랩 -t 실패 -> exit 1" 1 "$?"
check "부트스트랩 -t 실패 -> 새 값 유지(원복 대상 없음)" "server 127.0.0.1:18090;" "$(cat "$STATE")"

if [ "$FAILURES" -gt 0 ]; then
echo "$FAILURES case(s) failed" >&2
exit 1
fi

echo "all slot_switch.sh cases passed"
Loading