Add PG_TEST example - PostgreSQL testing suite

Example Terraform configuration for testing PostgreSQL integration:
- main.tf: VPC and database setup
- postgres.tf: Database resource definitions
- outputs.tf: Output values for connection
- test_basic.sh: Basic connectivity tests
- test_lifecycle.sh: Full lifecycle testing
- terraform.tfvars.example: Configuration template
- .gitignore: Ignore sensitive data and terraform artifacts
This commit is contained in:
Repinoid 2026-04-04 08:38:55 +03:00
parent f8fe790bb4
commit 333093ab6c
7 changed files with 511 additions and 0 deletions

21
PG_TEST/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Terraform provider plugins
.terraform/
.terraform.lock.hcl
# Terraform state
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.backup
# Sensitive data
terraform.tfvars
!terraform.tfvars.example
# Backup files
*.bak
*.bak_db
*.bak_*
# Test artifacts
test_*.log

59
PG_TEST/main.tf Normal file
View File

@ -0,0 +1,59 @@
// 2026-04-01 main.tf: провайдеры и объявления переменных.
// Этот файл не нужно редактировать. Все настройки в terraform.tfvars.
terraform {
required_providers {
nubes = {
source = "terra.k8c.ru/nubes/nubes"
version = "5.0.55"
}
}
}
// Объявления переменных
// Значения задаются в terraform.tfvars не трогать этот файл.
variable "api_token" {
type = string
sensitive = true
description = "Nubes API token"
}
variable "s3_uid" {
type = string
sensitive = true
description = "UUID S3-bucket для бэкапов PostgreSQL"
}
variable "realm" {
type = string
description = "Realm — идентификатор зоны/проекта в Nubes"
}
variable "pg_resource_name" {
type = string
description = "Имя инстанса PostgreSQL (уникально в рамках realm)"
}
variable "pg_username" {
type = string
description = "Имя пользователя PostgreSQL"
}
variable "pg_db_name" {
type = string
description = "Имя создаваемой базы данных"
}
variable "pg_role" {
type = string
description = "Роль пользователя"
}
// Провайдер
provider "nubes" {
api_token = var.api_token
log_level = "debug"
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
}

42
PG_TEST/outputs.tf Normal file
View File

@ -0,0 +1,42 @@
// 2026-04-01 outputs.tf: данные подключения к PostgreSQL после apply.
//
// Пароль не выводим напрямую только через sensitive output (не появляется
// в логах CI по умолчанию). Для явного показа: terraform output pg_password
output "pg_instance_id" {
description = "ID инстанса PostgreSQL в Nubes"
value = nubes_postgres.pg_test_instance.id
}
output "pg_host" {
description = "Внутренний адрес master-ноды PostgreSQL"
value = local.pg_host
}
output "pg_port" {
description = "Порт PostgreSQL"
value = local.pg_port
}
output "pg_database" {
description = "Имя базы данных"
value = nubes_postgres_database.pg_test_db.db_name
}
output "pg_username" {
description = "Имя пользователя PostgreSQL"
value = nubes_postgres_user.pg_test_user.username
}
output "pg_password" {
description = "Пароль пользователя из vault_secrets (пустой на первом apply — заполнится на следующем)"
value = local.pg_password
sensitive = true
}
// Удобная строка подключения для psql или приложений.
output "pg_dsn" {
description = "DSN для подключения: postgresql://user:pass@host:port/db"
value = "postgresql://${nubes_postgres_user.pg_test_user.username}:${local.pg_password}@${local.pg_host}:${local.pg_port}/${nubes_postgres_database.pg_test_db.db_name}"
sensitive = true
}

99
PG_TEST/postgres.tf Normal file
View File

@ -0,0 +1,99 @@
// 2026-04-01 postgres.tf: Managed PostgreSQL инстанс, пользователь и база данных.
//
// Порядок создания:
// 1. nubes_postgres сам инстанс PostgreSQL
// 2. nubes_postgres_user пользователь; пароль автоматически попадает в vault_secrets
// 3. nubes_postgres_database база данных с owner = созданный пользователь
//
// Важно: vault_secrets["users"] появляется только ПОСЛЕ первого apply (нет пользователя нет ключа).
// try() в locals страхует от ошибки на первом прогоне.
// Locals: credentials из vault
locals {
# Карта username{password, username} из vault_secrets, который Nubes заполняет после
# создания пользователя. try() нужен для первого apply, когда ключа ещё нет.
pg_creds_map = try(
jsondecode(lookup(nubes_postgres.pg_test_instance.vault_secrets, "users", "{}")),
{}
)
pg_password = try(local.pg_creds_map[var.pg_username]["password"], "")
# Адрес master-ноды (внутренний для подключения из кластера).
pg_host = nubes_postgres.pg_test_instance.state_out_flat["internalConnect.master"]
pg_port = 5432
}
// Инстанс PostgreSQL
resource "nubes_postgres" "pg_test_instance" {
resource_name = var.pg_resource_name
s3_uid = var.s3_uid
resource_realm = var.realm
# Минимальные ресурсы достаточно для тестирования.
resource_instances = 1
resource_memory = 512 # MiB
resource_c_p_u = 500 # millicores
resource_disk = "1" # GiB
app_version = "17"
# json_parameters убран при передаче пустого объекта API возвращает "Invalid JSON String".
# Если нужны кастомные параметры PG добавить после диагностики.
# Pooler не нужен для тестов упрощает топологию.
enable_pg_pooler_master = false
enable_pg_pooler_slave = false
allow_no_s_s_l = false
auto_scale = false
auto_scale_percentage = 10
auto_scale_tech_window = 0
auto_scale_quota_gb = "1"
# Внешний адрес не нужен подключаемся изнутри кластера.
need_external_address_master = false
operation_timeout = "11m"
# Позволяет импортировать уже существующий инстанс с тем же именем, не падая
# с "already exists" удобно при повторном apply после ручного создания.
adopt_existing_on_create = true
}
// Пользователь
resource "nubes_postgres_user" "pg_test_user" {
postgres_id = nubes_postgres.pg_test_instance.id
username = var.pg_username
role = var.pg_role
# Не падать если пользователь с таким именем уже существует.
adopt_existing_on_create = true
}
resource "nubes_postgres_user" "pg_test_user3" {
postgres_id = nubes_postgres.pg_test_instance.id
username = "u3"
role = var.pg_role
depends_on = [nubes_postgres_user.pg_test_user]
# Не падать если пользователь с таким именем уже существует.
adopt_existing_on_create = true
}
// База данных
resource "nubes_postgres_database" "pg_test_db" {
postgres_id = nubes_postgres.pg_test_instance.id
db_name = var.pg_db_name
db_owner = nubes_postgres_user.pg_test_user.username
# Не падать если БД уже существует.
adopt_existing_on_create = true
# ВАЖНО: из-за ограничения API Nubes (ERR-PG-08: "Concurrent operations are not supported")
# нужно явно ждать пользователя даже если он не выглядит dependency.
# других ресурс на инстансе ещё обрабатывает операции.
depends_on = [nubes_postgres_user.pg_test_user3]
}

