From d4ccbc7d15492c25758bafa1bdbdb083504c2221 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 22 Mar 2026 07:49:58 +0300 Subject: [PATCH] =?UTF-8?q?test:=20operator=5Ffailure=5Ftest.sh=20?= =?UTF-8?q?=E2=80=94=20group=2012=20failure=20modes=20(43/45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operator_failure_test.sh | 862 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 862 insertions(+) create mode 100644 operator_failure_test.sh diff --git a/operator_failure_test.sh b/operator_failure_test.sh new file mode 100644 index 0000000..230e6cc --- /dev/null +++ b/operator_failure_test.sh @@ -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 ""