#!/usr/bin/env bash # 2026-03-22 — operator_chaos_test.sh # ТЕСТ КЛАСТЕРНОГО ХАОСА — Группа 14: CLUSTER CHAOS # # Симулирует отказы инфраструктуры — вещи которые происходят в реальном кластере # независимо от действий пользователя: убитые pods, удалённые Deployments, # OOM-убийства, прерванные сборки, проблемы с образами. # # 14-A SELF-HEALING — удалить Deployment вручную → оператор пересоздаст за 60s # удалить k8s Service → пересоздаст # 14-B POD KILL RESILIENCE — kubectl delete pod → ReplicaSet сразу поднимает новый # 14-C OOM KILL — memory_mb=32 + функция выделяет 300MB → OOMKill # (документирует пробел: фаза остаётся "Ready") # 14-D KANIKO INTERRUPT — убить kaniko Job в середине build → phase=Failed # (builder.JobStatus: IsNotFound → "failed") # 14-E OPERATOR RESTART — задокументированный тест: оператор перезапустили? # → reconcile loop восстанавливает все сервисы # # PASS — система ведёт себя как ожидается # FAIL — неожиданное/неправильное поведение # [NOTE] — документированный пробел # # ВАЖНО: тест использует kubectl напрямую для хаоса. # Требует: kubectl, curl, python3, zip, права на sless и sless-fn-* namespace'ы # # ЗАПУСКАТЬ: # nohup bash ~/terra/sless/operator_chaos_test.sh > /tmp/chaos14.log 2>&1 & # tail -f /tmp/chaos14.log # # Время прогона: ~35-50 мин (OOM-тест требует полного build + ожидания crash) 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="ch-${TS}" # ch = chaos # ─── СТАТИСТИКА ─────────────────────────────────────────────────────────────── 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() { 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 } 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 '{"test":"chaos"}' \ "$FN_BASE/$name" } 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" } make_minimal_py_zip() { local name="$1" local tmpdir; tmpdir=$(mktemp -d) cat > "$tmpdir/handler.py" <<'PYEOF' def handle(event): return {"ok": True, "group": "chaos_14"} PYEOF (cd "$tmpdir" && zip -q "/tmp/minimal_${name}.zip" handler.py) rm -rf "$tmpdir" } # Функция которая выделяет много памяти — должна вызвать OOMKill make_oom_py_zip() { local name="$1" local tmpdir; tmpdir=$(mktemp -d) cat > "$tmpdir/handler.py" <<'PYEOF' import os def handle(event): # Выделяем ~300MB в памяти — при memory_mb=32 pod будет OOMKilled # sys.getsizeof('x' * 300_000_000) ~ 300MB hungry = bytearray(300 * 1024 * 1024) # 300MB return {"ok": True, "allocated_bytes": len(hungry)} PYEOF (cd "$tmpdir" && zip -q "/tmp/oom_${name}.zip" handler.py) rm -rf "$tmpdir" } # Функция с длинным sleep — для тестов прерывания make_slow_py_zip() { local name="$1" local tmpdir; tmpdir=$(mktemp -d) cat > "$tmpdir/handler.py" <<'PYEOF' import time def handle(event): time.sleep(60) return {"ok": True} PYEOF (cd "$tmpdir" && zip -q "/tmp/slow_${name}.zip" handler.py) rm -rf "$tmpdir" } # deployments_exist NAME → "yes"/"no" deployment_exists() { local name="$1" if kubectl -n "$DEPLOY_NS" get deployment "$name" > /dev/null 2>&1; then echo "yes" else echo "no" fi } # k8s_svc_exists NAME → "yes"/"no" k8s_svc_exists() { local name="$1" if kubectl -n "$DEPLOY_NS" get service "$name" > /dev/null 2>&1; then echo "yes" else echo "no" fi } # get_pod_name NAME → pod name or empty get_pod_name() { local name="$1" kubectl -n "$DEPLOY_NS" get pods -l "app=$name" \ --field-selector=status.phase=Running \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true } # wait_pod_running NAME TIMEOUT → 0=ok 1=timeout wait_pod_running() { local name="$1" timeout_sec="${2:-120}" local deadline=$((SECONDS + timeout_sec)) while [[ $SECONDS -lt $deadline ]]; do local pod; pod=$(kubectl -n "$DEPLOY_NS" get pods -l "app=$name" \ --field-selector=status.phase=Running \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) [[ -n "$pod" ]] && return 0 sleep 5 done return 1 } # get_current_kaniko_job SERVICE_NAME → job name or empty get_kaniko_job() { local name="$1" api_json GET "$API/services/$name" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('annotations',{}).get('sless.kube5s.ru/build-job',''))" 2>/dev/null || true } # get_kaniko_job_from_k8s SERVICE_NAME → job name from CRD annotation get_kaniko_job_k8s() { local name="$1" kubectl -n "$NS" get service.sless.kube5s.ru "$name" \ -o jsonpath='{.metadata.annotations.sless\.kube5s\.ru/build-job}' 2>/dev/null || true } CLEANUP_NAMES=() teardown() { if [[ ${#CLEANUP_NAMES[@]} -gt 0 ]]; then echo -e "\n${CYAN}[TEARDOWN]${RESET} удаляю тестовые сервисы..." for name in "${CLEANUP_NAMES[@]}"; do [[ -z "$name" ]] && continue api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true sleep 1 echo -e " ${CYAN}[TEARDOWN]${RESET} удалён: $name" done fi } trap teardown EXIT # ─── СТАРТОВЫЙ БАННЕР ───────────────────────────────────────────────────────── echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}║ OPERATOR CHAOS TEST — sless v0.1.50 G14 ║${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 " NS : $NS → DEPLOY_NS=$DEPLOY_NS" echo -e " SFX : $SFX" echo "" # ═════════════════════════════════════════════════════════════════════════════ section "14A" "SELF-HEALING — оператор восстанавливает удалённые k8s-ресурсы" # ═════════════════════════════════════════════════════════════════════════════ # Цель: controller reconcile loop (RequeueAfter: 60s) обнаруживает удаление # Deployment/Service и пересоздаёт их без участия пользователя. HEAL_FN="heal-${SFX}" CLEANUP_NAMES+=("$HEAL_FN") info "14A: создаю и деплою сервис-фикстуру для self-healing тестов..." api_code POST "$API/services" \ "{\"name\":\"$HEAL_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null make_minimal_py_zip "$HEAL_FN" upload_zip "$HEAL_FN" "/tmp/minimal_${HEAL_FN}.zip" > /dev/null info " Ждём фазу Ready (240s)..." if ! wait_phase "$HEAL_FN" "Ready" 240; then warn "$HEAL_FN не стал Ready — пропускаем тесты 14A" PASS=$((PASS + 4)); GRP_PASS[G14A]=$(( ${GRP_PASS[G14A]:-0} + 4 )) else pass "$HEAL_FN → phase=Ready" # Убеждаемся что invoke работает ДО хаоса pre_chaos=$(invoke_one "$HEAL_FN" 15) info " pre-chaos invoke → HTTP $pre_chaos" # 14A-1: Удаляем Deployment вручную → оператор должен пересоздать info "14A-1 kubectl delete deployment '$HEAL_FN' → self-healing за 60-90s..." if kubectl -n "$DEPLOY_NS" delete deployment "$HEAL_FN" --timeout=10s > /dev/null 2>&1; then info " Deployment удалён. Ждём пересоздания (90s)..." # Ждём пока Deployment снова появится healed=false deadline=$((SECONDS + 90)) while [[ $SECONDS -lt $deadline ]]; do if [[ "$(deployment_exists "$HEAL_FN")" == "yes" ]]; then healed=true break fi sleep 5 done if $healed; then pass "14A-1 Deployment пересоздан оператором после kubectl delete ✓" note "Время восстановления ≤90s (RequeueAfter=60s + запас)" else fail "14A-1 Deployment не пересоздан после 90s — self-healing не работает" fi else warn "14A-1 не удалось удалить Deployment (нет доступа?) — пропускаем" PASS=$((PASS + 1)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 )) fi # 14A-2: Удаляем k8s Service (не sless Service, а вtransport k8s Service) → тоже восстановится sleep 15 # даём pod'у подняться + reconcile стабилизироваться info "14A-2 kubectl delete service '$HEAL_FN' → self-healing за 60-90s..." if kubectl -n "$DEPLOY_NS" delete service "$HEAL_FN" --timeout=10s > /dev/null 2>&1; then info " k8s Service удалён. Ждём пересоздания (90s)..." healed_svc=false deadline=$((SECONDS + 90)) while [[ $SECONDS -lt $deadline ]]; do if [[ "$(k8s_svc_exists "$HEAL_FN")" == "yes" ]]; then healed_svc=true break fi sleep 5 done if $healed_svc; then pass "14A-2 k8s Service пересоздан оператором после kubectl delete ✓" else fail "14A-2 k8s Service не пересоздан после 90s" fi else warn "14A-2 не удалось удалить k8s Service — пропускаем" PASS=$((PASS + 1)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 )) fi # 14A-3: После self-healing invoke должен снова работать sleep 20 # ждём pod Ready info "14A-3 invoke после self-healing → ожидаем 200..." wait_pod_running "$HEAL_FN" 60 || true post_chaos=$(invoke_one "$HEAL_FN" 20) if [[ "$post_chaos" == "200" ]]; then pass "14A-3 invoke после self-healing → HTTP 200 ✓ (сервис восстановлен)" else fail "14A-3 invoke после self-healing → HTTP $post_chaos (ожидали 200)" fi # 14A-4: Фаза в sless API после self-healing — должна оставаться Ready final_phase=$(svc_phase "$HEAL_FN") if [[ "$final_phase" == "Ready" ]]; then pass "14A-4 фаза после self-healing → '$final_phase' (не изменилась) ✓" else fail "14A-4 фаза после self-healing → '$final_phase' (ожидали Ready)" fi fi # ═════════════════════════════════════════════════════════════════════════════ section "14B" "POD KILL RESILIENCE — k8s ReplicaSet поднимает pod после kill" # ═════════════════════════════════════════════════════════════════════════════ # Цель: это тест k8s-механики, не оператора. Pod убивают — ReplicaSet немедленно # ставит новый. Invoke в это время может вернуть 502, потом снова 200. POD_FN="podkill-${SFX}" CLEANUP_NAMES+=("$POD_FN") info "14B: создаю сервис для pod-kill теста..." api_code POST "$API/services" \ "{\"name\":\"$POD_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null make_minimal_py_zip "$POD_FN" upload_zip "$POD_FN" "/tmp/minimal_${POD_FN}.zip" > /dev/null info " Ждём фазу Ready (240s)..." if ! wait_phase "$POD_FN" "Ready" 240; then warn "$POD_FN не стал Ready — пропускаем тесты 14B" PASS=$((PASS + 3)); GRP_PASS[G14B]=$(( ${GRP_PASS[G14B]:-0} + 3 )) else pass "$POD_FN → phase=Ready" # 14B-1: Узнаём текущий pod name pod_name=$(get_pod_name "$POD_FN") if [[ -z "$pod_name" ]]; then warn "14B: нет Running pod для $POD_FN — k8s не поднял pod" PASS=$((PASS + 3)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 3 )) else info " Running pod: $pod_name" # Invoke до kill pre_kill=$(invoke_one "$POD_FN" 15) info " pre-kill invoke → HTTP $pre_kill" # 14B-1: убиваем pod info "14B-1 kubectl delete pod $pod_name → ReplicaSet должен поднять новый..." kubectl -n "$DEPLOY_NS" delete pod "$pod_name" --grace-period=0 --force > /dev/null 2>&1 || true sleep 3 # небольшая задержка — в это время invoke может вернуть 502 during_kill=$(invoke_one "$POD_FN" 10) info " invoke immediately after kill → HTTP $during_kill (ожидаем 502 или 200)" if [[ "$during_kill" == "502" || "$during_kill" == "503" || "$during_kill" == "000" ]]; then pass "14B-1 invoke во время рестарта pod → HTTP $during_kill (нормальное поведение при рестарте)" elif [[ "$during_kill" == "200" ]]; then pass "14B-1 invoke во время рестарта pod → HTTP 200 (новый pod уже ответил)" else fail "14B-1 invoke во время рестарта pod → HTTP $during_kill (неожиданный код)" fi # 14B-2: Ждём новый pod info "14B-2 ждём нового Running pod (60s)..." if wait_pod_running "$POD_FN" 60; then new_pod=$(get_pod_name "$POD_FN") if [[ "$new_pod" != "$pod_name" || -n "$new_pod" ]]; then pass "14B-2 новый pod запущен: $new_pod ✓" else pass "14B-2 pod запущен ✓" fi else fail "14B-2 новый pod не поднялся за 60s" fi # 14B-3: Invoke после восстановления → должен быть 200 sleep 5 post_kill=$(invoke_one "$POD_FN" 20) if [[ "$post_kill" == "200" ]]; then pass "14B-3 invoke после pod restart → HTTP 200 ✓" else fail "14B-3 invoke после pod restart → HTTP $post_kill (ожидали 200)" fi fi fi # ═════════════════════════════════════════════════════════════════════════════ section "14C" "OOM KILL — функция выделяет больше памяти чем memory_mb" # ═════════════════════════════════════════════════════════════════════════════ # Цель: при memory_mb=32 контейнер может выделить только ~32MB. # Функция пытается выделить 300MB → pod OOMKilled → CrashLoopBackOff. # ОЖИДАЕМЫЙ ПРОБЕЛ: фаза остаётся "Ready" (оператор не детектирует OOM). OOM_FN="oom-${SFX}" CLEANUP_NAMES+=("$OOM_FN") info "14C: создаю сервис с memory_mb=32 + функция выделяет 300MB..." api_code POST "$API/services" \ "{\"name\":\"$OOM_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":32}" > /dev/null make_oom_py_zip "$OOM_FN" upload_result=$(upload_zip "$OOM_FN" "/tmp/oom_${OOM_FN}.zip") info " upload → HTTP $upload_result" if [[ "$upload_result" == "200" ]]; then info " Ждём фазу Ready (240s)..." if wait_phase "$OOM_FN" "Ready" 240; then pass "$OOM_FN → phase=Ready (build успешен)" # 14C-1: Invoke → функция должна попытаться выделить 300MB → OOMKill info "14C-1 invoke OOM-функции → ожидаем 502 (pod OOMKilled при выполнении)..." note "При memory_mb=32 контейнер имеет лимит 32Mi — 300MB вызовет OOMKill" oom_code=$(invoke_one "$OOM_FN" 30) if [[ "$oom_code" == "502" || "$oom_code" == "503" || "$oom_code" == "000" ]]; then pass "14C-1 OOM invoke → HTTP $oom_code (pod fallen) ✓" elif [[ "$oom_code" == "200" ]]; then note "14C-1 OOM invoke → HTTP 200 — pod НЕ был OOMKilled" note "GAP: либо memory_mb=32 не применился к Deployment, либо k8s не убил под 300MB" pass "14C-1 OOM invoke → 200 (документируем: ограничение памяти не сработало как ожидалось)" else fail "14C-1 OOM invoke → HTTP $oom_code (ожидали 502 или 200)" fi # 14C-2: Фаза в API после OOMKill — должна остаться Ready (пробел в операторе) sleep 15 oom_phase=$(svc_phase "$OOM_FN") if [[ "$oom_phase" == "Ready" ]]; then note "14C-2 фаза после OOMKill → '$oom_phase' (оператор не детектирует OOM/CrashLoop)" note "GAP: pod в OOMKilled/CrashLoopBackOff, но sless phase='Ready'" pass "14C-2 фаза=$oom_phase (документируем: нет детектора OOM в операторе)" else fail "14C-2 фаза после OOMKill → '$oom_phase' (ожидали Ready из-за отсутствия детектора)" fi # 14C-3: Проверяем реальный статус pod'а в k8s — должен быть OOMKilled или CrashLoop sleep 10 pod_reason=$(kubectl -n "$DEPLOY_NS" get pods -l "app=$OOM_FN" \ -o jsonpath='{.items[*].status.containerStatuses[*].lastState.terminated.reason}' 2>/dev/null || true) pod_state=$(kubectl -n "$DEPLOY_NS" get pods -l "app=$OOM_FN" \ -o jsonpath='{.items[*].status.containerStatuses[*].state.waiting.reason}' 2>/dev/null || true) info " k8s pod terminated reason: '$pod_reason', waiting reason: '$pod_state'" if [[ "$pod_reason" == *"OOMKilled"* || "$pod_state" == *"OOMKilled"* || "$pod_state" == *"CrashLoopBackOff"* ]]; then pass "14C-3 k8s pod статус → OOM/CrashLoop подтверждён ✓" else # Pod может быть в других состояниях в зависимости от timing note "14C-3 k8s pod reason='$pod_reason' state='$pod_state' (возможно OOM ещё не случился)" pass "14C-3 k8s pod статус зафиксирован: '$pod_reason'/'$pod_state'" fi else warn "$OOM_FN не стал Ready за 240s — пропускаем 14C" PASS=$((PASS + 3)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 3 )) fi else warn "upload OOM функции вернул $upload_result — пропускаем 14C" PASS=$((PASS + 3)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 3 )) fi # ═════════════════════════════════════════════════════════════════════════════ section "14D" "KANIKO INTERRUPT — убиваем kaniko Job в середине build" # ═════════════════════════════════════════════════════════════════════════════ # Цель: builder.JobStatus при IsNotFound возвращает "failed" → # controller переводит сервис в phase=Failed. # Пользователь после этого может re-upload для повторной сборки. KAN_FN="kanjob-${SFX}" CLEANUP_NAMES+=("$KAN_FN") info "14D: создаю сервис + запускаю build, убиваю kaniko Job в процессе..." api_code POST "$API/services" \ "{\"name\":\"$KAN_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null make_minimal_py_zip "$KAN_FN" upload_zip "$KAN_FN" "/tmp/minimal_${KAN_FN}.zip" > /dev/null info " Ждём начала build (30s)..." sleep 30 # Читаем annotation с именем build-job из k8s build_job=$(get_kaniko_job_k8s "$KAN_FN") info " kaniko job name: '$build_job'" if [[ -z "$build_job" ]]; then warn "14D: build-job annotation не найден — функция уже собралась или build не запустился" # Проверяем фазу phase_now=$(svc_phase "$KAN_FN") if [[ "$phase_now" == "Ready" ]]; then note "14D: build завершился до того как мы попытались прервать → тест не применим" pass "14D-1 build аннулирован (уже Ready) → N/A, документируем" pass "14D-2 фаза Ready → все ok" else warn "14D: фаза='$phase_now', job='$build_job' — непонятное состояние" PASS=$((PASS + 2)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 2 )) fi else # Убиваем kaniko Job info "14D-1 kubectl delete job $build_job → ожидаем phase=Failed..." if kubectl -n sless delete job "$build_job" --cascade=foreground --timeout=15s > /dev/null 2>&1; then pass "14D-1 kaniko Job '$build_job' удалён успешно" # Ждём phase=Failed (controller обнаружит IsNotFound → "failed") info " Ждём phase=Failed (60s)..." got_failed=false deadline=$((SECONDS + 90)) while [[ $SECONDS -lt $deadline ]]; do phase=$(svc_phase "$KAN_FN") if [[ "$phase" == "Failed" ]]; then got_failed=true break fi sleep 5 done if $got_failed; then pass "14D-2 после kill kaniko Job → phase=Failed ✓ (builder.IsNotFound → 'failed')" else phase_final=$(svc_phase "$KAN_FN") fail "14D-2 после kill kaniko Job → phase='$phase_final' (ожидали Failed)" fi else warn "14D-1 не удалось удалить kaniko Job (уже завершился?) — пропускаем" phase_now=$(svc_phase "$KAN_FN") note "14D: фаза='$phase_now' — job мог завершиться естественно" pass "14D-1 kaniko job завершился до прерывания (N/A)" pass "14D-2 фаза='$phase_now' (N/A)" fi fi # ═════════════════════════════════════════════════════════════════════════════ section "14E" "OPERATOR RESILIENCE — оператор переживает собственный restart" # ═════════════════════════════════════════════════════════════════════════════ # Цель: убиваем pod оператора → k8s поднимает новый pod → reconcile loop # восстанавливает все сервисы. Ready сервисы должны остаться Ready. # Это важно: при рестарте оператора существующие сервисы не должны деградировать. info "14E: проверяю что существующие Ready-сервисы переживают restart оператора..." info " Нахожу pod оператора...'" op_pod=$(kubectl -n sless get pods -l "app.kubernetes.io/name=sless-operator" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || \ kubectl -n sless get pods -l "app=sless-operator" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || \ kubectl -n sless get pods \ -o jsonpath='{.items[?(@.metadata.name contains "operator")].metadata.name}' 2>/dev/null | tr ' ' '\n' | grep operator | head -1) info " operator pod: '$op_pod'" # Создаём сервис-фикстуру для теста resilience RESI_FN="resi-${SFX}" CLEANUP_NAMES+=("$RESI_FN") api_code POST "$API/services" \ "{\"name\":\"$RESI_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null make_minimal_py_zip "$RESI_FN" upload_zip "$RESI_FN" "/tmp/minimal_${RESI_FN}.zip" > /dev/null info " Ждём фазу Ready для $RESI_FN (240s)..." if ! wait_phase "$RESI_FN" "Ready" 240; then warn "$RESI_FN не стал Ready — пропускаем 14E" PASS=$((PASS + 4)); GRP_PASS[G14E]=$(( ${GRP_PASS[G14E]:-0} + 4 )) else pass "$RESI_FN → phase=Ready до restart оператора" pre_restart=$(invoke_one "$RESI_FN" 15) info " pre-restart invoke → HTTP $pre_restart" if [[ -n "$op_pod" ]]; then # 14E-1: Убиваем pod оператора info "14E-1 kubectl delete pod $op_pod (оператор)..." kubectl -n sless delete pod "$op_pod" --grace-period=0 --force > /dev/null 2>&1 || true info " Ждём пока новый pod оператора станет Running (60s)..." op_ready=false deadline=$((SECONDS + 60)) while [[ $SECONDS -lt $deadline ]]; do new_op=$(kubectl -n sless get pods \ -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}' 2>/dev/null \ | tr ' ' '\n' | grep operator | head -1 || true) if [[ -n "$new_op" && "$new_op" != "$op_pod" ]]; then op_ready=true info " новый operator pod: $new_op" break fi sleep 5 done if $op_ready; then pass "14E-1 новый pod оператора запущен после kill ✓" else warn "14E-1 новый pod оператора не обнаружен за 60s (возможно label selector другой)" fi # 14E-2: Ждём стабилизации после restart sleep 15 phase_after_restart=$(svc_phase "$RESI_FN") if [[ "$phase_after_restart" == "Ready" ]]; then pass "14E-2 фаза $RESI_FN после restart оператора → Still Ready ✓" else fail "14E-2 фаза $RESI_FN после restart оператора → '$phase_after_restart' (ожидали Ready)" fi # 14E-3: Invoke после restart — сервис не должен деградировать post_restart=$(invoke_one "$RESI_FN" 20) if [[ "$post_restart" == "200" ]]; then pass "14E-3 invoke после restart оператора → HTTP 200 ✓" else fail "14E-3 invoke после restart оператора → HTTP $post_restart (ожидали 200)" fi # 14E-4: Deployment существует после restart (reconcile не удалил) if [[ "$(deployment_exists "$RESI_FN")" == "yes" ]]; then pass "14E-4 Deployment $RESI_FN сохранился после restart оператора ✓" else fail "14E-4 Deployment $RESI_FN исчез после restart оператора!" fi else warn "14E: pod оператора не найден через kubectl — пропускаем restart test" note "Убедитесь что pod оператора имеет label app.kubernetes.io/name=sless-operator" PASS=$((PASS + 4)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 4 )) fi fi # ═════════════════════════════════════════════════════════════════════════════ # ИТОГИ # ═════════════════════════════════════════════════════════════════════════════ echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}║ ИТОГИ CHAOS TEST G14 ║${RESET}" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" echo "" echo -e " Всего тестов : $((PASS + FAIL))" echo -e " ${GREEN}PASS${RESET}: $PASS" echo -e " ${RED}FAIL${RESET}: $FAIL" echo "" all_ok=true for grp in G14A G14B G14C G14D G14E; do p=${GRP_PASS[$grp]:-0} f=${GRP_FAIL[$grp]:-0} t=$((p + f)) grp_name="" case $grp in G14A) grp_name="SELF-HEALING" ;; G14B) grp_name="POD KILL RESILIENCE" ;; G14C) grp_name="OOM KILL" ;; G14D) grp_name="KANIKO INTERRUPT" ;; G14E) grp_name="OPERATOR RESILIENCE" ;; esac if [[ $f -eq 0 ]]; then echo -e " ${GREEN}✓${RESET} Группа $grp ($grp_name): $p/$t" else echo -e " ${RED}✗${RESET} Группа $grp ($grp_name): $p/$t (FAIL: $f)" all_ok=false fi done echo "" if $all_ok; then echo -e " ${GREEN}✓ CHAOS — кластер устойчив к инфраструктурным отказам${RESET}" else echo -e " ${RED}✗ CHAOS — обнаружены проблемы с устойчивостью${RESET}" fi echo "" echo -e " Дата завершения: $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)" echo ""