View File

@ -0,0 +1,54 @@
# =============================================================================
# 2026-04-01 — terraform.tfvars
#
# ЕДИНСТВЕННЫЙ файл, который нужно заполнить перед запуском.
# Остальные .tf-файлы не трогать.
#
# Как запустить:
# 1. Скопировать этот файл: cp terraform.tfvars.example terraform.tfvars
# 2. Заполнить три обязательных поля ниже (ЗАПОЛНИТЬ)
# 3. terraform init
# 4. terraform apply
#
# После apply — увидеть данные подключения:
# terraform output pg_host
# terraform output pg_database
# terraform output pg_username
# terraform output -raw pg_password # пароль (показывается явно только с -raw)
# terraform output -raw pg_dsn # полная строка подключения
# =============================================================================
# =============================================================================
# ОБЯЗАТЕЛЬНО ЗАПОЛНИТЬ
# =============================================================================
# API-токен из личного кабинета Nubes.
# Где взять: https://deck-test.ngcloud.ru/ → Профиль → API-токены
api_token = "ЗАПОЛНИТЬ"
# UUID вашего S3-бакета — нужен PostgreSQL для хранения бэкапов.
# Пример: "332cdb0d-****-43bf-****-4adcc3b5****"
s3_uid = "ЗАПОЛНИТЬ"
# Realm — идентификатор вашей зоны/проекта.
# Пример: "k8s-3-sandbox-nubes-ru"
realm = "ЗАПОЛНИТЬ"
# =============================================================================
# МОЖНО ОСТАВИТЬ КАК ЕСТЬ (изменить при необходимости)
# =============================================================================
# Имя PostgreSQL-инстанса в Nubes.
# Должно быть уникальным в рамках realm. Менять если создаёте несколько стендов.
pg_resource_name = "pg-test-01"
# Имя пользователя базы данных.
pg_username = "pgtest_user"
# Имя базы данных.
pg_db_name = "pgtest_db"
# Роль пользователя.
pg_role = "ddl_user"

49
PG_TEST/test_basic.sh Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
# 2026-04-01 — test_basic.sh: простая проверка что ресурсы созданы и outputs заполнены.
# Не делает apply/destroy — только читает state и outputs.
# Запуск: bash test_basic.sh
set -uo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR"
GREEN="\033[0;32m"; RED="\033[0;31m"; NC="\033[0m"
PASS=0; FAIL=0
ok() { echo -e "${GREEN}PASS${NC} $1"; PASS=$((PASS+1)); }
fail() { echo -e "${RED}FAIL${NC} $1"; FAIL=$((FAIL+1)); }
echo "=== PG_TEST basic check — $(date '+%Y-%m-%d %H:%M:%S') ==="
echo ""
# ── 1. Нужные ресурсы есть в state ───────────────────────────────────────────
echo "--- state ---"
for res in \
"nubes_postgres.pg_test_instance" \
"nubes_postgres_user.pg_test_user" \
"nubes_postgres_database.pg_test_db"
do
if terraform state show "$res" > /dev/null 2>&1; then
ok "state: $res"
else
fail "state: $res — не найден"
fi
done
echo ""
# ── 2. Outputs непустые ───────────────────────────────────────────────────────
echo "--- outputs ---"
pg_host=$(terraform output -raw pg_host 2>/dev/null || true)
pg_db=$(terraform output -raw pg_database 2>/dev/null || true)
pg_user=$(terraform output -raw pg_username 2>/dev/null || true)
pg_pass=$(terraform output -raw pg_password 2>/dev/null || true)
[[ -n "$pg_host" ]] && ok "pg_host = $pg_host" || fail "pg_host пустой"
[[ -n "$pg_db" ]] && ok "pg_database = $pg_db" || fail "pg_database пустой"
[[ -n "$pg_user" ]] && ok "pg_username = $pg_user" || fail "pg_username пустой"
[[ -n "$pg_pass" ]] && ok "pg_password непустой" || fail "pg_password пустой (возможно нужен повторный apply)"
echo ""
echo "=== Итог: PASS=$PASS FAIL=$FAIL ==="

187
PG_TEST/test_lifecycle.sh Normal file
View File

@ -0,0 +1,187 @@
#!/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}"