Files
sless/operator_multiuser_test.sh
Naeel c762047234 fix(builder/go1.23): add require sless/fn/handler to server/go.mod at build time
go.work replace rule requires explicit require directive in server/go.mod.
Patch appended at kaniko build time - no base image rebuild needed.
2026-03-22 17:28:40 +03:00

381 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"