test: operator_failure_test.sh — group 12 failure modes (43/45)

This commit is contained in:
Naeel
2026-03-22 07:49:58 +03:00
parent 19fbfb5502
commit d4ccbc7d15
+862
View File
@@ -0,0 +1,862 @@
#!/usr/bin/env bash
# 2026-03-22 — operator_failure_test.sh
# ТЕСТ ОТКАЗОУСТОЙЧИВОСТИ — Группа 12: FAILURE MODES
#
# Проверяет поведение оператора при ошибочном коде/конфиге, который мог
# быть загружен пользователем по незнанию или небрежности:
#
# 12-A API VALIDATION — неверные параметры (memory, name, runtime) → 400
# 12-B UPLOAD FAILURES — испорченный zip, неверный runtime → 400 на upload
# 12-C BUILD FAILURES — несуществующий pip-пакет → канико падает → Failed
# 12-D CRASH AT STARTUP — синтаксическая ошибка, неверный entrypoint,
# отсутствующий модуль → контейнер падает при старте
# (ДОКУМЕНТИРУЕТ ПРОБЕЛ: оператор не детектирует
# CrashLoopBackOff, фаза остаётся "Ready")
# 12-E RUNTIME EXCEPTIONS — handle() бросает исключение → 500
# 12-F INVOKE TIMEOUT — handle() с бесконечным циклом → вызов зависает
# 12-G EDGE CASES — memory_mb=1 принимается API; DELETE из Failed
#
# ВАЖНО ПО РЕЗУЛЬТАТАМ:
# PASS — система ведёт себя как ожидается (в т.ч. корректно ловит ошибку)
# FAIL — система ведёт себя некорректно (баг или пробел)
# [NOTE] — поведение задокументировано, идеал отличается от факта
#
# ЗАПУСКАТЬ НА VM:
# nohup bash ~/terra/sless/operator_failure_test.sh > /tmp/failure.log 2>&1 &
# tail -f /tmp/failure.log
#
# Время прогона: ~25-40 минут (build failure ждёт kaniko ~3 мин).
# Зависимости: curl, python3, zip, kubectl
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="fm-${TS}" # fm = failure modes
# ─── СТАТИСТИКА ──────────────────────────────────────────────────────────────
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'
# ─── HELPERS ─────────────────────────────────────────────────────────────────
_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 METHOD URL [body] → HTTP status code
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 METHOD URL [body] → response body
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 NAME → phase string
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 NAME WANT TIMEOUT_SEC → 0=ok 1=timeout
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
}
# wait_failed NAME TIMEOUT_SEC → 0=got Failed 1=timeout 2=got Ready unexpectedly
wait_failed() {
local name="$1" timeout_sec="${2:-180}"
local deadline=$((SECONDS + timeout_sec))
while [[ $SECONDS -lt $deadline ]]; do
local phase; phase=$(svc_phase "$name")
[[ "$phase" == "Failed" ]] && return 0
[[ "$phase" == "Ready" ]] && return 2
sleep 5
done
return 1
}
# upload_raw NAME ZIPFILE → HTTP code
# Загружает zip через multipart form (стандартный upload endpoint)
upload_raw() {
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"
}
# invoke_one NAME [TIMEOUT] → HTTP code (000 при таймауте/connection refused)
invoke_one() {
local name="$1" timeout="${2:-30}"
curl -s -o /dev/null -w "%{http_code}" -m "$timeout" \
-X POST -H "Content-Type: application/json" \
-d '{"msg":"failure-test"}' \
"$FN_BASE/$name"
}
# invoke_json NAME [TIMEOUT] → response body
invoke_json() {
local name="$1" timeout="${2:-30}"
curl -s -m "$timeout" \
-X POST -H "Content-Type: application/json" \
-d '{"msg":"failure-test"}' \
"$FN_BASE/$name"
}
# ─── ZIP GENERATORS ───────────────────────────────────────────────────────────
# make_corrupt_zip → /tmp/corrupt_${SFX}.zip (просто мусорные байты, не zip)
make_corrupt_zip() {
printf '\xff\xfe\x00\x01THISISNOTAVALIDZIPARCHIVE\xff\xfe\x00\x02garbage\n' \
> "/tmp/corrupt_${SFX}.zip"
}
# make_empty_valid_zip → /tmp/empty_${SFX}.zip (валидный zip но без файлов)
make_empty_valid_zip() {
local tmpdir; tmpdir=$(mktemp -d)
# zip пустой директории → архив с заголовком но без файлов
(cd "$tmpdir" && touch PLACEHOLDER && zip -q "/tmp/empty_${SFX}.zip" PLACEHOLDER)
# Удалим placeholder из архива — оставим структуру пустого zip
rm -rf "$tmpdir"
# Просто создадим минимальный zip без handler.py — это ключевой кейс
local tmpdir2; tmpdir2=$(mktemp -d)
cat > "$tmpdir2/README.txt" <<'EOF'
No handler here. User forgot to put the actual code.
EOF
(cd "$tmpdir2" && zip -q "/tmp/empty_${SFX}.zip" README.txt)
rm -rf "$tmpdir2"
}
# make_bad_requirements_zip NAME → /tmp/badreq_${NAME}.zip
# handler.py нормальный, requirements.txt с несуществующим пакетом →
# pip install упадёт → kaniko job failed → phase=Failed
make_bad_requirements_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
def handle(event):
return {"ok": True, "msg": "i will never run"}
PYEOF
# Пакет надуманный — pip не найдёт его → build failure
printf 'sless-nonexistent-pkg-xyz==999.0.0\n' > "$tmpdir/requirements.txt"
(cd "$tmpdir" && zip -q "/tmp/badreq_${name}.zip" handler.py requirements.txt)
rm -rf "$tmpdir"
}
# make_syntax_error_zip NAME → /tmp/syntax_${NAME}.zip
# handler.py с синтаксической ошибкой Python → exec_module() bails at startup
make_syntax_error_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
# Пользователь перепутал скобки — классическая ошибка
def handle(event)
return {"ok": True}
PYEOF
(cd "$tmpdir" && zip -q "/tmp/syntax_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
# make_bad_import_zip NAME → /tmp/badimport_${NAME}.zip
# handler.py с top-level import несуществующего модуля →
# exec_module() падает с ImportError → контейнер падает при старте
make_bad_import_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
# Пользователь написал название библиотеки неправильно (опечатка)
import sless_nonexistent_module_xyz_typo as client
def handle(event):
return {"ok": True}
PYEOF
(cd "$tmpdir" && zip -q "/tmp/badimport_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
# make_exception_zip NAME → /tmp/exception_${NAME}.zip
# handle() всегда бросает исключение → server.py ловит → возвращает 500
make_exception_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
def handle(event):
# Пользователь забыл обработать ошибку и raise всегда срабатывает
raise ValueError("something went wrong in user code: division by zero: " + str(1 // 0))
PYEOF
(cd "$tmpdir" && zip -q "/tmp/exception_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
# make_infinite_zip NAME → /tmp/infinite_${NAME}.zip
# handle() никогда не возвращается → invoke зависает навсегда
make_infinite_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
import time
def handle(event):
# Пользователь написал бесконечный цикл вместо конечного
while True:
time.sleep(1)
PYEOF
(cd "$tmpdir" && zip -q "/tmp/infinite_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
# make_node_bad_export_zip NAME → /tmp/nodebad_${NAME}.zip
# Node handler экспортирует неправильное имя — не exports.handle
make_node_bad_export_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.js" <<'JSEOF'
'use strict';
// Пользователь перепутал имя экспорта — написал "run" вместо "handle"
exports.run = function(event) {
return { ok: true };
};
JSEOF
(cd "$tmpdir" && zip -q "/tmp/nodebad_${name}.zip" handler.js)
rm -rf "$tmpdir"
}
# make_minimal_py_zip NAME → /tmp/minimal_${NAME}.zip
# Простой рабочий handler для базовых Deploy-проверок
make_minimal_py_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
def handle(event):
return {"ok": True, "test": "failure_modes"}
PYEOF
(cd "$tmpdir" && zip -q "/tmp/minimal_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
# upload_zip NAME ZIPFILE → HTTP code
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"
}
# cleanup NAME — удалить сервис
cleanup() {
local name="$1"
[[ -z "$name" ]] && return 0
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
sleep 2
}
CLEANUP_NAMES=()
teardown() {
if [[ ${#CLEANUP_NAMES[@]} -gt 0 ]]; then
echo -e "\n${CYAN}[TEARDOWN]${RESET} удаляю тестовые сервисы..."
for name in "${CLEANUP_NAMES[@]}"; do
[[ -z "$name" ]] && continue
cleanup "$name"
echo -e " ${CYAN}[TEARDOWN]${RESET} удалён: $name"
done
fi
}
trap teardown EXIT
# ─── СТАРТОВЫЙ БАННЕР ────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║ OPERATOR FAILURE MODES TEST — sless v0.1.49 ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
echo -e " Дата : $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)"
echo -e " API : $API"
echo -e " SFX : $SFX"
echo ""
# ═══════════════════════════════════════════════════════════════════════════════
section "12A" "API VALIDATION — неверные параметры на входе"
# ═══════════════════════════════════════════════════════════════════════════════
# Цель: API должен возвращать 400 на явно мусорные запросы, не создавая CRD.
info "12A-1 memory_mb=0 → ожидаем 400..."
check_code "memory_mb=0" \
"$(api_code POST "$API/services" \
'{"name":"val-test","runtime":"python3.11","entrypoint":"h.h","memory_mb":0}')" \
"400"
info "12A-2 memory_mb=5000 (>4096) → ожидаем 400..."
check_code "memory_mb=5000" \
"$(api_code POST "$API/services" \
'{"name":"val-test","runtime":"python3.11","entrypoint":"h.h","memory_mb":5000}')" \
"400"
info "12A-3 memory_mb=-1 → ожидаем 400..."
check_code "memory_mb=-1" \
"$(api_code POST "$API/services" \
'{"name":"val-test","runtime":"python3.11","entrypoint":"h.h","memory_mb":-1}')" \
"400"
info "12A-4 name пустой → ожидаем 400..."
check_code "name empty" \
"$(api_code POST "$API/services" \
'{"name":"","runtime":"python3.11","entrypoint":"h.h","memory_mb":128}')" \
"400"
info "12A-5 runtime пустой → ожидаем 400..."
check_code "runtime empty" \
"$(api_code POST "$API/services" \
'{"name":"val-test","runtime":"","entrypoint":"h.h","memory_mb":128}')" \
"400"
info "12A-6 entrypoint пустой → ожидаем 400..."
check_code "entrypoint empty" \
"$(api_code POST "$API/services" \
'{"name":"val-test","runtime":"python3.11","entrypoint":"","memory_mb":128}')" \
"400"
info "12A-7 невалидный JSON body → ожидаем 400..."
check_code "invalid JSON" \
"$(api_code POST "$API/services" 'NOT JSON AT ALL {{{}')" \
"400"
info "12A-8 memory_mb=1 → ожидаем 201 (API принимает, нет нижней границы)"
code=$(api_code POST "$API/services" \
'{"name":"val-mem1-'"${SFX}"'","runtime":"python3.11","entrypoint":"handler.handle","memory_mb":1}')
if [[ "$code" == "201" ]]; then
pass "memory_mb=1 → HTTP 201 (создан)"
note "GAP: memory_mb=1 принимается API без предупреждения. Контейнер с 1Mi OOMKill'нется."
# Удалим сразу — нам build не нужен
api_code DELETE "$API/services/val-mem1-${SFX}" > /dev/null 2>&1 || true
else
fail "memory_mb=1 → HTTP $code (ожидали 201 — нет нижней границы в API)"
fi
# ═══════════════════════════════════════════════════════════════════════════════
section "12B" "UPLOAD FAILURES — испорченный zip / неверный runtime"
# ═══════════════════════════════════════════════════════════════════════════════
# 12B-1: Corrupt zip — случайные байты вместо zip-архива
CORRUPT_FN="corrupt-${SFX}"
CLEANUP_NAMES+=("$CORRUPT_FN")
info "12B-1 Создаём функцию для corrupt zip test..."
create_code=$(api_code POST "$API/services" \
"{\"name\":\"$CORRUPT_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$create_code" == "201" ]]; then
make_corrupt_zip
info " Upload испорченного файла → ожидаем 400..."
uc=$(upload_zip "$CORRUPT_FN" "/tmp/corrupt_${SFX}.zip")
if [[ "$uc" == "400" ]]; then
pass "corrupt zip → upload HTTP 400 (отклонён на этапе parse zip)"
else
fail "corrupt zip → upload HTTP $uc (ожидали 400)"
fi
info " DELETE функции после отклонённого upload → ожидаем 204..."
check_code "DELETE after corrupt upload" \
"$(api_code DELETE "$API/services/$CORRUPT_FN")" "204"
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$CORRUPT_FN}" )
else
fail "create $CORRUPT_FN → HTTP $create_code"
fi
# 12B-2: Неверный runtime ("ruby3.0") — создаётся, но upload падает
BADRT_FN="badrt-${SFX}"
CLEANUP_NAMES+=("$BADRT_FN")
info "12B-2 runtime='ruby3.0' → create ожидаем 201, upload ожидаем 400..."
create_code2=$(api_code POST "$API/services" \
"{\"name\":\"$BADRT_FN\",\"runtime\":\"ruby3.0\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$create_code2" == "201" ]]; then
pass "runtime=ruby3.0 → create HTTP 201 (API не валидирует runtime имена)"
note "GAP: API принимает любое runtime имя при создании; ошибка только на upload."
# Загружаем минимальный zip — PrepareContext упадёт на runtimeBaseImage("ruby3.0")
make_minimal_py_zip "$BADRT_FN"
uc2=$(upload_zip "$BADRT_FN" "/tmp/minimal_${BADRT_FN}.zip")
if [[ "$uc2" == "400" ]]; then
pass "runtime=ruby3.0 → upload HTTP 400 (unsupported runtime отклонён в builder)"
else
fail "runtime=ruby3.0 → upload HTTP $uc2 (ожидали 400)"
fi
check_code "DELETE after bad runtime" \
"$(api_code DELETE "$API/services/$BADRT_FN")" "204"
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$BADRT_FN}" )
else
fail "runtime=ruby3.0 → create HTTP $create_code2 (ожидали 201)"
fi
# 12B-3: Пустой zip (без handler.py) — upload и build проходят, но контейнер падает
EMPTY_FN="emptyzip-${SFX}"
CLEANUP_NAMES+=("$EMPTY_FN")
info "12B-3 Zip без handler.py → upload OK, build OK, но контейнер падает при старте..."
create_code3=$(api_code POST "$API/services" \
"{\"name\":\"$EMPTY_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$create_code3" == "201" ]]; then
make_empty_valid_zip
uc3=$(upload_zip "$EMPTY_FN" "/tmp/empty_${SFX}.zip")
if [[ "$uc3" == "200" ]]; then
pass "empty zip (без handler.py) → upload HTTP 200 (builder не проверяет наличие handler)"
note "GAP: builder не требует наличия handler.py в zip. Build пройдёт, контейнер упадёт."
info " Ждём Ready (build проходит успешно, 180s)..."
if wait_phase "$EMPTY_FN" "Ready" 180; then
pass "$EMPTY_FN → phase=Ready (build succeed без handler.py)"
info " invoke → ожидаем сбой (FileNotFoundError при загрузке handler.py)..."
sleep 5 # дать pod время запуститься и упасть
inv_code=$(invoke_one "$EMPTY_FN" 15)
if [[ "$inv_code" != "200" ]]; then
pass "invoke без handler.py → HTTP $inv_code (не 200 — контейнер падает при старте)"
note "GAP: фаза='Ready' но функция не работает. Оператор не детектирует CrashLoopBackOff."
else
fail "invoke без handler.py → HTTP 200 (неожиданно работает?)"
fi
else
warn "$EMPTY_FN не достиг Ready — ждём ещё или уже Failed"
fi
else
fail "empty zip → upload HTTP $uc3 (ожидали 200)"
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
section "12C" "BUILD FAILURES — несуществующий pip-пакет → phase=Failed"
# ═══════════════════════════════════════════════════════════════════════════════
# Цель: проверить что kaniko build failure → оператор переводит в Failed,
# а не зависает в Building навсегда.
BADPIP_FN="badpip-${SFX}"
CLEANUP_NAMES+=("$BADPIP_FN")
info "12C-1 Создаём функцию с несуществующим pip-пакетом (sless-nonexistent-pkg-xyz)..."
create_code4=$(api_code POST "$API/services" \
"{\"name\":\"$BADPIP_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$create_code4" == "201" ]]; then
make_bad_requirements_zip "$BADPIP_FN"
uc4=$(upload_zip "$BADPIP_FN" "/tmp/badreq_${BADPIP_FN}.zip")
if [[ "$uc4" == "200" ]]; then
pass "bad requirements.txt → upload HTTP 200 (zip валидный, ошибка позже при kaniko)"
info " Ждём phase=Failed (kaniko pip install упадёт, до 240s)..."
wait_failed_result=0
wait_failed "$BADPIP_FN" 240 || wait_failed_result=$?
case "$wait_failed_result" in
0) pass "bad requirements.txt → phase=Failed ✓ (kaniko build failure детектирован)" ;;
2) fail "bad requirements.txt → phase=Ready (build как-то прошёл — неожиданно)" ;;
1) fail "bad requirements.txt → timeout 240s, phase не стало Failed" ;;
esac
info " 12C-2 DELETE функции в Failed-состоянии → ожидаем 204..."
check_code "DELETE in Failed state" \
"$(api_code DELETE "$API/services/$BADPIP_FN")" "204"
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$BADPIP_FN}" )
pass "DELETE из Failed → 204 (recovery возможен)"
else
fail "bad requirements.txt → upload HTTP $uc4 (ожидали 200)"
fi
else
fail "create $BADPIP_FN → HTTP $create_code4"
fi
# ═══════════════════════════════════════════════════════════════════════════════
section "12D" "CRASH AT STARTUP — код ломает контейнер при загрузке"
# ═══════════════════════════════════════════════════════════════════════════════
# Сценарии где build успешен, но контейнер падает при первом импорте module.
# ФАКТ: оператор показывает phase=Ready, но invoke возвращает не-200.
# ИДЕАЛ: оператор должен детектировать CrashLoopBackOff → Failed.
# 12D-1: Python syntax error
SYNTAX_FN="syntax-${SFX}"
CLEANUP_NAMES+=("$SYNTAX_FN")
info "12D-1 Python syntax error в handler.py..."
c=$(api_code POST "$API/services" \
"{\"name\":\"$SYNTAX_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$c" == "201" ]]; then
make_syntax_error_zip "$SYNTAX_FN"
uc=$(upload_zip "$SYNTAX_FN" "/tmp/syntax_${SYNTAX_FN}.zip")
if [[ "$uc" == "200" ]]; then
pass "syntax error zip → upload 200 (парсер Python не вызывается при build)"
info " Ждём Ready (180s)..."
if wait_phase "$SYNTAX_FN" "Ready" 180; then
pass "$SYNTAX_FN → phase=Ready (build прошёл — kaniko только копирует файлы)"
note "GAP: сборка не проверяет синтаксис Python. Ошибка видна только при запуске контейнера."
sleep 5
inv_code=$(invoke_one "$SYNTAX_FN" 15)
if [[ "$inv_code" != "200" ]]; then
pass "syntax error → invoke HTTP $inv_code (не 200, контейнер упал на SyntaxError) ✓"
note "GAP: фаза=Ready, но invoke ломается. Нет детектора CrashLoopBackOff в операторе."
else
fail "syntax error → invoke HTTP 200 (Python как-то выполнил невалидный код?)"
fi
else
warn "$SYNTAX_FN не стал Ready — возможно Failed (неожиданно)"
fi
else
fail "syntax error zip → upload HTTP $uc"
fi
fi
# 12D-2: Неверное имя функции в entrypoint (handler.no_such_function)
BADEP_FN="badep-${SFX}"
CLEANUP_NAMES+=("$BADEP_FN")
info "12D-2 Entrypoint 'handler.no_such_function' — функции не существует..."
c2=$(api_code POST "$API/services" \
"{\"name\":\"$BADEP_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.no_such_function\",\"memory_mb\":128}")
if [[ "$c2" == "201" ]]; then
make_minimal_py_zip "$BADEP_FN" # handler.py есть, но функция называется handle, не no_such_function
uc2=$(upload_zip "$BADEP_FN" "/tmp/minimal_${BADEP_FN}.zip")
if [[ "$uc2" == "200" ]]; then
pass "wrong entrypoint → upload 200"
info " Ждём Ready (180s)..."
if wait_phase "$BADEP_FN" "Ready" 180; then
pass "$BADEP_FN → phase=Ready"
note "GAP: API не проверяет что entrypoint совпадает с функцией внутри zip."
sleep 5
inv_code2=$(invoke_one "$BADEP_FN" 15)
if [[ "$inv_code2" != "200" ]]; then
pass "wrong entrypoint → invoke HTTP $inv_code2 (не 200, AttributeError при старте) ✓"
else
fail "wrong entrypoint → invoke HTTP 200 (как вызвал несуществующую функцию?)"
fi
else
warn "$BADEP_FN не Ready"
fi
else
fail "wrong entrypoint zip → upload HTTP $uc2"
fi
fi
# 12D-3: Top-level неверный import (опечатка в имени библиотеки)
BADIMPORT_FN="badimport-${SFX}"
CLEANUP_NAMES+=("$BADIMPORT_FN")
info "12D-3 'import sless_nonexistent_module_xyz_typo' на верхнем уровне handler.py..."
c3=$(api_code POST "$API/services" \
"{\"name\":\"$BADIMPORT_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$c3" == "201" ]]; then
make_bad_import_zip "$BADIMPORT_FN"
uc3=$(upload_zip "$BADIMPORT_FN" "/tmp/badimport_${BADIMPORT_FN}.zip")
if [[ "$uc3" == "200" ]]; then
pass "bad import zip → upload 200"
info " Ждём Ready (180s)..."
if wait_phase "$BADIMPORT_FN" "Ready" 180; then
pass "$BADIMPORT_FN → phase=Ready (build не проверяет импорты)"
note "GAP: отсутствующий import на верхнем уровне не виден до старта контейнера."
sleep 5
inv_code3=$(invoke_one "$BADIMPORT_FN" 15)
if [[ "$inv_code3" != "200" ]]; then
pass "bad import → invoke HTTP $inv_code3 (не 200, ImportError при старте) ✓"
else
fail "bad import → invoke HTTP 200 (неожиданно — модуль как-то нашёлся?)"
fi
else
warn "$BADIMPORT_FN не Ready"
fi
else
fail "bad import zip → upload HTTP $uc3"
fi
fi
# 12D-4: Node.js — неправильный exports.name (exports.run вместо exports.handle)
NODEBAD_FN="nodebad-${SFX}"
CLEANUP_NAMES+=("$NODEBAD_FN")
info "12D-4 Node.js: exports.run вместо exports.handle (неверное имя экспорта)..."
c4=$(api_code POST "$API/services" \
"{\"name\":\"$NODEBAD_FN\",\"runtime\":\"nodejs20\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$c4" == "201" ]]; then
make_node_bad_export_zip "$NODEBAD_FN"
uc4=$(upload_zip "$NODEBAD_FN" "/tmp/nodebad_${NODEBAD_FN}.zip")
if [[ "$uc4" == "200" ]]; then
pass "Node.js wrong export → upload 200"
info " Ждём Ready (180s)..."
if wait_phase "$NODEBAD_FN" "Ready" 180; then
pass "$NODEBAD_FN → phase=Ready"
note "GAP: Node.js server.js делает process.exit(1) при неверном экспорте → CrashLoop → Ready всё равно."
sleep 5
inv_code4=$(invoke_one "$NODEBAD_FN" 15)
if [[ "$inv_code4" != "200" ]]; then
pass "Node.js wrong export → invoke HTTP $inv_code4 (не 200) ✓"
else
fail "Node.js wrong export → invoke HTTP 200 (как?"
fi
else
warn "$NODEBAD_FN не Ready"
fi
else
fail "Node.js wrong export → upload HTTP $uc4"
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
section "12E" "RUNTIME EXCEPTIONS — handle() бросает исключения"
# ═══════════════════════════════════════════════════════════════════════════════
# Здесь ошибка происходит внутри handle() — server.py ловит через try/except.
# Ожидаемое поведение: invoke возвращает 500, а не EOF/502.
EXC_FN="exception-${SFX}"
CLEANUP_NAMES+=("$EXC_FN")
info "12E-1 Создаём функцию с handle() который всегда бросает ValueError..."
c5=$(api_code POST "$API/services" \
"{\"name\":\"$EXC_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$c5" == "201" ]]; then
make_exception_zip "$EXC_FN"
uc5=$(upload_zip "$EXC_FN" "/tmp/exception_${EXC_FN}.zip")
if [[ "$uc5" == "200" ]]; then
pass "exception zip → upload 200"
info " Ждём Ready (180s)..."
if wait_phase "$EXC_FN" "Ready" 180; then
pass "$EXC_FN → phase=Ready"
sleep 3
info " 12E-2 invoke → ожидаем 500 (exception в handle() пойман server.py)..."
inv_code5=$(invoke_one "$EXC_FN" 30)
if [[ "$inv_code5" == "500" ]]; then
pass "exception → invoke HTTP 500 ✓ (server.py поймал ValueError, вернул JSON error)"
else
fail "exception → invoke HTTP $inv_code5 (ожидали 500)"
fi
info " 12E-3 Повторный invoke → тоже 500, функция не зависает..."
inv_code5b=$(invoke_one "$EXC_FN" 30)
check_code "repeat exception invoke" "$inv_code5b" "500"
info " 12E-4 Ответ 500 содержит поле error с описанием исключения..."
response=$(invoke_json "$EXC_FN" 30)
has_error=$(echo "$response" | python3 -c \
'import sys,json; d=json.load(sys.stdin); print("error" in d)' 2>/dev/null)
if [[ "$has_error" == "True" ]]; then
pass "500 response содержит поле 'error' ✓"
else
fail "500 response не содержит поле 'error': $response"
fi
else
warn "$EXC_FN не Ready"
fi
else
fail "exception zip → upload HTTP $uc5"
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
section "12F" "INVOKE TIMEOUT — handle() с бесконечным циклом"
# ═══════════════════════════════════════════════════════════════════════════════
# Цель: invoke зависает навсегда, вызывающий получает таймаут (не 502/EOF).
# Это тест НА ПОВЕДЕНИЕ вызывающего — оператор не может ограничить выполнение.
INF_FN="infinite-${SFX}"
CLEANUP_NAMES+=("$INF_FN")
info "12F-1 Создаём функцию с бесконечным циклом (while True: sleep(1))..."
c6=$(api_code POST "$API/services" \
"{\"name\":\"$INF_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$c6" == "201" ]]; then
make_infinite_zip "$INF_FN"
uc6=$(upload_zip "$INF_FN" "/tmp/infinite_${INF_FN}.zip")
if [[ "$uc6" == "200" ]]; then
pass "infinite loop zip → upload 200"
info " Ждём Ready (180s)..."
if wait_phase "$INF_FN" "Ready" 180; then
pass "$INF_FN → phase=Ready"
sleep 3
info " 12F-2 invoke с таймаутом 8s → ожидаем код 000 (curl timeout)..."
start_ts=$SECONDS
inf_code=$(invoke_one "$INF_FN" 8)
elapsed=$((SECONDS - start_ts))
if [[ "$inf_code" == "000" ]]; then
pass "infinite loop → invoke timeout за ${elapsed}s, код 000 ✓"
note "INFO: оператор не ограничивает время выполнения функции. Ответственность на вызывающем."
elif [[ "$inf_code" != "200" ]]; then
pass "infinite loop → invoke HTTP $inf_code (не 200, прервало ingress/proxy) за ${elapsed}s"
else
fail "infinite loop → invoke HTTP 200 за ${elapsed}s (неожиданно вернулся)"
fi
info " 12F-3 Функция доступна после таймаутного запроса (новый invoke ещё может зависнуть)..."
# Проверим что pod живой (не упал) и GET /health работает
health_code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
"$FN_BASE/$INF_FN" 2>/dev/null || echo "000")
if [[ "$health_code" != "200" ]]; then
pass "функция принимает новые запросы после предыдущего таймаута (висит снова: $health_code)"
fi
else
warn "$INF_FN не Ready"
fi
else
fail "infinite loop zip → upload HTTP $uc6"
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
section "12G" "EDGE CASES — компоновка крайних случаев"
# ═══════════════════════════════════════════════════════════════════════════════
# 12G-1: DELETE функции которая осталась в Pending (после failed upload)
PEND_DEL_FN="pendel-${SFX}"
CLEANUP_NAMES+=("$PEND_DEL_FN")
info "12G-1 Создаём функцию, не загружаем код, сразу DELETE → ожидаем 204..."
api_code POST "$API/services" \
"{\"name\":\"$PEND_DEL_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
sleep 1
check_code "DELETE Pending function (no upload)" \
"$(api_code DELETE "$API/services/$PEND_DEL_FN")" "204"
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$PEND_DEL_FN}" )
# 12G-2: Повторное создание с тем же именем после удаления — нет конфликта
REUSE_FN="reuse-${SFX}"
CLEANUP_NAMES+=("$REUSE_FN")
info "12G-2 Создаём, удаляем, повторно создаём с тем же именем → нет конфликта..."
c_r1=$(api_code POST "$API/services" \
"{\"name\":\"$REUSE_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
sleep 1
api_code DELETE "$API/services/$REUSE_FN" > /dev/null 2>&1
sleep 5 # даём finalizer-у отработать
c_r2=$(api_code POST "$API/services" \
"{\"name\":\"$REUSE_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$c_r1" == "201" && "$c_r2" == "201" ]]; then
pass "повторное создание '$REUSE_FN' → 201 (нет конфликта имени после DELETE) ✓"
else
fail "повторное создание: первое=$c_r1 второе=$c_r2 (ожидали оба 201)"
fi
api_code DELETE "$API/services/$REUSE_FN" > /dev/null 2>&1
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$REUSE_FN}" )
# 12G-3: PUT (update) с memory_mb=0 на существующую функцию → 400
UPD_FN="update-${SFX}"
CLEANUP_NAMES+=("$UPD_FN")
info "12G-3 PUT update с memory_mb=0 на существующую функцию → ожидаем 400..."
api_code POST "$API/services" \
"{\"name\":\"$UPD_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
sleep 1
check_code "PUT memory_mb=0 on existing function" \
"$(api_code PUT "$API/services/$UPD_FN" \
'{"runtime":"python3.11","entrypoint":"handler.handle","memory_mb":0}')" \
"400"
api_code DELETE "$API/services/$UPD_FN" > /dev/null 2>&1
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$UPD_FN}" )
# 12G-4: GET несуществующего сервиса → 404
info "12G-4 GET несуществующего сервиса → ожидаем 404..."
check_code "GET nonexistent function" \
"$(api_code GET "$API/services/totally-nonexistent-fn-xyz-${SFX}")" "404"
# 12G-5: DELETE несуществующего сервиса → 404
info "12G-5 DELETE несуществующего сервиса → ожидаем 404..."
check_code "DELETE nonexistent function" \
"$(api_code DELETE "$API/services/totally-nonexistent-fn-xyz-${SFX}")" "404"
# 12G-6: Дублированное создание (POST с тем же именем без delete) → 409 или 422
DUP_FN="dup-${SFX}"
CLEANUP_NAMES+=("$DUP_FN")
info "12G-6 Создать дубль (POST с тем же именем без delete) → ожидаем 409/422..."
api_code POST "$API/services" \
"{\"name\":\"$DUP_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
sleep 1
dup_code=$(api_code POST "$API/services" \
"{\"name\":\"$DUP_FN\",\"runtime\":\"python3.11\",\
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$dup_code" == "409" || "$dup_code" == "422" || "$dup_code" == "400" ]]; then
pass "duplicate create → HTTP $dup_code (дубль отклонён) ✓"
else
fail "duplicate create → HTTP $dup_code (ожидали 409/422, допустимо 400)"
fi
api_code DELETE "$API/services/$DUP_FN" > /dev/null 2>&1
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$DUP_FN}" )
# ═══════════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║ ИТОГИ FAILURE MODES TEST ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
echo ""
echo " Время завершения : $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)"
echo ""
echo " Всего тестов : $((PASS + FAIL))"
echo " PASS : $PASS"
echo " FAIL : $FAIL"
echo ""
for grp in G12A G12B G12C G12D G12E G12F G12G; do
g_pass=${GRP_PASS[$grp]:-0}
g_fail=${GRP_FAIL[$grp]:-0}
g_total=$((g_pass + g_fail))
label=""
case "$grp" in
G12A) label="API VALIDATION";; G12B) label="UPLOAD FAILURES";;
G12C) label="BUILD FAILURES";; G12D) label="CRASH AT STARTUP";;
G12E) label="RUNTIME EXCEPTIONS";; G12F) label="INVOKE TIMEOUT";;
G12G) label="EDGE CASES";;
esac
if [[ $g_fail -eq 0 ]]; then
echo -e " ${GREEN}${RESET} Группа $grp ($label): ${g_pass}/${g_total}"
else
echo -e " ${RED}${RESET} Группа $grp ($label): ${g_pass}/${g_total} (FAIL: $g_fail)"
fi
done
echo ""
if [[ $FAIL -eq 0 ]]; then
echo -e " ${GREEN}${BOLD}✓ FAILURE MODES — система корректно обрабатывает все сценарии${RESET}"
else
echo -e " ${RED}${BOLD}✗ FAILURE MODES — $FAIL тестов упало (изучить [FAIL] строки выше)${RESET}"
fi
echo ""
echo -e " ${YELLOW}[NOTE]${RESET} Строки [NOTE] выше описывают документированные пробелы в операторе,"
echo -e " не обязательно баги — это известное поведение для улучшения."
echo ""