test(operator): lifecycle test — 44/44 PASS; 2 бага оператора задокументированы
Группы тестов:
1. Валидация API (8 тестов) — memory_mb, timeout_sec, missing fields, 404
2. Полный цикл одной функции (18 тестов):
- create → build → ready → invoke → env update → memory update → timeout → delete
- delete → k8s Deployment/Service удалены
3. Граничные сценарии:
- DELETE while Building → kaniko убит, Deployment не создан
- Reconcile: kubectl delete Deployment → [БАГ: не пересоздаётся]
4. Параллельные функции (10 тестов):
- 3 функции одновременно → delete одной в Building → 2 доходят до Ready
- 40 параллельных invoke → 40/40 OK
Найденные баги оператора:
БАГ 1: ensureServiceDeployment не обновляет Resources (memory_mb через PUT игнорируется)
БАГ 2: нет self-healing — если Deployment удалён вручную, контроллер не пересоздаёт
(нет cross-namespace watch, RequeueAfter не задан)
This commit is contained in:
@@ -0,0 +1,693 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-21 — operator_lifecycle_test.sh
|
||||
# Тест жизненного цикла ОПЕРАТОРА: CREATE → BUILD → READY → UPDATE → DELETE
|
||||
#
|
||||
# Тестирует не функции, а поведение контроллера:
|
||||
# - Корректность API (валидация)
|
||||
# - Полный цикл одной функции (create → build → ready → update → delete)
|
||||
# - Граничные сценарии (delete during build, reconcile, duplicate)
|
||||
# - Несколько функций одновременно (параллельные builds + delete в середине)
|
||||
#
|
||||
# ЗАПУСКАТЬ НА VM:
|
||||
# ssh naeel@5.172.178.213 'bash ~/terra/sless/operator_lifecycle_test.sh'
|
||||
#
|
||||
# Зависимости: curl, python3, zip, kubectl
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ─── CONFIG ──────────────────────────────────────────────────────────────────
|
||||
|
||||
TOKEN=$(cat /home/naeel/terra/sless/test.token)
|
||||
NS="sless-ffd1f598c169b0ae"
|
||||
API="https://sless.kube5s.ru/v1/namespaces/$NS"
|
||||
FN_BASE="https://sless.kube5s.ru/fn/$NS"
|
||||
DEPLOY_NS="sless-fn-$NS"
|
||||
# Метка времени для уникальных имён — избегает коллизий с prod-сервисами
|
||||
TS=$(date +%s)
|
||||
|
||||
# ─── СТАТИСТИКА ──────────────────────────────────────────────────────────────
|
||||
|
||||
PASS=0; FAIL=0; KNOWN_BUGS=0
|
||||
|
||||
# ─── ЦВЕТА ───────────────────────────────────────────────────────────────────
|
||||
|
||||
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++)); }
|
||||
# known_bug: баг подтверждён — считаем отдельно, не ломаем failed count
|
||||
known_bug() { echo -e " ${YELLOW}[BUG!]${RESET} $1"; ((KNOWN_BUGS++)); }
|
||||
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
|
||||
section() { echo -e "\n${BOLD}━━━ $1 ━━━${RESET}"; }
|
||||
|
||||
# api_code METHOD URL [body] → HTTP status code (только код, без тела)
|
||||
api_code() {
|
||||
local method="$1" url="$2" body="${3:-}"
|
||||
local args=(-s -o /dev/null -w "%{http_code}" -m 120
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||||
curl "${args[@]}" "$url"
|
||||
}
|
||||
|
||||
# api_json METHOD URL [body] → response body (JSON)
|
||||
api_json() {
|
||||
local method="$1" url="$2" body="${3:-}"
|
||||
local args=(-s -m 120 -H "Authorization: Bearer $TOKEN")
|
||||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||||
curl "${args[@]}" "$url"
|
||||
}
|
||||
|
||||
# check_code LABEL GOT WANT — сравнить HTTP код
|
||||
check_code() {
|
||||
local label="$1" got="$2" want="$3"
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
pass "$label → HTTP $got"
|
||||
else
|
||||
fail "$label → HTTP $got (ожидали $want)"
|
||||
fi
|
||||
}
|
||||
|
||||
# svc_phase NAME → phase string (Pending/Building/Ready/Failed/"")
|
||||
svc_phase() {
|
||||
api_json GET "$API/services/$1" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null
|
||||
}
|
||||
|
||||
# wait_phase NAME WANT_PHASE TIMEOUT_SEC
|
||||
# return: 0=ok, 1=timeout, 2=unexpected Failed
|
||||
wait_phase() {
|
||||
local name="$1" want="$2" timeout_sec="${3:-300}"
|
||||
local deadline=$((SECONDS + timeout_sec))
|
||||
while [[ $SECONDS -lt $deadline ]]; do
|
||||
local phase
|
||||
phase=$(svc_phase "$name")
|
||||
[[ "$phase" == "$want" ]] && return 0
|
||||
# Если пришёл Failed а ждали не Failed — стоп
|
||||
if [[ "$phase" == "Failed" && "$want" != "Failed" ]]; then
|
||||
return 2
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# make_echo_zip NAME → /tmp/lc_echo_{NAME}.zip
|
||||
# Минимальный Python echo: {"ok": true, "env": TEST_VAR, "msg": тело запроса}
|
||||
make_echo_zip() {
|
||||
local name="$1"
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
import os, json
|
||||
|
||||
def handle(event):
|
||||
return {
|
||||
"ok": True,
|
||||
"env": os.environ.get("TEST_VAR", ""),
|
||||
"msg": event.get("msg", ""),
|
||||
}
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/lc_echo_${name}.zip" handler.py)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# upload_code NAME — загрузить zip на /upload, ждать что S3Key принят
|
||||
upload_code() {
|
||||
local name="$1"
|
||||
make_echo_zip "$name"
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -m 120 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "code=@/tmp/lc_echo_${name}.zip" \
|
||||
"$API/services/$name/upload")
|
||||
echo "$code"
|
||||
}
|
||||
|
||||
# create_and_deploy NAME — POST create + upload + wait Ready
|
||||
# return: 0=готов, 1=ошибка
|
||||
create_and_deploy() {
|
||||
local name="$1"
|
||||
# 1. Создать CRD
|
||||
local http_code
|
||||
http_code=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$name\",\"display_name\":\"$name lifecycle test\",\
|
||||
\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"initial\"}}")
|
||||
if [[ "$http_code" != "201" ]]; then
|
||||
fail "create $name → HTTP $http_code (ожидали 201)"
|
||||
return 1
|
||||
fi
|
||||
# 2. Загрузить код
|
||||
local upload_code_result
|
||||
upload_code_result=$(upload_code "$name")
|
||||
if [[ "$upload_code_result" != "200" ]]; then
|
||||
fail "upload $name → HTTP $upload_code_result (ожидали 200)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# cleanup NAME — удалить сервис (игнорировать ошибки, для teardown)
|
||||
cleanup() {
|
||||
local name="$1"
|
||||
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# deploy_ns_has NAME — 0 если Deployment существует в DEPLOY_NS
|
||||
deploy_ns_has() {
|
||||
kubectl get deployment -n "$DEPLOY_NS" "$1" --no-headers 2>/dev/null \
|
||||
| grep -q "$1"
|
||||
}
|
||||
|
||||
# ─── TEARDOWN (cleanup при выходе) ───────────────────────────────────────────
|
||||
|
||||
# Список имён для очистки — добавляется по ходу теста
|
||||
CLEANUP_NAMES=()
|
||||
|
||||
teardown() {
|
||||
if [[ ${#CLEANUP_NAMES[@]} -gt 0 ]]; then
|
||||
echo -e "\n${CYAN}[INFO]${RESET} teardown: удаляю тестовые сервисы..."
|
||||
for name in "${CLEANUP_NAMES[@]}"; do
|
||||
cleanup "$name"
|
||||
echo -e " ${CYAN}[INFO]${RESET} удалён: $name"
|
||||
done
|
||||
fi
|
||||
}
|
||||
trap teardown EXIT
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ГРУППА 1: Валидация API (без билда)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
info "1.1 memory_mb=0 → 400"
|
||||
check_code "memory_mb=0" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"lc-invalid","runtime":"python3.11","entrypoint":"h.h","memory_mb":0}')" \
|
||||
"400"
|
||||
|
||||
info "1.2 memory_mb=5000 → 400"
|
||||
check_code "memory_mb=5000" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"lc-invalid","runtime":"python3.11","entrypoint":"h.h","memory_mb":5000}')" \
|
||||
"400"
|
||||
|
||||
info "1.3 timeout_sec=-1 → 400"
|
||||
check_code "timeout_sec=-1" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"lc-invalid","runtime":"python3.11","entrypoint":"h.h","memory_mb":128,"timeout_sec":-1}')" \
|
||||
"400"
|
||||
|
||||
info "1.4 timeout_sec=901 → 400"
|
||||
check_code "timeout_sec=901" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"lc-invalid","runtime":"python3.11","entrypoint":"h.h","memory_mb":128,"timeout_sec":901}')" \
|
||||
"400"
|
||||
|
||||
info "1.5 без runtime → 400"
|
||||
check_code "без runtime" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"lc-invalid","entrypoint":"h.h","memory_mb":128}')" \
|
||||
"400"
|
||||
|
||||
info "1.6 GET несуществующего → 404"
|
||||
check_code "GET non-existent" \
|
||||
"$(api_code GET "$API/services/lc-does-not-exist-${TS}")" \
|
||||
"404"
|
||||
|
||||
info "1.7 PUT несуществующего → 404"
|
||||
check_code "PUT non-existent" \
|
||||
"$(api_code PUT "$API/services/lc-does-not-exist-${TS}" \
|
||||
'{"runtime":"python3.11","entrypoint":"h.h","memory_mb":128}')" \
|
||||
"404"
|
||||
|
||||
info "1.8 DELETE несуществующего → 404"
|
||||
check_code "DELETE non-existent" \
|
||||
"$(api_code DELETE "$API/services/lc-does-not-exist-${TS}")" \
|
||||
"404"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ГРУППА 2: Полный жизненный цикл одной функции"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
SOLO="lc-solo-${TS}"
|
||||
CLEANUP_NAMES+=("$SOLO")
|
||||
|
||||
info "2.1 CREATE $SOLO → 201"
|
||||
check_code "create $SOLO" \
|
||||
"$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$SOLO\",\"display_name\":\"lifecycle solo\",\
|
||||
\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"initial\"}}")" \
|
||||
"201"
|
||||
|
||||
info "2.2 UPLOAD код → 200"
|
||||
up_code=$(upload_code "$SOLO")
|
||||
check_code "upload $SOLO" "$up_code" "200"
|
||||
|
||||
info "2.3 Phase переходит в Building (ждём 60s)..."
|
||||
if wait_phase "$SOLO" "Building" 60; then
|
||||
pass "$SOLO достиг фазы Building"
|
||||
else
|
||||
info "Фаза Building не поймана (возможно уже перешёл дальше, продолжаем)"
|
||||
fi
|
||||
|
||||
info "2.4 Phase переходит в Ready (ждём 300s — канико + деплой)..."
|
||||
wait_result=0
|
||||
wait_phase "$SOLO" "Ready" 300 || wait_result=$?
|
||||
if [[ $wait_result -eq 0 ]]; then
|
||||
pass "$SOLO достиг фазы Ready"
|
||||
elif [[ $wait_result -eq 2 ]]; then
|
||||
fail "$SOLO перешёл в Failed (ошибка сборки)"
|
||||
else
|
||||
fail "$SOLO не достиг Ready за 300s (текущая фаза: $(svc_phase "$SOLO"))"
|
||||
fi
|
||||
|
||||
# phase=Ready ≠ pod запущен: ждём rollout чтобы pod был READY перед invoke
|
||||
info "2.4b Ждём rollout Deployment (pod READY, 90s)..."
|
||||
if kubectl rollout status deployment/"$SOLO" -n "$DEPLOY_NS" --timeout=90s \
|
||||
> /dev/null 2>&1; then
|
||||
pass "$SOLO pod запущен (rollout OK)"
|
||||
else
|
||||
info "rollout не завершился за 90s, продолжаем"
|
||||
fi
|
||||
sleep 3
|
||||
|
||||
info "2.5 INVOKE через /fn/ → 200 + {\"ok\": true}"
|
||||
fn_resp=$(curl -s -m 30 -X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"hello"}' "$FN_BASE/$SOLO")
|
||||
fn_ok=$(echo "$fn_resp" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))" 2>/dev/null)
|
||||
if [[ "$fn_ok" == "True" ]]; then
|
||||
pass "$SOLO invoke → ok=True"
|
||||
else
|
||||
fail "$SOLO invoke → ответ: $fn_resp"
|
||||
fi
|
||||
|
||||
info "2.6 Env var передаётся: env=initial"
|
||||
fn_env=$(echo "$fn_resp" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('env',''))" 2>/dev/null)
|
||||
if [[ "$fn_env" == "initial" ]]; then
|
||||
pass "$SOLO env=initial"
|
||||
else
|
||||
fail "$SOLO env='$fn_env' (ожидали 'initial')"
|
||||
fi
|
||||
|
||||
info "2.7 DUPLICATE create пока Ready → 409"
|
||||
check_code "duplicate create (Ready)" \
|
||||
"$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$SOLO\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")" \
|
||||
"409"
|
||||
|
||||
info "2.8 PUT: сменить env TEST_VAR=updated → 200"
|
||||
check_code "PUT env update" \
|
||||
"$(api_code PUT "$API/services/$SOLO" \
|
||||
"{\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"updated\"}}")" \
|
||||
"200"
|
||||
|
||||
info "2.9 Ждём rollout Deployment после смены env (60s)..."
|
||||
sleep 15 # дать контроллеру применить обновление
|
||||
if kubectl rollout status deployment/"$SOLO" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1; then
|
||||
pass "$SOLO Deployment rollout после env update"
|
||||
else
|
||||
fail "$SOLO Deployment rollout не завершился за 60s"
|
||||
fi
|
||||
|
||||
info "2.10 Проверяем env в k8s Deployment (TEST_VAR=updated)"
|
||||
k8s_env=$(kubectl get deployment "$SOLO" -n "$DEPLOY_NS" -o json 2>/dev/null \
|
||||
| python3 -c "import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
envs=d['spec']['template']['spec']['containers'][0].get('env',[])
|
||||
val=next((e['value'] for e in envs if e['name']=='TEST_VAR'), '')
|
||||
print(val)" 2>/dev/null)
|
||||
if [[ "$k8s_env" == "updated" ]]; then
|
||||
pass "k8s Deployment env TEST_VAR=updated ✓"
|
||||
else
|
||||
fail "k8s Deployment env TEST_VAR='$k8s_env' (ожидали 'updated')"
|
||||
fi
|
||||
|
||||
info "2.11 INVOKE после смены env → env=updated"
|
||||
sleep 5 # дать поду время запуститься
|
||||
fn_resp2=$(curl -s -m 30 -X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"env test"}' "$FN_BASE/$SOLO")
|
||||
fn_env2=$(echo "$fn_resp2" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('env',''))" 2>/dev/null)
|
||||
if [[ "$fn_env2" == "updated" ]]; then
|
||||
pass "$SOLO invoke после env update → env=updated"
|
||||
else
|
||||
fail "$SOLO invoke env='$fn_env2' (ожидали 'updated')"
|
||||
fi
|
||||
|
||||
info "2.12 PUT: сменить memory_mb 128 → 256 → 200"
|
||||
check_code "PUT memory update" \
|
||||
"$(api_code PUT "$API/services/$SOLO" \
|
||||
"{\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":256,\"env_vars\":{\"TEST_VAR\":\"updated\"}}")" \
|
||||
"200"
|
||||
|
||||
info "2.13 KNOWN BUG: k8s Deployment memory limit НЕ обновляется через PUT"
|
||||
sleep 5
|
||||
k8s_mem=$(kubectl get deployment "$SOLO" -n "$DEPLOY_NS" \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}' \
|
||||
2>/dev/null)
|
||||
if [[ "$k8s_mem" == "256Mi" ]]; then
|
||||
pass "k8s Deployment memory=256Mi (БАГ ПОЧИНЕН или не воспроизводится)"
|
||||
else
|
||||
# Ожидаемый баг: ensureServiceDeployment обновляет только Image+Env, не Resources
|
||||
known_bug "k8s Deployment memory=$k8s_mem (ожидали 256Mi) [БАГ: ensureServiceDeployment не обновляет Resources]"
|
||||
fi
|
||||
|
||||
info "2.14 PUT: timeout_sec=5 → 200"
|
||||
check_code "PUT timeout_sec=5" \
|
||||
"$(api_code PUT "$API/services/$SOLO" \
|
||||
"{\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":256,\"env_vars\":{\"TEST_VAR\":\"updated\"},\"timeout_sec\":5}")" \
|
||||
"200"
|
||||
|
||||
info "2.15 Phase остаётся Ready после PUT (не триггерит ребилд)"
|
||||
phase_after_put=$(svc_phase "$SOLO")
|
||||
if [[ "$phase_after_put" == "Ready" ]]; then
|
||||
pass "Phase=Ready после PUT (ребилд не запустился)"
|
||||
else
|
||||
fail "Phase=$phase_after_put после PUT (ожидали Ready)"
|
||||
fi
|
||||
|
||||
info "2.16 PUT: timeout_sec=0 (снять лимит) → 200"
|
||||
check_code "PUT timeout_sec=0 (no limit)" \
|
||||
"$(api_code PUT "$API/services/$SOLO" \
|
||||
"{\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":256,\"env_vars\":{\"TEST_VAR\":\"updated\"},\"timeout_sec\":0}")" \
|
||||
"200"
|
||||
|
||||
info "2.17 DELETE $SOLO → 204"
|
||||
check_code "DELETE $SOLO" \
|
||||
"$(api_code DELETE "$API/services/$SOLO")" \
|
||||
"204"
|
||||
|
||||
info "2.18 GET после DELETE → 404"
|
||||
sleep 3 # дать время finalizer отработать
|
||||
check_code "GET после DELETE" \
|
||||
"$(api_code GET "$API/services/$SOLO")" \
|
||||
"404"
|
||||
|
||||
info "2.19 k8s Deployment удалён (ждём до 30s)"
|
||||
deleted=false
|
||||
for i in $(seq 1 6); do
|
||||
if ! deploy_ns_has "$SOLO"; then deleted=true; break; fi
|
||||
sleep 5
|
||||
done
|
||||
if $deleted; then
|
||||
pass "k8s Deployment/$SOLO удалён после DELETE"
|
||||
else
|
||||
fail "k8s Deployment/$SOLO ещё существует (finalizer не отработал?)"
|
||||
fi
|
||||
|
||||
info "2.20 k8s Service удалён"
|
||||
if ! kubectl get service "$SOLO" -n "$DEPLOY_NS" > /dev/null 2>&1; then
|
||||
pass "k8s Service/$SOLO удалён"
|
||||
else
|
||||
fail "k8s Service/$SOLO ещё существует"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ГРУППА 3: Граничные сценарии"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ── 3A: DELETE пока Building ─────────────────────────────────────────────────
|
||||
|
||||
KILL_BUILD="lc-killbuild-${TS}"
|
||||
CLEANUP_NAMES+=("$KILL_BUILD")
|
||||
|
||||
info "3.1 Создаём $KILL_BUILD и сразу удаляем пока в Building..."
|
||||
http_create=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$KILL_BUILD\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{}}")
|
||||
check_code "create $KILL_BUILD" "$http_create" "201"
|
||||
|
||||
# Загружаем код — это запускает kaniko
|
||||
up_kb=$(upload_code "$KILL_BUILD")
|
||||
check_code "upload $KILL_BUILD" "$up_kb" "200"
|
||||
|
||||
info " Ждём фазу Building (макс 90s)..."
|
||||
if wait_phase "$KILL_BUILD" "Building" 90; then
|
||||
pass " $KILL_BUILD вошёл в фазу Building"
|
||||
|
||||
info " DELETE $KILL_BUILD пока Building..."
|
||||
check_code "DELETE while Building" \
|
||||
"$(api_code DELETE "$API/services/$KILL_BUILD")" \
|
||||
"204"
|
||||
|
||||
info " Ждём исчезновения CRD (finalizer убивает kaniko, 30s)..."
|
||||
sleep 10
|
||||
check_code "GET после DELETE while Building" \
|
||||
"$(api_code GET "$API/services/$KILL_BUILD")" \
|
||||
"404"
|
||||
|
||||
info " Deployment НЕ должен был создаться"
|
||||
if ! deploy_ns_has "$KILL_BUILD"; then
|
||||
pass "Deployment/$KILL_BUILD не существует (правильно: убит до Ready)"
|
||||
else
|
||||
fail "Deployment/$KILL_BUILD существует (неожиданно: функция не дошла до Ready)"
|
||||
fi
|
||||
else
|
||||
info " Фаза Building не поймана за 90s (уже завершился). Пропускаем 3A"
|
||||
cleanup "$KILL_BUILD"
|
||||
fi
|
||||
|
||||
# ── 3B: Reconcile — восстановление вручную удалённого Deployment ─────────────
|
||||
|
||||
RECONCILE="lc-reconcile-${TS}"
|
||||
CLEANUP_NAMES+=("$RECONCILE")
|
||||
|
||||
info "3.2 Reconcile: создаём $RECONCILE, ждём Ready, удаляем Deployment вручную"
|
||||
create_and_deploy "$RECONCILE"
|
||||
|
||||
info " Ждём Ready (300s)..."
|
||||
wait_reconcile=0
|
||||
wait_phase "$RECONCILE" "Ready" 300 || wait_reconcile=$?
|
||||
if [[ $wait_reconcile -eq 0 ]]; then
|
||||
pass "$RECONCILE готов"
|
||||
kubectl rollout status deployment/"$RECONCILE" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1 || true
|
||||
|
||||
info " kubectl delete deployment — симулируем сбой в k8s..."
|
||||
kubectl delete deployment "$RECONCILE" -n "$DEPLOY_NS" > /dev/null 2>&1
|
||||
|
||||
info " Ждём пока контроллер пересоздаст Deployment (90s)..."
|
||||
recreated=false
|
||||
for i in $(seq 1 18); do
|
||||
sleep 5
|
||||
if deploy_ns_has "$RECONCILE"; then recreated=true; break; fi
|
||||
done
|
||||
if $recreated; then
|
||||
pass "Deployment/$RECONCILE пересоздан контроллером (reconcile работает)"
|
||||
else
|
||||
# БАГ: контроллер не реагирует на удаление Deployment — нет watch cross-namespace.
|
||||
# ensureServiceDeployment вызывается только при изменении Service CRD.
|
||||
# Без RequeueAfter или Owns() в controller-runtime self-healing не работает.
|
||||
known_bug "Deployment/$RECONCILE не пересоздан за 90s [БАГ: нет self-healing — контроллер не смотрит за Deployment в sless-fn-* namespace]"
|
||||
fi
|
||||
|
||||
info " Invoke (прямой kubectl port-forward не доступен, ожидаем 502/200)..."
|
||||
# Если reconcile не пересоздал — вернёт 502 (нет pod), если пересоздал — 200
|
||||
sleep 5
|
||||
fn_reconcile=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{}' "$FN_BASE/$RECONCILE")
|
||||
info " invoke после reconcile → HTTP $fn_reconcile (200=работает, 502=pod отсутствует)"
|
||||
if [[ "$fn_reconcile" == "200" ]]; then
|
||||
pass "invoke после reconcile → 200 (pod пересоздан)"
|
||||
else
|
||||
info " invoke → $fn_reconcile (self-healing не сработал — известный баг)"
|
||||
fi
|
||||
else
|
||||
fail "$RECONCILE не достиг Ready, пропускаем 3B"
|
||||
fi
|
||||
|
||||
info " Удаляем $RECONCILE"
|
||||
check_code "DELETE $RECONCILE" \
|
||||
"$(api_code DELETE "$API/services/$RECONCILE")" "204"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ГРУППА 4: Несколько функций одновременно"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
MULTI1="lc-multi1-${TS}"
|
||||
MULTI2="lc-multi2-${TS}"
|
||||
MULTI3="lc-multi3-${TS}"
|
||||
CLEANUP_NAMES+=("$MULTI1" "$MULTI2" "$MULTI3")
|
||||
|
||||
info "4.1 Создаём 3 функции параллельно..."
|
||||
|
||||
# Параллельный create + upload. Каждую в фоне
|
||||
(
|
||||
set +e
|
||||
http=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$MULTI1\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{\"IDX\":\"1\"}}")
|
||||
if [[ "$http" == "201" ]]; then
|
||||
upload_code "$MULTI1" > /dev/null
|
||||
fi
|
||||
) &
|
||||
|
||||
(
|
||||
set +e
|
||||
http=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$MULTI2\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{\"IDX\":\"2\"}}")
|
||||
if [[ "$http" == "201" ]]; then
|
||||
upload_code "$MULTI2" > /dev/null
|
||||
fi
|
||||
) &
|
||||
|
||||
(
|
||||
set +e
|
||||
http=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$MULTI3\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128,\"env_vars\":{\"IDX\":\"3\"}}")
|
||||
if [[ "$http" == "201" ]]; then
|
||||
upload_code "$MULTI3" > /dev/null
|
||||
fi
|
||||
) &
|
||||
|
||||
wait # ждём все три create+upload
|
||||
|
||||
# Ждём пока контроллер выставит фазу (upload завершён, но reconcile асинхронный)
|
||||
sleep 15
|
||||
info " Проверяем что все три CRD созданы..."
|
||||
m1_phase=$(svc_phase "$MULTI1")
|
||||
m2_phase=$(svc_phase "$MULTI2")
|
||||
m3_phase=$(svc_phase "$MULTI3")
|
||||
info " $MULTI1: $m1_phase | $MULTI2: $m2_phase | $MULTI3: $m3_phase"
|
||||
|
||||
if [[ "$m1_phase" != "" && "$m2_phase" != "" && "$m3_phase" != "" ]]; then
|
||||
pass "все 3 сервиса созданы (фазы: $m1_phase / $m2_phase / $m3_phase)"
|
||||
else
|
||||
fail "не все сервисы созданы ($MULTI1=$m1_phase $MULTI2=$m2_phase $MULTI3=$m3_phase)"
|
||||
fi
|
||||
|
||||
info "4.2 Ждём пока $MULTI2 войдёт в Building, затем удалим его..."
|
||||
delete_done=false
|
||||
deadline_kill=$((SECONDS + 120))
|
||||
while [[ $SECONDS -lt $deadline_kill ]]; do
|
||||
phase2=$(svc_phase "$MULTI2")
|
||||
if [[ "$phase2" == "Building" ]]; then
|
||||
info " $MULTI2 в Building — удаляем!"
|
||||
check_code "DELETE $MULTI2 while Building" \
|
||||
"$(api_code DELETE "$API/services/$MULTI2")" \
|
||||
"204"
|
||||
delete_done=true
|
||||
break
|
||||
fi
|
||||
if [[ "$phase2" == "Ready" || "$phase2" == "Failed" ]]; then
|
||||
info " $MULTI2 уже $phase2 (пропустили Building) — удаляем всё равно"
|
||||
check_code "DELETE $MULTI2 (post-build)" \
|
||||
"$(api_code DELETE "$API/services/$MULTI2")" \
|
||||
"204"
|
||||
delete_done=true
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
if ! $delete_done; then
|
||||
info " $MULTI2 не нашли → видимо уже завершён, пробуем delete"
|
||||
api_code DELETE "$API/services/$MULTI2" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
info "4.3 Ждём Ready для $MULTI1 и $MULTI3 (300s)..."
|
||||
r1=0; r3=0
|
||||
wait_phase "$MULTI1" "Ready" 300 || r1=$?
|
||||
wait_phase "$MULTI3" "Ready" 300 || r3=$?
|
||||
|
||||
if [[ $r1 -eq 0 ]]; then pass "$MULTI1 → Ready"; else fail "$MULTI1 не Ready ($(svc_phase "$MULTI1"))"; fi
|
||||
if [[ $r3 -eq 0 ]]; then pass "$MULTI3 → Ready"; else fail "$MULTI3 не Ready ($(svc_phase "$MULTI3"))"; fi
|
||||
|
||||
# Ждём rollout у обоих чтобы pods READY перед параллельным invoke
|
||||
kubectl rollout status deployment/"$MULTI1" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1 || true
|
||||
kubectl rollout status deployment/"$MULTI3" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 3
|
||||
|
||||
info "4.4 $MULTI2 → 404 после удаления"
|
||||
check_code "$MULTI2 GET после удаления" \
|
||||
"$(api_code GET "$API/services/$MULTI2")" \
|
||||
"404"
|
||||
|
||||
info "4.5 Параллельный invoke: 20 запросов к $MULTI1 и $MULTI3 одновременно..."
|
||||
success=0; fail_count=0
|
||||
invoke_one() {
|
||||
local name="$1"
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{}' "$FN_BASE/$name")
|
||||
echo "$code"
|
||||
}
|
||||
|
||||
# Каждый subshell пишет в отдельный файл — нет race condition при параллельной записи
|
||||
results_dir=$(mktemp -d)
|
||||
for i in $(seq 1 20); do
|
||||
(
|
||||
r1_code=$(invoke_one "$MULTI1")
|
||||
r3_code=$(invoke_one "$MULTI3")
|
||||
echo "$r1_code" > "$results_dir/m1_${i}"
|
||||
echo "$r3_code" > "$results_dir/m3_${i}"
|
||||
) &
|
||||
done
|
||||
wait
|
||||
for f in "$results_dir"/*; do
|
||||
code=$(cat "$f")
|
||||
# Избегаем ((success++)) в && цепочке: post-increment при value=0 → exit 1 → || срабатывает
|
||||
if [[ "$code" == "200" ]]; then
|
||||
success=$((success + 1))
|
||||
else
|
||||
fail_count=$((fail_count + 1))
|
||||
fi
|
||||
done
|
||||
rm -rf "$results_dir"
|
||||
|
||||
if [[ $fail_count -eq 0 ]]; then
|
||||
pass "40 параллельных invoke: $success/40 → 200 ✓"
|
||||
else
|
||||
fail "40 параллельных invoke: $fail_count ошибок (${success}/40 OK)"
|
||||
fi
|
||||
|
||||
info "4.6 Удаляем $MULTI1 и $MULTI3..."
|
||||
check_code "DELETE $MULTI1" "$(api_code DELETE "$API/services/$MULTI1")" "204"
|
||||
check_code "DELETE $MULTI3" "$(api_code DELETE "$API/services/$MULTI3")" "204"
|
||||
# Убрать из CLEANUP_NAMES чтобы teardown не пытался удалить повторно
|
||||
CLEANUP_NAMES=()
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo -e "${BOLD}━━━ ИТОГИ ━━━${RESET}"
|
||||
echo ""
|
||||
echo " Всего тестов : $((PASS + FAIL))"
|
||||
echo " PASS : $PASS"
|
||||
echo " FAIL : $FAIL"
|
||||
if [[ $KNOWN_BUGS -gt 0 ]]; then
|
||||
echo -e " ${YELLOW}KNOWN BUGS : $KNOWN_BUGS${RESET} ← баги в коде оператора, не в тестах"
|
||||
fi
|
||||
echo ""
|
||||
if [[ $FAIL -eq 0 ]]; then
|
||||
echo -e " ${GREEN}✓ ВСЕ ТЕСТЫ ПРОШЛИ${RESET}"
|
||||
else
|
||||
echo -e " ${RED}✗ ЕСТЬ ПАДЕНИЯ — $FAIL тестов${RESET}"
|
||||
fi
|
||||
if [[ $KNOWN_BUGS -gt 0 ]]; then
|
||||
echo -e " ${YELLOW}⚠ НАЙДЕНЫ БАГИ В ОПЕРАТОРЕ — $KNOWN_BUGS штук${RESET}"
|
||||
fi
|
||||
echo ""
|
||||
Reference in New Issue
Block a user