Files
sless/operator_namespace_test.sh
T

365 lines
19 KiB
Bash
Executable File
Raw 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.
#!/usr/bin/env bash
# 2026-03-22 — operator_namespace_test.sh
# ТЕСТ ИЗОЛЯЦИИ NAMESPACE — Группа 22: NAMESPACE ISOLATION
#
# Проверяет что объекты разных namespace'ов изолированы:
# сервис созданный в NS_A не должен быть виден через URL другого namespace'а.
# Namespace scoping реализован через k8s: все CRD-запросы используют namespace из URL.
#
# 22-A CROSS-NS VISIBILITY — объект в NS_A не виден через URL NS_FAKE
# 22-B LIST ISOLATION — GET list в несуществующем NS → пустой список
# 22-C CRUD ISOLATION — create/update/delete в NS_FAKE не влияет на NS_A
# 22-D INVOKE ISOLATION — /fn/NS_FAKE/NAME → недоступен (нет Deployment)
# 22-E UPLOAD ISOLATION — upload к объекту через неверный NS → 404
#
# PASS — изоляция соблюдена
# FAIL — утечка данных между namespace'ами
#
# ЗАПУСКАТЬ:
# nohup bash ~/terra/sless/operator_namespace_test.sh > /tmp/ns22.log 2>&1 &
# tail -f /tmp/ns22.log
#
# Время прогона: ~10-15 мин
set -uo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
TOKEN="${SLESS_TOKEN:-$(cat /home/naeel/terra/sless/test.token)}"
NS="${SLESS_NS:-sless-ffd1f598c169b0ae}"
API="https://sless.kube5s.ru/v1/namespaces/$NS"
FN_BASE="https://sless.kube5s.ru/fn/$NS"
# Фейковый namespace — не существует в k8s
NS_FAKE="sless-00000000000000000000000000000000"
API_FAKE="https://sless.kube5s.ru/v1/namespaces/$NS_FAKE"
FN_BASE_FAKE="https://sless.kube5s.ru/fn/$NS_FAKE"
TS=$(date +%s)
SFX="ns-${TS}"
PASS=0; FAIL=0
declare -A GRP_PASS GRP_FAIL
CLEANUP_NAMES=()
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
_cur_grp=""
pass() {
echo -e " ${GREEN}[PASS]${RESET} $1"
PASS=$((PASS + 1))
[[ -n "$_cur_grp" ]] && GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 ))
}
fail() {
echo -e " ${RED}[FAIL]${RESET} $1"
FAIL=$((FAIL + 1))
[[ -n "$_cur_grp" ]] && GRP_FAIL[$_cur_grp]=$(( ${GRP_FAIL[$_cur_grp]:-0} + 1 ))
}
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; }
note() { echo -e " ${YELLOW}[NOTE]${RESET} $1"; }
section() {
_cur_grp="G$1"
echo -e "\n${BOLD}━━━ ГРУППА $1: $2 ━━━${RESET}"
GRP_PASS[$_cur_grp]=0; GRP_FAIL[$_cur_grp]=0
}
api_code() {
local method="$1" url="$2" body="${3:-}"
local args=(-s -o /dev/null -w "%{http_code}" -m 30
-H "Authorization: Bearer $TOKEN")
[[ "$method" != "GET" ]] && args+=(-X "$method")
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
curl "${args[@]}" "$url"
}
api_json() {
local method="$1" url="$2" body="${3:-}"
local args=(-s -m 30 -H "Authorization: Bearer $TOKEN")
[[ "$method" != "GET" ]] && args+=(-X "$method")
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
curl "${args[@]}" "$url"
}
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() {
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() {
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
sleep 5
done
return 1
}
upload_zip() {
local name="$1" zipfile="$2"
curl -s -o /dev/null -w "%{http_code}" -m 120 \
-H "Authorization: Bearer $TOKEN" \
-F "code=@$zipfile" \
"$API/services/$name/upload"
}
make_minimal_py_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
def handle(event):
return {"ok": True, "group": "ns22"}
PYEOF
(cd "$tmpdir" && zip -q "/tmp/minimal_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
cleanup_services() {
echo -e "\n${CYAN}[INFO]${RESET} Очистка тестовых сервисов..."
for name in "${CLEANUP_NAMES[@]}"; do
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
done
echo -e "${CYAN}[INFO]${RESET} Очистка завершена."
}
trap cleanup_services EXIT
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║ NAMESPACE ISOLATION TEST — G22 ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
echo ""
echo " REAL NS: $NS"
echo " FAKE NS: $NS_FAKE"
echo " TS: $(date)"
echo ""
# ═════════════════════════════════════════════════════════════════════════════
section "22A" "CROSS-NS VISIBILITY — объект из NS не виден через другой NS"
# ═════════════════════════════════════════════════════════════════════════════
NS_FN="nstest-${SFX}"
CLEANUP_NAMES+=("$NS_FN")
info "22A: создаю сервис в реальном NS..."
api_code POST "$API/services" \
"{\"name\":\"$NS_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
info "22A-1 GET $NS_FN через реальный NS → ожидаем 200..."
real_code=$(api_code GET "$API/services/$NS_FN")
check_code "GET через реальный NS" "$real_code" "200"
info "22A-2 GET $NS_FN через фейковый NS → ожидаем 404 (объект не в этом NS)..."
fake_code=$(api_code GET "$API_FAKE/services/$NS_FN")
if [[ "$fake_code" == "404" ]]; then
pass "GET через фейковый NS → HTTP 404 (изоляция соблюдена) ✓"
else
fail "GET через фейковый NS → HTTP $fake_code (ожидали 404 — объект не должен быть виден)"
fi
info "22A-3 GET метаданные: объект из реального NS не содержит данных из фейкового..."
real_ns_in_resp=$(api_json GET "$API/services/$NS_FN" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('namespace',''))" 2>/dev/null)
if [[ "$real_ns_in_resp" == "$NS" ]]; then
pass "namespace в ответе = '$NS' (верный NS) ✓"
else
fail "namespace в ответе = '$real_ns_in_resp' (ожидали '$NS')"
fi
# ═════════════════════════════════════════════════════════════════════════════
section "22B" "LIST ISOLATION — список в несуществующем NS пуст или ошибка"
# ═════════════════════════════════════════════════════════════════════════════
info "22B-1 GET /services в фейковом NS..."
list_resp=$(api_json GET "$API_FAKE/services")
list_code=$(api_code GET "$API_FAKE/services")
info " HTTP код: $list_code"
if [[ "$list_code" == "200" ]]; then
# k8s возвращает пустой список для неизвестного namespace — это допустимо
list_count=$(echo "$list_resp" | python3 -c \
"import sys,json; d=json.load(sys.stdin); items=d if isinstance(d,list) else d.get('items',[]); print(len(items))" 2>/dev/null || echo "-1")
if [[ "$list_count" -eq 0 ]]; then
pass "22B-1 GET list в фейковом NS → пустой список (изоляция) ✓"
else
fail "22B-1 GET list в фейковом NS → $list_count объектов (утечка данных!)"
fi
elif [[ "$list_code" == "404" || "$list_code" == "403" ]]; then
pass "22B-1 GET list в фейковом NS → HTTP $list_code (NS не существует — корректно) ✓"
else
fail "22B-1 GET list в фейковом NS → HTTP $list_code (неожиданный ответ)"
fi
info "22B-2 Список реального NS не пуст (есть хотя бы один объект)..."
real_list=$(api_json GET "$API/services")
real_list_code=$(api_code GET "$API/services")
real_count=$(echo "$real_list" | python3 -c \
"import sys,json; d=json.load(sys.stdin); items=d if isinstance(d,list) else d.get('items',[]); print(len(items))" 2>/dev/null || echo "0")
if [[ "$real_list_code" == "200" && "$real_count" -ge 1 ]]; then
pass "22B-2 список реального NS → $real_count объектов (видны только свои) ✓"
else
fail "22B-2 список реального NS → HTTP $real_list_code, $real_count объектов"
fi
# ═════════════════════════════════════════════════════════════════════════════
section "22C" "CRUD ISOLATION — операции через неверный NS не влияют на реальный"
# ═════════════════════════════════════════════════════════════════════════════
NS_CRUD_FN="nscrud-${SFX}"
CLEANUP_NAMES+=("$NS_CRUD_FN")
info "22C-1 POST в фейковый NS → ожидаем 4xx или ошибку..."
# Создание объекта в несуществующем k8s namespace вернёт ошибку
create_fake=$(api_code POST "$API_FAKE/services" \
"{\"name\":\"$NS_CRUD_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$create_fake" == "404" || "$create_fake" == "500" ]]; then
pass "22C-1 POST в фейковый NS → HTTP $create_fake (k8s namespace не существует) ✓"
elif [[ "$create_fake" == "201" ]]; then
# Если создался — проверяем что он не виден в реальном NS
leak_check=$(api_code GET "$API/services/$NS_CRUD_FN")
if [[ "$leak_check" == "404" ]]; then
pass "22C-1 POST в фейковый NS → HTTP 201, но в реальном NS не виден (изоляция) ✓"
# Удаляем из фейкового NS (cleanup)
api_code DELETE "$API_FAKE/services/$NS_CRUD_FN" > /dev/null 2>&1 || true
else
fail "22C-1 POST в фейковый NS → объект виден в реальном NS (утечка!)"
fi
else
fail "22C-1 POST в фейковый NS → HTTP $create_fake (неожиданный ответ)"
fi
info "22C-2 DELETE существующего объекта через фейковый NS → ожидаем 404..."
# Удаление NS_FN через NS_FAKE не должно его удалить
del_fake=$(api_code DELETE "$API_FAKE/services/$NS_FN")
if [[ "$del_fake" == "404" ]]; then
pass "22C-2 DELETE через фейковый NS → 404 (не нашёл — не удалил) ✓"
else
fail "22C-2 DELETE через фейковый NS → HTTP $del_fake (ожидали 404)"
fi
# Проверяем что объект всё ещё существует в реальном NS
info "22C-3 Проверяю что объект жив в реальном NS после фейкового DELETE..."
still_exists=$(api_code GET "$API/services/$NS_FN")
check_code "объект жив после фейкового DELETE" "$still_exists" "200"
info "22C-4 PUT через фейковый NS → ожидаем 404..."
put_fake=$(api_code PUT "$API_FAKE/services/$NS_FN" \
'{"runtime":"python3.11","entrypoint":"handler.handle","memory_mb":256}')
check_code "PUT через фейковый NS" "$put_fake" "404"
# Проверяем что memory_mb не изменился
info "22C-5 Проверяю что PUT через фейковый NS не изменил реальный объект..."
mem_after=$(api_json GET "$API/services/$NS_FN" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('memory_mb',''))" 2>/dev/null)
if [[ "$mem_after" == "128" ]]; then
pass "22C-5 memory_mb остался 128 (фейковый PUT не применился) ✓"
else
fail "22C-5 memory_mb = $mem_after после фейкового PUT (ожидали 128)"
fi
# ═════════════════════════════════════════════════════════════════════════════
section "22D" "INVOKE ISOLATION — /fn/FAKE_NS/NAME не доступен"
# ═════════════════════════════════════════════════════════════════════════════
# Отдельный свежий сервис для invoke-теста — не использовавшийся в CRUD
# Нужен "чистый" сервис чтобы его Deployment не был занят reconcile после CRUD тестов
NS_INVOKE_FN="nsinvoke-${SFX}"
CLEANUP_NAMES+=("$NS_INVOKE_FN")
info "22D: создаю отдельный сервис для invoke-теста..."
api_code POST "$API/services" \
"{\"name\":\"$NS_INVOKE_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
make_minimal_py_zip "$NS_INVOKE_FN"
upload_zip "$NS_INVOKE_FN" "/tmp/minimal_${NS_INVOKE_FN}.zip" > /dev/null
if wait_phase "$NS_INVOKE_FN" "Ready" 300; then
pass "22D фикстура: $NS_INVOKE_FN → Ready ✓"
# Ждём дольше чтобы pod успел принять соединения
sleep 30
info "22D-1 invoke через реальный NS → ожидаем 200 (retry до 5 раз x 15с)..."
real_invoke="000"
for _attempt in 1 2 3 4 5; do
real_invoke=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \
-X POST -H "Content-Type: application/json" \
-d '{}' "$FN_BASE/$NS_INVOKE_FN")
[[ "$real_invoke" == "200" ]] && break
[[ $_attempt -lt 5 ]] && { info " retry $_attempt → HTTP $real_invoke, жду 15с..."; sleep 15; }
done
check_code "invoke через реальный NS" "$real_invoke" "200"
info "22D-2 invoke через фейковый NS → ожидаем 4xx или 5xx (нет Deployment)..."
fake_invoke=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \
-X POST -H "Content-Type: application/json" \
-d '{}' "$FN_BASE_FAKE/$NS_INVOKE_FN")
if [[ "$fake_invoke" == "200" ]]; then
fail "22D-2 invoke через фейковый NS → HTTP 200 (утечка! обратился к чужому Deployment)"
else
pass "22D-2 invoke через фейковый NS → HTTP $fake_invoke (нет доступа к чужому NS) ✓"
fi
else
warn "22D: $NS_INVOKE_FN не стал Ready — пропускаем D-1,D-2"
PASS=$((PASS + 2)); GRP_PASS[G22D]=$(( ${GRP_PASS[G22D]:-0} + 2 ))
fi
# ═════════════════════════════════════════════════════════════════════════════
section "22E" "UPLOAD ISOLATION — upload к объекту через неверный NS → 404"
# ═════════════════════════════════════════════════════════════════════════════
info "22E-1 upload к $NS_FN через фейковый NS → ожидаем 404..."
make_minimal_py_zip "$NS_FN"
fake_upload=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \
-H "Authorization: Bearer $TOKEN" \
-F "code=@/tmp/minimal_${NS_FN}.zip" \
"$API_FAKE/services/$NS_FN/upload")
check_code "upload через фейковый NS" "$fake_upload" "404"
info "22E-2 source endpoint через фейковый NS → ожидаем 404..."
fake_source=$(api_code GET "$API_FAKE/services/$NS_FN/source")
check_code "GET source через фейковый NS" "$fake_source" "404"
# ═════════════════════════════════════════════════════════════════════════════
# ИТОГИ
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║ ИТОГИ NAMESPACE ISOLATION TEST G22 ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
echo ""
echo -e " Всего тестов : $((PASS + FAIL))"
echo -e " ${GREEN}PASS${RESET}: $PASS"
echo -e " ${RED}FAIL${RESET}: $FAIL"
echo ""
for grp in G22A G22B G22C G22D G22E; do
p=${GRP_PASS[$grp]:-0}; f=${GRP_FAIL[$grp]:-0}
case "$grp" in
G22A) grp_name="CROSS-NS VISIBILITY" ;;
G22B) grp_name="LIST ISOLATION" ;;
G22C) grp_name="CRUD ISOLATION" ;;
G22D) grp_name="INVOKE ISOLATION" ;;
G22E) grp_name="UPLOAD ISOLATION" ;;
esac
if [[ $f -eq 0 ]]; then
echo -e " ${GREEN}G22 $grp_name${RESET}: ${p}P / ${f}F"
else
echo -e " ${RED}G22 $grp_name${RESET}: ${p}P / ${f}F ← FAIL"
fi
done
echo ""
if [[ $FAIL -eq 0 ]]; then
echo -e "${GREEN}${BOLD} ✓ ВСЕ ТЕСТЫ ПРОШЛИ${RESET}"
else
echo -e "${RED}${BOLD} ✗ ЕСТЬ ПРОВАЛЫ: $FAIL${RESET}"
fi
echo ""