test: G17/G18/G19/G20/G22 - 154/154 PASS
This commit is contained in:
Executable
+364
@@ -0,0 +1,364 @@
|
||||
#!/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 ""
|
||||
Reference in New Issue
Block a user