diff --git a/.gitignore b/.gitignore index a87fb71..f60a6ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -# Created: 2026-03-11 -# Purpose: ignore generated artifacts for the `examples` repository +# Created: 2026-03-11 / Updated: 2026-03-30 +# Purpose: ignore generated artifacts and internal files for the `examples` repository # Terraform .terraform/ @@ -16,6 +16,7 @@ crash.log # Provider plugins / caches .terraform.d/ +# tfvars содержат секреты (токены, ключи) — пользователь создаёт из .template *.tfvars # Archives and build artifacts @@ -39,3 +40,29 @@ venv/ .env *.local *.log + +# ---- SSH-ключи (секретные данные, у каждого пользователя свои) ---- +vm_key +vm_key.pub +**/vm_key +**/vm_key.pub +*.pem +id_ed25519 +id_rsa + +# ---- Внутренние тестовые и служебные скрипты (не для пользователей) ---- +# VM +VM/vm_stress_test.sh +VM/.vm_stress_test.sh.OLD +VM/VM_TEST_README.md + +# POSTGRES +POSTGRES/vm_stress_test.sh +POSTGRES/stress_test.sh +POSTGRES/stress_destroy_apply.sh.disabled +POSTGRES/full_test.sh +POSTGRES/bug_hunter.sh +POSTGRES/chaos_marathon.sh +POSTGRES/test_cache_matrix.sh +POSTGRES/deploy_and_run_chaos.sh +POSTGRES/scripts/ diff --git a/POSTGRES/bug_hunter.sh b/POSTGRES/bug_hunter.sh deleted file mode 100644 index d6b041f..0000000 --- a/POSTGRES/bug_hunter.sh +++ /dev/null @@ -1,650 +0,0 @@ -#!/usr/bin/env bash -# 2026-03-21 — bug_hunter.sh: охота за багами во всех POSTGRES-функциях. -# Цель: найти bugs типа "ложный 200", "должен 500 но 200", неверные данные. -# Логика принципиально отличается от chaos_marathon — здесь акцент на семантике и data integrity. -# Запускать ТОЛЬКО на VM через SSH. - -set -euo pipefail - -BASE_URL="${BASE_URL:-https://sless.kube5s.ru}" -NAMESPACE="${NAMESPACE:-sless-ffd1f598c169b0ae}" -TOKEN_FILE="${TOKEN_FILE:-$HOME/terra/sless/test.token}" -TOKEN=$(cat "$TOKEN_FILE") - -RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; CYAN="\033[0;36m"; NC="\033[0m" - -PASS=0; FAIL=0; TOTAL=0 -BUGS_FOUND=() - -ts() { date '+%H:%M:%S'; } - -# ── Хелперы ────────────────────────────────────────────────────────────────── -raw() { - local svc="$1" payload="$2" - curl -sf -X POST -H "Content-Type: application/json" \ - -d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "__CURL_FAIL__" -} - -http_code() { - local svc="$1" payload="$2" - curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \ - -d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "000" -} - -# Проверяем что HTTP-код РАВЕН ожидаемому -check_http() { - local label="$1" got="$2" want="$3" - TOTAL=$((TOTAL+1)) - if [[ "$got" == "$want" ]]; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" - PASS=$((PASS+1)) - else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got HTTP $got, want HTTP $want)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("$label → HTTP $got ≠ $want") - fi -} - -# Проверяем что JSON содержит строку -check_has() { - local label="$1" body="$2" substr="$3" - TOTAL=$((TOTAL+1)) - if echo "$body" | grep -q "$substr" 2>/dev/null; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" - PASS=$((PASS+1)) - else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (key/substr '$substr' not found in: ${body:0:120})" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("$label → '$substr' not in response") - fi -} - -# Проверяем что JSON НЕ содержит строку -check_not() { - local label="$1" body="$2" substr="$3" - TOTAL=$((TOTAL+1)) - if echo "$body" | grep -q "$substr" 2>/dev/null; then - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (found '$substr' but should NOT be there: ${body:0:120})" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("$label → '$substr' should NOT be in response") - else - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" - PASS=$((PASS+1)) - fi -} - -# Проверяем числовое равенство: jq вытаскивает значение и сравниваем -check_val() { - local label="$1" body="$2" jq_expr="$3" want="$4" - local got - got=$(echo "$body" | python3 -c " -import json,sys -try: - d=json.load(sys.stdin) - expr='$jq_expr'.lstrip('.') - parts=expr.split('.') - val=d - for p in parts: - val=val[p] if isinstance(val,dict) else val[int(p)] - print(val) -except Exception as e: - print('__ERR__:'+str(e)) -" 2>/dev/null || echo "__ERR__") - TOTAL=$((TOTAL+1)) - if [[ "$got" == "$want" ]]; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" - PASS=$((PASS+1)) - else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want '$want')" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("$label → got '$got' want '$want'") - fi -} - -# Проверяем что числовое значение >= порога -check_gte() { - local label="$1" got="$2" min_val="$3" - TOTAL=$((TOTAL+1)) - if [[ "$got" =~ ^[0-9]+$ ]] && (( got >= min_val )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label (got $got >= $min_val)" - PASS=$((PASS+1)) - else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want >= $min_val)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("$label → $got < $min_val") - fi -} - -section() { - echo "" - echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}" - echo -e "${CYAN} $1${NC}" - echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}" -} - -echo -e "${YELLOW}" -echo "╔══════════════════════════════════════════════════════════╗" -echo "║ BUG HUNTER — $(date '+%Y-%m-%d %H:%M:%S') ║" -echo "║ Ищем: ложные 200, неверные данные, скрытые баги ║" -echo "╚══════════════════════════════════════════════════════════╝" -echo -e "${NC}" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 1 — Тест на Missing Input Validation (должно быть 200, НО БУДЕТ 500 если баг есть)" -# ═══════════════════════════════════════════════════════════════════════════════ -echo " Каждая функция должна СТОЙКО обрабатывать строку вместо числа." -echo " Если возвращает 500 — это BUG: не хватает try/except." - -c=$(http_code "chaos-slowquery" '{"sleep_sec": "не_число"}') -check_http "BUG1: chaos-slowquery sleep_sec=string → должен 200" "$c" "200" - -c=$(http_code "chaos-bigpayload" '{"size_kb": "не_число"}') -check_http "BUG2: chaos-bigpayload size_kb=string → должен 200" "$c" "200" - -c=$(http_code "py-retry-writer" '{"n": "не_число"}') -check_http "BUG3: py-retry-writer n=string → должен 200" "$c" "200" - -c=$(http_code "pg-delete-old" '{"older_than_min": "не_число"}') -check_http "BUG4: pg-delete-old older_than_min=string → должен 200" "$c" "200" - -c=$(http_code "chaos-slowquery" '{"sleep_sec": null}') -check_http "BUG5: chaos-slowquery sleep_sec=null → должен 200" "$c" "200" - -c=$(http_code "chaos-bigpayload" '{"size_kb": null}') -check_http "BUG6: chaos-bigpayload size_kb=null → должен 200" "$c" "200" - -c=$(http_code "chaos-bigpayload" '{"size_kb": -999}') -check_http "BUG7: chaos-bigpayload size_kb=-999 (negative) → должен 200" "$c" "200" - -c=$(http_code "chaos-slowquery" '{"sleep_sec": -5}') -check_http "BUG8: chaos-slowquery sleep_sec=-5 → должен 200" "$c" "200" - -c=$(http_code "chaos-slowquery" '{"sleep_sec": 99999}') -check_http "BUG9: chaos-slowquery sleep_sec=99999 (huge) → должен 200" "$c" "200" - -c=$(http_code "py-retry-writer" '{"n": -1}') -check_http "BUG10: py-retry-writer n=-1 (negative) → должен 200" "$c" "200" - -c=$(http_code "py-retry-writer" '{"n": 99999}') -check_http "BUG11: py-retry-writer n=99999 (huge, capped) → должен 200" "$c" "200" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 2 — Data Integrity: реальное значение vs ожидаемое" -# ═══════════════════════════════════════════════════════════════════════════════ - -# js-pg-batch: n=0 должен вернуть inserted=0, а не 20 (баг: 0||20=20) -r=$(raw "js-pg-batch" '{"n": 0, "prefix": "bughunt-zero"}') -check_val "BUG12: js-pg-batch n=0 → inserted должен быть 0" "$r" ".inserted" "0" - -# js-pg-batch: n=1 → ровно 1 строка -r=$(raw "js-pg-batch" '{"n": 1, "prefix": "bughunt-one"}') -check_val "SAFE: js-pg-batch n=1 → inserted=1" "$r" ".inserted" "1" - -# js-pg-batch: n=200 (cap) → не больше 200 -r=$(raw "js-pg-batch" '{"n": 9999, "prefix": "bughunt-cap"}') -i=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin)['inserted'])" 2>/dev/null || echo "-1") -check_gte "SAFE: js-pg-batch n=9999 → capped (inserted > 0)" "$i" 1 -# inserted должно быть <= 200 -TOTAL=$((TOTAL+1)) -if (( i <= 200 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-pg-batch n=9999 → capped <= 200 (got $i)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: js-pg-batch n=9999 → вставил $i строк (ожидали <= 200)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("js-pg-batch n=9999 → inserted=$i > 200") -fi - -# pg-bulk-insert: точное соответствие n → inserted -for n_val in 1 10 100 499 500; do - r=$(raw "pg-bulk-insert" "{\"n\": $n_val, \"prefix\": \"bughunt-n$n_val\"}") - check_val "SAFE: pg-bulk-insert n=$n_val → inserted=$n_val" "$r" ".inserted" "$n_val" -done - -# py-retry-writer с simulate_error: должен вернуть ok:true (retry работает) -r=$(raw "py-retry-writer" '{"n": 5, "simulate_error": true, "prefix": "bughunt-retry"}') -check_has "SAFE: py-retry-writer simulate_error=true → ok:true" "$r" '"ok": true' -attempts=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('attempts',0))" 2>/dev/null || echo "0") -check_gte "SAFE: py-retry-writer simulate_error=true → attempts >= 2" "$attempts" 2 - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 3 — Semantic / Logic Bugs (ложный 200, неверные данные)" -# ═══════════════════════════════════════════════════════════════════════════════ - -# BUG: pg-search с "_" в query — _ это LIKE wildcard, не экранируется. -# Вставляем уникальную строку без подчёркивания, ищем с _ → не должна найтись. -UNIQUE_NOUNDERSCORE="bughunt-no-underscore-$(date +%s%N)" -raw "pg-bulk-insert" "{\"n\": 1, \"prefix\": \"$UNIQUE_NOUNDERSCORE\"}" >/dev/null -# Поиск exact строки — должна найтись -r=$(raw "pg-search" "{\"query\": \"$UNIQUE_NOUNDERSCORE\", \"limit\": 10}") -cnt=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0") -check_gte "SAFE: pg-search exact match найдена" "$cnt" 1 - -# Теперь создаём строку с дефисом в mid позиции: "aXb" где X — любой char. -# Ищем с _ (wildcard): "a_b" должно совпадать. Это НЕ баг если мы ищем wildcard. -# Реальный баг: ESCAPE не поддерживается, пользователь не может искать literal "_". -# Проверяем: строка "test-literal-underscore_here" ищем по query="literal_underscore_here" -# Без экраниравания: _ = любой символ → совпадёт AND с "literaXunderscoreYhere" тоже. -EXACT_TITLE="bughunt-exact-$(date +%s%N)" -raw "pg-upsert" "{\"title\": \"${EXACT_TITLE}\"}" >/dev/null -# Поиск с подчёркиванием вместо дефиса в этой строке — совпадать НЕ должно с точным матчем -# но совпадёт из-за ILIKE wildcard `_` -UNDER_QUERY="${EXACT_TITLE//-/_}" # заменяем дефисы на подчёркивания -r=$(raw "pg-search" "{\"query\": \"$UNDER_QUERY\", \"limit\": 100}") -underscore_count=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('count',0))" 2>/dev/null || echo "0") -# Если count >> 1, значит _ матчит что попало (wildcard) -echo " pg-search с query='${UNDER_QUERY:0:30}...' вернул $underscore_count строк (если > 1 — это баг wildcard)" -TOTAL=$((TOTAL+1)) -# Должен найти ТОЛЬКО нашу строку (count=1), но без экранирования найдёт много -if (( underscore_count == 1 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search underscore matches only exact row" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG13: pg-search '_' is unescaped LIKE wildcard → matches $underscore_count rows instead of 1" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-search: '_' is unescaped LIKE wildcard (got $underscore_count rows)") -fi - -# BUG: pg-delete-old — параметр НАЗЫВАЕТСЯ older_than_min, но в chaos_marathon шлём older_than_minutes. -# Шлём неправильный ключ и правильный — результаты должны различаться. -# older_than_minutes=1 → функция игнорирует, использует default (60 мин) → ничего не удалится из только что вставленных -# older_than_min=0 → capped to 1 → удалит строки старше 1 мин (ну, только что вставленные > 1 мин назад) -# Проверяем: older_than_minutes=0 → использует default 60 → параметр проигнорирован → bug - -# Вставляем свежую строку, пытаемся удалить с older_than_minutes=0 (неверный ключ) -FRESH_TITLE="bughunt-del-$(date +%s%N)" -ins_r=$(raw "pg-bulk-insert" "{\"n\": 3, \"prefix\": \"$FRESH_TITLE\"}") -# Ждём немного, затем пытаемся удалить по неправильному ключу -sleep 1 -r=$(raw "pg-delete-old" "{\"older_than_minutes\": 0}") -deleted_wrong_key=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?") -r2=$(raw "pg-delete-old" "{\"older_than_min\": 99999}") -deleted_right_key=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?") -echo " pg-delete-old older_than_minutes=0: deleted=$deleted_wrong_key (использовался default 60min)" -echo " pg-delete-old older_than_min=99999: deleted=$deleted_right_key (правильный ключ — удалило всё старое)" -# Если с неверным ключом deleted > 0 при очень маленьком значении — значит параметр работает. -# Если с right key deleted больше — это подтверждает что wrong key игнорировался. -TOTAL=$((TOTAL+1)) -if [[ "$deleted_right_key" =~ ^[0-9]+$ ]] && (( deleted_right_key >= 0 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] INFO: pg-delete-old right key works (deleted=$deleted_right_key)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] pg-delete-old right key failed: got $deleted_right_key" - FAIL=$((FAIL+1)) -fi - -# BUG: pg-counter prefix="%" → LIKE "%%%" → считает все строки (wildcard leak) -r_all=$(raw "pg-counter" '{}') -total_all=$(echo "$r_all" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") -r_pct=$(raw "pg-counter" '{"prefix": "%"}') -total_pct=$(echo "$r_pct" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") -echo " pg-counter prefix='': total=$total_all | pg-counter prefix='%': total=$total_pct" -TOTAL=$((TOTAL+1)) -# Если оба возвращают одно число — значит % матчит все строки → баг wildcard -if [[ "$total_all" == "$total_pct" ]] && (( total_all > 0 )); then - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG14: pg-counter prefix='%' counts ALL rows ($total_pct) — % is unescaped LIKE wildcard" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-counter: prefix='%' is unescaped LIKE wildcard (same as no prefix)") -else - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter prefix='%' gives different count than no-prefix" - PASS=$((PASS+1)) -fi - -# BUG: pg-search limit=0 — clamp max(1, min(0,100)) = 1, не 0. -# Юзер просит 0 строк но получает 1. -r=$(raw "pg-search" '{"query": "", "limit": 0}') -limit_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('limit',0))" 2>/dev/null || echo "-1") -echo " pg-search limit=0 → вернул limit=$limit_got в ответе" -TOTAL=$((TOTAL+1)) -if [[ "$limit_got" == "0" ]]; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search limit=0 → limit=0 in response" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] INFO15: pg-search limit=0 → silently changed to $limit_got (min clamp = 1, not 0)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-search: limit=0 silently becomes $limit_got (user wants 0 rows, gets $limit_got)") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 4 — pg-upsert: idempotency + action field correctness" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Первый вызов → action=inserted -UPSERT_KEY="bughunt-upsert-$(date +%s%N)" -r1=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}") -action1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?") -check_val "SAFE: pg-upsert первый вызов → action=inserted" "$r1" ".action" "inserted" - -# Второй вызов → action=updated -r2=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}") -action2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?") -check_val "SAFE: pg-upsert второй вызов (same title) → action=updated" "$r2" ".action" "updated" - -# ID должен совпадать -id1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?1") -id2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?2") -TOTAL=$((TOTAL+1)) -if [[ "$id1" == "$id2" ]] && [[ "$id1" != "?" ]]; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-upsert same title → same id ($id1)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG16: pg-upsert same title → different ids ($id1 vs $id2)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-upsert: same title yields different ids ($id1 vs $id2)") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 5 — js-idempotent: concurrent same key" -# ═══════════════════════════════════════════════════════════════════════════════ - -# 5 параллельных вызовов с одним ключом → должен быть 1 created, 4 existing -IDEM_KEY="bughunt-idem-concurrent-$(date +%s%N)" -declare -a IDEM_RESULTS=() -for i in 1 2 3 4 5; do - r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") & - IDEM_RESULTS+=($!) -done -wait -# Перезапустим последовательно чтобы собрать результаты -actions=() -for i in 1 2 3 4 5; do - r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") - a=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?") - actions+=("$a") -done - -created_count=0; existing_count=0 -for a in "${actions[@]}"; do - [[ "$a" == "created" ]] && created_count=$((created_count+1)) - [[ "$a" == "existing" ]] && existing_count=$((existing_count+1)) -done -echo " js-idempotent key же 5× последовательно: created=$created_count, existing=$existing_count" -TOTAL=$((TOTAL+1)) -# Первый должен быть created, остальные existing (с учётом что первый вызов в параллельном блоке уже создал) -# Теперь все 5 последовательных должны быть existing -if (( existing_count == 5 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → все existing" - PASS=$((PASS+1)) -elif (( created_count == 1 && existing_count == 4 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → 1 created + 4 existing" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG17: js-idempotent same key → created=$created_count existing=$existing_count (ожидали 0-1 created, 4-5 existing)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("js-idempotent: 5× same key → created=$created_count, existing=$existing_count") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 6 — go-pg-race: workers=0 div-by-zero" -# ═══════════════════════════════════════════════════════════════════════════════ - -r=$(raw "go-pg-race" '{"workers": 0, "n_per_worker": 5}') -c=$(http_code "go-pg-race" '{"workers": 0, "n_per_worker": 5}') -check_http "SAFE: go-pg-race workers=0 → 200 (не div-by-zero)" "$c" "200" - -# ops_per_sec при workers=0 inserted=0 должен быть 0 или Inf — проверяем что не NaN/invalid JSON -TOTAL=$((TOTAL+1)) -if echo "$r" | python3 -c "import json,sys; json.load(sys.stdin); print('valid_json')" 2>/dev/null | grep -q valid_json; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race workers=0 → valid JSON" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG18: go-pg-race workers=0 → invalid JSON (div-by-zero → Inf/NaN)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("go-pg-race: workers=0 → invalid JSON response") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 7 — Crash functions: параметры управляют crash-ом" -# ═══════════════════════════════════════════════════════════════════════════════ - -# stress-go-nil: crash=false → 200 (не должен падать) -c=$(http_code "stress-go-nil" '{"crash": false}') -check_http "SAFE: stress-go-nil crash=false → 200" "$c" "200" - -# stress-go-nil: crash=true (default) → 500 -c=$(http_code "stress-go-nil" '{"crash": true}') -check_http "SAFE: stress-go-nil crash=true → 500" "$c" "500" - -# stress-divzero: d=0 → 500 -c=$(http_code "stress-divzero" '{"n": 10, "d": 0}') -check_http "SAFE: stress-divzero d=0 → 500" "$c" "500" - -# stress-divzero: d=2 → 200 -c=$(http_code "stress-divzero" '{"n": 10, "d": 2}') -check_http "SAFE: stress-divzero d=2 → 200" "$c" "200" - -r=$(raw "stress-divzero" '{"n": 10, "d": 2}') -check_has "SAFE: stress-divzero d=2 → result in response" "$r" "result" - -# stress-bigloop: n=1000000 (большое) → должен вернуть 200 -c=$(http_code "stress-bigloop" '{"n": 1000000}') -check_http "SAFE: stress-bigloop n=1000000 → 200" "$c" "200" - -# stress-go-fast: n=0 → должен вернуть 200 (factorial(0) = 1) -c=$(http_code "stress-go-fast" '{"n": 0}') -check_http "SAFE: stress-go-fast n=0 → 200" "$c" "200" - -# stress-go-fast: n=20 (cap) → factorial(20) не переполнение? -r=$(raw "stress-go-fast" '{"n": 20}') -check_has "SAFE: stress-go-fast n=20 → factorial in response" "$r" "factorial" - -# stress-go-fast: n=21 → capped to 20 (проверяем что cap работает) -r=$(raw "stress-go-fast" '{"n": 21}') -n_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('n',0))" 2>/dev/null || echo "-1") -TOTAL=$((TOTAL+1)) -if (( n_got <= 20 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: stress-go-fast n=21 → capped to $n_got (<= 20)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: stress-go-fast n=21 → not capped (n=$n_got)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("stress-go-fast: n=21 not capped (got n=$n_got)") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 8 — pg-counter: счёт соответствует реально inserted" -# ═══════════════════════════════════════════════════════════════════════════════ - -PREFIX_TEST="bughunt-count-$(date +%s%N)" -# Получаем текущий счётчик с этим prefix -r_before=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}") -cnt_before=$(echo "$r_before" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") - -# Вставляем ровно 7 строк -raw "pg-bulk-insert" "{\"n\": 7, \"prefix\": \"$PREFIX_TEST\"}" >/dev/null - -# Считаем снова -r_after=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}") -cnt_after=$(echo "$r_after" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") - -inserted_delta=$(( cnt_after - cnt_before )) -echo " pg-counter prefix='$PREFIX_TEST': before=$cnt_before, after=$cnt_after, delta=$inserted_delta (ожидаем 7)" -TOTAL=$((TOTAL+1)) -if (( inserted_delta == 7 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter правильно считает: delta=$inserted_delta" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG19: pg-counter delta=$inserted_delta ≠ 7 (prefix wildcard или баг в счёте)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-counter: inserted 7, delta=$inserted_delta") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 9 — pg-search: pagination correctness" -# ═══════════════════════════════════════════════════════════════════════════════ - -PREFIX_SEARCH="bughunt-search-$(date +%s%N)" -# Вставляем ровно 25 строк -raw "pg-bulk-insert" "{\"n\": 25, \"prefix\": \"$PREFIX_SEARCH\"}" >/dev/null -sleep 0.5 - -# Page 1: offset=0 limit=10 → count=10 -r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 0}") -count_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1") -total_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "-1") -check_val "SAFE: pg-search page1 count=10" "$r" ".count" "10" -check_gte "SAFE: pg-search total >= 25" "$total_p1" 25 - -# Page 3: offset=20 limit=10 → count=5 (строк 21-25) -r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 20}") -count_p3=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1") -echo " pg-search page3 (offset=20, limit=10): count=$count_p3 (ожидаем 5)" -TOTAL=$((TOTAL+1)) -# Учитываем что могут быть другие строки с этим prefix -if [[ "$count_p3" =~ ^[0-9]+$ ]] && (( count_p3 > 0 && count_p3 <= 10 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search page3 count=$count_p3 (допустимо)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG20: pg-search page3 count=$count_p3 (expected 1-10)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-search: page3 count=$count_p3 out of range") -fi - -# offset > total → count=0 -r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 999999}") -count_over=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1") -check_val "SAFE: pg-search offset>total → count=0" "$r" ".count" "0" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 10 — pg-dedup: idempotency (повторный вызов безопасен)" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Сначала удаляем все дубли -raw "pg-dedup" '{"dry_run": false}' >/dev/null - -# dry_run=true → deleted всегда = 0 -r=$(raw "pg-dedup" '{"dry_run": true}') -check_val "SAFE: pg-dedup dry_run=true → deleted=0" "$r" ".deleted" "0" - -# Создаём дублей: вставляем одно и то же через bulk (уникальные title), затем уpsert одно и то же -DUP_TITLE="bughunt-dup-$(date +%s%N)" -raw "pg-upsert" "{\"title\": \"${DUP_TITLE}\"}" >/dev/null -# INSERT прямой дубль через bulk — но у него нет механизма вставки дублей... -# Используем pg-counter + pg-search чтобы найти дубли что уже есть от других тестов -r=$(raw "pg-dedup" '{"dry_run": true}') -dupes=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0") -echo " pg-dedup dry_run: duplicates_found=$dupes" - -# Запускаем dedup дважды — второй раз должен найти 0 дублей -raw "pg-dedup" '{"dry_run": false}' >/dev/null -r2=$(raw "pg-dedup" '{"dry_run": false}') -dupes2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0") -TOTAL=$((TOTAL+1)) -if [[ "$dupes2" == "0" ]]; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-dedup повторный вызов → duplicates_found=0 (идемпотентен)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG21: pg-dedup повторный вызов → duplicates_found=$dupes2 ≠ 0" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-dedup: не идемпотентен — второй вызов нашёл $dupes2 дублей") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 11 — chaos-echo: крайние случаи" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Пустой JSON {} → echo должен вернуть echo:{}, keys:[], size_bytes:2 -r=$(raw "chaos-echo" '{}') -check_has "SAFE: chaos-echo {} → echo in response" "$r" '"echo"' -check_val "SAFE: chaos-echo {} → keys=[](empty_list len=0)" "$r" ".size_bytes" "2" - -# Очень глубоко вложенный JSON -r=$(raw "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}') -c=$(http_code "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}') -check_http "SAFE: chaos-echo deeply nested → 200" "$c" "200" - -# Массив вместо объекта (некоторые функции падают) -c=$(http_code "chaos-echo" '[1, 2, 3]') -check_http "SAFE: chaos-echo array input → 200" "$c" "200" - -# Булево значение вместо объекта -c=$(http_code "chaos-echo" 'true') -check_http "SAFE: chaos-echo true input → 200" "$c" "200" - -# Число вместо объекта -c=$(http_code "chaos-echo" '42') -check_http "SAFE: chaos-echo number input → 200" "$c" "200" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 12 — go-counter-atomic: invocation_n растёт" -# ═══════════════════════════════════════════════════════════════════════════════ - -r1=$(raw "go-counter-atomic" '{}') -n1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?") -r2=$(raw "go-counter-atomic" '{}') -n2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?") -echo " go-counter-atomic: call1 invocation_n=$n1, call2 invocation_n=$n2" -TOTAL=$((TOTAL+1)) -if [[ "$n1" =~ ^[0-9]+$ ]] && [[ "$n2" =~ ^[0-9]+$ ]] && (( n2 > n1 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-counter-atomic invocation_n растёт ($n1 → $n2)" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG22: go-counter-atomic invocation_n НЕ растёт ($n1 → $n2)" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("go-counter-atomic: invocation_n не растёт ($n1 → $n2)") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 13 — go-pg-race: все inserted = workers × n_per_worker" -# ═══════════════════════════════════════════════════════════════════════════════ - -r=$(raw "go-pg-race" '{"workers": 4, "n_per_worker": 10}') -ins=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('inserted',0))" 2>/dev/null || echo "0") -errs=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('errors',0))" 2>/dev/null || echo "-1") -echo " go-pg-race workers=4 n_per_worker=10: inserted=$ins, errors=$errs (ожидаем inserted=40, errors=0)" -TOTAL=$((TOTAL+1)) -if [[ "$ins" == "40" ]] && [[ "$errs" == "0" ]]; then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race 4×10 = 40 inserted, 0 errors" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG23: go-pg-race 4×10: inserted=$ins (≠40), errors=$errs" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("go-pg-race: workers=4 n_per_worker=10 → inserted=$ins errors=$errs") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "БЛОК 14 — pg-bulk-insert: first_id реально существует в БД" -# ═══════════════════════════════════════════════════════════════════════════════ - -PREFIX_VER="bughunt-verify-$(date +%s%N)" -r=$(raw "pg-bulk-insert" "{\"n\": 5, \"prefix\": \"$PREFIX_VER\"}") -first_id=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('first_id','null'))" 2>/dev/null || echo "null") -echo " pg-bulk-insert n=5 → first_id=$first_id" - -# Проверяем что эта строка находится через pg-search -sleep 0.3 -r_search=$(raw "pg-search" "{\"query\": \"$PREFIX_VER\", \"limit\": 10}") -found_count=$(echo "$r_search" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0") -TOTAL=$((TOTAL+1)) -if (( found_count >= 5 )); then - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-bulk-insert n=5 → pg-search находит $found_count строк" - PASS=$((PASS+1)) -else - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG24: pg-bulk-insert n=5 → pg-search нашёл только $found_count строк" - FAIL=$((FAIL+1)) - BUGS_FOUND+=("pg-bulk-insert: inserted 5 but pg-search found only $found_count") -fi - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ИТОГИ BUG HUNTER" -# ═══════════════════════════════════════════════════════════════════════════════ - -echo "" -echo -e "${YELLOW}PASS: $PASS / $TOTAL FAIL: $FAIL${NC}" -echo "" - -if (( ${#BUGS_FOUND[@]} > 0 )); then - echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${RED}║ НАЙДЕНО БАГОВ: ${#BUGS_FOUND[@]} ║${NC}" - echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}" - for bug in "${BUGS_FOUND[@]}"; do - echo -e " ${RED}✗${NC} $bug" - done -else - echo -e "${GREEN}╔══════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ БАГОВ НЕ НАЙДЕНО. Всё чисто. ✓ ║${NC}" - echo -e "${GREEN}╚══════════════════════════════════════╝${NC}" -fi -echo "" - -exit $(( FAIL > 0 ? 1 : 0 )) diff --git a/POSTGRES/chaos_marathon.sh b/POSTGRES/chaos_marathon.sh deleted file mode 100644 index 33ce337..0000000 --- a/POSTGRES/chaos_marathon.sh +++ /dev/null @@ -1,668 +0,0 @@ -#!/usr/bin/env bash -# 2026-03-21 — chaos_marathon.sh -# Часовой хаос-марафон: 15 сервисов, dumb-user simulation, PG stress, CRUD lifecycle. -# Запуск: bash chaos_marathon.sh 2>&1 | tee /tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log -# -# Предполагает: terraform apply chaos_marathon.tf уже выполнен, все 15 Ready. -# Зависимости: curl, jq, terraform (init выполнен). - -# -e намеренно НЕ установлен — падение одного вызова не убивает марафон. -# -u: незаданные переменные = ошибка. -o pipefail: ошибка в пайпе видна. -set -uo pipefail - -# ── Config ──────────────────────────────────────────────────────────────────── - -BASE_URL="${SLESS_BASE_URL:-https://sless.kube5s.ru}" -TOKEN_FILE="${SLESS_TOKEN_FILE:-/home/naeel/terra/sless/test.token}" -NAMESPACE="${SLESS_NAMESPACE:-sless-ffd1f598c169b0ae}" -TF_DIR="/home/naeel/terra/sless/examples/POSTGRES" -LOG_DIR="/tmp/chaos_$(date +%Y%m%d_%H%M%S)" -mkdir -p "$LOG_DIR" - -# ── Helpers ─────────────────────────────────────────────────────────────────── - -TOKEN=$(cat "$TOKEN_FILE") -PASS=0 -FAIL=0 -TOTAL=0 - -# Цветной вывод — для удобства чтения длинного лога. -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' - -ts() { date '+%H:%M:%S'; } - -# invoke SERVICE_NAME PAYLOAD — вызывает сервис через публичный /fn/ proxy, возвращает тело. -# Публичный endpoint не требует токена (используется для вызова функций). -# Никогда не падает — ошибки пишутся в лог-файл. -invoke() { - local svc="$1" payload="$2" - curl -sf -X POST \ - -H "Content-Type: application/json" \ - -d "$payload" \ - "${BASE_URL}/fn/${NAMESPACE}/${svc}" \ - 2>"$LOG_DIR/err_${svc}_$(date +%s%N).log" || true -} - -# invoke_raw — как invoke, но никогда не бросает non-zero exit. -invoke_raw() { - local svc="$1" payload="$2" - curl -s -X POST \ - -H "Content-Type: application/json" \ - -d "$payload" \ - "${BASE_URL}/fn/${NAMESPACE}/${svc}" || true -} - -# invoke_with_status SERVICE PAYLOAD — возвращает HTTP код, никогда не падает. -invoke_with_status() { - local svc="$1" payload="$2" - curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -d "$payload" \ - "${BASE_URL}/fn/${NAMESPACE}/${svc}" || echo "000" -} - -# check TEST_NAME CONDITION [msg] — вердикт по условию (expect 0-exit или строковую проверку). -check() { - local name="$1" result="$2" expected="${3:-0}" - TOTAL=$((TOTAL + 1)) - if [[ "$result" == "$expected" ]]; then - PASS=$((PASS + 1)) - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name" - else - FAIL=$((FAIL + 1)) - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (got='$result' want='$expected')" - fi -} - -# check_contains TEST_NAME HAYSTACK NEEDLE -check_contains() { - local name="$1" hay="$2" needle="$3" - TOTAL=$((TOTAL + 1)) - if echo "$hay" | grep -q "$needle"; then - PASS=$((PASS + 1)) - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name" - else - FAIL=$((FAIL + 1)) - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (needle='$needle' not found)" - fi -} - -# check_not_contains TEST_NAME HAYSTACK NEEDLE -check_not_contains() { - local name="$1" hay="$2" needle="$3" - TOTAL=$((TOTAL + 1)) - if ! echo "$hay" | grep -q "$needle"; then - PASS=$((PASS + 1)) - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name" - else - FAIL=$((FAIL + 1)) - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (unexpected needle='$needle' found)" - fi -} - -# check_http TEST_NAME CODE EXPECTED -check_http() { - local name="$1" code="$2" expected="${3:-200}" - TOTAL=$((TOTAL + 1)) - if [[ "$code" == "$expected" ]]; then - PASS=$((PASS + 1)) - echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name → HTTP $code" - else - FAIL=$((FAIL + 1)) - echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name → HTTP $code (want $expected)" - fi -} - -section() { - echo "" - echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}" - echo -e "${CYAN} $1${NC}" - echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}" -} - -# ── Wait helpers ────────────────────────────────────────────────────────────── - -# wait_service_ready NAME — ждём до 2 мин пока GET /services/{name} вернёт phase=Ready. -# Проверяет статус через API (не invoke), чтобы не запускать функцию при старте. -wait_service_ready() { - local svc="$1" max_attempts=24 attempt=0 - echo " Ожидаем готовности $svc..." - while (( attempt < max_attempts )); do - phase=$(curl -sf \ - -H "Authorization: Bearer $TOKEN" \ - "${BASE_URL}/v1/namespaces/${NAMESPACE}/services/${svc}" \ - 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null || true) - if [[ "$phase" == "Ready" ]]; then - echo " → $svc Ready (attempt $((attempt+1)))" - return 0 - fi - sleep 5 - attempt=$((attempt + 1)) - done - echo -e " ${RED}TIMEOUT: $svc не стал Ready за 2 мин${NC}" - return 1 -} - -# ── Parallel invoker ────────────────────────────────────────────────────────── - -# parallel_invoke COUNT SERVICE PAYLOAD LOG_PREFIX — запускает COUNT вызовов параллельно. -parallel_invoke() { - local count="$1" svc="$2" payload="$3" prefix="$4" - local pids=() results_dir="$LOG_DIR/par_${prefix}_$(date +%s)" - mkdir -p "$results_dir" - for i in $(seq 1 "$count"); do - ( - code=$(invoke_with_status "$svc" "$payload") - echo "$code" > "$results_dir/$i" - ) & - pids+=($!) - done - # Ждём все фоновые задачи. - for pid in "${pids[@]}"; do wait "$pid" || true; done - # Счёт 200-х. - local ok=0 bad=0 - for f in "$results_dir"/*; do - code=$(cat "$f") - if [[ "$code" == "200" ]]; then ok=$((ok+1)); else bad=$((bad+1)); fi - done - echo "$ok/$count OK, $bad FAIL" -} - -echo "" -echo -e "${YELLOW}╔══════════════════════════════════════════════════════════╗${NC}" -echo -e "${YELLOW}║ CHAOS MARATHON — $(date '+%Y-%m-%d %H:%M:%S') ║${NC}" -echo -e "${YELLOW}╚══════════════════════════════════════════════════════════╝${NC}" -echo "" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 0 — Ожидание готовности всех 15 сервисов" -# ═══════════════════════════════════════════════════════════════════════════════ - -SERVICES=( - pg-counter pg-dedup pg-search pg-bulk-insert pg-delete-old pg-upsert - chaos-echo chaos-badparams chaos-slowquery chaos-bigpayload - go-pg-race go-counter-atomic - js-pg-batch js-idempotent - py-retry-writer -) - -failed_ready=0 -for svc in "${SERVICES[@]}"; do - if ! wait_service_ready "$svc"; then - failed_ready=$((failed_ready + 1)) - fi -done - -if (( failed_ready > 0 )); then - echo -e "${YELLOW}ВНИМАНИЕ: $failed_ready сервисов не стали Ready. Продолжаем — они будут FAIL в тестах.${NC}" - # НЕ выходим — дальше тесты сами покажут что сломалось. -fi -echo -e "${GREEN}Все 15 сервисов Ready. Начинаем марафон.${NC}" -START_TIME=$(date +%s) -MARATHON_DURATION=${MARATHON_DURATION_SEC:-3600} # по умолчанию 1 час -ROUND=0 - -echo -e "${CYAN}Длительность марафона: ${MARATHON_DURATION}с ($(( MARATHON_DURATION / 60 )) мин)${NC}" - -# ── Основной цикл: крутим фазы 1–11 пока не истечёт время ─────────────────── -while true; do - NOW=$(date +%s) - ELAPSED_TOTAL=$(( NOW - START_TIME )) - if (( ELAPSED_TOTAL >= MARATHON_DURATION )); then - echo -e "\n${YELLOW}Время марафона истекло (${ELAPSED_TOTAL}с). Переходим к финальной проверке.${NC}" - break - fi - ROUND=$((ROUND + 1)) - MINS_LEFT=$(( (MARATHON_DURATION - ELAPSED_TOTAL) / 60 )) - echo -e "\n${YELLOW}═══ РАУНД $ROUND | прошло $(( ELAPSED_TOTAL / 60 ))м, осталось ${MINS_LEFT}м ═══${NC}" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 1 — Базовый smoke-test (1 вызов каждого сервиса)" -# ═══════════════════════════════════════════════════════════════════════════════ - -# pg-counter: простой счёт всех строк. -r=$(invoke_raw "pg-counter" '{}') -check_contains "pg-counter smoke" "$r" "total" - -# pg-dedup: dry_run — ничего не удаляем, проверяем связность. -r=$(invoke_raw "pg-dedup" '{"dry_run": true}') -check_contains "pg-dedup smoke (dry_run)" "$r" "duplicates_found" - -# pg-search: самый простой запрос. -r=$(invoke_raw "pg-search" '{"query": "a"}') -check_contains "pg-search smoke" "$r" "rows" - -# pg-bulk-insert: 5 строк. -r=$(invoke_raw "pg-bulk-insert" '{"n": 5, "prefix": "smoke"}') -check_contains "pg-bulk-insert smoke" "$r" "inserted" - -# pg-delete-old: смотрим что вернёт, не упадёт. -r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}') -check_contains "pg-delete-old smoke" "$r" "deleted" - -# pg-upsert: вставляем одну строку. -r=$(invoke_raw "pg-upsert" '{"title": "smoke-test-upsert-01"}') -check_contains "pg-upsert smoke" "$r" "action" - -# chaos-echo: простое отражение. -r=$(invoke_raw "chaos-echo" '{"hello": "world"}') -check_contains "chaos-echo smoke" "$r" "echo" - -# chaos-badparams: валидный вызов. -r=$(invoke_raw "chaos-badparams" '{"n": 5, "name": "test", "flag": true}') -check_contains "chaos-badparams smoke" "$r" "n" - -# chaos-slowquery: sleep 1s. -r=$(invoke_raw "chaos-slowquery" '{"seconds": 1}') -check_contains "chaos-slowquery smoke" "$r" "slept" - -# chaos-bigpayload: 16KB. -r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 16}') -check_contains "chaos-bigpayload smoke" "$r" "items" - -# go-pg-race: 2 горутины × 3 INSERT. -r=$(invoke_raw "go-pg-race" '{"workers": 2, "n_per_worker": 3}') -check_contains "go-pg-race smoke" "$r" "inserted" - -# go-counter-atomic: один вызов. -r=$(invoke_raw "go-counter-atomic" '{}') -check_contains "go-counter-atomic smoke" "$r" "invocation" - -# js-pg-batch: 5 строк. -r=$(invoke_raw "js-pg-batch" '{"n": 5, "prefix": "smoke-js"}') -check_contains "js-pg-batch smoke" "$r" "inserted" - -# js-idempotent: новый уникальный ключ. -r=$(invoke_raw "js-idempotent" '{"idempotency_key": "smoke-key-001"}') -check_contains "js-idempotent smoke" "$r" "action" - -# py-retry-writer: 3 строки без simulate_error. -r=$(invoke_raw "py-retry-writer" '{"n": 3, "prefix": "smoke"}') -check_contains "py-retry-writer smoke" "$r" "inserted" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 2 — Dumb User Simulation (тупой юзер ломает всё)" -# ═══════════════════════════════════════════════════════════════════════════════ - -echo " Тест: передаём мусор в каждый сервис — никто не должен вернуть 500." - -# chaos-echo: пустой объект. -c=$(invoke_with_status "chaos-echo" '{}') -check_http "dumb: chaos-echo empty" "$c" - -# chaos-echo: огромный unicode payload. -big_unicode=$(python3 -c "import json; print(json.dumps({'text': '中文テスト🎉' * 500}))") -c=$(invoke_with_status "chaos-echo" "$big_unicode") -check_http "dumb: chaos-echo unicode×500" "$c" - -# chaos-echo: числа вместо строк. -c=$(invoke_with_status "chaos-echo" '{"key": 99999999, "nested": {"a": null, "b": [1,2,3]}}') -check_http "dumb: chaos-echo nested nulls" "$c" - -# chaos-badparams: n="строка" вместо числа — должен survive. -c=$(invoke_with_status "chaos-badparams" '{"n": "сто пятьдесят", "name": null, "flag": "yes please"}') -check_http "dumb: badparams n=string flag=string" "$c" - -# chaos-badparams: n=-999999. -c=$(invoke_with_status "chaos-badparams" '{"n": -999999}') -check_http "dumb: badparams n negative huge" "$c" - -# chaos-badparams: полностью пустой payload. -c=$(invoke_with_status "chaos-badparams" '{}') -check_http "dumb: badparams empty payload" "$c" - -# chaos-badparams: n=Infinity (JSON не поддерживает, строка). -c=$(invoke_with_status "chaos-badparams" '{"n": "Infinity"}') -check_http "dumb: badparams n=Infinity" "$c" - -# pg-counter: prefix = 2000 символов — должен обрезать, а не упасть. -long_prefix=$(python3 -c "print('x' * 2000)") -c=$(invoke_with_status "pg-counter" "{\"prefix\": \"$long_prefix\"}") -check_http "dumb: pg-counter prefix 2000 chars" "$c" - -# pg-search: SQL injection attempt. -c=$(invoke_with_status "pg-search" '{"query": "a OR 1=1; DROP TABLE terraform_demo_table; --"}') -check_http "dumb: pg-search SQL injection attempt" "$c" - -# pg-search: query пустая строка. -c=$(invoke_with_status "pg-search" '{"query": ""}') -check_http "dumb: pg-search empty query" "$c" - -# pg-search: limit=-1. -c=$(invoke_with_status "pg-search" '{"query": "a", "limit": -1}') -check_http "dumb: pg-search limit=-1" "$c" - -# pg-search: offset="много" (строка). -c=$(invoke_with_status "pg-search" '{"query": "a", "offset": "много"}') -check_http "dumb: pg-search offset=string" "$c" - -# pg-bulk-insert: n=99999 — должен cap до 500. -c=$(invoke_with_status "pg-bulk-insert" '{"n": 99999, "prefix": "dumb"}') -check_http "dumb: pg-bulk-insert n=99999 (capped)" "$c" - -# pg-bulk-insert: n=0 — граничный случай. -c=$(invoke_with_status "pg-bulk-insert" '{"n": 0, "prefix": "dumb"}') -check_http "dumb: pg-bulk-insert n=0" "$c" - -# pg-upsert: title null. -c=$(invoke_with_status "pg-upsert" '{"title": null}') -# null title — можно вернуть 400 или 200 с ошибкой — главное не 500. -r=$(invoke_raw "pg-upsert" '{"title": null}') -check_not_contains "dumb: pg-upsert title=null no 500 in body" "$r" '"error"' || true -# Просто проверяем что не упает с 5xx. -[[ "$c" != "5"* ]] && check "dumb: pg-upsert title=null not 5xx" "ok" "ok" \ - || check "dumb: pg-upsert title=null not 5xx" "fail" "ok" - -# pg-delete-old: older_than_minutes=0 (граничный). -c=$(invoke_with_status "pg-delete-old" '{"older_than_minutes": 0}') -check_http "dumb: pg-delete-old older_than=0" "$c" - -# go-pg-race: workers=0. -c=$(invoke_with_status "go-pg-race" '{"workers": 0, "n_per_worker": 10}') -check_http "dumb: go-pg-race workers=0" "$c" - -# go-pg-race: workers=9999 — должен cap до 20. -c=$(invoke_with_status "go-pg-race" '{"workers": 9999, "n_per_worker": 1}') -check_http "dumb: go-pg-race workers=9999 (capped)" "$c" - -# chaos-slowquery: seconds=-5 — отрицательное (должен cap до 0 или 1). -c=$(invoke_with_status "chaos-slowquery" '{"seconds": -5}') -check_http "dumb: chaos-slowquery seconds=-5" "$c" - -# chaos-slowquery: seconds=9999 — должен cap до 8, выполниться за ~8s. -c=$(invoke_with_status "chaos-slowquery" '{"seconds": 9999}') -check_http "dumb: chaos-slowquery seconds=9999 (capped)" "$c" - -# chaos-bigpayload: size_kb=0. -c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 0}') -check_http "dumb: chaos-bigpayload size_kb=0" "$c" - -# chaos-bigpayload: size_kb=9999 — должен cap до 256. -c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 9999}') -check_http "dumb: chaos-bigpayload size_kb=9999 (capped)" "$c" - -# js-pg-batch: n="много" — строка вместо числа. -c=$(invoke_with_status "js-pg-batch" '{"n": "много", "prefix": "dumb"}') -check_http "dumb: js-pg-batch n=string" "$c" - -# js-idempotent: idempotency_key отсутствует. -c=$(invoke_with_status "js-idempotent" '{}') -[[ "$c" != "5"* ]] && check "dumb: js-idempotent no key not 5xx" "ok" "ok" \ - || check "dumb: js-idempotent no key not 5xx" "fail" "ok" - -# py-retry-writer: simulate_error=true и n=1. -r=$(invoke_raw "py-retry-writer" '{"n": 1, "simulate_error": true}') -check_contains "dumb: py-retry-writer simulate_error n=1" "$r" "attempts" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 3 — Idempotency Suite" -# ═══════════════════════════════════════════════════════════════════════════════ - -echo " Тест: повторные вызовы с одинаковыми ключами дают предсказуемый результат." - -# pg-upsert: вызываем 20× с одним title — в таблице должна быть одна строка. -UPSERT_TITLE="idempotent-title-$(date +%s)" -for i in $(seq 1 20); do - invoke_raw "pg-upsert" "{\"title\": \"$UPSERT_TITLE\"}" >/dev/null 2>&1 || true -done -# Проверяем через pg-counter + pg-search. -r=$(invoke_raw "pg-search" "{\"query\": \"$UPSERT_TITLE\", \"limit\": 100}") -count=$(echo "$r" | jq -r '.rows | length' 2>/dev/null || echo "0") -check "idempotency: pg-upsert 20× same title = 1 row" "$count" "1" - -# js-idempotent: 10× одинаковый ключ — должно быть action=existing после первого вызова. -IDEM_KEY="js-idempotent-key-$(date +%s)" -# Первый вызов. -r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") -check_contains "idempotency: js-idempotent first call=created" "$r" "created" -# Следующие 5 вызовов. -for i in $(seq 2 6); do - r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") - check_contains "idempotency: js-idempotent call $i=existing" "$r" "existing" -done - -# pg-dedup: вставляем дубли, затем проверяем что dedup убирает лишние. -DUP_TITLE="dedup-test-$(date +%s)" -invoke_raw "pg-bulk-insert" "{\"n\": 10, \"prefix\": \"$DUP_TITLE\"}" >/dev/null -# Не все строки будут дупликатами (prefix ≠ title), вставляем явно через upsert без конфликта. -# Вставляем одно и то же 5 раз через pg-upsert (он обновляет → НЕ дубль). -# Для настоящих дублей вставляем через bulk-insert с одинаковым prefix (title = prefix_N). -# dry_run у dedup должен показать 0 дублей (bulk-insert генерирует уникальные titles). -r=$(invoke_raw "pg-dedup" '{"dry_run": true}') -check_contains "idempotency: pg-dedup dry_run returns json" "$r" "duplicates_found" - -# py-retry-writer: записываем 5 строк с retry, без ошибок. -r=$(invoke_raw "py-retry-writer" '{"n": 5, "prefix": "retry-idem"}') -inserted=$(echo "$r" | jq -r '.inserted' 2>/dev/null || echo "0") -check "idempotency: py-retry-writer inserts 5" "$inserted" "5" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 4 — PG Parallel Stress" -# ═══════════════════════════════════════════════════════════════════════════════ - -echo " Нагрузка: параллельные вызовы к PG-функциям." - -# pg-counter: 30 параллельных чтений. -echo -n " pg-counter ×30: " -parallel_invoke 30 "pg-counter" '{"prefix": ""}' "counter30" - -# pg-bulk-insert: 10 параллельных × 200 строк. -echo -n " pg-bulk-insert ×10 (n=200): " -parallel_invoke 10 "pg-bulk-insert" '{"n": 200, "prefix": "par-bulk"}' "bulk10" - -# go-pg-race: 5 параллельных × (10 горутин × 10 INSERT). -echo -n " go-pg-race ×5 (workers=10 n=10): " -parallel_invoke 5 "go-pg-race" '{"workers": 10, "n_per_worker": 10}' "race5" - -# go-counter-atomic: 50 параллельных. -echo -n " go-counter-atomic ×50: " -parallel_invoke 50 "go-counter-atomic" '{}' "atomic50" - -# js-pg-batch: 10 параллельных × 50 строк. -echo -n " js-pg-batch ×10 (n=50): " -parallel_invoke 10 "js-pg-batch" '{"n": 50, "prefix": "par-js"}' "jsbatch10" - -# pg-search: 40 параллельных с разными запросами. -echo -n " pg-search ×40: " -parallel_invoke 40 "pg-search" '{"query": "par", "limit": 10}' "search40" - -# Проверяем что после нагрузки счётчик всё ещё работает. -r=$(invoke_raw "pg-counter" '{}') -check_contains "pg stress: counter still returns total" "$r" "total" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 5 — Chaos Payload & Echo Storm" -# ═══════════════════════════════════════════════════════════════════════════════ - -# chaos-bigpayload: 20 параллельных 64KB. -echo -n " chaos-bigpayload ×20 (64KB): " -parallel_invoke 20 "chaos-bigpayload" '{"size_kb": 64}' "big20" - -# chaos-echo: 30 параллельных с 1KB payload. -medium_payload=$(python3 -c "import json; print(json.dumps({'data': 'x' * 1000}))") -echo -n " chaos-echo ×30 (1KB): " -parallel_invoke 30 "chaos-echo" "$medium_payload" "echo30" - -# chaos-bigpayload: один раз 256KB. -r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 256}') -check_contains "chaos-bigpayload 256KB single" "$r" "items" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 6 — Slow Query Handling" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Один медленный запрос 8 секунд — ожидаем 200. -c=$(invoke_with_status "chaos-slowquery" '{"seconds": 8}') -check_http "slowquery: sleep 8s = 200" "$c" - -# 5 параллельных запросов 3s. -echo -n " chaos-slowquery ×5 (3s each): " -parallel_invoke 5 "chaos-slowquery" '{"seconds": 3}' "slow5" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 7 — Search Storm (special chars)" -# ═══════════════════════════════════════════════════════════════════════════════ - -special_queries=( - '{"query": "%"}' - '{"query": "_"}' - '{"query": "'"'"'"}' - '{"query": "\\"}' - '{"query": ""}' - '{"query": "union select"}' - '{"query": "★ ☆ ♡"}' - '{"query": " "}' - '{"query": "а б в г д е ё ж з и й к л м н"}' - '{"query": "你好世界"}' -) - -for q in "${special_queries[@]}"; do - c=$(invoke_with_status "pg-search" "$q") - check_http "search: special chars $(echo "$q" | cut -c1-40)" "$c" -done - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 8 — Dedup & Delete-Old Cycle" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Считаем строки до. -r_before=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}') -total_before=$(echo "$r_before" | jq -r '.total' 2>/dev/null || echo "0") -echo " Строк до цикла: $total_before" - -# Вставляем 300 строк с prefix lifecycle-. -invoke_raw "pg-bulk-insert" '{"n": 300, "prefix": "lifecycle-"}' >/dev/null || true - -# Считаем после вставки. -r_after=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}') -total_after=$(echo "$r_after" | jq -r '.total' 2>/dev/null || echo "0") -echo " После bulk-insert lifecycle-: $total_after" - -# Ищем lifecycle строки. -r=$(invoke_raw "pg-search" '{"query": "lifecycle-", "limit": 5}') -check_contains "dedup-cycle: search finds lifecycle rows" "$r" "lifecycle-" - -# Dry-run dedup — смотрим сколько дублей нашлось. -r=$(invoke_raw "pg-dedup" '{"dry_run": true}') -dups=$(echo "$r" | jq -r '.duplicates_found' 2>/dev/null || echo "?") -echo " Дублей найдено (dry_run): $dups" -check_contains "dedup-cycle: dedup dry_run ok" "$r" "duplicates_found" - -# Настоящий dedup (выполняем). -r=$(invoke_raw "pg-dedup" '{"dry_run": false}') -check_contains "dedup-cycle: real dedup ok" "$r" "deleted" - -# delete-old: удаляем строки старше 99999 минут (практически всё старое). -r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}') -check_contains "dedup-cycle: delete-old returns deleted" "$r" "deleted" - -# Считаем финальный total. -r_final=$(invoke_raw "pg-counter" '{}') -total_final=$(echo "$r_final" | jq -r '.total' 2>/dev/null || echo "?") -echo " Финальный total строк: $total_final" -check_contains "dedup-cycle: counter after cleanup" "$r_final" "total" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 9 — Retry Writer Stress" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Retry с simulate_error — проверяем что retry отрабатывает и данные записаны. -r=$(invoke_raw "py-retry-writer" '{"n": 10, "simulate_error": true, "prefix": "retry-err"}') -check_contains "retry: simulate_error=true returns attempts" "$r" "attempts" -attempts=$(echo "$r" | jq -r '.attempts' 2>/dev/null || echo "0") -echo " Retry attempts: $attempts" -[[ "$attempts" -ge 2 ]] && check "retry: минимум 2 попытки при simulate_error" "ok" "ok" \ - || check "retry: минимум 2 попытки при simulate_error" "fail" "ok" - -# 5 параллельных py-retry-writer с simulate_error. -echo -n " py-retry-writer ×5 (simulate_error): " -parallel_invoke 5 "py-retry-writer" '{"n": 5, "simulate_error": true}' "retry5err" - -# 10 параллельных без ошибок. -echo -n " py-retry-writer ×10 (no error): " -parallel_invoke 10 "py-retry-writer" '{"n": 5, "prefix": "par-retry"}' "retry10" - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 10 — Mixed Concurrent Load (пиковый тест)" -# ═══════════════════════════════════════════════════════════════════════════════ - -echo " Запускаем все сервисы одновременно..." -pids_mixed=() -results_mixed="$LOG_DIR/mixed" -mkdir -p "$results_mixed" - -# Запускаем 3–5 параллельных вызовов каждого сервиса одновременно. -for svc in "${SERVICES[@]}"; do - for i in 1 2 3; do - ( - code=$(invoke_with_status "$svc" '{"n": 2, "workers": 2, "n_per_worker": 2, "size_kb": 8, "seconds": 1, "query": "x", "prefix": "mixed", "idempotency_key": "mixed-'$RANDOM'", "title": "mixed-'$RANDOM'"}' 2>/dev/null || true) - echo "${svc}:${i}:${code}" >> "$results_mixed/results.txt" - ) & - pids_mixed+=($!) - done -done - -echo " Ждём завершения всех ${#pids_mixed[@]} параллельных вызовов..." -for pid in "${pids_mixed[@]}"; do wait "$pid" || true; done - -total_mixed=$(wc -l < "$results_mixed/results.txt") -ok_mixed=$(grep -c ":200$" "$results_mixed/results.txt" || true) -bad_mixed=$(( total_mixed - ok_mixed )) -echo " Mixed: $ok_mixed/$total_mixed OK, $bad_mixed FAIL" -check "mixed: >90% success rate" "$(( ok_mixed * 100 / total_mixed ))" \ - "$(( ok_mixed * 100 / total_mixed ))" # Всегда pass — выводим статистику - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 11 — Проверка уже существующих crash-сервисов (регрессия)" -# ═══════════════════════════════════════════════════════════════════════════════ - -# Убеждаемся что старые crash-сервисы из stress.tf всё ещё возвращают 500. -CRASH_SERVICES=("stress-go-nil" "stress-divzero") -for svc in "${CRASH_SERVICES[@]}"; do - c=$(invoke_with_status "$svc" '{}' 2>/dev/null || echo "000") - if [[ "$c" == "500" ]]; then - check "regression: $svc returns 500" "ok" "ok" - elif [[ "$c" == "000" ]]; then - echo -e "$(ts) ${YELLOW}SKIP${NC} $svc недоступен ($(( TOTAL+1 )))" - TOTAL=$((TOTAL+1)) - else - check "regression: $svc returns 500" "$c" "500" - fi -done - -done # конец основного цикла while true (фазы 1–11) - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ФАЗА 12 — Финальная проверка всех 15 сервисов" -# ═══════════════════════════════════════════════════════════════════════════════ - -for svc in "${SERVICES[@]}"; do - c=$(invoke_with_status "$svc" '{"n": 1, "prefix": "final", "query": "f", "size_kb": 1, "title": "final-check-'$(date +%s%N)'", "idempotency_key": "final-'$(date +%s%N)'"}') - check_http "final: $svc still responds 200" "$c" -done - -# ═══════════════════════════════════════════════════════════════════════════════ -section "ИТОГИ МАРАФОНА" -# ═══════════════════════════════════════════════════════════════════════════════ - -END_TIME=$(date +%s) -DURATION=$(( END_TIME - START_TIME )) -MINUTES=$(( DURATION / 60 )) -SECONDS_REM=$(( DURATION % 60 )) - -echo "" -echo -e "${YELLOW}Время выполнения: ${MINUTES}м ${SECONDS_REM}с${NC}" -echo "" -if (( FAIL == 0 )); then - echo -e "${GREEN}╔══════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ ВСЕ ${TOTAL} ТЕСТОВ ПРОШЛИ ✓ ║${NC}" - echo -e "${GREEN}╚══════════════════════════════════════╝${NC}" -else - echo -e "${RED}╔══════════════════════════════════════╗${NC}" - echo -e "${RED}║ PASS: ${PASS}/${TOTAL} FAIL: ${FAIL} ║${NC}" - echo -e "${RED}╚══════════════════════════════════════╝${NC}" -fi -echo "" -echo "Логи: $LOG_DIR" - -exit $(( FAIL > 0 ? 1 : 0 )) diff --git a/POSTGRES/deploy_and_run_chaos.sh b/POSTGRES/deploy_and_run_chaos.sh deleted file mode 100644 index 481dc3e..0000000 --- a/POSTGRES/deploy_and_run_chaos.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# 2026-03-21 — deploy_and_run_chaos.sh -# ЗАПУСКАТЬ НА VM: ssh naeel@5.172.178.213 -# cd /home/naeel/terra/sless/examples/POSTGRES -# bash deploy_and_run_chaos.sh - -set -euo pipefail - -TF_DIR="/home/naeel/terra/sless/examples/POSTGRES" -cd "$TF_DIR" - -echo "=== [1/2] terraform apply chaos_marathon.tf ===" - -terraform apply \ - -target=sless_service.pg_counter \ - -target=sless_service.pg_dedup \ - -target=sless_service.pg_search \ - -target=sless_service.pg_bulk_insert \ - -target=sless_service.pg_delete_old \ - -target=sless_service.pg_upsert \ - -target=sless_service.chaos_echo \ - -target=sless_service.chaos_badparams \ - -target=sless_service.chaos_slowquery \ - -target=sless_service.chaos_bigpayload \ - -target=sless_service.go_pg_race \ - -target=sless_service.go_counter_atomic \ - -target=sless_service.js_pg_batch \ - -target=sless_service.js_idempotent \ - -target=sless_service.py_retry_writer \ - -auto-approve - -echo "" -echo "=== [2/2] Запуск chaos_marathon.sh ===" - -LOG="/tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log" -bash chaos_marathon.sh 2>&1 | tee "$LOG" - -echo "" -echo "Лог сохранён: $LOG" diff --git a/POSTGRES/full_test.sh b/POSTGRES/full_test.sh deleted file mode 100755 index 917c8f9..0000000 --- a/POSTGRES/full_test.sh +++ /dev/null @@ -1,437 +0,0 @@ -#!/bin/bash -# 2026-03-21 — full_test.sh: комплексный тест всех sless-ресурсов. -# -# Фазы: -# 1. CRUD — проверяем наличие всех сервисов через API -# 2. Функциональные — корректность ответов, правильные значения -# 3. PG-стресс — параллельные write/read, pgstorm (Go), js-async storm -# 4. Краш-шторм — параллельные паники, проверяем что платформа жива после -# -# Запуск: bash full_test.sh -# Зависимости: curl, python3, terraform (для CRUD destroy/create) -# -# Среда: namespace sless-ffd1f598c169b0ae, токен в ~/terra/sless/test.token - -set -uo pipefail - -TOKEN=$(cat /home/naeel/terra/sless/test.token) -NS="sless-ffd1f598c169b0ae" -BASE="https://sless.kube5s.ru/fn/$NS" -API="https://sless.kube5s.ru/v1/namespaces/$NS" - -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -PASS=0 -FAIL=0 - -pass() { echo -e " ${GREEN}[PASS]${NC} $1"; ((PASS++)); } -fail() { echo -e " ${RED}[FAIL]${NC} $1"; ((FAIL++)); } -section() { echo -e "\n${YELLOW}━━━ $1 ━━━${NC}"; } -info() { echo -e " ${CYAN}[INFO]${NC} $1"; } - -# Вызвать URL и вернуть JSON (не проверяя код) -call() { - local url="$1" body="${2:-}" extra_headers="${3:-}" - local args=(-s -m 90 -H "Authorization: Bearer $TOKEN") - [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") - [[ -n "$extra_headers" ]] && args+=(-H "$extra_headers") - curl "${args[@]}" "$url" -} - -# Проверить HTTP-код (только код, без тела) -check_http() { - local label="$1" url="$2" method="${3:-GET}" body="${4:-}" expect="${5:-200}" - local args=(-s -o /dev/null -w "%{http_code}" -m 90 -H "Authorization: Bearer $TOKEN") - [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") - [[ "$method" != "GET" ]] && args+=(-X "$method") - local code - code=$(curl "${args[@]}" "$url") - if [[ "$code" == "$expect" ]]; then - pass "$label → HTTP $code" - else - fail "$label → HTTP $code (ожидали $expect)" - fi -} - -# Вызвать функцию, проверить поле JSON == expected -check_field() { - local label="$1" url="$2" body="$3" field="$4" expected="$5" - local resp - resp=$(call "$url" "$body") - local actual - actual=$(echo "$resp" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - v = d.get('$field', '__MISSING__') - print(str(v)) -except Exception as e: - print('PARSE_ERROR: ' + str(e)) -" 2>/dev/null) - if [[ "$actual" == "$expected" ]]; then - pass "$label" - else - fail "$label → got '$actual' (ожидали '$expected') | resp: $(echo "$resp" | head -c 200)" - fi -} - -# Вызвать функцию, проверить что поле JSON > 0 (числовое) -check_field_gt0() { - local label="$1" url="$2" body="$3" field="$4" - local resp - resp=$(call "$url" "$body") - local actual - actual=$(echo "$resp" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - v = d.get('$field', 0) - print(1 if float(str(v)) > 0 else 0) -except: - print(0) -" 2>/dev/null) - if [[ "$actual" == "1" ]]; then - pass "$label" - else - fail "$label → resp: $(echo "$resp" | head -c 200)" - fi -} - -# ═══════════════════════════════════════════════════════════════ -section "ФАЗА 1: CRUD — проверяем что все сервисы существуют" -# ═══════════════════════════════════════════════════════════════ - -ALL_SERVICES=( - pg-info pg-table-reader pg-table-writer - stress-go-fast stress-go-nil stress-go-pgstorm - stress-js-async stress-js-badenv - stress-slow stress-bigloop stress-divzero stress-writer pg-stats -) - -for svc in "${ALL_SERVICES[@]}"; do - code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \ - -H "Authorization: Bearer $TOKEN" "$API/services/$svc") - if [[ "$code" == "200" ]]; then - pass "API GET /services/$svc → 200" - else - fail "API GET /services/$svc → $code" - fi -done - -info "Проверяем несуществующий сервис → 404" -check_http "GET /services/THIS-SERVICE-DOES-NOT-EXIST → 404" \ - "$API/services/this-service-does-not-exist" "GET" "" "404" - -info "Проверяем jobs" -code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \ - -H "Authorization: Bearer $TOKEN" "$API/jobs/pg-create-table-job-main-v13") -if [[ "$code" == "200" ]]; then - pass "API GET /jobs/pg-create-table-job-main-v13 → 200" -else - fail "API GET /jobs/pg-create-table-job-main-v13 → $code" -fi - -# ═══════════════════════════════════════════════════════════════ -section "ФАЗА 2: Функциональные тесты (корректность ответов)" -# ═══════════════════════════════════════════════════════════════ - -info "── Go 1.23 ──" - -# stress-go-fast: factorial(10) = 3628800 -check_field "go-fast runtime=go1.23" \ - "$BASE/stress-go-fast" '{"n":10}' "runtime" "go1.23" -check_field "go-fast factorial(10)=3628800" \ - "$BASE/stress-go-fast" '{"n":10}' "factorial" "3628800" -check_field "go-fast fib(10)=55" \ - "$BASE/stress-go-fast" '{"n":10}' "fib" "55" -# n>20 обрезается до 20 — проверяем граничный случай -check_field "go-fast n=21 обрезается до 20: fib(20)=6765" \ - "$BASE/stress-go-fast" '{"n":21}' "fib" "6765" - -# stress-go-nil crash=false → crashed:false -check_field "go-nil crash=false → crashed=False" \ - "$BASE/stress-go-nil" '{"crash":false}' "crashed" "False" -# stress-go-nil crash=true → 500 -check_http "go-nil crash=true → HTTP 500" \ - "$BASE/stress-go-nil" "POST" '{"crash":true}' "500" -# stress-go-nil default (no body) → 500 (по умолчанию crash=true) -check_http "go-nil без параметров → HTTP 500" \ - "$BASE/stress-go-nil" "GET" "" "500" - -info "── Node.js 20 ──" - -# stress-js-async: чтение PG, возвращает pg_version -check_field "js-async runtime=nodejs20" \ - "$BASE/stress-js-async" "" "runtime" "nodejs20" -check_field_gt0 "js-async total_rows > 0" \ - "$BASE/stress-js-async" "" "total_rows" -# stress-js-badenv crash=false → ok -check_field "js-badenv crash=false → runtime=nodejs20" \ - "$BASE/stress-js-badenv" '{"crash":false}' "runtime" "nodejs20" -# stress-js-badenv crash=true → 500 -check_http "js-badenv crash=true → HTTP 500" \ - "$BASE/stress-js-badenv" "POST" '{"crash":true}' "500" - -info "── Python 3.11 ──" - -# stress-slow -check_field "slow: slept_sec=3" \ - "$BASE/stress-slow" '{"sleep":3}' "slept_sec" "3" -check_field "slow: version=v1" \ - "$BASE/stress-slow" '{"sleep":1}' "version" "v1" - -# stress-bigloop: sum(i*i for i in range(10)) = 285 -check_field "bigloop n=10 sum_of_squares=285" \ - "$BASE/stress-bigloop" '{"n":10}' "sum_of_squares" "285" -# range(100): 0+1+4+...+9801 = sum(i^2,0..99) = 99*100*199/6 = 328350 -check_field "bigloop n=100 sum_of_squares=328350" \ - "$BASE/stress-bigloop" '{"n":100}' "sum_of_squares" "328350" - -# stress-divzero 42/7 = 6.0 -check_field "divzero 42/7=6.0" \ - "$BASE/stress-divzero" '{"n":42,"d":7}' "result" "6.0" -# divzero d=0 → 500 -check_http "divzero d=0 → HTTP 500" \ - "$BASE/stress-divzero" "POST" '{"n":1,"d":0}' "500" - -# stress-writer: записывает 3 строки -check_field "writer rows=3 → count=3" \ - "$BASE/stress-writer" '{"rows":3,"prefix":"functional-test"}' "count" "3" -check_field "writer rows=1 → count=1" \ - "$BASE/stress-writer" '{"rows":1,"prefix":"functional-single"}' "count" "1" - -# pg-stats -check_field "pg-stats version=v1-test7" \ - "$BASE/pg-stats" "" "version" "v1-test7" -check_field_gt0 "pg-stats total_rows > 0" \ - "$BASE/pg-stats" "" "total_rows" - -# pg-info (nodejs) -check_field "pg-info runtime=nodejs20" \ - "$BASE/pg-info" "" "runtime" "nodejs20" - -# pg-table-reader -check_http "table-reader HTTP 200" "$BASE/pg-table-reader" -READER_RESP=$(call "$BASE/pg-table-reader") -READER_COUNT=$(echo "$READER_RESP" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - print(d.get('count', 0)) -except: - print(0) -" 2>/dev/null) -if [[ "$READER_COUNT" -gt 0 ]] 2>/dev/null; then - pass "table-reader count=$READER_COUNT строк" -else - fail "table-reader ожидали >0 строк, получили: $READER_COUNT | $(echo "$READER_RESP" | head -c 200)" -fi - -# pg-table-writer: POST JSON должен вставить строку и вернуть JSON -info "pg-table-writer POST (ожидаем JSON если платформа инжектит _method)" -WRITER_RESP=$(curl -s -m 30 -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d '{"title":"full-test-insert-2026"}' \ - "$BASE/pg-table-writer") -WRITER_OK=$(echo "$WRITER_RESP" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - print(d.get('ok', False)) -except: - print('NOT_JSON') -" 2>/dev/null) -if [[ "$WRITER_OK" == "True" ]]; then - pass "table-writer POST → ok=True, строка вставлена" -else - # HTML ответ — платформа не инжектит _method - info "table-writer вернул не JSON (вероятно HTML), ok=$WRITER_OK" - info "resp: $(echo "$WRITER_RESP" | head -c 100)" - # Это не баг, но фиксируем как наблюдение -fi - -# ═══════════════════════════════════════════════════════════════ -section "ФАЗА 3: PG-стресс (параллельная нагрузка на PostgreSQL)" -# ═══════════════════════════════════════════════════════════════ - -info "Запуск 40 параллельных stress-writer × 5 строк = 200 INSERT..." -ROWS_BEFORE=$(echo "$READER_COUNT") -WRITER_PIDS=() -for i in $(seq 1 40); do - curl -s -m 60 -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"rows\":5,\"prefix\":\"pgstorm-w$i\"}" \ - "$BASE/stress-writer" > "/tmp/sw_$i.json" 2>&1 & - WRITER_PIDS+=($!) -done -wait "${WRITER_PIDS[@]}" - -WRITER_OK=0; WRITER_FAIL=0 -for i in $(seq 1 40); do - cnt=$(python3 -c " -import json -try: - d = json.load(open('/tmp/sw_$i.json')) - print(d.get('count', 0)) -except: - print(0) -" 2>/dev/null) - if [[ "$cnt" == "5" ]]; then - ((WRITER_OK++)) - else - ((WRITER_FAIL++)) - info " writer batch $i: cnt=$cnt | $(cat /tmp/sw_$i.json | head -c 150)" - fi -done -info "writer: $WRITER_OK/40 OK, $WRITER_FAIL failed" -[[ "$WRITER_FAIL" == "0" ]] \ - && pass "40× parallel writer: все 40 вернули count=5 (200 строк)" \ - || fail "40× parallel writer: $WRITER_FAIL пакетов с ошибкой" - -# Проверим что строки реально появились в таблице -NEW_COUNT=$(call "$BASE/pg-stats" | python3 -c " -import sys, json -try: - print(json.load(sys.stdin).get('total_rows', 0)) -except: - print(0) -") -info "pg-stats: total_rows=$NEW_COUNT (было $ROWS_BEFORE до stress)" -[[ "$NEW_COUNT" -gt "$ROWS_BEFORE" ]] \ - && pass "pg-stats: строки выросли ($ROWS_BEFORE → $NEW_COUNT)" \ - || fail "pg-stats: строки не выросли ($ROWS_BEFORE → $NEW_COUNT)" - -info "Запуск 30 параллельных stress-js-async (3 PG-запроса каждый = 90 одновременных)..." -JS_PIDS=() -for i in $(seq 1 30); do - curl -s -m 30 -H "Authorization: Bearer $TOKEN" \ - "$BASE/stress-js-async" > "/tmp/jsa_$i.json" 2>&1 & - JS_PIDS+=($!) -done -wait "${JS_PIDS[@]}" - -JS_OK=0; JS_FAIL=0 -for i in $(seq 1 30); do - rt=$(python3 -c " -import json -try: - print(json.load(open('/tmp/jsa_$i.json')).get('runtime', 'err')) -except: - print('err') -" 2>/dev/null) - if [[ "$rt" == "nodejs20" ]]; then ((JS_OK++)); else ((JS_FAIL++)); fi -done -[[ "$JS_FAIL" == "0" ]] \ - && pass "30× parallel js-async: все 30 OK" \ - || fail "30× parallel js-async: $JS_OK ok, $JS_FAIL failed" - -info "Запуск stress-go-pgstorm workers=50 duration=45s..." -PGSTORM_RESP=$(curl -s -m 120 \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"workers":50,"duration_sec":45,"max_delay_ms":50}' \ - "$BASE/stress-go-pgstorm") -PGSTORM_OK=$(echo "$PGSTORM_RESP" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - print(d.get('ok_ops', 0)) -except: - print(0) -") -PGSTORM_ERR=$(echo "$PGSTORM_RESP" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - print(d.get('err_ops', 0)) -except: - print(-1) -") -PGSTORM_RPS=$(echo "$PGSTORM_RESP" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - print(d.get('ops_per_sec', '?')) -except: - print('?') -") -info "pgstorm: ok=$PGSTORM_OK err=$PGSTORM_ERR ops/s=$PGSTORM_RPS" -[[ "$PGSTORM_OK" -gt 0 ]] 2>/dev/null \ - && pass "stress-go-pgstorm: $PGSTORM_OK ops OK, $PGSTORM_ERR err, $PGSTORM_RPS ops/s" \ - || fail "stress-go-pgstorm: 0 операций | $(echo "$PGSTORM_RESP" | head -c 300)" - -# Итоговая статистика таблицы -FINAL_STATS=$(call "$BASE/pg-stats") -FINAL_ROWS=$(echo "$FINAL_STATS" | python3 -c " -import sys, json -try: - print(json.load(sys.stdin).get('total_rows', 0)) -except: - print(0) -") -info "Итого строк в terraform_demo_table: $FINAL_ROWS" - -# ═══════════════════════════════════════════════════════════════ -section "ФАЗА 4: Краш-шторм (параллельные паники — платформа должна жить)" -# ═══════════════════════════════════════════════════════════════ - -info "25× го-nil crash + 25× divzero + 25× js-badenv = 75 параллельных крашей..." -CRASH_PIDS=() -for i in $(seq 1 25); do - curl -s -o /dev/null -w "%{http_code}" -m 15 \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{"crash":true}' "$BASE/stress-go-nil" > "/tmp/c_nil_$i.txt" 2>&1 & - CRASH_PIDS+=($!) - - curl -s -o /dev/null -w "%{http_code}" -m 15 \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{"n":1,"d":0}' "$BASE/stress-divzero" > "/tmp/c_dz_$i.txt" 2>&1 & - CRASH_PIDS+=($!) - - curl -s -o /dev/null -w "%{http_code}" -m 15 \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{"crash":true}' "$BASE/stress-js-badenv" > "/tmp/c_js_$i.txt" 2>&1 & - CRASH_PIDS+=($!) -done -wait "${CRASH_PIDS[@]}" - -C500=0; CNOT500=0 -for i in $(seq 1 25); do - for f in "/tmp/c_nil_$i.txt" "/tmp/c_dz_$i.txt" "/tmp/c_js_$i.txt"; do - code=$(cat "$f" 2>/dev/null || echo "0") - if [[ "$code" == "500" ]]; then ((C500++)); else ((CNOT500++)); info " неожиданный $f: code=$code"; fi - done -done -info "Краши: $C500 × 500, $CNOT500 неожиданных" -[[ "$CNOT500" == "0" ]] \ - && pass "75× краш-шторм: все вернули HTTP 500 (платформа устойчива)" \ - || fail "75× краш-шторм: $CNOT500 ответов не 500" - -info "Проверяем что сервисы живы после краш-шторма..." -check_http "go-fast: жив после штормов" "$BASE/stress-go-fast" "GET" "" "200" -check_http "js-async: жив после штормов" "$BASE/stress-js-async" "GET" "" "200" -check_http "pg-table-reader: жив после штормов" "$BASE/pg-table-reader" "GET" "" "200" -check_http "pg-stats: жив после штормов" "$BASE/pg-stats" "GET" "" "200" - -# ═══════════════════════════════════════════════════════════════ -section "ИТОГИ" -# ═══════════════════════════════════════════════════════════════ -echo "" -TOTAL=$((PASS + FAIL)) -echo -e " Всего тестов: $TOTAL" -echo -e " ${GREEN}PASS: $PASS${NC}" -echo -e " ${RED}FAIL: $FAIL${NC}" -echo "" - -if [[ "$FAIL" == "0" ]]; then - echo -e " ${GREEN}✓ ВСЕ ТЕСТЫ ПРОШЛИ${NC}" - exit 0 -else - echo -e " ${RED}✗ ЕСТЬ ПАДЕНИЯ ($FAIL)${NC}" - exit 1 -fi diff --git a/POSTGRES/scripts/pg-debug-pod.yaml b/POSTGRES/scripts/pg-debug-pod.yaml deleted file mode 100644 index 652ed5b..0000000 --- a/POSTGRES/scripts/pg-debug-pod.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# 2026-03-18 — debug pod для проверки psql-соединения из namespace функций. -# Запускается разово. Подключается к тому же postgres, что и sless_function. -# kubectl apply -f /tmp/pg-debug-pod.yaml -# kubectl logs -n sless-fn-sless-ffd1f598c169b0ae pg-debug-pod - -apiVersion: v1 -kind: Pod -metadata: - name: pg-debug-pod - namespace: sless-fn-sless-ffd1f598c169b0ae - labels: - purpose: debug-postgres-connectivity -spec: - restartPolicy: Never - containers: - - name: psql - image: postgres:17-alpine - command: - - sh - - -c - - | - echo "=== Testing TCP connectivity to postgres ===" - nc -zv -w5 $PGHOST 5432 && echo "TCP OK" || echo "TCP FAILED" - - echo "" - echo "=== Testing psql connection ===" - PGCONNECT_TIMEOUT=10 psql \ - "host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \ - --command="SELECT current_user, current_database(), version();" \ - 2>&1 - - echo "" - echo "=== Listing tables ===" - PGCONNECT_TIMEOUT=10 psql \ - "host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \ - --command="\dt" \ - 2>&1 - env: - - name: PGHOST - value: "postgresqlk8s-master.36875359-dcea-48c4-a593-b4531f20fe96.svc.cluster.local" - - name: PGPORT - value: "5432" - - name: PGDATABASE - value: "db_terra" - - name: PGUSER - value: "u-user0" - - name: PGPASSWORD - # Актуальный пароль из vault_secrets (совпадает с tfvars.pg_password на 2026-03-18) - value: "M03O6fRsngWcVHB2YGivyLfbfxoii2R21nyh2A2r7WSZS5deLwBgLKkc9Wk24Zyl" - - name: PGSSLMODE - value: "require" diff --git a/POSTGRES/scripts/read_pg_user_secret.py b/POSTGRES/scripts/read_pg_user_secret.py deleted file mode 100644 index b0735f3..0000000 --- a/POSTGRES/scripts/read_pg_user_secret.py +++ /dev/null @@ -1,40 +0,0 @@ -# 2026-03-17 13:05 -# read_pg_user_secret.py — читает пароль пользователя managed PostgreSQL из k8s Secret. -# Используется из Terraform external data source, чтобы apply сам получал актуальный пароль -# даже для уже существующего пользователя, созданного вне текущего state. - -import base64 -import json -import subprocess -import sys - - -def main(): - # Читаем query от Terraform external provider из stdin. - query = json.load(sys.stdin) - namespace = query["namespace"] - secret_name = query["secret"] - - # kubectl уже настроен на удалённой машине; читаем ровно поле data.password. - result = subprocess.run( - [ - "kubectl", - "get", - "secret", - "-n", - namespace, - secret_name, - "-o", - "jsonpath={.data.password}", - ], - check=True, - capture_output=True, - text=True, - ) - - password = base64.b64decode(result.stdout.strip()).decode() - json.dump({"password": password}, sys.stdout) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/POSTGRES/stress_destroy_apply.sh.disabled b/POSTGRES/stress_destroy_apply.sh.disabled deleted file mode 100755 index 76bb6ca..0000000 --- a/POSTGRES/stress_destroy_apply.sh.disabled +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# 2026-03-20 -# stress_destroy_apply.sh — 5 итераций terraform destroy + apply для проверки lifecycle PG. -# Запускать вручную с VM: bash stress_destroy_apply.sh -# Логи каждой итерации пишутся в stress_log_N.txt - -set -e - -ITERATIONS=5 -DIR="$(cd "$(dirname "$0")" && pwd)" -cd "$DIR" - -echo "=== Старт stress-теста: $ITERATIONS итераций destroy+apply ===" -echo "Workdir: $DIR" -echo "" - -for i in $(seq 1 $ITERATIONS); do - LOG="stress_log_${i}.txt" - echo "--- Итерация $i/$ITERATIONS ---" - echo "Лог: $LOG" - - echo "[$i] DESTROY — $(date)" | tee "$LOG" - terraform destroy -auto-approve 2>&1 | tee -a "$LOG" - DESTROY_CODE=${PIPESTATUS[0]} - - if [ $DESTROY_CODE -ne 0 ]; then - echo "[!] destroy завершился с ошибкой (код $DESTROY_CODE), итерация $i. Прерывание." | tee -a "$LOG" - exit $DESTROY_CODE - fi - - echo "" | tee -a "$LOG" - echo "[$i] APPLY — $(date)" | tee -a "$LOG" - terraform apply -auto-approve 2>&1 | tee -a "$LOG" - APPLY_CODE=${PIPESTATUS[0]} - - if [ $APPLY_CODE -ne 0 ]; then - echo "[!] apply завершился с ошибкой (код $APPLY_CODE), итерация $i. Прерывание." | tee -a "$LOG" - exit $APPLY_CODE - fi - - echo "" | tee -a "$LOG" - echo "[$i] Итерация завершена успешно — $(date)" | tee -a "$LOG" - echo "" -done - -echo "=== Все $ITERATIONS итераций прошли успешно ===" diff --git a/POSTGRES/stress_test.sh b/POSTGRES/stress_test.sh deleted file mode 100644 index c695a37..0000000 --- a/POSTGRES/stress_test.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# 2026-03-19 — stress test script: параллельный запуск всех 8 стресс-функций -BASE="https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae" - -echo "=== РАУНД 1: первый холодный запуск ===" -curl -s -m 35 "$BASE/stress-slow" -d '{"sleep":3}' -H "Content-Type:application/json" > /tmp/r_slow.json & -curl -s -m 10 "$BASE/stress-divzero" > /tmp/r_divzero.json & -curl -s -m 40 "$BASE/stress-bigloop" -d '{"n":1000000}' -H "Content-Type:application/json"> /tmp/r_bigloop.json & -curl -s -m 35 "$BASE/stress-writer" -d '{"rows":3,"prefix":"batch1"}' -H "Content-Type:application/json" > /tmp/r_writer.json & -curl -s -m 15 "$BASE/stress-go-fast" -d '{"n":15}' -H "Content-Type:application/json" > /tmp/r_go_fast.json & -curl -s -m 10 "$BASE/stress-go-nil" > /tmp/r_go_nil.json & -curl -s -m 20 "$BASE/stress-js-async" > /tmp/r_js_async.json & -curl -s -m 10 "$BASE/stress-js-badenv" > /tmp/r_js_badenv.json & -wait - -echo "[slow]: $(cat /tmp/r_slow.json)" -echo "[divzero]: $(cat /tmp/r_divzero.json)" -echo "[bigloop]: $(cat /tmp/r_bigloop.json)" -echo "[writer]: $(cat /tmp/r_writer.json)" -echo "[go-fast]: $(cat /tmp/r_go_fast.json)" -echo "[go-nil]: $(cat /tmp/r_go_nil.json)" -echo "[js-async]: $(cat /tmp/r_js_async.json)" -echo "[js-badenv]:$(cat /tmp/r_js_badenv.json)" - -echo "" -echo "=== РАУНД 2: повторный (горячий кэш) ===" -curl -s -m 15 "$BASE/stress-bigloop" -d '{"n":2000000}' -H "Content-Type:application/json" > /tmp/r2_bigloop.json & -curl -s -m 10 "$BASE/stress-go-fast" -d '{"n":20}' -H "Content-Type:application/json" > /tmp/r2_go_fast.json & -curl -s -m 20 "$BASE/stress-js-async" > /tmp/r2_async.json & -curl -s -m 35 "$BASE/stress-writer" -d '{"rows":10,"prefix":"batch2"}' -H "Content-Type:application/json" > /tmp/r2_writer.json & -wait -echo "[bigloop-2M]: $(cat /tmp/r2_bigloop.json)" -echo "[go-fast-20]: $(cat /tmp/r2_go_fast.json)" -echo "[js-async-2]: $(cat /tmp/r2_async.json)" -echo "[writer-10]: $(cat /tmp/r2_writer.json)" - -echo "" -echo "=== РАУНД 3: crash функции с неверными параметрами ===" -curl -s -m 10 "$BASE/stress-divzero" -d '{"n":100,"d":0}' -H "Content-Type:application/json" > /tmp/r3_dz.json & -curl -s -m 10 "$BASE/stress-go-nil" -d '{"crash":true}' -H "Content-Type:application/json" > /tmp/r3_nil.json & -curl -s -m 10 "$BASE/stress-js-badenv" -d '{"crash":true}' -H "Content-Type:application/json" > /tmp/r3_bad.json & -# divzero с нормальным делителем — должен вернуть результат -curl -s -m 10 "$BASE/stress-divzero" -d '{"n":42,"d":7}' -H "Content-Type:application/json" > /tmp/r3_ok.json & -# go-nil без краша — должен вернуть ok -curl -s -m 10 "$BASE/stress-go-nil" -d '{"crash":false}' -H "Content-Type:application/json" > /tmp/r3_nil_ok.json & -wait -echo "[divzero crash]: $(cat /tmp/r3_dz.json)" -echo "[go-nil crash]: $(cat /tmp/r3_nil.json)" -echo "[js-badenv crash]: $(cat /tmp/r3_bad.json)" -echo "[divzero ok 42/7]: $(cat /tmp/r3_ok.json)" -echo "[go-nil ok]: $(cat /tmp/r3_nil_ok.json)" - -echo "" -echo "=== ИТОГ: количество строк в таблице ===" -curl -s -m 15 "$BASE/pg-table-reader" -echo "" -echo "=== DONE ===" diff --git a/POSTGRES/test_cache_matrix.sh b/POSTGRES/test_cache_matrix.sh deleted file mode 100755 index 10d9ddb..0000000 --- a/POSTGRES/test_cache_matrix.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/bin/bash -# test_cache_matrix.sh — 2026-03-23 (v4) -# Комплексный тест кэша registry: -# Phase 1 — полный деплой всех 24 ресурсов (kaniko builds, т.к. нет образов) -# Phase 2 — destroy sless_* + re-apply (все образы из кэша) -# Phase 3 — одновременно: удаление 2, смена кода 2, смена параметров 2 -# ВАЖНО: postgres.tf НЕ переименовывается и НЕ трогается никогда. -# Destroy sless-ресурсов делается путём переименования tf-файлов в .tf.bak, -# затем terraform apply (видит что ресурсов нет → удаляет их из state+кластера), -# затем файлы возвращаются обратно. Никаких -target. - -set -euo pipefail -DIR="$(cd "$(dirname "$0")" && pwd)" -LOG="$DIR/test_cache_matrix_$(date +%Y%m%d_%H%M%S).log" -TIMINGS="$LOG.timings" -PASS=0 -FAIL=0 - -log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG"; } -sep() { log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; } - -timed_op() { - local label="$1"; shift - log "▶ START: $label" - local t0; t0=$(date +%s%3N) - "$@" 2>&1 | tee -a "$LOG" - local rc=${PIPESTATUS[0]} - local t1; t1=$(date +%s%3N) - local elapsed=$(( (t1 - t0) / 1000 )) - if [[ $rc -eq 0 ]]; then - log "✓ DONE: $label — ${elapsed}s" - PASS=$((PASS+1)) - else - log "✗ FAIL: $label — ${elapsed}s (exit $rc)" - FAIL=$((FAIL+1)) - fi - echo "$label: ${elapsed}s" >> "$TIMINGS" - return $rc -} - -destroy_sless_only() { - # Переименовываем tf-файлы с sless-ресурсами в .tf.bak → terraform apply их удалит. - # Никаких -target — чтобы не затрагивать postgres и не получать state drift. - local label="$1" - local SLESS_FILES=("chaos_marathon.tf" "functions.tf" "stress.tf") - - local has_state - has_state=$(terraform state list 2>/dev/null | grep -cE '^(sless_service|sless_job)' || true) - if [[ "$has_state" -eq 0 ]]; then - log " (nothing to destroy for $label — state empty)" - return 0 - fi - log " Hiding sless tf-files → apply will destroy $has_state resources" - - for f in "${SLESS_FILES[@]}"; do - [[ -f "$DIR/$f" ]] && mv "$DIR/$f" "$DIR/$f.bak" - done - - timed_op "$label" terraform apply -auto-approve - - for f in "${SLESS_FILES[@]}"; do - [[ -f "$DIR/$f.bak" ]] && mv "$DIR/$f.bak" "$DIR/$f" - done - log " sless tf-files restored" -} - -cd "$DIR" - -sep -log "PHASE 1: Полный начальный деплой" -sep -destroy_sless_only "phase1-pre-clean" -timed_op "phase1-apply-all" terraform apply -auto-approve - -log "--- Образы в registry после Phase 1 ---" -kubectl exec -n sless deployment/sless-registry -- sh -c 'find /var/lib/registry -name "*.json" -path "*/tags/*" 2>/dev/null | sed "s|.*repository/||;s|/_manifests.*||" | sort | uniq -c | sort -rn' 2>/dev/null | head -30 | tee -a "$LOG" || log "(registry inspect failed)" - -sep -log "PHASE 2: Destroy sless_* → Re-apply (ожидаем cache hits)" -sep -destroy_sless_only "phase2-destroy" -timed_op "phase2-apply-cached" terraform apply -auto-approve - -sep -log "PHASE 3: Mixed ops (delete+code+params)" -sep - -log "--- 3a: destroy stress_divzero, chaos_echo (comment out → apply → restore) ---" -python3 - <<'PYEOF' -import re, pathlib - -def comment_out_resource(path, resource_type, resource_name): - text = pathlib.Path(path).read_text() - # Находим блок resource "type" "name" { ... } и оборачиваем в /* */ - pattern = rf'(resource\s+"{re.escape(resource_type)}"\s+"{re.escape(resource_name)}"\s*\{{)' - match = re.search(pattern, text) - if not match: - print(f" WARNING: {resource_type}.{resource_name} not found in {path}") - return - # Найти закрывающую скобку блока - start = match.start() - depth = 0 - i = match.start() - while i < len(text): - if text[i] == '{': depth += 1 - elif text[i] == '}': - depth -= 1 - if depth == 0: - end = i + 1 - break - i += 1 - block = text[start:end] - commented = "/* COMMENTED_OUT_FOR_TEST\n" + block + "\nCOMMENTED_OUT_FOR_TEST */" - pathlib.Path(path).write_text(text[:start] + commented + text[end:]) - print(f" commented out: {resource_type}.{resource_name} in {path}") - -comment_out_resource("stress.tf", "sless_service", "stress_divzero") -comment_out_resource("chaos_marathon.tf", "sless_service", "chaos_echo") -PYEOF -timed_op "phase3a-destroy-2" terraform apply -auto-approve -# Восстанавливаем закомментированные блоки -python3 - <<'PYEOF' -import pathlib, re - -for fname in ("stress.tf", "chaos_marathon.tf"): - p = pathlib.Path(fname) - text = p.read_text() - text = re.sub(r'/\* COMMENTED_OUT_FOR_TEST\n', '', text) - text = re.sub(r'\nCOMMENTED_OUT_FOR_TEST \*/', '', text) - p.write_text(text) - print(f" restored: {fname}") -PYEOF -log " stress_divzero, chaos_echo removed from state and k8s" - -log "--- 3b: code changes (new sha256 → kaniko) ---" -echo "" >> "$DIR/code/pg-counter/pg_counter.py" -echo "# cache-test-$(date +%s)" >> "$DIR/code/pg-counter/pg_counter.py" -echo "" >> "$DIR/code/stress-js-async/stress_js_async.js" -echo "// cache-test-$(date +%s)" >> "$DIR/code/stress-js-async/stress_js_async.js" -log " changed: pg_counter.py, stress_js_async.js" - -log "--- 3c: param changes (same sha256 → no kaniko) ---" -python3 - <<'PYEOF' -import re, sys -with open("stress.tf") as f: - content = f.read() -orig = content -content = re.sub( - r'(resource "sless_service" "stress_slow" \{[^}]*?)memory_mb\s*=\s*\d+', - lambda m: m.group(1) + 'memory_mb = 192', - content, flags=re.DOTALL -) -content = re.sub( - r'(resource "sless_service" "pg_stats" \{[^}]*?)timeout_sec\s*=\s*\d+', - lambda m: m.group(1) + 'timeout_sec = 20', - content, flags=re.DOTALL -) -if content == orig: - print(" stress.tf: no changes (already patched?)", file=sys.stderr) -else: - with open("stress.tf", "w") as f: - f.write(content) - print(" stress.tf: stress_slow→memory_mb=192, pg_stats→timeout_sec=20") -PYEOF - -log "--- 3d: apply всех mixed изменений ---" -log " Expected: stress_divzero+chaos_echo=cache_hit, pg_counter+stress_js_async=kaniko, stress_slow+pg_stats=k8s_only" -timed_op "phase3d-mixed-apply" terraform apply -auto-approve - -sep -log "ИТОГ" -sep -log "Timings:" -cat "$TIMINGS" 2>/dev/null | tee -a "$LOG" -log "Pass: $PASS | Fail: $FAIL" -log "Лог: $LOG" diff --git a/VM/.vm_stress_test.sh.OLD b/VM/.vm_stress_test.sh.OLD deleted file mode 100755 index 8047a31..0000000 --- a/VM/.vm_stress_test.sh.OLD +++ /dev/null @@ -1,852 +0,0 @@ -#!/usr/bin/env bash -# 2026-03-30 — vm_stress_test.sh -# Автономный stress/chaos тест для examples/VM. -# -# ЦЕЛЬ: прогнать полную матрицу сценариев lifecycle VM + sless_job без -# вмешательства человека. Скрипт самовосстанавливается при ошибках -# и продолжает следующую фазу. -# -# ФАЗЫ: -# 1 BASELINE — apply с полным набором (packages + nginx + docker) -# 2 IDEMPOTENT — повторный apply → "No changes" -# 3 PARTIAL_DISABLE — выключить nginx + docker → apply (2 job уничтожаются) -# 4 PARTIAL_ENABLE — включить обратно → apply (2 job создаются) -# 5 REORDER_PACKAGES — изменить порядок и состав base_packages -# 6 MANUAL_PURGE — удалить все пакеты с VM по SSH → re-run jobs -# 7 DESTROY — terraform destroy → VM уходит в suspend -# 8 RESURRECT — apply после destroy → VM просыпается, всё ставится -# 9 STRESS_CYCLES — N подряд destroy/apply циклов -# 10 FINAL_SANITY — проверить доступность VM и установленные пакеты -# -# ЗАПУСК: -# bash ~/terra/sless/examples/VM/vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log -# -# ПАРАМЕТРЫ (env): -# STRESS_CYCLES=2 — количество destroy/apply циклов в фазе 9 (default: 2) -# SKIP_DESTROY=1 — пропустить фазы 7-9 (для быстрого прогона) -# -# ТРЕБОВАНИЯ: terraform, ssh, python3 — всё доступно на VM naeel@5.172.178.213 -# -# ДЛЯ БЕСПЛАТНЫХ AI-АГЕНТОВ: -# 1. Подключиться по SSH к VM -# 2. Запустить: bash ~/terra/sless/examples/VM/vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log -# 3. Дождаться завершения (exit code 0 = все тесты прошли) -# 4. Проанализировать вывод: grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log -# 5. Итоговая сводка печатается в конце лога - -set -uo pipefail - -# ── CONFIG ──────────────────────────────────────────────────────────────────── - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -VM_KEY="$SCRIPT_DIR/vm_key" -TFVARS="$SCRIPT_DIR/terraform.tfvars" -TFVARS_BACKUP="$SCRIPT_DIR/.terraform.tfvars.test-backup" -STRESS_CYCLES="${STRESS_CYCLES:-2}" -SKIP_DESTROY="${SKIP_DESTROY:-0}" - -# ── СТАТИСТИКА ──────────────────────────────────────────────────────────────── - -PASS=0; FAIL=0; SKIP=0 -PHASE_RESULTS=() # "PHASE_NAME:PASS|FAIL|SKIP" -START_TIME=$SECONDS - -# ── ЦВЕТА ───────────────────────────────────────────────────────────────────── - -GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' -CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' - -# ── HELPERS ─────────────────────────────────────────────────────────────────── - -pass() { echo -e " ${GREEN}[PASS]${RESET} $1"; ((PASS++)); } -fail() { echo -e " ${RED}[FAIL]${RESET} $1"; ((FAIL++)); } -skip() { echo -e " ${YELLOW}[SKIP]${RESET} $1"; ((SKIP++)); } -info() { echo -e " ${CYAN}[INFO]${RESET} $1"; } -warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; } - -phase_header() { - local num="$1" name="$2" - echo "" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo -e "${BOLD}${CYAN} ФАЗА $num: $name${RESET}" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo -e " ${CYAN}[TIME]${RESET} $(date '+%H:%M:%S')" -} - -phase_result() { - local name="$1" result="$2" - PHASE_RESULTS+=("$name:$result") - echo -e " ${CYAN}[PHASE]${RESET} $name → ${result}" -} - -# ── TERRAFORM HELPERS ───────────────────────────────────────────────────────── - -# tf_apply: terraform apply с retry при сетевых ошибках. -# Возвращает 0 при успехе, 1 при ошибке. -tf_apply() { - local attempt=1 max=3 - while [[ $attempt -le $max ]]; do - info "terraform apply (попытка $attempt/$max)..." - # Явное указание var-file и input=false чтобы избежать запросов в терминале - if terraform apply -var-file="terraform.tfvars" -auto-approve -input=false -no-color 2>&1 | tee /tmp/vm_tf_apply.log; then - return 0 - fi - if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_apply.log && [[ $attempt -lt $max ]]; then - warn "сетевой сбой, retry через $((attempt * 5))s..." - sleep $((attempt * 5)) - ((attempt++)) - continue - fi - return 1 - done - return 1 -} - -# tf_destroy: terraform destroy с retry. -tf_destroy() { - local attempt=1 max=3 - while [[ $attempt -le $max ]]; do - info "terraform destroy (попытка $attempt/$max)..." - # Добавляем -var-file и -input=false сюда тоже - if terraform destroy -var-file="terraform.tfvars" -auto-approve -input=false -no-color 2>&1 | tee /tmp/vm_tf_destroy.log; then - return 0 - fi - if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_destroy.log && [[ $attempt -lt $max ]]; then - warn "сетевой сбой, retry через $((attempt * 5))s..." - sleep $((attempt * 5)) - ((attempt++)) - continue - fi - return 1 - done - return 1 -} - -# tf_plan_no_changes: проверить что plan не содержит изменений. -# Возвращает 0 если "No changes", 1 если есть изменения. -tf_plan_no_changes() { - # Добавляем -var-file и -input=false чтобы не было запросов в терминал - terraform plan -var-file="terraform.tfvars" -input=false -no-color 2>&1 | tee /tmp/vm_tf_plan.log - if grep -q 'No changes' /tmp/vm_tf_plan.log; then - return 0 - fi - return 1 -} - -# tf_state_count: количество ресурсов в state. -tf_state_count() { - terraform state list 2>/dev/null | wc -l -} - -# ── TFVARS HELPERS ──────────────────────────────────────────────────────────── - -# Сохранять/восстанавливать tfvars для самовосстановления. -backup_tfvars() { - if [[ ! -f "$TFVARS_BACKUP" ]]; then - cp "$TFVARS" "$TFVARS_BACKUP" - info "Создан бэкап tfvars: $TFVARS_BACKUP" - fi -} -restore_tfvars() { - if [[ -f "$TFVARS_BACKUP" ]]; then - # Читаем токен и ключ из бэкапа перед перезаписью - local token key - token=$(grep 'api_token' "$TFVARS_BACKUP" | cut -d'"' -f2) - key=$(grep 'vm_public_key' "$TFVARS_BACKUP" | cut -d'"' -f2) - - # Если в бэкапе пусто (такое бывает при сбоях сохранения), не портим оригинал - if [[ -n "$token" && -n "$key" ]]; then - cp "$TFVARS_BACKUP" "$TFVARS" - info "tfvars восстановлен из бэкапа" - else - warn "Бэкап tfvars поврежден или пуст, восстановление пропущено" - fi - fi -} - -# write_tfvars: перезаписать tfvars программно. -# Аргументы: install_packages install_nginx install_docker run_id base_packages_json -write_tfvars() { - local pkgs="$1" nginx="$2" docker="$3" run_id="$4" base_pkgs="$5" - - # Читаем токен напрямую из файла secrets если в бэкапе пусто - local token key - token=$(grep 'api_token' "$TFVARS_BACKUP" 2>/dev/null | cut -d'=' -f2- | cut -d'#' -f1 | xargs | sed 's/^"//;s/"$//') - if [[ -z "$token" ]]; then - info "Токен не найден в бэкапе, берем из ~/terra/sless/secrets/tazetnarodtest.token" - token=$(ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=5 "ubuntu@185.247.187.154" "cat ~/terra/sless/secrets/tazetnarodtest.token" 2>/dev/null || cat "$SCRIPT_DIR/../../secrets/tazetnarodtest.token" 2>/dev/null) - fi - - key=$(grep 'vm_public_key' "$TFVARS_BACKUP" 2>/dev/null | cut -d'=' -f2- | cut -d'#' -f1 | xargs | sed 's/^"//;s/"$//') - if [[ -z "$key" ]]; then - key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQB+Kyevn0H8QSdfm6ZpCU9jtskBxUS9BV0+i1/M04A sless-demo-vm" - fi - - cat > "$TFVARS" </dev/null \ - | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('externalConnect',''))" 2>/dev/null -} - -# vm_ssh: выполнить команду на VM. Возвращает exit code команды. -vm_ssh() { - local ip="$1"; shift - ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ - -o ServerAliveInterval=5 -o ServerAliveCountMax=3 \ - "ubuntu@$ip" "$@" -} - -# vm_alive: проверить доступность VM по SSH. -vm_alive() { - local ip="$1" - vm_ssh "$ip" 'echo alive' &>/dev/null -} - -# vm_wait_alive: ждать пока VM ответит по SSH (до timeout_sec). -vm_wait_alive() { - local ip="$1" timeout_sec="${2:-120}" - local deadline=$((SECONDS + timeout_sec)) - while [[ $SECONDS -lt $deadline ]]; do - if vm_alive "$ip"; then return 0; fi - sleep 10 - done - return 1 -} - -# vm_check_package: проверить что пакет установлен. -vm_check_package() { - local ip="$1" pkg="$2" - vm_ssh "$ip" "dpkg -l $pkg 2>/dev/null | grep -q '^ii'" 2>/dev/null -} - -# vm_check_binary: проверить что бинарник доступен. -vm_check_binary() { - local ip="$1" bin="$2" - vm_ssh "$ip" "command -v $bin" &>/dev/null -} - -# vm_purge_all: удалить все установленные пакеты с VM. -vm_purge_all() { - local ip="$1" - info "удаляю пакеты с VM $ip..." - vm_ssh "$ip" 'sudo systemctl stop nginx docker 2>/dev/null; sudo apt-get purge -y jq python3-pip htop unzip nginx docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null; sudo apt-get autoremove -y 2>/dev/null; sudo rm -rf /var/lib/docker /var/lib/containerd; echo purge_done' 2>/dev/null -} - -# ── SELF-RECOVERY ───────────────────────────────────────────────────────────── - -# ensure_baseline: гарантировать что VM и все 3 job в state. -# Используется для восстановления после сбоя фазы. -ensure_baseline() { - info "восстанавливаю baseline state..." - restore_tfvars - # Перезаписать tfvars на полный набор с текущим run_id - local run_id - run_id=$(grep 'install_run_id' "$TFVARS_BACKUP" | grep -oP '\d+' | head -1) - write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' - tf_apply || warn "baseline apply не удался — продолжаю" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 1: BASELINE — apply с полным набором -# ══════════════════════════════════════════════════════════════════════════════ - -phase_1_baseline() { - phase_header 1 "BASELINE — apply с полным набором" - - # Сохранить оригинальный tfvars - backup_tfvars - - # Читаем текущий run_id и инкрементируем - local run_id - run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) - run_id=$((run_id + 1)) - - write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' - - if tf_apply; then - pass "1.1 terraform apply завершился успешно" - else - fail "1.1 terraform apply упал" - phase_result "BASELINE" "FAIL" - return 1 - fi - - # Проверить количество ресурсов - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "1.2 state содержит $count ресурсов (ожидали ≥5)" - else - fail "1.2 state содержит $count ресурсов (ожидали ≥5)" - fi - - # Проверить outputs - local out - out=$(terraform output -json 2>/dev/null) - - if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'ok' in d['install_packages_result']['value'] or 'already' in d['install_packages_result']['value']" 2>/dev/null; then - pass "1.3 install_packages_result содержит ok/already_installed" - else - fail "1.3 install_packages_result не содержит ok" - fi - - if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_nginx_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then - pass "1.4 install_nginx_result содержит ok/already" - else - fail "1.4 install_nginx_result не содержит ok" - fi - - if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_docker_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then - pass "1.5 install_docker_result содержит ok/already" - else - fail "1.5 install_docker_result не содержит ok" - fi - - # Проверить VM по SSH - local ip - ip=$(get_vm_ip) - if [[ -n "$ip" ]] && vm_alive "$ip"; then - pass "1.6 VM $ip доступна по SSH" - else - fail "1.6 VM не доступна по SSH (ip=$ip)" - fi - - # Обновить бэкап с новым run_id - backup_tfvars - - phase_result "BASELINE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 2: IDEMPOTENT — повторный apply без изменений -# ══════════════════════════════════════════════════════════════════════════════ - -phase_2_idempotent() { - phase_header 2 "IDEMPOTENT — повторный apply без изменений" - - if tf_plan_no_changes; then - pass "2.1 terraform plan → No changes" - else - fail "2.1 terraform plan показывает изменения (ожидали No changes)" - # Показать что именно - grep -E 'will be|must be' /tmp/vm_tf_plan.log 2>/dev/null | head -5 | while read -r line; do - info " $line" - done - fi - - phase_result "IDEMPOTENT" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 3: PARTIAL_DISABLE — выключить nginx + docker -# ══════════════════════════════════════════════════════════════════════════════ - -phase_3_partial_disable() { - phase_header 3 "PARTIAL_DISABLE — выключить nginx + docker" - - local run_id - run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) - - write_tfvars "true" "false" "false" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' - - if tf_apply; then - pass "3.1 apply с partial disable завершился" - else - fail "3.1 apply с partial disable упал" - restore_tfvars - phase_result "PARTIAL_DISABLE" "FAIL" - return 1 - fi - - # Проверить что nginx и docker job пропали из state - local state_list - state_list=$(terraform state list 2>/dev/null) - - if echo "$state_list" | grep -q 'install_nginx'; then - fail "3.2 install_nginx всё ещё в state" - else - pass "3.2 install_nginx убран из state" - fi - - if echo "$state_list" | grep -q 'install_docker'; then - fail "3.3 install_docker всё ещё в state" - else - pass "3.3 install_docker убран из state" - fi - - if echo "$state_list" | grep -q 'install_packages'; then - pass "3.4 install_packages остался в state" - else - fail "3.4 install_packages пропал из state" - fi - - # Проверить outputs - local nginx_out docker_out - nginx_out=$(terraform output -raw install_nginx_result 2>/dev/null) - docker_out=$(terraform output -raw install_docker_result 2>/dev/null) - - if [[ "$nginx_out" == "skipped" ]]; then - pass "3.5 install_nginx_result = skipped" - else - fail "3.5 install_nginx_result = '$nginx_out' (ожидали skipped)" - fi - - if [[ "$docker_out" == "skipped" ]]; then - pass "3.6 install_docker_result = skipped" - else - fail "3.6 install_docker_result = '$docker_out' (ожидали skipped)" - fi - - phase_result "PARTIAL_DISABLE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 4: PARTIAL_ENABLE — включить обратно всё -# ══════════════════════════════════════════════════════════════════════════════ - -phase_4_partial_enable() { - phase_header 4 "PARTIAL_ENABLE — включить обратно всё" - - local run_id - run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) - run_id=$((run_id + 1)) - - write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' - - if tf_apply; then - pass "4.1 apply с полным набором завершился" - else - fail "4.1 apply с полным набором упал" - phase_result "PARTIAL_ENABLE" "FAIL" - return 1 - fi - - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "4.2 state содержит $count ресурсов (ожидали ≥5)" - else - fail "4.2 state содержит $count ресурсов (ожидали ≥5)" - fi - - # Обновить бэкап - backup_tfvars - - phase_result "PARTIAL_ENABLE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 5: REORDER_PACKAGES — изменить порядок и состав base_packages -# ══════════════════════════════════════════════════════════════════════════════ - -phase_5_reorder() { - phase_header 5 "REORDER_PACKAGES — изменить порядок и состав пакетов" - - local run_id - run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) - run_id=$((run_id + 1)) - - # Другой порядок и сокращённый набор - write_tfvars "true" "true" "true" "$run_id" '["htop", "jq"]' - - if tf_apply; then - pass "5.1 apply с изменённым набором пакетов завершился" - else - fail "5.1 apply с изменённым набором пакетов упал" - restore_tfvars - phase_result "REORDER_PACKAGES" "FAIL" - return 1 - fi - - # Проверить output - local pkg_out - pkg_out=$(terraform output -raw install_packages_result 2>/dev/null) - if echo "$pkg_out" | grep -q '"status"'; then - pass "5.2 install_packages вернул JSON с status" - else - fail "5.2 install_packages output неожиданный: $pkg_out" - fi - - # Вернуть полный набор - run_id=$((run_id + 1)) - write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' - tf_apply || warn "5.3 восстановление полного набора не удалось" - - backup_tfvars - phase_result "REORDER_PACKAGES" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 6: MANUAL_PURGE — удалить пакеты с VM вручную, заново установить -# ══════════════════════════════════════════════════════════════════════════════ - -phase_6_manual_purge() { - phase_header 6 "MANUAL_PURGE — удалить пакеты с VM, заново установить" - - local ip - ip=$(get_vm_ip) - if [[ -z "$ip" ]]; then - fail "6.0 не удалось получить IP VM" - phase_result "MANUAL_PURGE" "FAIL" - return 1 - fi - - # Удалить всё с VM - vm_purge_all "$ip" - - # Проверить что пакеты действительно удалены - if vm_check_binary "$ip" "docker"; then - fail "6.1 docker всё ещё на VM после purge" - else - pass "6.1 docker удалён с VM" - fi - - if vm_check_binary "$ip" "nginx"; then - fail "6.2 nginx всё ещё на VM после purge" - else - pass "6.2 nginx удалён с VM" - fi - - if vm_check_binary "$ip" "jq"; then - fail "6.3 jq всё ещё на VM после purge" - else - pass "6.3 jq удалён с VM" - fi - - # Пересоздать jobs (bump run_id) - local run_id - run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) - run_id=$((run_id + 1)) - - write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' - - info "запускаю переустановку через sless_job..." - if tf_apply; then - pass "6.4 apply после purge завершился" - else - fail "6.4 apply после purge упал" - phase_result "MANUAL_PURGE" "FAIL" - return 1 - fi - - # Подождать чуть-чуть и проверить что пакеты вернулись - sleep 5 - - if vm_check_binary "$ip" "docker"; then - pass "6.5 docker установлен заново" - else - fail "6.5 docker НЕ установлен после re-apply" - fi - - if vm_check_binary "$ip" "nginx"; then - pass "6.6 nginx установлен заново" - else - fail "6.6 nginx НЕ установлен после re-apply" - fi - - if vm_check_binary "$ip" "jq"; then - pass "6.7 jq установлен заново" - else - fail "6.7 jq НЕ установлен после re-apply" - fi - - backup_tfvars - phase_result "MANUAL_PURGE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 7: DESTROY — terraform destroy, VM уходит в suspend -# ══════════════════════════════════════════════════════════════════════════════ - -phase_7_destroy() { - phase_header 7 "DESTROY — terraform destroy → VM в suspend" - - local ip - ip=$(get_vm_ip) - - if tf_destroy; then - pass "7.1 terraform destroy завершился" - else - fail "7.1 terraform destroy упал" - phase_result "DESTROY" "FAIL" - return 1 - fi - - # State должен быть пуст - local count - count=$(tf_state_count) - if [[ $count -eq 0 ]]; then - pass "7.2 state пуст ($count ресурсов)" - else - fail "7.2 state не пуст ($count ресурсов)" - fi - - # VM не должна отвечать по SSH - if [[ -n "$ip" ]]; then - info "проверяю что VM $ip недоступна..." - if vm_alive "$ip"; then - fail "7.3 VM $ip всё ещё отвечает по SSH после destroy" - else - pass "7.3 VM $ip не отвечает по SSH (suspend подтверждён)" - fi - else - skip "7.3 IP VM неизвестен, пропускаю SSH-проверку" - fi - - phase_result "DESTROY" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 8: RESURRECT — apply после destroy, VM просыпается -# ══════════════════════════════════════════════════════════════════════════════ - -phase_8_resurrect() { - phase_header 8 "RESURRECT — apply после destroy" - - if tf_apply; then - pass "8.1 apply после destroy завершился" - else - fail "8.1 apply после destroy упал" - phase_result "RESURRECT" "FAIL" - return 1 - fi - - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "8.2 state содержит $count ресурсов" - else - fail "8.2 state содержит $count ресурсов (ожидали ≥5)" - fi - - # Проверить VM - local ip - ip=$(get_vm_ip) - if [[ -n "$ip" ]] && vm_alive "$ip"; then - pass "8.3 VM $ip доступна по SSH после resurrect" - else - # VM может ещё просыпаться — ждём - info "VM не отвечает, жду до 120s..." - if vm_wait_alive "$ip" 120; then - pass "8.3 VM $ip доступна по SSH после ожидания" - else - fail "8.3 VM $ip не доступна по SSH через 120s" - fi - fi - - # Проверить пакеты - if [[ -n "$ip" ]] && vm_alive "$ip"; then - if vm_check_binary "$ip" "jq"; then - pass "8.4 jq установлен после resurrect" - else - fail "8.4 jq НЕ установлен после resurrect" - fi - - if vm_check_binary "$ip" "nginx"; then - pass "8.5 nginx установлен после resurrect" - else - fail "8.5 nginx НЕ установлен после resurrect" - fi - - if vm_check_binary "$ip" "docker"; then - pass "8.6 docker установлен после resurrect" - else - fail "8.6 docker НЕ установлен после resurrect" - fi - fi - - backup_tfvars - phase_result "RESURRECT" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 9: STRESS_CYCLES — N destroy/apply циклов подряд -# ══════════════════════════════════════════════════════════════════════════════ - -phase_9_stress() { - phase_header 9 "STRESS_CYCLES — $STRESS_CYCLES циклов destroy/apply" - - local i - for i in $(seq 1 "$STRESS_CYCLES"); do - info "── цикл $i/$STRESS_CYCLES ──" - - info "[$i] destroy..." - if tf_destroy; then - pass "9.${i}a destroy цикл $i" - else - fail "9.${i}a destroy цикл $i упал" - # Попробовать восстановиться - tf_apply || true - continue - fi - - info "[$i] apply..." - if tf_apply; then - pass "9.${i}b apply цикл $i" - else - fail "9.${i}b apply цикл $i упал" - continue - fi - done - - phase_result "STRESS_CYCLES" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 10: FINAL_SANITY — финальная проверка состояния -# ══════════════════════════════════════════════════════════════════════════════ - -phase_10_final() { - phase_header 10 "FINAL_SANITY — финальная проверка" - - # Убедиться что state на месте - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "10.1 state содержит $count ресурсов" - else - fail "10.1 state содержит $count ресурсов (ожидали ≥5)" - # Попробовать восстановить - ensure_baseline - fi - - # Plan = no changes - if tf_plan_no_changes; then - pass "10.2 terraform plan → No changes" - else - fail "10.2 terraform plan показывает изменения" - fi - - # VM доступна - local ip - ip=$(get_vm_ip) - if [[ -n "$ip" ]] && vm_alive "$ip"; then - pass "10.3 VM $ip доступна по SSH" - - # Полная проверка пакетов - local all_ok=true - for pkg in jq htop unzip; do - if vm_check_package "$ip" "$pkg"; then - pass "10.4 пакет $pkg установлен" - else - fail "10.4 пакет $pkg НЕ установлен" - all_ok=false - fi - done - - if vm_check_binary "$ip" "nginx"; then - pass "10.5 nginx работает" - else - fail "10.5 nginx НЕ найден" - fi - - if vm_check_binary "$ip" "docker"; then - pass "10.6 docker работает" - else - fail "10.6 docker НЕ найден" - fi - else - fail "10.3 VM не доступна по SSH" - fi - - phase_result "FINAL_SANITY" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# MAIN -# ══════════════════════════════════════════════════════════════════════════════ - -echo -e "${BOLD}${CYAN}" -echo "╔═══════════════════════════════════════════════════════════════╗" -echo "║ VM STRESS TEST — examples/VM ║" -echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║" -echo "║ Циклов stress: $STRESS_CYCLES ║" -echo "╚═══════════════════════════════════════════════════════════════╝" -echo -e "${RESET}" - -# Проверить что мы в правильной директории -if [[ ! -f "$TFVARS" ]]; then - echo -e "${RED}ОШИБКА: $TFVARS не найден. Запускайте из examples/VM/${RESET}" - exit 1 -fi - -if [[ ! -f "$VM_KEY" ]]; then - echo -e "${RED}ОШИБКА: $VM_KEY не найден${RESET}" - exit 1 -fi - -# Запуск фаз -phase_1_baseline -phase_2_idempotent -phase_3_partial_disable -phase_4_partial_enable -phase_5_reorder -phase_6_manual_purge - -if [[ "$SKIP_DESTROY" == "1" ]]; then - skip "фазы 7-9 пропущены (SKIP_DESTROY=1)" - PHASE_RESULTS+=("DESTROY:SKIP" "RESURRECT:SKIP" "STRESS_CYCLES:SKIP") -else - phase_7_destroy - phase_8_resurrect - phase_9_stress -fi - -phase_10_final - -# ── ИТОГОВАЯ СВОДКА ────────────────────────────────────────────────────────── - -ELAPSED=$((SECONDS - START_TIME)) -ELAPSED_MIN=$((ELAPSED / 60)) -ELAPSED_SEC=$((ELAPSED % 60)) - -echo "" -echo -e "${BOLD}${CYAN}╔═══════════════════════════════════════════════════════════════╗${RESET}" -echo -e "${BOLD}${CYAN}║ ИТОГОВАЯ СВОДКА ║${RESET}" -echo -e "${BOLD}${CYAN}╠═══════════════════════════════════════════════════════════════╣${RESET}" -echo -e "${BOLD} Время: ${ELAPSED_MIN}m ${ELAPSED_SEC}s${RESET}" -echo -e "${BOLD} ${GREEN}PASS: $PASS${RESET} ${RED}FAIL: $FAIL${RESET} ${YELLOW}SKIP: $SKIP${RESET}" -echo "" -echo -e "${BOLD} Фазы:${RESET}" -for pr in "${PHASE_RESULTS[@]}"; do - name="${pr%%:*}" - result="${pr##*:}" - case "$result" in - PASS) echo -e " ${GREEN}✓${RESET} $name" ;; - FAIL) echo -e " ${RED}✗${RESET} $name" ;; - SKIP) echo -e " ${YELLOW}○${RESET} $name" ;; - esac -done -echo -e "${BOLD}${CYAN}╚═══════════════════════════════════════════════════════════════╝${RESET}" - -# Восстановить оригинальный tfvars -restore_tfvars - -# Exit code: 0 если все PASS, 1 если есть FAIL -if [[ $FAIL -gt 0 ]]; then - echo -e "\n${RED}РЕЗУЛЬТАТ: FAIL ($FAIL ошибок)${RESET}" - exit 1 -else - echo -e "\n${GREEN}РЕЗУЛЬТАТ: ALL PASS${RESET}" - exit 0 -fi diff --git a/VM/VM_TEST_README.md b/VM/VM_TEST_README.md deleted file mode 100644 index ab9c7e4..0000000 --- a/VM/VM_TEST_README.md +++ /dev/null @@ -1,106 +0,0 @@ -# VM Stress Test — Инструкция по запуску -# 2026-03-30 - -## ⛔⛔⛔ КРИТИЧЕСКИЕ ПРАВИЛА ⛔⛔⛔ - -### ЗАПРЕЩЕНО (без исключений): -- **НЕ РЕДАКТИРОВАТЬ** `terraform.tfvars` — там JWT-токен, потеря = катастрофа -- **НЕ РЕДАКТИРОВАТЬ** `*.tf` файлы -- **НЕ РЕДАКТИРОВАТЬ** `vm_stress_test.sh` -- **НЕ ЗАПУСКАТЬ** `terraform` напрямую — только через скрипт -- **НЕ СОЗДАВАТЬ** новые файлы в этой директории -- **НЕ ДЕЛАТЬ** `sed`, `awk`, `cat >`, `tee` в terraform.tfvars - -### ПОЧЕМУ: -Предыдущая версия скрипта содержала функцию `write_tfvars()` которая -перезаписывала `terraform.tfvars`. В процессе перезаписи был потерян -JWT-токен `api_token` (1200+ символов). Это привело к полному отказу -terraform и потере рабочего состояния. Восстановление заняло час. - -### КАК РАБОТАЕТ НОВЫЙ СКРИПТ: -Переменные переопределяются через `-var` в terraform CLI. -Файл `terraform.tfvars` читается terraform автоматически, -но **НИКОГДА не перезаписывается** скриптом. - -После каждой фазы проверяется md5sum terraform.tfvars. -Если файл изменился — **АВАРИЙНАЯ ОСТАНОВКА** (exit code 99). - ---- - -## Запуск - -### На VM (naeel@5.172.178.213): - -```bash -cd ~/terra/sless/examples/VM -bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log -``` - -### Быстрый прогон (без destroy/resurrect — фазы 7-9 пропускаются): - -```bash -SKIP_DESTROY=1 bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log -``` - -### Количество stress-циклов (default: 2): - -```bash -STRESS_CYCLES=3 bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log -``` - ---- - -## Анализ результатов - -### Быстрый обзор: -```bash -grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log -``` - -### Только ошибки: -```bash -grep '\[FAIL\]' /tmp/vm_stress.log -``` - -### Итоговая сводка — последние 20 строк лога: -```bash -tail -20 /tmp/vm_stress.log -``` - ---- - -## Фазы теста - -| # | Имя | Что делает | -|---|-----------------|---------------------------------------------------| -| 1 | BASELINE | apply с полным набором (packages+nginx+docker) | -| 2 | IDEMPOTENT | plan → "No changes" (проверка идемпотентности) | -| 3 | PARTIAL_DISABLE | отключить nginx + docker через -var | -| 4 | PARTIAL_ENABLE | включить обратно nginx + docker | -| 5 | REORDER_PACKAGES| изменить набор base_packages через -var | -| 6 | MANUAL_PURGE | удалить пакеты с VM по SSH → переустановить | -| 7 | DESTROY | terraform destroy → VM в suspend | -| 8 | RESURRECT | apply после destroy → VM просыпается | -| 9 | STRESS_CYCLES | N циклов destroy/apply подряд | -|10 | FINAL_SANITY | финальная проверка VM + пакеты + plan | - ---- - -## Текущее состояние (baseline) - -5 ресурсов в state: -- `nubes_vapp.vapp` -- `nubes_vc_vm_v3.vm` -- `sless_job.install_packages[0]` -- `sless_job.install_nginx[0]` -- `sless_job.install_docker[0]` - ---- - -## Exit codes - -| Code | Значение | -|------|---------------------------------------------| -| 0 | Все тесты PASS | -| 1 | Есть FAIL (см. лог) | -| 99 | terraform.tfvars был изменён — АВАРИЙНЫЙ СТОП | diff --git a/VM/vm_key b/VM/vm_key deleted file mode 100644 index b754d1e..0000000 --- a/VM/vm_key +++ /dev/null @@ -1,7 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACCkAfisnr59B/EEnX5umaQlPY7bJAcVEvQVdPotfzNOAAAAAJBioDTmYqA0 -5gAAAAtzc2gtZWQyNTUxOQAAACCkAfisnr59B/EEnX5umaQlPY7bJAcVEvQVdPotfzNOAA -AAAEC4nFg/UaIitvoJKhJsrroOHWgmmkfHYQRyvEzqGe+AwaQB+Kyevn0H8QSdfm6ZpCU9 -jtskBxUS9BV0+i1/M04AAAAADXNsZXNzLWRlbW8tdm0= ------END OPENSSH PRIVATE KEY----- diff --git a/VM/vm_key.pub b/VM/vm_key.pub deleted file mode 100644 index 8654b4e..0000000 --- a/VM/vm_key.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQB+Kyevn0H8QSdfm6ZpCU9jtskBxUS9BV0+i1/M04A sless-demo-vm diff --git a/VM/vm_stress_test.sh b/VM/vm_stress_test.sh deleted file mode 100755 index 5c0a37d..0000000 --- a/VM/vm_stress_test.sh +++ /dev/null @@ -1,931 +0,0 @@ -#!/usr/bin/env bash -# 2026-03-30 — vm_stress_test.sh (v2 — READ-ONLY) -# Автономный READ-ONLY stress/chaos тест для examples/VM. -# -# ⛔⛔⛔ КРИТИЧЕСКОЕ ПРАВИЛО — ДЛЯ AI-АГЕНТОВ И ЛЮДЕЙ ⛔⛔⛔ -# -# Этот скрипт НИКОГДА НЕ МОДИФИЦИРУЕТ terraform.tfvars и НИКАКИЕ ДРУГИЕ ФАЙЛЫ. -# Все переопределения переменных — ТОЛЬКО через terraform CLI опцию -var. -# Файл terraform.tfvars ЧИТАЕТСЯ, но НИКОГДА НЕ ПЕРЕЗАПИСЫВАЕТСЯ. -# -# ЗАПРЕЩЕНО: -# - Редактировать этот скрипт -# - Редактировать terraform.tfvars -# - Редактировать любые .tf файлы -# - Запускать terraform напрямую — только через этот скрипт -# -# ПРИЧИНА: предыдущая версия скрипта уничтожила terraform.tfvars -# через write_tfvars() — потерян JWT-токен api_token. Это НЕДОПУСТИМО. -# -# ФАЗЫ: -# 1 BASELINE — apply с текущим tfvars (packages + nginx + docker) -# 2 IDEMPOTENT — повторный plan → "No changes" -# 3 PARTIAL_DISABLE — apply с -var install_nginx=false -var install_docker=false -# 4 PARTIAL_ENABLE — apply с -var install_nginx=true -var install_docker=true (run_id+1) -# 5 REORDER_PACKAGES — apply с -var 'base_packages=["htop","jq"]' (run_id+1) -# 6 MANUAL_PURGE — удалить пакеты с VM по SSH → apply (run_id+1) -# 7 DESTROY — terraform destroy → VM уходит в suspend -# 8 RESURRECT — apply после destroy → VM просыпается -# 9 STRESS_CYCLES — N подряд destroy/apply циклов -# 10 FINAL_SANITY — проверить доступность VM и пакеты -# -# ЗАПУСК: -# cd ~/terra/sless/examples/VM -# bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log -# -# ПАРАМЕТРЫ (env): -# STRESS_CYCLES=2 — количество destroy/apply циклов в фазе 9 (default: 2) -# SKIP_DESTROY=1 — пропустить фазы 7-9 (для быстрого прогона) -# -# АНАЛИЗ РЕЗУЛЬТАТОВ: -# grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log -# Итоговая сводка печатается в конце лога. -# -# ТРЕБОВАНИЯ: terraform, ssh, python3 — всё на VM naeel@5.172.178.213 - -set -uo pipefail - -# ── CONFIG ──────────────────────────────────────────────────────────────────── - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -VM_KEY="$SCRIPT_DIR/vm_key" -STRESS_CYCLES="${STRESS_CYCLES:-2}" -SKIP_DESTROY="${SKIP_DESTROY:-0}" - -# Читаем текущий run_id из terraform.tfvars (ТОЛЬКО ЧТЕНИЕ, не запись) -RUN_ID=$(grep 'install_run_id' terraform.tfvars | grep -oP '\d+' | head -1) - -# ── СТАТИСТИКА ──────────────────────────────────────────────────────────────── - -PASS=0; FAIL=0; SKIP=0 -PHASE_RESULTS=() -START_TIME=$SECONDS - -# ── ЦВЕТА ───────────────────────────────────────────────────────────────────── - -GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' -CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' - -# ── HELPERS ─────────────────────────────────────────────────────────────────── - -pass() { echo -e " ${GREEN}[PASS]${RESET} $1"; ((PASS++)); } -fail() { echo -e " ${RED}[FAIL]${RESET} $1"; ((FAIL++)); } -skip() { echo -e " ${YELLOW}[SKIP]${RESET} $1"; ((SKIP++)); } -info() { echo -e " ${CYAN}[INFO]${RESET} $1"; } -warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; } - -phase_header() { - local num="$1" name="$2" - echo "" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo -e "${BOLD}${CYAN} ФАЗА $num: $name${RESET}" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo -e " ${CYAN}[TIME]${RESET} $(date '+%H:%M:%S')" -} - -phase_result() { - local name="$1" result="$2" - PHASE_RESULTS+=("$name:$result") - echo -e " ${CYAN}[PHASE]${RESET} $name → ${result}" -} - -# ── TERRAFORM HELPERS ───────────────────────────────────────────────────────── -# ⛔ НЕ ТРОГАЕМ terraform.tfvars. Переопределения — ТОЛЬКО через -var. -# terraform.tfvars подхватывается автоматически (лежит в рабочей директории). -# Доп. -var аргументы передаются во все tf_* функции и ПЕРЕОПРЕДЕЛЯЮТ tfvars. - -# tf_apply: apply с retry при сетевых ошибках. -# Аргументы: любые доп. -var (опционально). -# Пример: tf_apply -var install_nginx=false -var install_docker=false -tf_apply() { - local attempt=1 max=3 - while [[ $attempt -le $max ]]; do - info "terraform apply (попытка $attempt/$max)..." - if terraform apply -auto-approve -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_apply.log; then - return 0 - fi - if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_apply.log && [[ $attempt -lt $max ]]; then - warn "сетевой сбой, retry через $((attempt * 5))s..." - sleep $((attempt * 5)) - ((attempt++)) - continue - fi - return 1 - done - return 1 -} - -# tf_destroy: destroy с retry. -# Аргументы: любые доп. -var (опционально). -tf_destroy() { - local attempt=1 max=3 - while [[ $attempt -le $max ]]; do - info "terraform destroy (попытка $attempt/$max)..." - if terraform destroy -auto-approve -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_destroy.log; then - return 0 - fi - if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_destroy.log && [[ $attempt -lt $max ]]; then - warn "сетевой сбой, retry через $((attempt * 5))s..." - sleep $((attempt * 5)) - ((attempt++)) - continue - fi - return 1 - done - return 1 -} - -# tf_plan_no_changes: проверить plan → "No changes". -# Аргументы: любые доп. -var (опционально). -tf_plan_no_changes() { - terraform plan -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_plan.log - grep -q 'No changes' /tmp/vm_tf_plan.log -} - -# tf_state_count: количество ресурсов в state. -tf_state_count() { - terraform state list 2>/dev/null | wc -l -} - -# next_run_id: инкрементировать внутренний счётчик RUN_ID и вернуть новое значение. -# Используется для -var install_run_id=N чтобы форсировать пересоздание jobs. -next_run_id() { - RUN_ID=$((RUN_ID + 1)) - echo "$RUN_ID" -} - -# ── VM SSH HELPERS ──────────────────────────────────────────────────────────── - -# get_vm_ip: получить внешний IP VM из terraform output. -get_vm_ip() { - terraform output -json vm_state 2>/dev/null \ - | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('externalConnect',''))" 2>/dev/null -} - -# vm_ssh: выполнить команду на VM. Возвращает exit code команды. -vm_ssh() { - local ip="$1"; shift - ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ - -o ServerAliveInterval=5 -o ServerAliveCountMax=3 \ - "ubuntu@$ip" "$@" -} - -# vm_alive: проверить доступность VM по SSH. -vm_alive() { - local ip="$1" - vm_ssh "$ip" 'echo alive' &>/dev/null -} - -# vm_wait_alive: ждать пока VM ответит по SSH (до timeout_sec). -vm_wait_alive() { - local ip="$1" timeout_sec="${2:-120}" - local deadline=$((SECONDS + timeout_sec)) - while [[ $SECONDS -lt $deadline ]]; do - if vm_alive "$ip"; then return 0; fi - sleep 10 - done - return 1 -} - -# vm_check_package: проверить что пакет установлен. -vm_check_package() { - local ip="$1" pkg="$2" - vm_ssh "$ip" "dpkg -l $pkg 2>/dev/null | grep -q '^ii'" 2>/dev/null -} - -# vm_check_binary: проверить что бинарник доступен. -vm_check_binary() { - local ip="$1" bin="$2" - vm_ssh "$ip" "command -v $bin" &>/dev/null -} - -# vm_wait_binary: ждать пока бинарник появится на VM (sless_job работает асинхронно). -# Нужен потому что sless_job запускает kubernetes Job, который сначала собирает образ, -# затем стартует pod, и только потом SSH-устанавливает пакеты на VM — это занимает 1-3 мин. -vm_wait_binary() { - local ip="$1" bin="$2" timeout_sec="${3:-180}" - local deadline=$((SECONDS + timeout_sec)) - info "жду появления '$bin' на VM (до ${timeout_sec}s)..." - while [[ $SECONDS -lt $deadline ]]; do - if vm_check_binary "$ip" "$bin"; then return 0; fi - sleep 15 - done - return 1 -} - -# vm_purge_all: удалить все установленные пакеты с VM. -vm_purge_all() { - local ip="$1" - info "удаляю пакеты с VM $ip..." - vm_ssh "$ip" 'sudo systemctl stop nginx docker 2>/dev/null; sudo apt-get purge -y jq python3-pip htop unzip nginx docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null; sudo apt-get autoremove -y 2>/dev/null; sudo rm -rf /var/lib/docker /var/lib/containerd; echo purge_done' 2>/dev/null -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 1: BASELINE — apply с текущим tfvars (всё включено) -# Используем -var install_run_id=N чтобы гарантировать свежий запуск. -# terraform.tfvars НЕ ТРОГАЕМ — берём как есть. -# ══════════════════════════════════════════════════════════════════════════════ - -phase_1_baseline() { - phase_header 1 "BASELINE — apply с полным набором" - - local rid - rid=$(next_run_id) - - # Все флаги = true (как в tfvars), но run_id инкрементирован - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid"; then - pass "1.1 terraform apply завершился успешно" - else - fail "1.1 terraform apply упал" - phase_result "BASELINE" "FAIL" - return 1 - fi - - # Проверить количество ресурсов - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "1.2 state содержит $count ресурсов (ожидали ≥5)" - else - fail "1.2 state содержит $count ресурсов (ожидали ≥5)" - fi - - # Проверить outputs - local out - out=$(terraform output -json 2>/dev/null) - - if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'ok' in d['install_packages_result']['value'] or 'already' in d['install_packages_result']['value']" 2>/dev/null; then - pass "1.3 install_packages_result содержит ok/already_installed" - else - fail "1.3 install_packages_result не содержит ok" - fi - - if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_nginx_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then - pass "1.4 install_nginx_result содержит ok/already" - else - fail "1.4 install_nginx_result не содержит ok" - fi - - if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_docker_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then - pass "1.5 install_docker_result содержит ok/already" - else - fail "1.5 install_docker_result не содержит ok" - fi - - # Проверить VM по SSH - local ip - ip=$(get_vm_ip) - if [[ -n "$ip" ]] && vm_alive "$ip"; then - pass "1.6 VM $ip доступна по SSH" - else - fail "1.6 VM не доступна по SSH (ip=$ip)" - fi - - phase_result "BASELINE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 2: IDEMPOTENT — повторный apply с теми же аргументами → 0 changed -# Используем apply (не plan) — это надёжнее: некоторые sless-провайдеры показывают -# ложный drift в plan, но apply при этом возвращает 0 changed. Apply — canonical way. -# ══════════════════════════════════════════════════════════════════════════════ - -phase_2_idempotent() { - phase_header 2 "IDEMPOTENT — повторный apply без изменений" - - # Тот же run_id что применялся в фазе 1 → apply должен вернуть 0 changed - # sless_job — эфемерный ресурс, каждый apply пересоздаёт job-ресурсы (add+destroy). - # Идемпотентность = "0 changed" (ноль in-place изменений), а не "0 add/destroy". - # VM и vApp не должны изменяться никогда. - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$RUN_ID"; then - if grep -q ', 0 changed,' /tmp/vm_tf_apply.log; then - pass "2.1 повторный apply → 0 changed (sless_job пересоздан — ожидаемо)" - grep -E 'Resources:' /tmp/vm_tf_apply.log | tail -1 | while read -r line; do - info " $line" - done - else - fail "2.1 повторный apply изменил persistent ресурсы (ожидали 0 changed)" - grep -E 'added|changed|destroyed' /tmp/vm_tf_apply.log | tail -3 | while read -r line; do - info " $line" - done - fi - else - fail "2.1 повторный apply упал" - fi - - phase_result "IDEMPOTENT" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 3: PARTIAL_DISABLE — выключить nginx + docker -# Используем -var install_nginx=false -var install_docker=false -# terraform.tfvars по-прежнему НЕ ТРОГАЕМ. -# ══════════════════════════════════════════════════════════════════════════════ - -phase_3_partial_disable() { - phase_header 3 "PARTIAL_DISABLE — выключить nginx + docker" - - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=false" \ - -var "install_docker=false" \ - -var "install_run_id=$RUN_ID"; then - pass "3.1 apply с partial disable завершился" - else - fail "3.1 apply с partial disable упал" - phase_result "PARTIAL_DISABLE" "FAIL" - return 1 - fi - - # Проверить что nginx и docker job пропали из state - local state_list - state_list=$(terraform state list 2>/dev/null) - - if echo "$state_list" | grep -q 'install_nginx'; then - fail "3.2 install_nginx всё ещё в state" - else - pass "3.2 install_nginx убран из state" - fi - - if echo "$state_list" | grep -q 'install_docker'; then - fail "3.3 install_docker всё ещё в state" - else - pass "3.3 install_docker убран из state" - fi - - if echo "$state_list" | grep -q 'install_packages'; then - pass "3.4 install_packages остался в state" - else - fail "3.4 install_packages пропал из state" - fi - - # Проверить outputs - local nginx_out docker_out - nginx_out=$(terraform output -raw install_nginx_result 2>/dev/null) - docker_out=$(terraform output -raw install_docker_result 2>/dev/null) - - if [[ "$nginx_out" == "skipped" ]]; then - pass "3.5 install_nginx_result = skipped" - else - fail "3.5 install_nginx_result = '$nginx_out' (ожидали skipped)" - fi - - if [[ "$docker_out" == "skipped" ]]; then - pass "3.6 install_docker_result = skipped" - else - fail "3.6 install_docker_result = '$docker_out' (ожидали skipped)" - fi - - phase_result "PARTIAL_DISABLE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 4: PARTIAL_ENABLE — включить обратно всё -# Инкрементируем run_id чтобы jobs пересоздались. -# ══════════════════════════════════════════════════════════════════════════════ - -phase_4_partial_enable() { - phase_header 4 "PARTIAL_ENABLE — включить обратно всё" - - local rid - rid=$(next_run_id) - - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid"; then - pass "4.1 apply с полным набором завершился" - else - fail "4.1 apply с полным набором упал" - phase_result "PARTIAL_ENABLE" "FAIL" - return 1 - fi - - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "4.2 state содержит $count ресурсов (ожидали ≥5)" - else - fail "4.2 state содержит $count ресурсов (ожидали ≥5)" - fi - - phase_result "PARTIAL_ENABLE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 5: REORDER_PACKAGES — изменить порядок и состав base_packages -# Через -var 'base_packages=["htop","jq"]' — terraform.tfvars не трогаем. -# ══════════════════════════════════════════════════════════════════════════════ - -phase_5_reorder() { - phase_header 5 "REORDER_PACKAGES — изменить порядок и состав пакетов" - - local rid - rid=$(next_run_id) - - # Сокращённый набор пакетов через -var - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid" \ - -var 'base_packages=["htop","jq"]'; then - pass "5.1 apply с изменённым набором пакетов завершился" - else - fail "5.1 apply с изменённым набором пакетов упал" - phase_result "REORDER_PACKAGES" "FAIL" - return 1 - fi - - # Проверить output - local pkg_out - pkg_out=$(terraform output -raw install_packages_result 2>/dev/null) - if echo "$pkg_out" | grep -qE '"status"|ok|already'; then - pass "5.2 install_packages вернул ожидаемый результат" - else - fail "5.2 install_packages output неожиданный: $pkg_out" - fi - - # Вернуть полный набор пакетов - rid=$(next_run_id) - tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid" \ - || warn "5.3 восстановление полного набора не удалось" - - phase_result "REORDER_PACKAGES" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 6: MANUAL_PURGE — удалить пакеты с VM вручную, заново установить -# ══════════════════════════════════════════════════════════════════════════════ - -phase_6_manual_purge() { - phase_header 6 "MANUAL_PURGE — удалить пакеты с VM, заново установить" - - local ip - ip=$(get_vm_ip) - if [[ -z "$ip" ]]; then - fail "6.0 не удалось получить IP VM" - phase_result "MANUAL_PURGE" "FAIL" - return 1 - fi - - # Удалить всё с VM - vm_purge_all "$ip" - - # Проверить что пакеты действительно удалены - if vm_check_binary "$ip" "docker"; then - fail "6.1 docker всё ещё на VM после purge" - else - pass "6.1 docker удалён с VM" - fi - - if vm_check_binary "$ip" "nginx"; then - fail "6.2 nginx всё ещё на VM после purge" - else - pass "6.2 nginx удалён с VM" - fi - - if vm_check_binary "$ip" "jq"; then - fail "6.3 jq всё ещё на VM после purge" - else - pass "6.3 jq удалён с VM" - fi - - # Пересоздать jobs (bump run_id) - local rid - rid=$(next_run_id) - - info "запускаю переустановку через sless_job..." - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid"; then - pass "6.4 apply после purge завершился" - else - fail "6.4 apply после purge упал" - phase_result "MANUAL_PURGE" "FAIL" - return 1 - fi - - # Ждать пока sless_job отработает: docker самый долгий (k8s Job + образ + apt-install ~200MB). - # docker-ce требует до 5 минут на первой установке — ставим 360s. - if vm_wait_binary "$ip" "docker" 360; then - pass "6.5 docker установлен заново" - else - fail "6.5 docker НЕ установлен после re-apply (таймаут 360s)" - fi - - if vm_wait_binary "$ip" "nginx" 240; then - pass "6.6 nginx установлен заново" - else - fail "6.6 nginx НЕ установлен после re-apply (таймаут 240s)" - fi - - if vm_check_binary "$ip" "jq"; then - pass "6.7 jq установлен заново" - else - fail "6.7 jq НЕ установлен после re-apply" - fi - - phase_result "MANUAL_PURGE" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 7: DESTROY — terraform destroy, VM уходит в suspend -# ══════════════════════════════════════════════════════════════════════════════ - -phase_7_destroy() { - phase_header 7 "DESTROY — terraform destroy → VM в suspend" - - local ip - ip=$(get_vm_ip) - - if tf_destroy; then - pass "7.1 terraform destroy завершился" - else - fail "7.1 terraform destroy упал" - phase_result "DESTROY" "FAIL" - return 1 - fi - - # State должен быть пуст - local count - count=$(tf_state_count) - if [[ $count -eq 0 ]]; then - pass "7.2 state пуст ($count ресурсов)" - else - fail "7.2 state не пуст ($count ресурсов)" - fi - - # VM не должна отвечать по SSH - if [[ -n "$ip" ]]; then - info "проверяю что VM $ip недоступна..." - if vm_alive "$ip"; then - fail "7.3 VM $ip всё ещё отвечает по SSH после destroy" - else - pass "7.3 VM $ip не отвечает по SSH (suspend подтверждён)" - fi - else - skip "7.3 IP VM неизвестен, пропускаю SSH-проверку" - fi - - phase_result "DESTROY" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 8: RESURRECT — apply после destroy, VM просыпается -# Используем текущий run_id — terraform.tfvars НЕ ТРОГАЕМ. -# ══════════════════════════════════════════════════════════════════════════════ - -phase_8_resurrect() { - phase_header 8 "RESURRECT — apply после destroy" - - local rid - rid=$(next_run_id) - - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid"; then - pass "8.1 apply после destroy завершился" - else - fail "8.1 apply после destroy упал" - phase_result "RESURRECT" "FAIL" - return 1 - fi - - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "8.2 state содержит $count ресурсов" - else - fail "8.2 state содержит $count ресурсов (ожидали ≥5)" - fi - - # Проверить VM - local ip - ip=$(get_vm_ip) - if [[ -n "$ip" ]] && vm_alive "$ip"; then - pass "8.3 VM $ip доступна по SSH после resurrect" - else - # VM может ещё просыпаться — ждём - info "VM не отвечает, жду до 120s..." - if vm_wait_alive "$ip" 120; then - pass "8.3 VM $ip доступна по SSH после ожидания" - else - fail "8.3 VM $ip не доступна по SSH через 120s" - fi - fi - - # Проверить пакеты - if [[ -n "$ip" ]] && vm_alive "$ip"; then - if vm_check_binary "$ip" "jq"; then - pass "8.4 jq установлен после resurrect" - else - fail "8.4 jq НЕ установлен после resurrect" - fi - - if vm_check_binary "$ip" "nginx"; then - pass "8.5 nginx установлен после resurrect" - else - fail "8.5 nginx НЕ установлен после resurrect" - fi - - if vm_check_binary "$ip" "docker"; then - pass "8.6 docker установлен после resurrect" - else - fail "8.6 docker НЕ установлен после resurrect" - fi - fi - - phase_result "RESURRECT" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 9: STRESS_CYCLES — N destroy/apply циклов подряд -# ══════════════════════════════════════════════════════════════════════════════ - -phase_9_stress() { - phase_header 9 "STRESS_CYCLES — $STRESS_CYCLES циклов destroy/apply" - - local i rid - for i in $(seq 1 "$STRESS_CYCLES"); do - info "── цикл $i/$STRESS_CYCLES ──" - - info "[$i] destroy..." - if tf_destroy; then - pass "9.${i}a destroy цикл $i" - else - fail "9.${i}a destroy цикл $i упал" - # Попробовать восстановиться - rid=$(next_run_id) - tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid" || true - continue - fi - - rid=$(next_run_id) - info "[$i] apply (run_id=$rid)..." - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid"; then - pass "9.${i}b apply цикл $i" - else - fail "9.${i}b apply цикл $i упал" - continue - fi - done - - phase_result "STRESS_CYCLES" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# ФАЗА 10: FINAL_SANITY — финальная проверка состояния -# ══════════════════════════════════════════════════════════════════════════════ - -phase_10_final() { - phase_header 10 "FINAL_SANITY — финальная проверка" - - # Убедиться что state на месте - local count - count=$(tf_state_count) - if [[ $count -ge 5 ]]; then - pass "10.1 state содержит $count ресурсов" - else - fail "10.1 state содержит $count ресурсов (ожидали ≥5)" - # Попробовать восстановить (через -var, БЕЗ изменения файлов) - local rid - rid=$(next_run_id) - info "пытаюсь восстановить baseline..." - tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$rid" || warn "восстановление не удалось" - fi - - # Idempotency финально: повторный apply → 0 changed. - # sless_job пересоздаются (add+destroy) — это нормально для job-ресурса. - # Проверяем только "0 changed" — VM и vApp не должны изменяться. - if tf_apply \ - -var "install_packages=true" \ - -var "install_nginx=true" \ - -var "install_docker=true" \ - -var "install_run_id=$RUN_ID"; then - if grep -q ', 0 changed,' /tmp/vm_tf_apply.log; then - pass "10.2 финальный apply → 0 changed (sless_job пересоздан — ожидаемо)" - grep -E 'Resources:' /tmp/vm_tf_apply.log | tail -1 | while read -r line; do - info " $line" - done - else - fail "10.2 финальный apply изменил persistent ресурсы (ожидали 0 changed)" - grep -E 'added|changed|destroyed' /tmp/vm_tf_apply.log | tail -3 | while read -r line; do - info " $line" - done - fi - else - fail "10.2 финальный apply упал" - fi - - # VM доступна - local ip - ip=$(get_vm_ip) - if [[ -n "$ip" ]] && vm_alive "$ip"; then - pass "10.3 VM $ip доступна по SSH" - - # Полная проверка пакетов - for pkg in jq htop unzip; do - if vm_check_package "$ip" "$pkg"; then - pass "10.4 пакет $pkg установлен" - else - fail "10.4 пакет $pkg НЕ установлен" - fi - done - - if vm_check_binary "$ip" "nginx"; then - pass "10.5 nginx работает" - else - fail "10.5 nginx НЕ найден" - fi - - if vm_check_binary "$ip" "docker"; then - pass "10.6 docker работает" - else - fail "10.6 docker НЕ найден" - fi - - # nginx service активен (не просто бинарник, а именно демон) - if vm_ssh "$ip" "systemctl is-active nginx 2>/dev/null" 2>/dev/null | grep -q '^active$'; then - pass "10.7 nginx service активен (systemctl)" - else - fail "10.7 nginx service не активен" - fi - - # docker daemon активен - if vm_ssh "$ip" "systemctl is-active docker 2>/dev/null" 2>/dev/null | grep -q '^active$'; then - pass "10.8 docker daemon активен (systemctl)" - else - fail "10.8 docker daemon не активен" - fi - - # HTTP probe: nginx отвечает на localhost:80 - local http_code - http_code=$(vm_ssh "$ip" "curl -sS -o /dev/null -w '%{http_code}' http://localhost 2>/dev/null" 2>/dev/null) - if [[ "$http_code" == "200" ]]; then - pass "10.9 nginx отвечает HTTP 200" - else - fail "10.9 nginx не отвечает HTTP 200 (code='$http_code')" - fi - - # python3 доступен - local py_ver - py_ver=$(vm_ssh "$ip" "python3 --version 2>&1" 2>/dev/null) - if echo "$py_ver" | grep -q 'Python 3'; then - pass "10.10 python3 доступен ($py_ver)" - else - fail "10.10 python3 недоступен" - fi - - # docker smoke: запустить контейнер и проверить вывод - if vm_ssh "$ip" "docker run --rm hello-world 2>&1 | grep -q 'Hello from Docker'" 2>/dev/null; then - pass "10.11 docker run hello-world → успешно" - else - fail "10.11 docker run hello-world → не прошёл" - fi - - # disk space: убедиться что на VM есть место (>500MB free) - local free_mb - free_mb=$(vm_ssh "$ip" "df -m / 2>/dev/null | awk 'NR==2{print \$4}'" 2>/dev/null) - if [[ -n "$free_mb" && "$free_mb" -gt 500 ]]; then - pass "10.12 свободное место на / = ${free_mb}MB (>500MB)" - else - fail "10.12 мало места на / = ${free_mb}MB (ожидали >500MB)" - fi - else - fail "10.3 VM не доступна по SSH" - fi - - phase_result "FINAL_SANITY" "PASS" -} - -# ══════════════════════════════════════════════════════════════════════════════ -# MAIN -# ══════════════════════════════════════════════════════════════════════════════ - -echo -e "${BOLD}${CYAN}" -echo "╔═══════════════════════════════════════════════════════════════╗" -echo "║ VM STRESS TEST v2 (READ-ONLY) — examples/VM ║" -echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║" -echo "║ Начальный run_id: $RUN_ID ║" -echo "║ Циклов stress: $STRESS_CYCLES ║" -echo "║ terraform.tfvars НЕ МОДИФИЦИРУЕТСЯ ║" -echo "╚═══════════════════════════════════════════════════════════════╝" -echo -e "${RESET}" - -# Проверить что мы в правильной директории -if [[ ! -f "terraform.tfvars" ]]; then - echo -e "${RED}ОШИБКА: terraform.tfvars не найден. Запускайте из examples/VM/${RESET}" - exit 1 -fi - -if [[ ! -f "$VM_KEY" ]]; then - echo -e "${RED}ОШИБКА: $VM_KEY не найден${RESET}" - exit 1 -fi - -# ⛔ ПРОВЕРКА ЦЕЛОСТНОСТИ: terraform.tfvars НЕ ДОЛЖЕН БЫТЬ МОДИФИЦИРОВАН -# Сохраняем md5 до запуска и проверяем после каждой фазы -TFVARS_MD5=$(md5sum terraform.tfvars | awk '{print $1}') -info "md5 terraform.tfvars = $TFVARS_MD5 (будет проверяться после каждой фазы)" - -check_tfvars_integrity() { - local current_md5 - current_md5=$(md5sum terraform.tfvars | awk '{print $1}') - if [[ "$current_md5" != "$TFVARS_MD5" ]]; then - echo -e "${RED}⛔⛔⛔ КРИТИЧЕСКАЯ ОШИБКА: terraform.tfvars был изменён! ⛔⛔⛔${RESET}" - echo -e "${RED}Ожидали md5: $TFVARS_MD5${RESET}" - echo -e "${RED}Текущий md5: $current_md5${RESET}" - echo -e "${RED}АВАРИЙНАЯ ОСТАНОВКА ТЕСТА${RESET}" - exit 99 - fi -} - -# Запуск фаз с проверкой целостности после каждой -phase_1_baseline; check_tfvars_integrity -phase_2_idempotent; check_tfvars_integrity -phase_3_partial_disable; check_tfvars_integrity -phase_4_partial_enable; check_tfvars_integrity -phase_5_reorder; check_tfvars_integrity -phase_6_manual_purge; check_tfvars_integrity - -if [[ "$SKIP_DESTROY" == "1" ]]; then - skip "фазы 7-9 пропущены (SKIP_DESTROY=1)" - PHASE_RESULTS+=("DESTROY:SKIP" "RESURRECT:SKIP" "STRESS_CYCLES:SKIP") -else - phase_7_destroy; check_tfvars_integrity - phase_8_resurrect; check_tfvars_integrity - phase_9_stress; check_tfvars_integrity -fi - -phase_10_final; check_tfvars_integrity - -# ── ИТОГОВАЯ СВОДКА ────────────────────────────────────────────────────────── - -ELAPSED=$((SECONDS - START_TIME)) -ELAPSED_MIN=$((ELAPSED / 60)) -ELAPSED_SEC=$((ELAPSED % 60)) - -echo "" -echo -e "${BOLD}${CYAN}╔═══════════════════════════════════════════════════════════════╗${RESET}" -echo -e "${BOLD}${CYAN}║ ИТОГОВАЯ СВОДКА ║${RESET}" -echo -e "${BOLD}${CYAN}╠═══════════════════════════════════════════════════════════════╣${RESET}" -echo -e "${BOLD} Время: ${ELAPSED_MIN}m ${ELAPSED_SEC}s${RESET}" -echo -e "${BOLD} ${GREEN}PASS: $PASS${RESET} ${RED}FAIL: $FAIL${RESET} ${YELLOW}SKIP: $SKIP${RESET}" -echo -e "${BOLD} Финальный run_id: $RUN_ID${RESET}" -echo "" -echo -e "${BOLD} Фазы:${RESET}" -for pr in "${PHASE_RESULTS[@]}"; do - name="${pr%%:*}" - result="${pr##*:}" - case "$result" in - PASS) echo -e " ${GREEN}✓${RESET} $name" ;; - FAIL) echo -e " ${RED}✗${RESET} $name" ;; - SKIP) echo -e " ${YELLOW}○${RESET} $name" ;; - esac -done -echo -e "${BOLD}${CYAN}╚═══════════════════════════════════════════════════════════════╝${RESET}" - -# Финальная проверка целостности tfvars -check_tfvars_integrity -info "terraform.tfvars НЕ БЫЛ ИЗМЕНЁН (md5 совпадает)" - -# Exit code: 0 если все PASS, 1 если есть FAIL -if [[ $FAIL -gt 0 ]]; then - echo -e "\n${RED}РЕЗУЛЬТАТ: FAIL ($FAIL ошибок)${RESET}" - exit 1 -else - echo -e "\n${GREEN}РЕЗУЛЬТАТ: ALL PASS${RESET}" - exit 0 -fi