From dca98aac9734e6c0533346441a4a1b476e23210e Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 22 Mar 2026 14:36:52 +0300 Subject: [PATCH] =?UTF-8?q?test:=20G=5FMULTIUSER=20=E2=80=94=2010=20parall?= =?UTF-8?q?el=20users=20+=20Postgres=20(ready=20to=20run)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/progress.md | 38 ++-- operator_multiuser_test.sh | 354 +++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+), 19 deletions(-) create mode 100755 operator_multiuser_test.sh diff --git a/doc/progress.md b/doc/progress.md index d42e2d2..a174695 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -13,30 +13,30 @@ --- -## TODO (завтра — приоритет) +## TODO (в работе — сессия 9) -### ПЛАН: multi-user тест с Postgres +### G_MULTIUSER: 10 параллельных пользователей с Postgres -**Шаг 1 — Добавить Postgres в survival тест (один юзер):** -- Функции в `operator_survival_test.sh` сейчас echo-хендлеры без PG -- Добавить группу тестов где функция делает реальные INSERT/SELECT в Postgres -- Postgres креды берутся из параметров ресурса `sless_pg` (через `env_vars` функции) -- Изоляция: таблица называется `test_` — каждый юзер пишет в свою -- Прогнать от одного юзера → убедиться что работает +**Статус:** пишем `operator_multiuser_test.sh` -**Шаг 2 — Параметризовать survival тест:** -- `TOKEN` и `NAMESPACE` через env переменные (сейчас хардкодные) -- `PG_DSN` через env переменную (берётся из параметров Postgres ресурса) +**Реализация (упрощённая, без SHA256):** +- 10 фейковых JWT: sub=`test-user-01`..`test-user-10` (подпись не проверяется оператором) +- Namespace: `sless-mu01`..`sless-mu10` (создаются через `/ensure`) +- Каждый пользователь создаёт сервис `pg-muXX` с Python 3.11 + psycopg2 +- Функция INSERT/SELECT в таблицу `test_sless_muXX` (изоляция по таблице) +- Параллельный запуск 10 пользователей через `&` + `wait` -**Шаг 3 — Обёртка на 10 юзеров:** -- `operator_multiuser_test.sh` -- Генерирует 10 фейковых JWT (`test-user-01` ... `test-user-10`) -- Для каждого вычисляет namespace (SHA256(sub)[:8 байт]) -- `POST /v1/namespaces/{ns}/ensure` — создаёт namespace -- Параллельно запускает полный survival тест в каждом namespace (`&`) -- Ждёт всех (`wait`), собирает итоговый отчёт: PASS/FAIL по каждому юзеру +**Технические детали:** +- PG host: `postgresqlk8s-master.bba55e0f-4c51-4312-8ad3-efc557de331d.svc.cluster.local` +- PG creds: `user0 / db0` (из `examples/POSTGRES/terraform.tfvars`) +- `sless-fn-{ns}` создаётся автоматически контроллером при первом сервисе +- Таймаут Kaniko: 900s (psycopg2-binary тяжёлый пакет) +- Ожидаемый результат: **80 тестов (8×10)** -**Ожидаемый результат:** ~340 тестов (34×10), ~15-20 минут параллельно +**Тесты на пользователя (8 шт):** +U-1: ensure namespace | U-2: create service | U-3: upload code +U-4: wait Ready | U-5: invoke1 (count≥1) | U-6: invoke2 (count grew) +U-7: isolation (other ns → 404) | U-8: delete --- diff --git a/operator_multiuser_test.sh b/operator_multiuser_test.sh new file mode 100755 index 0000000..0321f6a --- /dev/null +++ b/operator_multiuser_test.sh @@ -0,0 +1,354 @@ +#!/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). +# --------------------------------------------------------------------------- +make_py_zip() { + local zipfile="$1" + local tmpdir + tmpdir=$(mktemp -d) + # Одинарные кавычки в 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 + # ----------------------------------------------------------------------- + local create_json + create_json=$(printf \ + '{"name":"%s","runtime":"python3.11","entrypoint":"pg_tester.handle","env_vars":{"PGHOST":"%s","PGDATABASE":"%s","PGUSER":"%s","PGPASSWORD":"%s","TEST_TABLE":"%s"}}' \ + "$svc" "$PG_HOST" "$PG_DB" "$PG_USER" "$PG_PASS" "$table") + code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "$API_BASE/namespaces/${ns}/services" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d "$create_json") + if [[ "$code" == "201" ]]; then + echo "U${n}-2 PASS create($code)" + (( pass++ )) + else + echo "U${n}-2 FAIL create=$code" + (( fail++ )) + 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 "file=@${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++ )) + 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 20: pod startup race (known limitation: Ready до pod-ready) + # ----------------------------------------------------------------------- + sleep 20 + body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 5 15) + 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 "" + +# Запускаем всех пользователей параллельно +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"