#!/bin/bash # 2026-03-22 — G_MULTIUSER: 10 параллельных пользователей с Postgres # # Проверяет: # - Namespace isolation при параллельной работе 10 пользователей # - Реальное PG подключение из функции (INSERT/SELECT через env_vars) # - Fake JWT (sub уникален, подпись не проверяется оператором — auth.go) # - /v1/namespaces/{ns}/ensure для создания namespace # # Технические детали: # - 10 пользователей: ns = sless-mu01..sless-mu10, sub = test-user-01..10 # - Каждый: ensure → create service → upload → wait Ready → invoke×2 → isolation → delete # - 8 тестов × 10 пользователей = 80 тестов суммарно # - Параллельный запуск через & + wait, результаты через temp-файлы # # Предусловия: # - PG поднят (examples/POSTGRES/terraform.tfvars) # - Оператор v0.1.51+ запущен в кластере # - sshfs mount / VM доступен API_BASE="https://sless.kube5s.ru/v1" FN_BASE="https://sless.kube5s.ru/fn" PG_HOST="postgresqlk8s-master.bba55e0f-4c51-4312-8ad3-efc557de331d.svc.cluster.local" PG_USER="user0" PG_PASS="iuEHVxxIPfdFr2kWWmSfNJbgCmDzuEC1rQy38aCNj0rYF23BHguhtKFUAioR1Z4F" PG_DB="db0" NUM_USERS=10 RESULTS_DIR=$(mktemp -d) # --------------------------------------------------------------------------- # make_fake_jwt sub — генерирует JWT с нужным sub. # Подпись фейковая — auth.go проверяет только структуру, sub и exp. # Почему фейковый: nubes JWKS недоступен внутри кластера (см. auth.go). # --------------------------------------------------------------------------- make_fake_jwt() { local sub="$1" local exp=$(( $(date +%s) + 86400 )) local iat=$(date +%s) local header='{"alg":"RS256","typ":"JWT"}' local payload payload=$(printf \ '{"iss":"auth-api","sub":"%s","exp":%d,"iat":%d,"jti":"fake-%s","auth_time":0,"typ":"","azp":"","session_state":"","acr":"","allowed-origins":null,"realm_access":{"roles":null},"resource_access":{"account":{"roles":null}},"scope":"","sid":"","email_verified":false,"name":"","ClientID":"","company_id":"","groups":null,"preferred_username":"","given_name":"","family_name":"","email":"multitest@example.com"}' \ "$sub" "$exp" "$iat" "$sub") local h p h=$(printf '%s' "$header" | base64 -w0 | tr '+/' '-_' | tr -d '=') p=$(printf '%s' "$payload" | base64 -w0 | tr '+/' '-_' | tr -d '=') printf '%s.%s.fakesig_multiuser_test' "$h" "$p" } # --------------------------------------------------------------------------- # make_py_zip zipfile — создаёт zip с Python-функцией + requirements.txt. # Функция: CREATE TABLE IF NOT EXISTS → INSERT → SELECT COUNT(*) → return. # Таблица приходит через env TEST_TABLE (изоляция по namespace). # Важно: zipfile не должен существовать до вызова zip (иначе zip пытается его # открыть как существующий архив → "Zip file structure invalid"). # --------------------------------------------------------------------------- make_py_zip() { local zipfile="$1" local tmpdir tmpdir=$(mktemp -d) rm -f "$zipfile" # гарантируем чистый файл — zip создаёт с нуля # Одинарные кавычки в EOF — чтобы bash не раскрывал $ внутри heredoc cat > "$tmpdir/pg_tester.py" << 'PYEOF' # pg_tester.py — тестовая функция: INSERT/SELECT в PG через env_vars. # Entrypoint: pg_tester.handle import os import psycopg2 def handle(event): table = os.environ.get("TEST_TABLE", "test_default") conn = psycopg2.connect( host=os.environ["PGHOST"], dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], password=os.environ["PGPASSWORD"], sslmode="require", ) cur = conn.cursor() # Безопасно: table = hardcoded test name из env_vars, не user input cur.execute( f"CREATE TABLE IF NOT EXISTS {table} " "(id SERIAL PRIMARY KEY, ns TEXT, ts TIMESTAMPTZ DEFAULT now())" ) cur.execute(f"INSERT INTO {table} (ns) VALUES (%s)", (table,)) conn.commit() cur.execute(f"SELECT COUNT(*) FROM {table}") count = cur.fetchone()[0] conn.close() return {"ok": True, "table": table, "count": int(count)} PYEOF printf 'psycopg2-binary==2.9.9\n' > "$tmpdir/requirements.txt" (cd "$tmpdir" && zip -q "$zipfile" pg_tester.py requirements.txt) rm -rf "$tmpdir" } # --------------------------------------------------------------------------- # wait_phase_fn url token [phase] [timeout] — ждёт нужной фазы CRD. # Возвращает 0 если фаза достигнута, 1 если timeout. # --------------------------------------------------------------------------- wait_phase_fn() { local url="$1" token="$2" want="${3:-Ready}" timeout="${4:-900}" local elapsed=0 phase while [[ $elapsed -lt $timeout ]]; do phase=$(curl -s "$url" -H "Authorization: Bearer $token" \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null) [[ "$phase" == "$want" ]] && return 0 sleep 10 elapsed=$(( elapsed + 10 )) done return 1 } # --------------------------------------------------------------------------- # invoke_fn url token [retries] [delay] — вызывает функцию с retry. # Печатает тело ответа. Считает успехом ответ содержащий "ok":true. # --------------------------------------------------------------------------- invoke_fn() { local url="$1" token="$2" retries="${3:-5}" delay="${4:-15}" local body ok for i in $(seq 1 "$retries"); do body=$(curl -s -X POST "$url" \ -H "Authorization: Bearer $token" \ -H "Content-Type: application/json" \ -d '{}') ok=$(printf '%s' "$body" \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('ok',''))" 2>/dev/null) if [[ "$ok" == "True" ]]; then printf '%s' "$body" return 0 fi [[ $i -lt $retries ]] && sleep "$delay" done printf '%s' "$body" return 1 } # --------------------------------------------------------------------------- # run_user_test N — полный lifecycle теста для пользователя N. # Результат: "pass fail" записывается в $RESULTS_DIR/user_N.result # Запускается в subshell, stdout → $RESULTS_DIR/user_N.log # --------------------------------------------------------------------------- run_user_test() { local n="$1" local ns="sless-mu$(printf '%02d' "$n")" local sub="test-user-$(printf '%02d' "$n")" local token token=$(make_fake_jwt "$sub") local table="test_sless_mu$(printf '%02d' "$n")" local svc="pg-mu$(printf '%02d' "$n")" local pass=0 fail=0 local code body c1 c2 echo "=== User $n ns=$ns svc=$svc table=$table ===" # ----------------------------------------------------------------------- # U-1: Ensure namespace # ----------------------------------------------------------------------- code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ "$API_BASE/namespaces/${ns}/ensure" \ -H "Authorization: Bearer $token") if [[ "$code" =~ ^(200|201)$ ]]; then echo "U${n}-1 PASS ensure($code)" (( pass++ )) else echo "U${n}-1 FAIL ensure=$code" (( fail++ )) fi # ----------------------------------------------------------------------- # U-2: Create service с PG env_vars + memory_mb (обязательное поле, мин 1) # ----------------------------------------------------------------------- local create_json create_json=$(printf \ '{"name":"%s","runtime":"python3.11","entrypoint":"pg_tester.handle","memory_mb":128,"env_vars":{"PGHOST":"%s","PGDATABASE":"%s","PGUSER":"%s","PGPASSWORD":"%s","TEST_TABLE":"%s"}}' \ "$svc" "$PG_HOST" "$PG_DB" "$PG_USER" "$PG_PASS" "$table") local create_body create_body=$(curl -s -w "\n%{http_code}" -X POST \ "$API_BASE/namespaces/${ns}/services" \ -H "Authorization: Bearer $token" \ -H "Content-Type: application/json" \ -d "$create_json") code=$(printf '%s' "$create_body" | tail -1) if [[ "$code" == "201" ]]; then echo "U${n}-2 PASS create($code)" (( pass++ )) else echo "U${n}-2 FAIL create=$code body=$(printf '%s' "$create_body" | head -1)" (( fail++ )) printf '%d %d\n' "$pass" "$fail" > "$RESULTS_DIR/user_${n}.result" return fi # ----------------------------------------------------------------------- # U-3: Upload code (zip с pg_tester.py + requirements.txt) # ----------------------------------------------------------------------- local zipf zipf=$(mktemp --suffix=.zip) make_py_zip "$zipf" code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ "$API_BASE/namespaces/${ns}/services/${svc}/upload" \ -H "Authorization: Bearer $token" \ -F "code=@${zipf}") rm -f "$zipf" if [[ "$code" == "200" ]]; then echo "U${n}-3 PASS upload($code)" (( pass++ )) else echo "U${n}-3 FAIL upload=$code" (( fail++ )) printf '%d %d\n' "$pass" "$fail" > "$RESULTS_DIR/user_${n}.result" return fi # ----------------------------------------------------------------------- # U-4: Wait for Ready (Kaniko + pod startup; psycopg2-binary тяжёлый) # Timeout 900s — увеличен относительно G20 (там без pip install) # ----------------------------------------------------------------------- echo "U${n}: ожидаем Ready (max 900s)..." sleep 15 if wait_phase_fn "$API_BASE/namespaces/${ns}/services/${svc}" "$token" "Ready" 900; then echo "U${n}-4 PASS Ready" (( pass++ )) else echo "U${n}-4 FAIL не стал Ready за 900s" (( fail++ )) printf '%d %d\n' "$pass" "$fail" > "$RESULTS_DIR/user_${n}.result" return fi # ----------------------------------------------------------------------- # U-5: Первый invoke — функция должна создать таблицу и вставить строку # sleep 40: при 10 параллельных Kaniko-сборках pod-сеть поднимается дольше # ----------------------------------------------------------------------- sleep 40 body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 8 20) c1=$(printf '%s' "$body" \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('count',0))" 2>/dev/null) if [[ "$c1" =~ ^[0-9]+$ && "$c1" -ge 1 ]]; then echo "U${n}-5 PASS invoke1 count=$c1" (( pass++ )) else echo "U${n}-5 FAIL invoke1 resp=$body" (( fail++ )) c1=0 fi # ----------------------------------------------------------------------- # U-6: Второй invoke — count должен вырасти (ещё один INSERT) # ----------------------------------------------------------------------- sleep 3 body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 3 10) c2=$(printf '%s' "$body" \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('count',0))" 2>/dev/null) if [[ "$c2" =~ ^[0-9]+$ && "$c2" -gt "$c1" ]]; then echo "U${n}-6 PASS invoke2 count $c1→$c2" (( pass++ )) else echo "U${n}-6 FAIL invoke2 c1=$c1 c2=$c2 resp=$body" (( fail++ )) fi # ----------------------------------------------------------------------- # U-7: Isolation — сервис svc видим ТОЛЬКО в своём ns, не в соседнем. # Проверяем: GET своего сервиса в namespace следующего пользователя → 404 # ----------------------------------------------------------------------- local other_n=$(( (n % NUM_USERS) + 1 )) local other_ns="sless-mu$(printf '%02d' "$other_n")" code=$(curl -s -o /dev/null -w "%{http_code}" -X GET \ "$API_BASE/namespaces/${other_ns}/services/${svc}" \ -H "Authorization: Bearer $token") if [[ "$code" == "404" ]]; then echo "U${n}-7 PASS isolation: ${svc} в ${other_ns} → 404" (( pass++ )) else echo "U${n}-7 FAIL isolation: ${svc} в ${other_ns} → $code (ожидали 404)" (( fail++ )) fi # ----------------------------------------------------------------------- # U-8: Delete service — cleanup # ----------------------------------------------------------------------- code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ "$API_BASE/namespaces/${ns}/services/${svc}" \ -H "Authorization: Bearer $token") if [[ "$code" == "204" ]]; then echo "U${n}-8 PASS delete($code)" (( pass++ )) else echo "U${n}-8 FAIL delete=$code" (( fail++ )) fi echo "=== User $n DONE: pass=$pass fail=$fail ===" printf '%d %d\n' "$pass" "$fail" > "$RESULTS_DIR/user_${n}.result" } # =========================================================================== # MAIN # =========================================================================== echo "============================================================" echo "G_MULTIUSER: $NUM_USERS параллельных пользователей с Postgres" echo "============================================================" date "+%Y-%m-%d %H:%M:%S" echo "API: $API_BASE" echo "PG: $PG_HOST / $PG_DB" echo "Тестов: $NUM_USERS × 8 = $((NUM_USERS * 8))" echo "" # --------------------------------------------------------------------------- # Cleanup — удаляем остатки от предыдущих прогонов (409 если не почистить). # Игнорируем ошибки: если сервис не существует — не страшно. # --------------------------------------------------------------------------- echo "Cleanup: удаляем сервисы от предыдущих прогонов..." for i in $(seq 1 $NUM_USERS); do ns="sless-mu$(printf '%02d' "$i")" svc="pg-mu$(printf '%02d' "$i")" token=$(make_fake_jwt "test-user-$(printf '%02d' "$i")") curl -s -o /dev/null -X DELETE \ "$API_BASE/namespaces/${ns}/services/${svc}" \ -H "Authorization: Bearer $token" & done wait echo "Cleanup готов." echo "" # Запускаем всех пользователей параллельно for i in $(seq 1 $NUM_USERS); do run_user_test "$i" > "$RESULTS_DIR/user_${i}.log" 2>&1 & done echo "Запущено $NUM_USERS параллельных пользователей. Ждём завершения..." wait echo "Все завершились." echo "" # --------------------------------------------------------------------------- # Сбор результатов # --------------------------------------------------------------------------- total_pass=0 total_fail=0 echo "============================================================" echo "Результаты по пользователям:" echo "============================================================" for i in $(seq 1 $NUM_USERS); do resf="$RESULTS_DIR/user_${i}.result" logf="$RESULTS_DIR/user_${i}.log" if [[ -f "$resf" ]]; then read -r p f < "$resf" total_pass=$(( total_pass + p )) total_fail=$(( total_fail + f )) echo " User $i (sless-mu$(printf '%02d' "$i")): $p PASS / $f FAIL" else echo " User $i: НЕТ РЕЗУЛЬТАТА (timeout или crash)" total_fail=$(( total_fail + 8 )) fi done echo "" echo "============================================================" echo "ИТОГО: $total_pass PASS / $total_fail FAIL" echo "(ожидалось: $((NUM_USERS * 8)) тестов)" if [[ $total_fail -eq 0 ]]; then echo "РЕЗУЛЬТАТ: PASS ✓" else echo "РЕЗУЛЬТАТ: FAIL ✗" fi echo "============================================================" echo "" # --------------------------------------------------------------------------- # Детальные логи каждого пользователя # --------------------------------------------------------------------------- echo "=== Детальные логи ===" for i in $(seq 1 $NUM_USERS); do echo "" echo "--- User $i (sless-mu$(printf '%02d' "$i")) ---" cat "$RESULTS_DIR/user_${i}.log" 2>/dev/null || echo "(нет лога)" done rm -rf "$RESULTS_DIR"