#!/usr/bin/env bash # 2026-04-01 — test_lifecycle.sh # Гоняет реальный API Nubes: создание/удаление/модификация пользователей и БД. # Каждый шаг — отдельный terraform apply с живым выводом. # Запуск: bash test_lifecycle.sh set -uo pipefail DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DIR" GREEN="\033[0;32m"; RED="\033[0;31m"; YELLOW="\033[1;33m"; CYAN="\033[0;36m"; NC="\033[0m" PASS=0; FAIL=0 ok() { echo -e "\n${GREEN}>>> PASS${NC} $1"; PASS=$((PASS+1)); } fail() { echo -e "\n${RED}>>> FAIL${NC} $1"; FAIL=$((FAIL+1)); } section() { echo -e "\n${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n $1\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; } step() { echo -e "\n${CYAN}--- $1 ---${NC}"; } # run_apply — terraform apply с живым выводом в терминал. run_apply() { echo "" terraform apply -auto-approve return $? } # run_apply_expect_fail — apply должен упасть (ошибка API = успех теста). run_apply_expect_fail() { local label="$1" echo "" if terraform apply -auto-approve; then fail "$label — ожидали ошибку API, но apply прошёл!" else ok "$label — API вернул ошибку (ожидаемо)" fi } echo -e "\n${YELLOW}╔══════════════════════════════════════════════════════╗" echo "║ PG_TEST lifecycle — $(date '+%Y-%m-%d %H:%M:%S') ║" echo -e "╚══════════════════════════════════════════════════════╝${NC}" # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 0 — Очистка: destroy всего перед стартом" # ───────────────────────────────────────────────────────────────────────────── # Гарантируем чистый старт — убираем все ресурсы и state. step "terraform destroy (убираем всё что осталось от предыдущих прогонов)" terraform destroy -auto-approve || true # не падаем если уже пусто # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 1 — Создать: 2 пользователя + 2 БД + 1 app_user" # ───────────────────────────────────────────────────────────────────────────── step "terraform apply (postgres.tf + postgres_extra.tf)" if run_apply; then ok "Создание прошло" else fail "Создание упало — дальше не идём" exit 1 fi echo ""; echo "Ресурсы в state:"; terraform state list | grep -v pg_test_instance # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 2 — Удалить extra_user2 и extra_db2" # ───────────────────────────────────────────────────────────────────────────── step "Убираем test_extra_user2 и test_extra_db2 из tf" python3 - <<'PYEOF' import re, pathlib def comment_block(path, resource_type, resource_name): text = pathlib.Path(path).read_text() pattern = rf'(resource\s+"{re.escape(resource_type)}"\s+"{re.escape(resource_name)}"\s*\{{)' match = re.search(pattern, text) if not match: print(f" WARNING: {resource_type}.{resource_name} not found"); return start = match.start(); depth, i = 0, start while i < len(text): if text[i] == '{': depth += 1 elif text[i] == '}': depth -= 1 if depth == 0: end = i + 1; break i += 1 pathlib.Path(path).write_text( text[:start] + "/* DISABLED\n" + text[start:end] + "\nDISABLED */" + text[end:] ) print(f" скрыт: {resource_type}.{resource_name}") comment_block("postgres_extra.tf", "nubes_postgres_user", "test_extra_user2") comment_block("postgres_extra.tf", "nubes_postgres_database", "test_extra_db2") PYEOF step "terraform apply — API удаляет user2 и db2" if run_apply; then ok "Удаление прошло"; else fail "Удаление упало"; fi echo ""; echo "Ресурсы в state:"; terraform state list | grep -v pg_test_instance # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 3 — Воссоздать extra_user2 и extra_db2" # ───────────────────────────────────────────────────────────────────────────── step "Восстанавливаем tf" python3 - <<'PYEOF' import pathlib, re p = pathlib.Path("postgres_extra.tf") text = re.sub(r'/\* DISABLED\n', '', p.read_text()) text = re.sub(r'\nDISABLED \*/', '', text) p.write_text(text); print(" postgres_extra.tf восстановлен") PYEOF step "terraform apply — API воссоздаёт user2 и db2" if run_apply; then ok "Воссоздание прошло"; else fail "Воссоздание упало"; fi echo ""; echo "Ресурсы в state:"; terraform state list | grep -v pg_test_instance # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 4 — Модификация: сменить db_owner у extra_db1" # ───────────────────────────────────────────────────────────────────────────── step "db_owner test_extra_db1: extra_user1 → extra_user2" python3 - <<'PYEOF' import pathlib p = pathlib.Path("postgres_extra.tf") text = p.read_text().replace( 'nubes_postgres_user.test_extra_user1.username', 'nubes_postgres_user.test_extra_user2.username', 1) p.write_text(text); print(" db_owner: user1 → user2") PYEOF step "terraform apply — API обновляет db_owner" if run_apply; then ok "Смена db_owner прошла"; else fail "Смена db_owner упала"; fi step "Откат db_owner обратно (user2 → user1)" python3 - <<'PYEOF' import pathlib p = pathlib.Path("postgres_extra.tf") text = p.read_text().replace( 'nubes_postgres_user.test_extra_user2.username', 'nubes_postgres_user.test_extra_user1.username', 1) p.write_text(text); print(" db_owner: user2 → user1") PYEOF run_apply > /dev/null 2>&1 || true # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 5 — Невалидные параметры: ждём ошибку API" # ───────────────────────────────────────────────────────────────────────────── step "Тест 5a: db_owner = несуществующий пользователь" cat > ./pg_test_invalid.tf <<'TFEOF' resource "nubes_postgres_database" "test_invalid_owner" { postgres_id = nubes_postgres.pg_test_instance.id db_name = "invalid_owner_db" db_owner = "this_user_does_not_exist" adopt_existing_on_create = false } TFEOF run_apply_expect_fail "5a: db_owner='this_user_does_not_exist'" rm -f ./pg_test_invalid.tf; run_apply > /dev/null 2>&1 || true step "Тест 5b: role = несуществующая строка" cat > ./pg_test_invalid.tf <<'TFEOF' resource "nubes_postgres_user" "test_invalid_role" { postgres_id = nubes_postgres.pg_test_instance.id username = "invalid_role_user" role = "fantasy_role_xyz" adopt_existing_on_create = false } TFEOF run_apply_expect_fail "5b: role='fantasy_role_xyz'" rm -f ./pg_test_invalid.tf; run_apply > /dev/null 2>&1 || true # ───────────────────────────────────────────────────────────────────────────── section "ШАГ 6 — app_user пытается стать db_owner" # ───────────────────────────────────────────────────────────────────────────── # app_user — базовые права. Нельзя быть db_owner — это прерогатива ddl_user. step "Тест 6a: test_app_user (role=app_user) назначается db_owner" cat > ./pg_test_invalid.tf <<'TFEOF' resource "nubes_postgres_database" "test_appuser_as_owner" { postgres_id = nubes_postgres.pg_test_instance.id db_name = "appuser_owned_db" db_owner = nubes_postgres_user.test_app_user.username adopt_existing_on_create = false } TFEOF run_apply_expect_fail "6a: app_user как db_owner — API должен отклонить" rm -f ./pg_test_invalid.tf; run_apply > /dev/null 2>&1 || true # ───────────────────────────────────────────────────────────────────────────── section "ИТОГ" # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e " PASS: ${GREEN}${PASS}${NC} FAIL: ${RED}${FAIL}${NC}" echo "" [[ "$FAIL" -eq 0 ]] \ && echo -e "${GREEN}Все тесты прошли.${NC}" \ || echo -e "${RED}Есть ошибки — проверь вывод выше.${NC}"