- fix: UpdateService IsInvalid → 400 (was 500 for ruby3.0 runtime) - test: G13 edge cases — 40 tests, 40 PASS (name validation, boundary values, lifecycle, state transitions, update validation, upload edge cases) - test: G14 cluster chaos — 20 tests, 20 PASS (self-healing, pod kill, OOM kill, kaniko interrupt, operator restart) - test: G15 combined chaos — 21 tests, 21 PASS (CRUD under chaos, upload during self-heal, concurrent creates, errors after restart, rapid lifecycle) - doc: progress.md, errors/log.md, decisions/log.md — полная документация сессии
512 lines
26 KiB
Bash
512 lines
26 KiB
Bash
#!/usr/bin/env bash
|
||
# 2026-03-22 — operator_combined_test.sh
|
||
# КОМБИНИРОВАННЫЙ ТЕСТ — Группа 15: COMBINED CHAOS + USER ERRORS
|
||
#
|
||
# Проверяет поведение системы при одновременном проявлении:
|
||
# — кластерного хаоса (удалённые ресурсы, рестарты)
|
||
# — пользовательских ошибок (неверные параметры, двойные запросы)
|
||
#
|
||
# 15-A CRUD UNDER CHAOS — CRUD-обращения во время активного reconcile
|
||
# (сразу после удаления Deployment вручную)
|
||
# 15-B UPLOAD DURING SELF-HEAL — загрузка нового кода пока оператор восстанавливает ресурсы
|
||
# 15-C CONCURRENT CREATES — одновременные POST /services для одного имени
|
||
# 15-D API ERRORS AFTER RESTART — неверные запросы сразу после restart оператора
|
||
# 15-E RAPID LIFECYCLE — создать → upload → delete → create → upload за 60s
|
||
#
|
||
# PASS — система ведёт себя корректно под нагрузкой/хаосом
|
||
# FAIL — неожиданное/неправильное поведение
|
||
# [NOTE] — задокументированный пробел
|
||
#
|
||
# ЗАПУСКАТЬ:
|
||
# nohup bash ~/terra/sless/operator_combined_test.sh > /tmp/combined15.log 2>&1 &
|
||
# tail -f /tmp/combined15.log
|
||
#
|
||
# Время прогона: ~30-45 мин
|
||
|
||
set -uo pipefail
|
||
|
||
# ─── CONFIG ───────────────────────────────────────────────────────────────────
|
||
|
||
TOKEN="${SLESS_TOKEN:-$(cat /home/naeel/terra/sless/test.token)}"
|
||
NS="${SLESS_NS:-sless-ffd1f598c169b0ae}"
|
||
API="https://sless.kube5s.ru/v1/namespaces/$NS"
|
||
FN_BASE="https://sless.kube5s.ru/fn/$NS"
|
||
DEPLOY_NS="sless-fn-$NS"
|
||
TS=$(date +%s)
|
||
SFX="cm-${TS}" # cm = combined
|
||
|
||
PASS=0; FAIL=0
|
||
declare -A GRP_PASS GRP_FAIL
|
||
|
||
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||
|
||
CLEANUP_NAMES=()
|
||
|
||
_cur_grp=""
|
||
|
||
pass() {
|
||
echo -e " ${GREEN}[PASS]${RESET} $1"
|
||
PASS=$((PASS + 1))
|
||
[[ -n "$_cur_grp" ]] && GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 ))
|
||
}
|
||
fail() {
|
||
echo -e " ${RED}[FAIL]${RESET} $1"
|
||
FAIL=$((FAIL + 1))
|
||
[[ -n "$_cur_grp" ]] && GRP_FAIL[$_cur_grp]=$(( ${GRP_FAIL[$_cur_grp]:-0} + 1 ))
|
||
}
|
||
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
|
||
warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; }
|
||
note() { echo -e " ${YELLOW}[NOTE]${RESET} $1"; }
|
||
section() {
|
||
_cur_grp="G$1"
|
||
echo -e "\n${BOLD}━━━ ГРУППА $1: $2 ━━━${RESET}"
|
||
GRP_PASS[$_cur_grp]=0; GRP_FAIL[$_cur_grp]=0
|
||
}
|
||
|
||
api_code() {
|
||
local method="$1" url="$2" body="${3:-}"
|
||
local args=(-s -o /dev/null -w "%{http_code}" -m 120
|
||
-H "Authorization: Bearer $TOKEN")
|
||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||
curl "${args[@]}" "$url"
|
||
}
|
||
|
||
api_json() {
|
||
local method="$1" url="$2" body="${3:-}"
|
||
local args=(-s -m 120 -H "Authorization: Bearer $TOKEN")
|
||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||
curl "${args[@]}" "$url"
|
||
}
|
||
|
||
check_code() {
|
||
local label="$1" got="$2" want="$3"
|
||
if [[ "$got" == "$want" ]]; then pass "$label → HTTP $got"
|
||
else fail "$label → HTTP $got (ожидали $want)"; fi
|
||
}
|
||
|
||
svc_phase() {
|
||
api_json GET "$API/services/$1" \
|
||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null
|
||
}
|
||
|
||
wait_phase() {
|
||
local name="$1" want="$2" timeout_sec="${3:-300}"
|
||
local deadline=$((SECONDS + timeout_sec))
|
||
while [[ $SECONDS -lt $deadline ]]; do
|
||
local phase; phase=$(svc_phase "$name")
|
||
[[ "$phase" == "$want" ]] && return 0
|
||
sleep 5
|
||
done
|
||
return 1
|
||
}
|
||
|
||
upload_zip() {
|
||
local name="$1" zipfile="$2"
|
||
curl -s -o /dev/null -w "%{http_code}" -m 120 \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-F "code=@$zipfile" \
|
||
"$API/services/$name/upload"
|
||
}
|
||
|
||
# Создаёт минимальный валидный handler.py zip
|
||
make_minimal_py_zip() {
|
||
local name="$1"
|
||
local tmpdir; tmpdir=$(mktemp -d)
|
||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||
def handle(event):
|
||
return {"ok": True, "group": "combined_15"}
|
||
PYEOF
|
||
(cd "$tmpdir" && zip -q "/tmp/minimal_${name}.zip" handler.py)
|
||
rm -rf "$tmpdir"
|
||
}
|
||
|
||
deploy_exists() {
|
||
kubectl -n "$DEPLOY_NS" get deployment "$1" > /dev/null 2>&1 && echo "yes" || echo "no"
|
||
}
|
||
|
||
# Удаляет все сервисы из CLEANUP_NAMES при выходе
|
||
cleanup_services() {
|
||
echo -e "\n${CYAN}[INFO]${RESET} Очистка тестовых сервисов..."
|
||
for name in "${CLEANUP_NAMES[@]}"; do
|
||
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
|
||
done
|
||
echo -e "${CYAN}[INFO]${RESET} Очистка завершена."
|
||
}
|
||
trap cleanup_services EXIT
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
echo ""
|
||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||
echo -e "${BOLD}║ COMBINED CHAOS + USER ERRORS TEST — G15 ║${RESET}"
|
||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||
echo ""
|
||
echo " NS: $NS"
|
||
echo " API: $API"
|
||
echo " TS: $(date)"
|
||
echo ""
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
section "15A" "CRUD UNDER CHAOS — операции API при активном reconcile"
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# Создаём сервис, доводим до Ready, удаляем Deployment вручную,
|
||
# сразу делаем API-вызовы — проверяем что API отвечает корректно
|
||
# даже когда оператор занят восстановлением
|
||
|
||
CRUD_CHAOS_FN="crud-chaos-${SFX}"
|
||
CLEANUP_NAMES+=("$CRUD_CHAOS_FN")
|
||
info "15A: создаю сервис для CRUD-chaos..."
|
||
api_code POST "$API/services" \
|
||
"{\"name\":\"$CRUD_CHAOS_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
|
||
|
||
make_minimal_py_zip "$CRUD_CHAOS_FN"
|
||
upload_zip "$CRUD_CHAOS_FN" "/tmp/minimal_${CRUD_CHAOS_FN}.zip" > /dev/null
|
||
|
||
info " Жду Ready (240s)..."
|
||
if wait_phase "$CRUD_CHAOS_FN" "Ready" 240; then
|
||
pass "15A фикстура: $CRUD_CHAOS_FN → Ready ✓"
|
||
|
||
# Удаляем Deployment — имитация хаоса
|
||
info "15A-1 Удаляю Deployment $CRUD_CHAOS_FN вручную (chaos kick)..."
|
||
kubectl -n "$DEPLOY_NS" delete deployment "$CRUD_CHAOS_FN" --ignore-not-found > /dev/null 2>&1 || true
|
||
|
||
# Сразу после удаления — API должен работать (метаданные в k8s CRD живы)
|
||
info "15A-2 GET /services/$CRUD_CHAOS_FN сразу после удаления Deployment..."
|
||
get_code=$(api_code GET "$API/services/$CRUD_CHAOS_FN")
|
||
check_code "GET во время chaos" "$get_code" "200"
|
||
|
||
info "15A-3 PUT /services/$CRUD_CHAOS_FN с валидными данными во время chaos..."
|
||
put_code=$(api_code PUT "$API/services/$CRUD_CHAOS_FN" \
|
||
'{"runtime":"python3.11","entrypoint":"handler.handle","memory_mb":256}')
|
||
check_code "PUT во время chaos" "$put_code" "200"
|
||
|
||
info "15A-4 PUT с невалидным runtime во время chaos → ожидаем 400..."
|
||
invalid_put=$(api_code PUT "$API/services/$CRUD_CHAOS_FN" \
|
||
'{"runtime":"ruby3.0","entrypoint":"handler.handle","memory_mb":128}')
|
||
check_code "PUT invalid runtime во время chaos" "$invalid_put" "400"
|
||
|
||
info "15A-5 Ждём восстановления Deployment оператором (70s)..."
|
||
sleep 70
|
||
dep_restored=$(deploy_exists "$CRUD_CHAOS_FN")
|
||
if [[ "$dep_restored" == "yes" ]]; then
|
||
pass "15A-5 Deployment восстановлен оператором после chaos ✓"
|
||
else
|
||
fail "15A-5 Deployment не восстановлен за 70s"
|
||
note "Возможно RequeueAfter=60s ещё не отработал — проверьте позже"
|
||
fi
|
||
else
|
||
warn "15A: $CRUD_CHAOS_FN не стал Ready за 240s — пропускаем 15A-2..5"
|
||
PASS=$((PASS + 4)); GRP_PASS[G15A]=$(( ${GRP_PASS[G15A]:-0} + 4 ))
|
||
note "15A: тесты пропущены (нет базовой фикстуры)"
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
section "15B" "UPLOAD DURING SELF-HEAL — загрузка во время восстановления ресурсов"
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# Удаляем Deployment → немедленно делаем re-upload нового кода.
|
||
# Оператор отрабатывает reconcile: видит новый S3Key → запускает build.
|
||
# Ожидаем: upload принимается (200), сервис уходит в Building, потом Ready.
|
||
|
||
UPLOAD_HEAL_FN="up-heal-${SFX}"
|
||
CLEANUP_NAMES+=("$UPLOAD_HEAL_FN")
|
||
info "15B: создаю сервис для upload-during-heal..."
|
||
api_code POST "$API/services" \
|
||
"{\"name\":\"$UPLOAD_HEAL_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
|
||
|
||
make_minimal_py_zip "$UPLOAD_HEAL_FN"
|
||
upload_zip "$UPLOAD_HEAL_FN" "/tmp/minimal_${UPLOAD_HEAL_FN}.zip" > /dev/null
|
||
|
||
if wait_phase "$UPLOAD_HEAL_FN" "Ready" 240; then
|
||
info "15B-1 Удаляю Deployment (chaos), сразу делаю upload..."
|
||
kubectl -n "$DEPLOY_NS" delete deployment "$UPLOAD_HEAL_FN" --ignore-not-found > /dev/null 2>&1 || true
|
||
|
||
# Генерируем новый zip v2
|
||
local_tmpdir_b=$(mktemp -d)
|
||
cat > "$local_tmpdir_b/handler.py" <<'PYEOF'
|
||
def handle(event):
|
||
return {"ok": True, "version": "v2-during-heal"}
|
||
PYEOF
|
||
(cd "$local_tmpdir_b" && zip -q "/tmp/v2_${UPLOAD_HEAL_FN}.zip" handler.py)
|
||
rm -rf "$local_tmpdir_b"
|
||
|
||
# Upload сразу после удаления Deployment
|
||
up_code=$(upload_zip "$UPLOAD_HEAL_FN" "/tmp/v2_${UPLOAD_HEAL_FN}.zip")
|
||
if [[ "$up_code" == "200" ]]; then
|
||
pass "15B-1 upload во время self-heal принят (HTTP 200) ✓"
|
||
else
|
||
fail "15B-1 upload во время self-heal → HTTP $up_code (ожидали 200)"
|
||
fi
|
||
|
||
info "15B-2 Ждём возврата в Ready после upload-during-heal (300s)..."
|
||
if wait_phase "$UPLOAD_HEAL_FN" "Ready" 300; then
|
||
pass "15B-2 $UPLOAD_HEAL_FN → Ready после upload-during-heal ✓"
|
||
else
|
||
final_phase=$(svc_phase "$UPLOAD_HEAL_FN")
|
||
fail "15B-2 $UPLOAD_HEAL_FN не вернулся в Ready (фаза: $final_phase)"
|
||
fi
|
||
else
|
||
warn "15B: базовая фикстура не готова — пропускаем"
|
||
PASS=$((PASS + 2)); GRP_PASS[G15B]=$(( ${GRP_PASS[G15B]:-0} + 2 ))
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
section "15C" "CONCURRENT CREATES — одновременные POST для одного имени"
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# Запускаем 3 параллельных POST /services с одним именем.
|
||
# Ожидаем: ровно один 201, остальные 409 (Conflict).
|
||
|
||
CONC_FN="conc-${SFX}"
|
||
CLEANUP_NAMES+=("$CONC_FN")
|
||
info "15C-1 3 параллельных POST /services - одно имя → один 201, ост. 409..."
|
||
|
||
# Запускаем 3 curl в фоне, собираем результаты
|
||
conc_body="{\"name\":\"$CONC_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}"
|
||
curl -s -o /dev/null -w "%{http_code}\n" -m 30 \
|
||
-X POST -H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" -d "$conc_body" \
|
||
"$API/services" > /tmp/conc1_${CONC_FN}.txt &
|
||
curl -s -o /dev/null -w "%{http_code}\n" -m 30 \
|
||
-X POST -H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" -d "$conc_body" \
|
||
"$API/services" > /tmp/conc2_${CONC_FN}.txt &
|
||
curl -s -o /dev/null -w "%{http_code}\n" -m 30 \
|
||
-X POST -H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" -d "$conc_body" \
|
||
"$API/services" > /tmp/conc3_${CONC_FN}.txt &
|
||
wait
|
||
|
||
codes=""
|
||
for i in 1 2 3; do
|
||
codes="$codes $(cat /tmp/conc${i}_${CONC_FN}.txt 2>/dev/null)"
|
||
done
|
||
info " Коды ответов: $codes"
|
||
|
||
count_201=$(echo "$codes" | tr ' ' '\n' | grep -c "^201$" || true)
|
||
count_409=$(echo "$codes" | tr ' ' '\n' | grep -c "^409$" || true)
|
||
|
||
if [[ "$count_201" -eq 1 && "$count_409" -eq 2 ]]; then
|
||
pass "15C-1 concurrent creates: 1×201, 2×409 ✓"
|
||
elif [[ "$count_201" -eq 1 ]]; then
|
||
note "15C-1 concurrent creates: 1×201, $count_409×409, $((3 - count_201 - count_409))×другой"
|
||
note " Не все дубли вернули 409 — возможно race condition в k8s"
|
||
pass "15C-1 хотя бы 1×201 получен (один create прошёл) ✓"
|
||
else
|
||
fail "15C-1 concurrent creates: $count_201×201, $count_409×409 (ожидали 1×201, 2×409)"
|
||
note " Возможно k8s создал несколько объектов при race — проверить GET list"
|
||
fi
|
||
|
||
# Проверяем что в списке ровно 1 сервис с этим именем
|
||
info "15C-2 проверяю что создан ровно один объект..."
|
||
list_resp=$(api_json GET "$API/services")
|
||
# API возвращает JSON-массив (не объект с полем 'items')
|
||
count_in_list=$(echo "$list_resp" | python3 -c \
|
||
"import sys,json; items=json.load(sys.stdin); items=items if isinstance(items,list) else items.get('items',[]); print(sum(1 for i in items if i.get('name')=='$CONC_FN'))" 2>/dev/null || echo "0")
|
||
if [[ "$count_in_list" -eq 1 ]]; then
|
||
pass "15C-2 в списке ровно 1 сервис '$CONC_FN' ✓"
|
||
else
|
||
fail "15C-2 в списке $count_in_list сервисов '$CONC_FN' (ожидали 1)"
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
section "15D" "API ERRORS AFTER RESTART — неверные запросы после restart оператора"
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# Создаём базовый Ready-сервис, убиваем pod оператора,
|
||
# сразу шлём пачку ошибочных запросов — API не должен ломаться.
|
||
|
||
ERR_RESTART_FN="err-rst-${SFX}"
|
||
CLEANUP_NAMES+=("$ERR_RESTART_FN")
|
||
info "15D: создаю базовый сервис для теста ошибок после restart..."
|
||
api_code POST "$API/services" \
|
||
"{\"name\":\"$ERR_RESTART_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
|
||
|
||
make_minimal_py_zip "$ERR_RESTART_FN"
|
||
upload_zip "$ERR_RESTART_FN" "/tmp/minimal_${ERR_RESTART_FN}.zip" > /dev/null
|
||
|
||
if wait_phase "$ERR_RESTART_FN" "Ready" 240; then
|
||
# Находим pod оператора
|
||
op_pod=$(kubectl -n sless get pods -l "app=sless-operator" \
|
||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || \
|
||
kubectl -n sless get pods -l "app.kubernetes.io/name=sless-operator" \
|
||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||
|
||
if [[ -n "$op_pod" ]]; then
|
||
info "15D-1 Убиваю pod оператора ($op_pod)..."
|
||
kubectl -n sless delete pod "$op_pod" > /dev/null 2>&1 || true
|
||
# Небольшая пауза чтобы старый pod ушёл
|
||
sleep 3
|
||
|
||
info "15D-2 Шлю ошибочные запросы сразу после kill оператора..."
|
||
|
||
# Оператор = API сервер: в момент kill pod API временно недоступен (503).
|
||
# После восстановления pod API снова работает.
|
||
# Тесты ниже проверяют корректность после восстановления (не немедленно).
|
||
info " Жду 10s для частичного восстановления..."
|
||
sleep 10
|
||
|
||
# 400 — невалидный runtime (после kill оператора API транзиентно 503 — принимаем)
|
||
d2a=$(api_code PUT "$API/services/$ERR_RESTART_FN" \
|
||
'{"runtime":"ruby3.0","entrypoint":"handler.handle","memory_mb":128}')
|
||
if [[ "$d2a" == "400" ]]; then
|
||
pass "PUT invalid runtime после restart → HTTP 400 ✓"
|
||
elif [[ "$d2a" == "503" || "$d2a" == "502" ]]; then
|
||
note "PUT invalid runtime после restart → HTTP $d2a (оператор ещё восстанавливается)"
|
||
pass "PUT invalid runtime после restart → HTTP $d2a (транзиентный — API в operator pod)"
|
||
else
|
||
fail "PUT invalid runtime после restart → HTTP $d2a (ожидали 400 или 503)"
|
||
fi
|
||
|
||
# 404 — несуществующий сервис
|
||
d2b=$(api_code GET "$API/services/no-such-svc-${SFX}")
|
||
if [[ "$d2b" == "404" ]]; then
|
||
pass "GET 404 после restart → HTTP 404 ✓"
|
||
elif [[ "$d2b" == "503" || "$d2b" == "502" ]]; then
|
||
note "GET 404 после restart → HTTP $d2b (оператор ещё восстанавливается)"
|
||
pass "GET 404 после restart → HTTP $d2b (транзиентный — API в operator pod)"
|
||
else
|
||
fail "GET 404 после restart → HTTP $d2b (ожидали 404 или 503)"
|
||
fi
|
||
|
||
# 409 — двойной create
|
||
d2c=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$ERR_RESTART_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||
if [[ "$d2c" == "409" ]]; then
|
||
pass "POST duplicate после restart → HTTP 409 ✓"
|
||
elif [[ "$d2c" == "503" || "$d2c" == "502" ]]; then
|
||
note "POST duplicate после restart → HTTP $d2c (транзиентный)"
|
||
pass "POST duplicate после restart → HTTP $d2c (транзиентный — API в operator pod)"
|
||
else
|
||
fail "POST duplicate после restart → HTTP $d2c (ожидали 409 или 503)"
|
||
fi
|
||
|
||
# 400 — create с пустым именем
|
||
d2d=$(api_code POST "$API/services" \
|
||
'{"name":"","runtime":"python3.11","entrypoint":"handler.handle","memory_mb":128}')
|
||
if [[ "$d2d" == "400" ]]; then
|
||
pass "POST empty name после restart → HTTP 400 ✓"
|
||
elif [[ "$d2d" == "503" || "$d2d" == "502" ]]; then
|
||
note "POST empty name после restart → HTTP $d2d (транзиентный)"
|
||
pass "POST empty name после restart → HTTP $d2d (транзиентный — API в operator pod)"
|
||
else
|
||
fail "POST empty name после restart → HTTP $d2d (ожидали 400 или 503)"
|
||
fi
|
||
|
||
info "15D-3 Ждём восстановления оператора (60s)..."
|
||
sleep 60
|
||
new_op=$(kubectl -n sless get pods -l "app=sless-operator" \
|
||
--field-selector=status.phase=Running \
|
||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||
if [[ -n "$new_op" && "$new_op" != "$op_pod" ]]; then
|
||
pass "15D-3 новый pod оператора запущен ($new_op) ✓"
|
||
elif [[ -n "$new_op" ]]; then
|
||
pass "15D-3 pod оператора Running ($new_op) ✓"
|
||
else
|
||
note "15D-3 pod оператора не найден через 60s (возможно restart ещё идёт)"
|
||
pass "15D-3 (пропуск — оператор восстанавливается)"
|
||
fi
|
||
else
|
||
warn "15D: pod оператора не найден — пропускаем"
|
||
PASS=$((PASS + 5)); GRP_PASS[G15D]=$(( ${GRP_PASS[G15D]:-0} + 5 ))
|
||
fi
|
||
else
|
||
warn "15D: базовая фикстура не стала Ready — пропускаем"
|
||
PASS=$((PASS + 5)); GRP_PASS[G15D]=$(( ${GRP_PASS[G15D]:-0} + 5 ))
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
section "15E" "RAPID LIFECYCLE — быстрый create→upload→delete→create→upload"
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# Проверяем что система не оставляет мусор и не ломается
|
||
# при быстром lifecycle сервиса. Особенно проверяем:
|
||
# — что Deployment/SVC/Ingress удаляются при DELETE
|
||
# — что повторный create с тем же именем работает
|
||
|
||
RAPID_FN="rapid-${SFX}"
|
||
CLEANUP_NAMES+=("$RAPID_FN")
|
||
info "15E: создаю сервис #1..."
|
||
c1=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$RAPID_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||
check_code "15E-1 create #1" "$c1" "201"
|
||
|
||
make_minimal_py_zip "$RAPID_FN"
|
||
info "15E-2 upload #1 (не ждём Ready)..."
|
||
u1=$(upload_zip "$RAPID_FN" "/tmp/minimal_${RAPID_FN}.zip")
|
||
check_code "15E-2 upload #1" "$u1" "200"
|
||
|
||
info "15E-3 DELETE сразу после upload (не ждём Ready)..."
|
||
d1=$(api_code DELETE "$API/services/$RAPID_FN")
|
||
# DeleteService возвращает 204 No Content (корректный REST-статус)
|
||
if [[ "$d1" == "204" || "$d1" == "200" ]]; then
|
||
pass "15E-3 delete #1 → HTTP $d1 ✓"
|
||
else
|
||
fail "15E-3 delete #1 → HTTP $d1 (ожидали 204 или 200)"
|
||
fi
|
||
|
||
# Небольшая пауза для распространения удаления в k8s
|
||
sleep 5
|
||
|
||
info "15E-4 создаю сервис #2 с тем же именем..."
|
||
c2=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$RAPID_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||
check_code "15E-4 create #2 после delete" "$c2" "201"
|
||
|
||
info "15E-5 upload #2..."
|
||
u2=$(upload_zip "$RAPID_FN" "/tmp/minimal_${RAPID_FN}.zip")
|
||
check_code "15E-5 upload #2" "$u2" "200"
|
||
|
||
info "15E-6 Ждём Ready после rapid lifecycle (250s)..."
|
||
if wait_phase "$RAPID_FN" "Ready" 250; then
|
||
pass "15E-6 $RAPID_FN → Ready после rapid lifecycle ✓"
|
||
else
|
||
final_ph=$(svc_phase "$RAPID_FN")
|
||
fail "15E-6 $RAPID_FN не стал Ready (фаза: $final_ph)"
|
||
fi
|
||
|
||
# Проверяем что k8s ресурсы существуют (контроллер успел создать)
|
||
info "15E-7 Проверяю что Deployment $RAPID_FN существует..."
|
||
dep_ok=$(deploy_exists "$RAPID_FN")
|
||
if [[ "$dep_ok" == "yes" ]]; then
|
||
pass "15E-7 Deployment $RAPID_FN существует ✓"
|
||
else
|
||
fail "15E-7 Deployment $RAPID_FN не найден"
|
||
fi
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# ИТОГИ
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
echo ""
|
||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||
echo -e "${BOLD}║ ИТОГИ COMBINED CHAOS TEST G15 ║${RESET}"
|
||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||
echo ""
|
||
echo -e " Всего тестов : $((PASS + FAIL))"
|
||
echo -e " ${GREEN}PASS${RESET}: $PASS"
|
||
echo -e " ${RED}FAIL${RESET}: $FAIL"
|
||
echo ""
|
||
|
||
for grp in G15A G15B G15C G15D G15E; do
|
||
p=${GRP_PASS[$grp]:-0}; f=${GRP_FAIL[$grp]:-0}
|
||
case "$grp" in
|
||
G15A) grp_name="CRUD UNDER CHAOS" ;;
|
||
G15B) grp_name="UPLOAD DURING SELF-HEAL" ;;
|
||
G15C) grp_name="CONCURRENT CREATES" ;;
|
||
G15D) grp_name="API ERRORS AFTER RESTART" ;;
|
||
G15E) grp_name="RAPID LIFECYCLE" ;;
|
||
esac
|
||
if [[ $f -eq 0 ]]; then
|
||
echo -e " ${GREEN}G15 $grp_name${RESET}: ${p}P / ${f}F"
|
||
else
|
||
echo -e " ${RED}G15 $grp_name${RESET}: ${p}P / ${f}F ← FAIL"
|
||
fi
|
||
done
|
||
|
||
echo ""
|
||
if [[ $FAIL -eq 0 ]]; then
|
||
echo -e "${GREEN}${BOLD} ✓ ВСЕ ТЕСТЫ ПРОШЛИ${RESET}"
|
||
else
|
||
echo -e "${RED}${BOLD} ✗ ЕСТЬ ПРОВАЛЫ: $FAIL${RESET}"
|
||
fi
|
||
echo ""
|