Compare commits
8 Commits
feat/stres
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 333093ab6c | |||
|
|
f8fe790bb4 | ||
|
|
00fce3919a | ||
|
|
3978241851 | ||
|
|
6cb94e07ea | ||
|
|
82c9e44b1a | ||
|
|
cf5ebc0b70 | ||
|
|
5f7b8ec324 |
36
.gitignore
vendored
36
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
# Created: 2026-03-11
|
# Created: 2026-03-11 / Updated: 2026-03-30
|
||||||
# Purpose: ignore generated artifacts for the `examples` repository
|
# Purpose: ignore generated artifacts and internal files for the `examples` repository
|
||||||
|
|
||||||
# Terraform
|
# Terraform
|
||||||
.terraform/
|
.terraform/
|
||||||
@ -16,6 +16,7 @@ crash.log
|
|||||||
# Provider plugins / caches
|
# Provider plugins / caches
|
||||||
.terraform.d/
|
.terraform.d/
|
||||||
|
|
||||||
|
# tfvars содержат секреты (токены, ключи) — пользователь создаёт из .template
|
||||||
*.tfvars
|
*.tfvars
|
||||||
|
|
||||||
# Archives and build artifacts
|
# Archives and build artifacts
|
||||||
@ -39,3 +40,34 @@ venv/
|
|||||||
.env
|
.env
|
||||||
*.local
|
*.local
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# ---- SSH-ключи (секретные данные, у каждого пользователя свои) ----
|
||||||
|
vm_key
|
||||||
|
vm_key.pub
|
||||||
|
**/vm_key
|
||||||
|
**/vm_key.pub
|
||||||
|
*.pem
|
||||||
|
id_ed25519
|
||||||
|
id_rsa
|
||||||
|
|
||||||
|
# ---- Внутренние тестовые и служебные скрипты (не для пользователей) ----
|
||||||
|
# VM
|
||||||
|
VM/vm_stress_test.sh
|
||||||
|
VM/.vm_stress_test.sh.OLD
|
||||||
|
VM/VM_TEST_README.md
|
||||||
|
|
||||||
|
# POSTGRES
|
||||||
|
POSTGRES/vm_stress_test.sh
|
||||||
|
POSTGRES/stress_test.sh
|
||||||
|
POSTGRES/stress_destroy_apply.sh.disabled
|
||||||
|
POSTGRES/full_test.sh
|
||||||
|
POSTGRES/bug_hunter.sh
|
||||||
|
POSTGRES/chaos_marathon.sh
|
||||||
|
POSTGRES/test_cache_matrix.sh
|
||||||
|
POSTGRES/deploy_and_run_chaos.sh
|
||||||
|
POSTGRES/scripts/
|
||||||
|
|
||||||
|
# ---- Примеры в разработке (временно скрыты) ----
|
||||||
|
POSTGRES/
|
||||||
|
NODEJS/
|
||||||
|
DEVfromGround/
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
// 2026-03-26 — main.tf: провайдер Nubes для DEV-стенда.
|
|
||||||
// DEV API endpoint: https://deck-api-dev.ngcloud.ru/api/v1
|
|
||||||
// Токен: secrets/dev.token (tazet@narod.ru)
|
|
||||||
|
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
nubes = {
|
|
||||||
source = "terra.k8c.ru/nubes/nubes"
|
|
||||||
version = "5.0.31"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "api_token" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
description = "Nubes API токен (DEV-стенд). Значение — в terraform.tfvars."
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "resource_realm" {
|
|
||||||
type = string
|
|
||||||
description = "Платформа развёртывания (например k8s-3.ext.nubes.ru). Уточнить у сервис-менеджера."
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "nubes" {
|
|
||||||
api_token = var.api_token
|
|
||||||
api_endpoint = "https://deck-api-dev.ngcloud.ru/api/v1/index.cfm"
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
// 2026-03-26 — vc_org.tf: ресурс «Организация в Cloud Director» для DEV-стенда.
|
|
||||||
// nubes_vc_org — тенант vCloud Director (organization_type = "iaas").
|
|
||||||
// resource_realm задаётся через переменную (terraform.tfvars или -var).
|
|
||||||
|
|
||||||
resource "nubes_vc_org" "dev_org" {
|
|
||||||
resource_name = "vcOrg-2"
|
|
||||||
resource_realm = var.resource_realm
|
|
||||||
|
|
||||||
# organization_type "iaas" — единственный вариант с доступом к организации.
|
|
||||||
# Значение по умолчанию "iaas", явно прописано для читаемости.
|
|
||||||
organization_type = "iaas"
|
|
||||||
|
|
||||||
# v_i_p_configure — JSON-список ipSpaces для операции modify.
|
|
||||||
# При create провайдер не передаёт его в API, но требует non-null значение в плане.
|
|
||||||
v_i_p_configure = ""
|
|
||||||
|
|
||||||
# adopt_existing_on_create = true — берёт существующий инстанс (dev-org-sless-demo уже создан с null realm от предыдущей попытки).
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
|
|
||||||
# suspend_on_destroy = true (по умолчанию) — при destroy инстанс уходит в Suspend, не удаляется.
|
|
||||||
suspend_on_destroy = true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ─── Outputs ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
output "dev_org_id" {
|
|
||||||
description = "ID созданной организации (используется в зависимых ресурсах)"
|
|
||||||
value = nubes_vc_org.dev_org.id
|
|
||||||
}
|
|
||||||
|
|
||||||
output "dev_org_state_flat" {
|
|
||||||
description = "Плоский state организации — endpoints, статусы"
|
|
||||||
value = nubes_vc_org.dev_org.state_out_flat
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
// Создано: 2026-03-23
|
|
||||||
// main.tf — провайдер Nubes + переменные для примера NODEJS.
|
|
||||||
// Ресурс nubes_nodejs: managed Node.js приложение в облаке (не sless-функция).
|
|
||||||
|
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
nubes = {
|
|
||||||
source = "terra.k8c.ru/nubes/nubes"
|
|
||||||
version = "5.0.19"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "api_token" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "realm" {
|
|
||||||
type = string
|
|
||||||
description = "resource_realm — зона размещения ресурса (например: k8s-3-sandbox-nubes-ru)"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "git_path" {
|
|
||||||
type = string
|
|
||||||
description = "URL git-репозитория с кодом приложения"
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "nubes" {
|
|
||||||
api_token = var.api_token
|
|
||||||
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# Создано: 2026-03-23
|
|
||||||
# nodejs.tf — ресурс nubes_nodejs: managed Node.js приложение.
|
|
||||||
# Параметры взяты из документации terra.k8c.ru/docs/nubes/nubes/5.0.19/30_registry/resources/nodejs_params_create/
|
|
||||||
|
|
||||||
resource "nubes_nodejs" "app" {
|
|
||||||
resource_name = "nodejsdemo1"
|
|
||||||
domain = "domma"
|
|
||||||
resource_realm = var.realm
|
|
||||||
git_path = var.git_path
|
|
||||||
app_version = "23"
|
|
||||||
resource_c_p_u = 500
|
|
||||||
resource_memory = 1024
|
|
||||||
resource_instances = 1
|
|
||||||
json_env = jsonencode({})
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
# health_path не задан — используется дефолтный /
|
|
||||||
}
|
|
||||||
|
|
||||||
output "nodejs_domain" {
|
|
||||||
description = "Домен развёрнутого Node.js приложения"
|
|
||||||
value = nubes_nodejs.app.domain
|
|
||||||
}
|
|
||||||
21
PG_TEST/.gitignore
vendored
Normal file
21
PG_TEST/.gitignore
vendored
Normal 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
59
PG_TEST/main.tf
Normal 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
42
PG_TEST/outputs.tf
Normal 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
99
PG_TEST/postgres.tf
Normal 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]
|
||||||
|
}
|
||||||
54
PG_TEST/terraform.tfvars.example
Normal file
54
PG_TEST/terraform.tfvars.example
Normal 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
49
PG_TEST/test_basic.sh
Normal 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
187
PG_TEST/test_lifecycle.sh
Normal 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}"
|
||||||
@ -1,100 +0,0 @@
|
|||||||
# POSTGRES — Пример: Serverless-функции с Managed PostgreSQL
|
|
||||||
|
|
||||||
Демонстрирует интеграцию sless (serverless functions) с управляемым PostgreSQL (nubes_postgres).
|
|
||||||
|
|
||||||
## Что делает этот пример
|
|
||||||
|
|
||||||
1. **Создаёт Managed PostgreSQL** через Terraform (nubes_postgres + nubes_postgres_user + nubes_postgres_database)
|
|
||||||
2. **Инициализирует БД**: одноразовый `sless_job` создаёт таблицу `terraform_demo_table`
|
|
||||||
3. **Запускает 3 HTTP-сервиса**:
|
|
||||||
- `pg-info` (Node.js 20) — версия PostgreSQL-сервера + количество строк в таблице
|
|
||||||
- `pg-table-reader` (Python 3.11) — чтение всех строк из таблицы
|
|
||||||
- `pg-table-writer` (Python 3.11) — добавление новой строки
|
|
||||||
|
|
||||||
## Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
POSTGRES/
|
|
||||||
├── main.tf # terraform + провайдеры (sless, nubes_cloud)
|
|
||||||
├── postgres.tf # Managed PostgreSQL: DB, пользователь, locals с credentials
|
|
||||||
├── resources.tf # Namespace и сетевые ресурсы
|
|
||||||
├── functions.tf # sless_job (init) + 3 x sless_service
|
|
||||||
├── terraform.tfvars # Переменные: realm, s3_uid, token
|
|
||||||
├── stress_test.sh # Стресс-тест функций (не трогает PG lifecycle)
|
|
||||||
├── stress_destroy_apply.sh.disabled # ОТКЛЮЧЁН — стресс-тест PG lifecycle
|
|
||||||
├── code/
|
|
||||||
│ ├── sql-runner/ # Python: одноразовое выполнение SQL (CREATE TABLE)
|
|
||||||
│ ├── pg-info/ # Node.js: версия PG + строки
|
|
||||||
│ ├── table-rw/ # Python: list_rows + add_row
|
|
||||||
│ ├── pg-stats/ # Python: расширенная статистика PG
|
|
||||||
│ ├── funcs-list/ # Утилита: листинг функций
|
|
||||||
│ └── stress-*/ # Функции для стресс-тестирования
|
|
||||||
└── scripts/ # Вспомогательные скрипты
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как запустить
|
|
||||||
|
|
||||||
### Предварительные требования
|
|
||||||
|
|
||||||
- Terraform >= 1.3
|
|
||||||
- Токен sless: `SLESS_TOKEN` (или в `terraform.tfvars`)
|
|
||||||
- Токен nubes_cloud: `NUBES_TOKEN`
|
|
||||||
- Доступ к realm (например, `ffd1f598c169b0ae`)
|
|
||||||
|
|
||||||
### Запуск
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Инициализация
|
|
||||||
terraform init
|
|
||||||
|
|
||||||
# 2. Проверка плана
|
|
||||||
terraform plan
|
|
||||||
|
|
||||||
# 3. Применение (создаст PG + сервисы, запустит init job)
|
|
||||||
terraform apply
|
|
||||||
```
|
|
||||||
|
|
||||||
> Первый `apply` может занять 10–15 минут: создание PG-инстанса + kaniko-сборка образов.
|
|
||||||
|
|
||||||
### Переменные (`terraform.tfvars`)
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
realm = "ffd1f598c169b0ae" # Реалм (namespace в sless)
|
|
||||||
s3_uid = "s01234" # S3 bucket для nubes_postgres бэкапов
|
|
||||||
sless_token = "..." # Bearer-токен для sless API
|
|
||||||
nubes_token = "..." # Bearer-токен для nubes_cloud API
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вывод после apply
|
|
||||||
|
|
||||||
```
|
|
||||||
Outputs:
|
|
||||||
table_reader_url = "https://sless.kube5s.ru/v1/namespaces/.../services/pg-table-reader/invoke"
|
|
||||||
table_writer_url = "https://sless.kube5s.ru/v1/namespaces/.../services/pg-table-writer/invoke"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вызов функций
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Информация о PG (Node.js)
|
|
||||||
curl https://.../services/pg-info/invoke
|
|
||||||
|
|
||||||
# Список строк таблицы (Python)
|
|
||||||
curl https://.../services/pg-table-reader/invoke
|
|
||||||
|
|
||||||
# Добавить строку (Python)
|
|
||||||
curl -X POST https://.../services/pg-table-writer/invoke \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"title": "Hello from sless!"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Стресс-тест
|
|
||||||
|
|
||||||
`stress_test.sh` — нагружает функции HTTP-запросами. Запускать после `terraform apply`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./stress_test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
> `stress_destroy_apply.sh.disabled` — ранний тест PG lifecycle (destroy+apply цикл).
|
|
||||||
> **Отключён** из-за проблем с удалением postgres_user в определённых сценариях.
|
|
||||||
@ -1,650 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 2026-03-21 — bug_hunter.sh: охота за багами во всех POSTGRES-функциях.
|
|
||||||
# Цель: найти bugs типа "ложный 200", "должен 500 но 200", неверные данные.
|
|
||||||
# Логика принципиально отличается от chaos_marathon — здесь акцент на семантике и data integrity.
|
|
||||||
# Запускать ТОЛЬКО на VM через SSH.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-https://sless.kube5s.ru}"
|
|
||||||
NAMESPACE="${NAMESPACE:-sless-ffd1f598c169b0ae}"
|
|
||||||
TOKEN_FILE="${TOKEN_FILE:-$HOME/terra/sless/test.token}"
|
|
||||||
TOKEN=$(cat "$TOKEN_FILE")
|
|
||||||
|
|
||||||
RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; CYAN="\033[0;36m"; NC="\033[0m"
|
|
||||||
|
|
||||||
PASS=0; FAIL=0; TOTAL=0
|
|
||||||
BUGS_FOUND=()
|
|
||||||
|
|
||||||
ts() { date '+%H:%M:%S'; }
|
|
||||||
|
|
||||||
# ── Хелперы ──────────────────────────────────────────────────────────────────
|
|
||||||
raw() {
|
|
||||||
local svc="$1" payload="$2"
|
|
||||||
curl -sf -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "__CURL_FAIL__"
|
|
||||||
}
|
|
||||||
|
|
||||||
http_code() {
|
|
||||||
local svc="$1" payload="$2"
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "000"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем что HTTP-код РАВЕН ожидаемому
|
|
||||||
check_http() {
|
|
||||||
local label="$1" got="$2" want="$3"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$got" == "$want" ]]; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got HTTP $got, want HTTP $want)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("$label → HTTP $got ≠ $want")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем что JSON содержит строку
|
|
||||||
check_has() {
|
|
||||||
local label="$1" body="$2" substr="$3"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if echo "$body" | grep -q "$substr" 2>/dev/null; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (key/substr '$substr' not found in: ${body:0:120})"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("$label → '$substr' not in response")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем что JSON НЕ содержит строку
|
|
||||||
check_not() {
|
|
||||||
local label="$1" body="$2" substr="$3"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if echo "$body" | grep -q "$substr" 2>/dev/null; then
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (found '$substr' but should NOT be there: ${body:0:120})"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("$label → '$substr' should NOT be in response")
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем числовое равенство: jq вытаскивает значение и сравниваем
|
|
||||||
check_val() {
|
|
||||||
local label="$1" body="$2" jq_expr="$3" want="$4"
|
|
||||||
local got
|
|
||||||
got=$(echo "$body" | python3 -c "
|
|
||||||
import json,sys
|
|
||||||
try:
|
|
||||||
d=json.load(sys.stdin)
|
|
||||||
expr='$jq_expr'.lstrip('.')
|
|
||||||
parts=expr.split('.')
|
|
||||||
val=d
|
|
||||||
for p in parts:
|
|
||||||
val=val[p] if isinstance(val,dict) else val[int(p)]
|
|
||||||
print(val)
|
|
||||||
except Exception as e:
|
|
||||||
print('__ERR__:'+str(e))
|
|
||||||
" 2>/dev/null || echo "__ERR__")
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$got" == "$want" ]]; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want '$want')"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("$label → got '$got' want '$want'")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем что числовое значение >= порога
|
|
||||||
check_gte() {
|
|
||||||
local label="$1" got="$2" min_val="$3"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$got" =~ ^[0-9]+$ ]] && (( got >= min_val )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label (got $got >= $min_val)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want >= $min_val)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("$label → $got < $min_val")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
section() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
|
||||||
echo -e "${CYAN} $1${NC}"
|
|
||||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo -e "${YELLOW}"
|
|
||||||
echo "╔══════════════════════════════════════════════════════════╗"
|
|
||||||
echo "║ BUG HUNTER — $(date '+%Y-%m-%d %H:%M:%S') ║"
|
|
||||||
echo "║ Ищем: ложные 200, неверные данные, скрытые баги ║"
|
|
||||||
echo "╚══════════════════════════════════════════════════════════╝"
|
|
||||||
echo -e "${NC}"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 1 — Тест на Missing Input Validation (должно быть 200, НО БУДЕТ 500 если баг есть)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
echo " Каждая функция должна СТОЙКО обрабатывать строку вместо числа."
|
|
||||||
echo " Если возвращает 500 — это BUG: не хватает try/except."
|
|
||||||
|
|
||||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": "не_число"}')
|
|
||||||
check_http "BUG1: chaos-slowquery sleep_sec=string → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "chaos-bigpayload" '{"size_kb": "не_число"}')
|
|
||||||
check_http "BUG2: chaos-bigpayload size_kb=string → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "py-retry-writer" '{"n": "не_число"}')
|
|
||||||
check_http "BUG3: py-retry-writer n=string → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "pg-delete-old" '{"older_than_min": "не_число"}')
|
|
||||||
check_http "BUG4: pg-delete-old older_than_min=string → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": null}')
|
|
||||||
check_http "BUG5: chaos-slowquery sleep_sec=null → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "chaos-bigpayload" '{"size_kb": null}')
|
|
||||||
check_http "BUG6: chaos-bigpayload size_kb=null → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "chaos-bigpayload" '{"size_kb": -999}')
|
|
||||||
check_http "BUG7: chaos-bigpayload size_kb=-999 (negative) → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": -5}')
|
|
||||||
check_http "BUG8: chaos-slowquery sleep_sec=-5 → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": 99999}')
|
|
||||||
check_http "BUG9: chaos-slowquery sleep_sec=99999 (huge) → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "py-retry-writer" '{"n": -1}')
|
|
||||||
check_http "BUG10: py-retry-writer n=-1 (negative) → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
c=$(http_code "py-retry-writer" '{"n": 99999}')
|
|
||||||
check_http "BUG11: py-retry-writer n=99999 (huge, capped) → должен 200" "$c" "200"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 2 — Data Integrity: реальное значение vs ожидаемое"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# js-pg-batch: n=0 должен вернуть inserted=0, а не 20 (баг: 0||20=20)
|
|
||||||
r=$(raw "js-pg-batch" '{"n": 0, "prefix": "bughunt-zero"}')
|
|
||||||
check_val "BUG12: js-pg-batch n=0 → inserted должен быть 0" "$r" ".inserted" "0"
|
|
||||||
|
|
||||||
# js-pg-batch: n=1 → ровно 1 строка
|
|
||||||
r=$(raw "js-pg-batch" '{"n": 1, "prefix": "bughunt-one"}')
|
|
||||||
check_val "SAFE: js-pg-batch n=1 → inserted=1" "$r" ".inserted" "1"
|
|
||||||
|
|
||||||
# js-pg-batch: n=200 (cap) → не больше 200
|
|
||||||
r=$(raw "js-pg-batch" '{"n": 9999, "prefix": "bughunt-cap"}')
|
|
||||||
i=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin)['inserted'])" 2>/dev/null || echo "-1")
|
|
||||||
check_gte "SAFE: js-pg-batch n=9999 → capped (inserted > 0)" "$i" 1
|
|
||||||
# inserted должно быть <= 200
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if (( i <= 200 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-pg-batch n=9999 → capped <= 200 (got $i)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: js-pg-batch n=9999 → вставил $i строк (ожидали <= 200)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("js-pg-batch n=9999 → inserted=$i > 200")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# pg-bulk-insert: точное соответствие n → inserted
|
|
||||||
for n_val in 1 10 100 499 500; do
|
|
||||||
r=$(raw "pg-bulk-insert" "{\"n\": $n_val, \"prefix\": \"bughunt-n$n_val\"}")
|
|
||||||
check_val "SAFE: pg-bulk-insert n=$n_val → inserted=$n_val" "$r" ".inserted" "$n_val"
|
|
||||||
done
|
|
||||||
|
|
||||||
# py-retry-writer с simulate_error: должен вернуть ok:true (retry работает)
|
|
||||||
r=$(raw "py-retry-writer" '{"n": 5, "simulate_error": true, "prefix": "bughunt-retry"}')
|
|
||||||
check_has "SAFE: py-retry-writer simulate_error=true → ok:true" "$r" '"ok": true'
|
|
||||||
attempts=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('attempts',0))" 2>/dev/null || echo "0")
|
|
||||||
check_gte "SAFE: py-retry-writer simulate_error=true → attempts >= 2" "$attempts" 2
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 3 — Semantic / Logic Bugs (ложный 200, неверные данные)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# BUG: pg-search с "_" в query — _ это LIKE wildcard, не экранируется.
|
|
||||||
# Вставляем уникальную строку без подчёркивания, ищем с _ → не должна найтись.
|
|
||||||
UNIQUE_NOUNDERSCORE="bughunt-no-underscore-$(date +%s%N)"
|
|
||||||
raw "pg-bulk-insert" "{\"n\": 1, \"prefix\": \"$UNIQUE_NOUNDERSCORE\"}" >/dev/null
|
|
||||||
# Поиск exact строки — должна найтись
|
|
||||||
r=$(raw "pg-search" "{\"query\": \"$UNIQUE_NOUNDERSCORE\", \"limit\": 10}")
|
|
||||||
cnt=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0")
|
|
||||||
check_gte "SAFE: pg-search exact match найдена" "$cnt" 1
|
|
||||||
|
|
||||||
# Теперь создаём строку с дефисом в mid позиции: "aXb" где X — любой char.
|
|
||||||
# Ищем с _ (wildcard): "a_b" должно совпадать. Это НЕ баг если мы ищем wildcard.
|
|
||||||
# Реальный баг: ESCAPE не поддерживается, пользователь не может искать literal "_".
|
|
||||||
# Проверяем: строка "test-literal-underscore_here" ищем по query="literal_underscore_here"
|
|
||||||
# Без экраниравания: _ = любой символ → совпадёт AND с "literaXunderscoreYhere" тоже.
|
|
||||||
EXACT_TITLE="bughunt-exact-$(date +%s%N)"
|
|
||||||
raw "pg-upsert" "{\"title\": \"${EXACT_TITLE}\"}" >/dev/null
|
|
||||||
# Поиск с подчёркиванием вместо дефиса в этой строке — совпадать НЕ должно с точным матчем
|
|
||||||
# но совпадёт из-за ILIKE wildcard `_`
|
|
||||||
UNDER_QUERY="${EXACT_TITLE//-/_}" # заменяем дефисы на подчёркивания
|
|
||||||
r=$(raw "pg-search" "{\"query\": \"$UNDER_QUERY\", \"limit\": 100}")
|
|
||||||
underscore_count=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('count',0))" 2>/dev/null || echo "0")
|
|
||||||
# Если count >> 1, значит _ матчит что попало (wildcard)
|
|
||||||
echo " pg-search с query='${UNDER_QUERY:0:30}...' вернул $underscore_count строк (если > 1 — это баг wildcard)"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
# Должен найти ТОЛЬКО нашу строку (count=1), но без экранирования найдёт много
|
|
||||||
if (( underscore_count == 1 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search underscore matches only exact row"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG13: pg-search '_' is unescaped LIKE wildcard → matches $underscore_count rows instead of 1"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-search: '_' is unescaped LIKE wildcard (got $underscore_count rows)")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# BUG: pg-delete-old — параметр НАЗЫВАЕТСЯ older_than_min, но в chaos_marathon шлём older_than_minutes.
|
|
||||||
# Шлём неправильный ключ и правильный — результаты должны различаться.
|
|
||||||
# older_than_minutes=1 → функция игнорирует, использует default (60 мин) → ничего не удалится из только что вставленных
|
|
||||||
# older_than_min=0 → capped to 1 → удалит строки старше 1 мин (ну, только что вставленные > 1 мин назад)
|
|
||||||
# Проверяем: older_than_minutes=0 → использует default 60 → параметр проигнорирован → bug
|
|
||||||
|
|
||||||
# Вставляем свежую строку, пытаемся удалить с older_than_minutes=0 (неверный ключ)
|
|
||||||
FRESH_TITLE="bughunt-del-$(date +%s%N)"
|
|
||||||
ins_r=$(raw "pg-bulk-insert" "{\"n\": 3, \"prefix\": \"$FRESH_TITLE\"}")
|
|
||||||
# Ждём немного, затем пытаемся удалить по неправильному ключу
|
|
||||||
sleep 1
|
|
||||||
r=$(raw "pg-delete-old" "{\"older_than_minutes\": 0}")
|
|
||||||
deleted_wrong_key=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?")
|
|
||||||
r2=$(raw "pg-delete-old" "{\"older_than_min\": 99999}")
|
|
||||||
deleted_right_key=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?")
|
|
||||||
echo " pg-delete-old older_than_minutes=0: deleted=$deleted_wrong_key (использовался default 60min)"
|
|
||||||
echo " pg-delete-old older_than_min=99999: deleted=$deleted_right_key (правильный ключ — удалило всё старое)"
|
|
||||||
# Если с неверным ключом deleted > 0 при очень маленьком значении — значит параметр работает.
|
|
||||||
# Если с right key deleted больше — это подтверждает что wrong key игнорировался.
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$deleted_right_key" =~ ^[0-9]+$ ]] && (( deleted_right_key >= 0 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] INFO: pg-delete-old right key works (deleted=$deleted_right_key)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] pg-delete-old right key failed: got $deleted_right_key"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# BUG: pg-counter prefix="%" → LIKE "%%%" → считает все строки (wildcard leak)
|
|
||||||
r_all=$(raw "pg-counter" '{}')
|
|
||||||
total_all=$(echo "$r_all" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
|
||||||
r_pct=$(raw "pg-counter" '{"prefix": "%"}')
|
|
||||||
total_pct=$(echo "$r_pct" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
|
||||||
echo " pg-counter prefix='': total=$total_all | pg-counter prefix='%': total=$total_pct"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
# Если оба возвращают одно число — значит % матчит все строки → баг wildcard
|
|
||||||
if [[ "$total_all" == "$total_pct" ]] && (( total_all > 0 )); then
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG14: pg-counter prefix='%' counts ALL rows ($total_pct) — % is unescaped LIKE wildcard"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-counter: prefix='%' is unescaped LIKE wildcard (same as no prefix)")
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter prefix='%' gives different count than no-prefix"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# BUG: pg-search limit=0 — clamp max(1, min(0,100)) = 1, не 0.
|
|
||||||
# Юзер просит 0 строк но получает 1.
|
|
||||||
r=$(raw "pg-search" '{"query": "", "limit": 0}')
|
|
||||||
limit_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('limit',0))" 2>/dev/null || echo "-1")
|
|
||||||
echo " pg-search limit=0 → вернул limit=$limit_got в ответе"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$limit_got" == "0" ]]; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search limit=0 → limit=0 in response"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] INFO15: pg-search limit=0 → silently changed to $limit_got (min clamp = 1, not 0)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-search: limit=0 silently becomes $limit_got (user wants 0 rows, gets $limit_got)")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 4 — pg-upsert: idempotency + action field correctness"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Первый вызов → action=inserted
|
|
||||||
UPSERT_KEY="bughunt-upsert-$(date +%s%N)"
|
|
||||||
r1=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}")
|
|
||||||
action1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
|
||||||
check_val "SAFE: pg-upsert первый вызов → action=inserted" "$r1" ".action" "inserted"
|
|
||||||
|
|
||||||
# Второй вызов → action=updated
|
|
||||||
r2=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}")
|
|
||||||
action2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
|
||||||
check_val "SAFE: pg-upsert второй вызов (same title) → action=updated" "$r2" ".action" "updated"
|
|
||||||
|
|
||||||
# ID должен совпадать
|
|
||||||
id1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?1")
|
|
||||||
id2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?2")
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$id1" == "$id2" ]] && [[ "$id1" != "?" ]]; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-upsert same title → same id ($id1)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG16: pg-upsert same title → different ids ($id1 vs $id2)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-upsert: same title yields different ids ($id1 vs $id2)")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 5 — js-idempotent: concurrent same key"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# 5 параллельных вызовов с одним ключом → должен быть 1 created, 4 existing
|
|
||||||
IDEM_KEY="bughunt-idem-concurrent-$(date +%s%N)"
|
|
||||||
declare -a IDEM_RESULTS=()
|
|
||||||
for i in 1 2 3 4 5; do
|
|
||||||
r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") &
|
|
||||||
IDEM_RESULTS+=($!)
|
|
||||||
done
|
|
||||||
wait
|
|
||||||
# Перезапустим последовательно чтобы собрать результаты
|
|
||||||
actions=()
|
|
||||||
for i in 1 2 3 4 5; do
|
|
||||||
r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
|
||||||
a=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
|
||||||
actions+=("$a")
|
|
||||||
done
|
|
||||||
|
|
||||||
created_count=0; existing_count=0
|
|
||||||
for a in "${actions[@]}"; do
|
|
||||||
[[ "$a" == "created" ]] && created_count=$((created_count+1))
|
|
||||||
[[ "$a" == "existing" ]] && existing_count=$((existing_count+1))
|
|
||||||
done
|
|
||||||
echo " js-idempotent key же 5× последовательно: created=$created_count, existing=$existing_count"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
# Первый должен быть created, остальные existing (с учётом что первый вызов в параллельном блоке уже создал)
|
|
||||||
# Теперь все 5 последовательных должны быть existing
|
|
||||||
if (( existing_count == 5 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → все existing"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
elif (( created_count == 1 && existing_count == 4 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → 1 created + 4 existing"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG17: js-idempotent same key → created=$created_count existing=$existing_count (ожидали 0-1 created, 4-5 existing)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("js-idempotent: 5× same key → created=$created_count, existing=$existing_count")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 6 — go-pg-race: workers=0 div-by-zero"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
r=$(raw "go-pg-race" '{"workers": 0, "n_per_worker": 5}')
|
|
||||||
c=$(http_code "go-pg-race" '{"workers": 0, "n_per_worker": 5}')
|
|
||||||
check_http "SAFE: go-pg-race workers=0 → 200 (не div-by-zero)" "$c" "200"
|
|
||||||
|
|
||||||
# ops_per_sec при workers=0 inserted=0 должен быть 0 или Inf — проверяем что не NaN/invalid JSON
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if echo "$r" | python3 -c "import json,sys; json.load(sys.stdin); print('valid_json')" 2>/dev/null | grep -q valid_json; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race workers=0 → valid JSON"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG18: go-pg-race workers=0 → invalid JSON (div-by-zero → Inf/NaN)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("go-pg-race: workers=0 → invalid JSON response")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 7 — Crash functions: параметры управляют crash-ом"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# stress-go-nil: crash=false → 200 (не должен падать)
|
|
||||||
c=$(http_code "stress-go-nil" '{"crash": false}')
|
|
||||||
check_http "SAFE: stress-go-nil crash=false → 200" "$c" "200"
|
|
||||||
|
|
||||||
# stress-go-nil: crash=true (default) → 500
|
|
||||||
c=$(http_code "stress-go-nil" '{"crash": true}')
|
|
||||||
check_http "SAFE: stress-go-nil crash=true → 500" "$c" "500"
|
|
||||||
|
|
||||||
# stress-divzero: d=0 → 500
|
|
||||||
c=$(http_code "stress-divzero" '{"n": 10, "d": 0}')
|
|
||||||
check_http "SAFE: stress-divzero d=0 → 500" "$c" "500"
|
|
||||||
|
|
||||||
# stress-divzero: d=2 → 200
|
|
||||||
c=$(http_code "stress-divzero" '{"n": 10, "d": 2}')
|
|
||||||
check_http "SAFE: stress-divzero d=2 → 200" "$c" "200"
|
|
||||||
|
|
||||||
r=$(raw "stress-divzero" '{"n": 10, "d": 2}')
|
|
||||||
check_has "SAFE: stress-divzero d=2 → result in response" "$r" "result"
|
|
||||||
|
|
||||||
# stress-bigloop: n=1000000 (большое) → должен вернуть 200
|
|
||||||
c=$(http_code "stress-bigloop" '{"n": 1000000}')
|
|
||||||
check_http "SAFE: stress-bigloop n=1000000 → 200" "$c" "200"
|
|
||||||
|
|
||||||
# stress-go-fast: n=0 → должен вернуть 200 (factorial(0) = 1)
|
|
||||||
c=$(http_code "stress-go-fast" '{"n": 0}')
|
|
||||||
check_http "SAFE: stress-go-fast n=0 → 200" "$c" "200"
|
|
||||||
|
|
||||||
# stress-go-fast: n=20 (cap) → factorial(20) не переполнение?
|
|
||||||
r=$(raw "stress-go-fast" '{"n": 20}')
|
|
||||||
check_has "SAFE: stress-go-fast n=20 → factorial in response" "$r" "factorial"
|
|
||||||
|
|
||||||
# stress-go-fast: n=21 → capped to 20 (проверяем что cap работает)
|
|
||||||
r=$(raw "stress-go-fast" '{"n": 21}')
|
|
||||||
n_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('n',0))" 2>/dev/null || echo "-1")
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if (( n_got <= 20 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: stress-go-fast n=21 → capped to $n_got (<= 20)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: stress-go-fast n=21 → not capped (n=$n_got)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("stress-go-fast: n=21 not capped (got n=$n_got)")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 8 — pg-counter: счёт соответствует реально inserted"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
PREFIX_TEST="bughunt-count-$(date +%s%N)"
|
|
||||||
# Получаем текущий счётчик с этим prefix
|
|
||||||
r_before=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}")
|
|
||||||
cnt_before=$(echo "$r_before" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
# Вставляем ровно 7 строк
|
|
||||||
raw "pg-bulk-insert" "{\"n\": 7, \"prefix\": \"$PREFIX_TEST\"}" >/dev/null
|
|
||||||
|
|
||||||
# Считаем снова
|
|
||||||
r_after=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}")
|
|
||||||
cnt_after=$(echo "$r_after" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
inserted_delta=$(( cnt_after - cnt_before ))
|
|
||||||
echo " pg-counter prefix='$PREFIX_TEST': before=$cnt_before, after=$cnt_after, delta=$inserted_delta (ожидаем 7)"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if (( inserted_delta == 7 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter правильно считает: delta=$inserted_delta"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG19: pg-counter delta=$inserted_delta ≠ 7 (prefix wildcard или баг в счёте)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-counter: inserted 7, delta=$inserted_delta")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 9 — pg-search: pagination correctness"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
PREFIX_SEARCH="bughunt-search-$(date +%s%N)"
|
|
||||||
# Вставляем ровно 25 строк
|
|
||||||
raw "pg-bulk-insert" "{\"n\": 25, \"prefix\": \"$PREFIX_SEARCH\"}" >/dev/null
|
|
||||||
sleep 0.5
|
|
||||||
|
|
||||||
# Page 1: offset=0 limit=10 → count=10
|
|
||||||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 0}")
|
|
||||||
count_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
|
||||||
total_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "-1")
|
|
||||||
check_val "SAFE: pg-search page1 count=10" "$r" ".count" "10"
|
|
||||||
check_gte "SAFE: pg-search total >= 25" "$total_p1" 25
|
|
||||||
|
|
||||||
# Page 3: offset=20 limit=10 → count=5 (строк 21-25)
|
|
||||||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 20}")
|
|
||||||
count_p3=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
|
||||||
echo " pg-search page3 (offset=20, limit=10): count=$count_p3 (ожидаем 5)"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
# Учитываем что могут быть другие строки с этим prefix
|
|
||||||
if [[ "$count_p3" =~ ^[0-9]+$ ]] && (( count_p3 > 0 && count_p3 <= 10 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search page3 count=$count_p3 (допустимо)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG20: pg-search page3 count=$count_p3 (expected 1-10)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-search: page3 count=$count_p3 out of range")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# offset > total → count=0
|
|
||||||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 999999}")
|
|
||||||
count_over=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
|
||||||
check_val "SAFE: pg-search offset>total → count=0" "$r" ".count" "0"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 10 — pg-dedup: idempotency (повторный вызов безопасен)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Сначала удаляем все дубли
|
|
||||||
raw "pg-dedup" '{"dry_run": false}' >/dev/null
|
|
||||||
|
|
||||||
# dry_run=true → deleted всегда = 0
|
|
||||||
r=$(raw "pg-dedup" '{"dry_run": true}')
|
|
||||||
check_val "SAFE: pg-dedup dry_run=true → deleted=0" "$r" ".deleted" "0"
|
|
||||||
|
|
||||||
# Создаём дублей: вставляем одно и то же через bulk (уникальные title), затем уpsert одно и то же
|
|
||||||
DUP_TITLE="bughunt-dup-$(date +%s%N)"
|
|
||||||
raw "pg-upsert" "{\"title\": \"${DUP_TITLE}\"}" >/dev/null
|
|
||||||
# INSERT прямой дубль через bulk — но у него нет механизма вставки дублей...
|
|
||||||
# Используем pg-counter + pg-search чтобы найти дубли что уже есть от других тестов
|
|
||||||
r=$(raw "pg-dedup" '{"dry_run": true}')
|
|
||||||
dupes=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0")
|
|
||||||
echo " pg-dedup dry_run: duplicates_found=$dupes"
|
|
||||||
|
|
||||||
# Запускаем dedup дважды — второй раз должен найти 0 дублей
|
|
||||||
raw "pg-dedup" '{"dry_run": false}' >/dev/null
|
|
||||||
r2=$(raw "pg-dedup" '{"dry_run": false}')
|
|
||||||
dupes2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0")
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$dupes2" == "0" ]]; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-dedup повторный вызов → duplicates_found=0 (идемпотентен)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG21: pg-dedup повторный вызов → duplicates_found=$dupes2 ≠ 0"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-dedup: не идемпотентен — второй вызов нашёл $dupes2 дублей")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 11 — chaos-echo: крайние случаи"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Пустой JSON {} → echo должен вернуть echo:{}, keys:[], size_bytes:2
|
|
||||||
r=$(raw "chaos-echo" '{}')
|
|
||||||
check_has "SAFE: chaos-echo {} → echo in response" "$r" '"echo"'
|
|
||||||
check_val "SAFE: chaos-echo {} → keys=[](empty_list len=0)" "$r" ".size_bytes" "2"
|
|
||||||
|
|
||||||
# Очень глубоко вложенный JSON
|
|
||||||
r=$(raw "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}')
|
|
||||||
c=$(http_code "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}')
|
|
||||||
check_http "SAFE: chaos-echo deeply nested → 200" "$c" "200"
|
|
||||||
|
|
||||||
# Массив вместо объекта (некоторые функции падают)
|
|
||||||
c=$(http_code "chaos-echo" '[1, 2, 3]')
|
|
||||||
check_http "SAFE: chaos-echo array input → 200" "$c" "200"
|
|
||||||
|
|
||||||
# Булево значение вместо объекта
|
|
||||||
c=$(http_code "chaos-echo" 'true')
|
|
||||||
check_http "SAFE: chaos-echo true input → 200" "$c" "200"
|
|
||||||
|
|
||||||
# Число вместо объекта
|
|
||||||
c=$(http_code "chaos-echo" '42')
|
|
||||||
check_http "SAFE: chaos-echo number input → 200" "$c" "200"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 12 — go-counter-atomic: invocation_n растёт"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
r1=$(raw "go-counter-atomic" '{}')
|
|
||||||
n1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?")
|
|
||||||
r2=$(raw "go-counter-atomic" '{}')
|
|
||||||
n2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?")
|
|
||||||
echo " go-counter-atomic: call1 invocation_n=$n1, call2 invocation_n=$n2"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$n1" =~ ^[0-9]+$ ]] && [[ "$n2" =~ ^[0-9]+$ ]] && (( n2 > n1 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-counter-atomic invocation_n растёт ($n1 → $n2)"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG22: go-counter-atomic invocation_n НЕ растёт ($n1 → $n2)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("go-counter-atomic: invocation_n не растёт ($n1 → $n2)")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 13 — go-pg-race: все inserted = workers × n_per_worker"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
r=$(raw "go-pg-race" '{"workers": 4, "n_per_worker": 10}')
|
|
||||||
ins=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('inserted',0))" 2>/dev/null || echo "0")
|
|
||||||
errs=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('errors',0))" 2>/dev/null || echo "-1")
|
|
||||||
echo " go-pg-race workers=4 n_per_worker=10: inserted=$ins, errors=$errs (ожидаем inserted=40, errors=0)"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if [[ "$ins" == "40" ]] && [[ "$errs" == "0" ]]; then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race 4×10 = 40 inserted, 0 errors"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG23: go-pg-race 4×10: inserted=$ins (≠40), errors=$errs"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("go-pg-race: workers=4 n_per_worker=10 → inserted=$ins errors=$errs")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "БЛОК 14 — pg-bulk-insert: first_id реально существует в БД"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
PREFIX_VER="bughunt-verify-$(date +%s%N)"
|
|
||||||
r=$(raw "pg-bulk-insert" "{\"n\": 5, \"prefix\": \"$PREFIX_VER\"}")
|
|
||||||
first_id=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('first_id','null'))" 2>/dev/null || echo "null")
|
|
||||||
echo " pg-bulk-insert n=5 → first_id=$first_id"
|
|
||||||
|
|
||||||
# Проверяем что эта строка находится через pg-search
|
|
||||||
sleep 0.3
|
|
||||||
r_search=$(raw "pg-search" "{\"query\": \"$PREFIX_VER\", \"limit\": 10}")
|
|
||||||
found_count=$(echo "$r_search" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0")
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
if (( found_count >= 5 )); then
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-bulk-insert n=5 → pg-search находит $found_count строк"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG24: pg-bulk-insert n=5 → pg-search нашёл только $found_count строк"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
BUGS_FOUND+=("pg-bulk-insert: inserted 5 but pg-search found only $found_count")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ИТОГИ BUG HUNTER"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}PASS: $PASS / $TOTAL FAIL: $FAIL${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if (( ${#BUGS_FOUND[@]} > 0 )); then
|
|
||||||
echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${RED}║ НАЙДЕНО БАГОВ: ${#BUGS_FOUND[@]} ║${NC}"
|
|
||||||
echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
|
|
||||||
for bug in "${BUGS_FOUND[@]}"; do
|
|
||||||
echo -e " ${RED}✗${NC} $bug"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${GREEN}║ БАГОВ НЕ НАЙДЕНО. Всё чисто. ✓ ║${NC}"
|
|
||||||
echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
exit $(( FAIL > 0 ? 1 : 0 ))
|
|
||||||
@ -1,668 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 2026-03-21 — chaos_marathon.sh
|
|
||||||
# Часовой хаос-марафон: 15 сервисов, dumb-user simulation, PG stress, CRUD lifecycle.
|
|
||||||
# Запуск: bash chaos_marathon.sh 2>&1 | tee /tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log
|
|
||||||
#
|
|
||||||
# Предполагает: terraform apply chaos_marathon.tf уже выполнен, все 15 Ready.
|
|
||||||
# Зависимости: curl, jq, terraform (init выполнен).
|
|
||||||
|
|
||||||
# -e намеренно НЕ установлен — падение одного вызова не убивает марафон.
|
|
||||||
# -u: незаданные переменные = ошибка. -o pipefail: ошибка в пайпе видна.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
# ── Config ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
BASE_URL="${SLESS_BASE_URL:-https://sless.kube5s.ru}"
|
|
||||||
TOKEN_FILE="${SLESS_TOKEN_FILE:-/home/naeel/terra/sless/test.token}"
|
|
||||||
NAMESPACE="${SLESS_NAMESPACE:-sless-ffd1f598c169b0ae}"
|
|
||||||
TF_DIR="/home/naeel/terra/sless/examples/POSTGRES"
|
|
||||||
LOG_DIR="/tmp/chaos_$(date +%Y%m%d_%H%M%S)"
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
TOKEN=$(cat "$TOKEN_FILE")
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
TOTAL=0
|
|
||||||
|
|
||||||
# Цветной вывод — для удобства чтения длинного лога.
|
|
||||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
|
||||||
|
|
||||||
ts() { date '+%H:%M:%S'; }
|
|
||||||
|
|
||||||
# invoke SERVICE_NAME PAYLOAD — вызывает сервис через публичный /fn/ proxy, возвращает тело.
|
|
||||||
# Публичный endpoint не требует токена (используется для вызова функций).
|
|
||||||
# Никогда не падает — ошибки пишутся в лог-файл.
|
|
||||||
invoke() {
|
|
||||||
local svc="$1" payload="$2"
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$payload" \
|
|
||||||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" \
|
|
||||||
2>"$LOG_DIR/err_${svc}_$(date +%s%N).log" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# invoke_raw — как invoke, но никогда не бросает non-zero exit.
|
|
||||||
invoke_raw() {
|
|
||||||
local svc="$1" payload="$2"
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$payload" \
|
|
||||||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# invoke_with_status SERVICE PAYLOAD — возвращает HTTP код, никогда не падает.
|
|
||||||
invoke_with_status() {
|
|
||||||
local svc="$1" payload="$2"
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$payload" \
|
|
||||||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" || echo "000"
|
|
||||||
}
|
|
||||||
|
|
||||||
# check TEST_NAME CONDITION [msg] — вердикт по условию (expect 0-exit или строковую проверку).
|
|
||||||
check() {
|
|
||||||
local name="$1" result="$2" expected="${3:-0}"
|
|
||||||
TOTAL=$((TOTAL + 1))
|
|
||||||
if [[ "$result" == "$expected" ]]; then
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
|
||||||
else
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (got='$result' want='$expected')"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# check_contains TEST_NAME HAYSTACK NEEDLE
|
|
||||||
check_contains() {
|
|
||||||
local name="$1" hay="$2" needle="$3"
|
|
||||||
TOTAL=$((TOTAL + 1))
|
|
||||||
if echo "$hay" | grep -q "$needle"; then
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
|
||||||
else
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (needle='$needle' not found)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# check_not_contains TEST_NAME HAYSTACK NEEDLE
|
|
||||||
check_not_contains() {
|
|
||||||
local name="$1" hay="$2" needle="$3"
|
|
||||||
TOTAL=$((TOTAL + 1))
|
|
||||||
if ! echo "$hay" | grep -q "$needle"; then
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
|
||||||
else
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (unexpected needle='$needle' found)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# check_http TEST_NAME CODE EXPECTED
|
|
||||||
check_http() {
|
|
||||||
local name="$1" code="$2" expected="${3:-200}"
|
|
||||||
TOTAL=$((TOTAL + 1))
|
|
||||||
if [[ "$code" == "$expected" ]]; then
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name → HTTP $code"
|
|
||||||
else
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name → HTTP $code (want $expected)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
section() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
|
||||||
echo -e "${CYAN} $1${NC}"
|
|
||||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Wait helpers ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# wait_service_ready NAME — ждём до 2 мин пока GET /services/{name} вернёт phase=Ready.
|
|
||||||
# Проверяет статус через API (не invoke), чтобы не запускать функцию при старте.
|
|
||||||
wait_service_ready() {
|
|
||||||
local svc="$1" max_attempts=24 attempt=0
|
|
||||||
echo " Ожидаем готовности $svc..."
|
|
||||||
while (( attempt < max_attempts )); do
|
|
||||||
phase=$(curl -sf \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
"${BASE_URL}/v1/namespaces/${NAMESPACE}/services/${svc}" \
|
|
||||||
2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null || true)
|
|
||||||
if [[ "$phase" == "Ready" ]]; then
|
|
||||||
echo " → $svc Ready (attempt $((attempt+1)))"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 5
|
|
||||||
attempt=$((attempt + 1))
|
|
||||||
done
|
|
||||||
echo -e " ${RED}TIMEOUT: $svc не стал Ready за 2 мин${NC}"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Parallel invoker ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# parallel_invoke COUNT SERVICE PAYLOAD LOG_PREFIX — запускает COUNT вызовов параллельно.
|
|
||||||
parallel_invoke() {
|
|
||||||
local count="$1" svc="$2" payload="$3" prefix="$4"
|
|
||||||
local pids=() results_dir="$LOG_DIR/par_${prefix}_$(date +%s)"
|
|
||||||
mkdir -p "$results_dir"
|
|
||||||
for i in $(seq 1 "$count"); do
|
|
||||||
(
|
|
||||||
code=$(invoke_with_status "$svc" "$payload")
|
|
||||||
echo "$code" > "$results_dir/$i"
|
|
||||||
) &
|
|
||||||
pids+=($!)
|
|
||||||
done
|
|
||||||
# Ждём все фоновые задачи.
|
|
||||||
for pid in "${pids[@]}"; do wait "$pid" || true; done
|
|
||||||
# Счёт 200-х.
|
|
||||||
local ok=0 bad=0
|
|
||||||
for f in "$results_dir"/*; do
|
|
||||||
code=$(cat "$f")
|
|
||||||
if [[ "$code" == "200" ]]; then ok=$((ok+1)); else bad=$((bad+1)); fi
|
|
||||||
done
|
|
||||||
echo "$ok/$count OK, $bad FAIL"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}╔══════════════════════════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${YELLOW}║ CHAOS MARATHON — $(date '+%Y-%m-%d %H:%M:%S') ║${NC}"
|
|
||||||
echo -e "${YELLOW}╚══════════════════════════════════════════════════════════╝${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 0 — Ожидание готовности всех 15 сервисов"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
SERVICES=(
|
|
||||||
pg-counter pg-dedup pg-search pg-bulk-insert pg-delete-old pg-upsert
|
|
||||||
chaos-echo chaos-badparams chaos-slowquery chaos-bigpayload
|
|
||||||
go-pg-race go-counter-atomic
|
|
||||||
js-pg-batch js-idempotent
|
|
||||||
py-retry-writer
|
|
||||||
)
|
|
||||||
|
|
||||||
failed_ready=0
|
|
||||||
for svc in "${SERVICES[@]}"; do
|
|
||||||
if ! wait_service_ready "$svc"; then
|
|
||||||
failed_ready=$((failed_ready + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if (( failed_ready > 0 )); then
|
|
||||||
echo -e "${YELLOW}ВНИМАНИЕ: $failed_ready сервисов не стали Ready. Продолжаем — они будут FAIL в тестах.${NC}"
|
|
||||||
# НЕ выходим — дальше тесты сами покажут что сломалось.
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}Все 15 сервисов Ready. Начинаем марафон.${NC}"
|
|
||||||
START_TIME=$(date +%s)
|
|
||||||
MARATHON_DURATION=${MARATHON_DURATION_SEC:-3600} # по умолчанию 1 час
|
|
||||||
ROUND=0
|
|
||||||
|
|
||||||
echo -e "${CYAN}Длительность марафона: ${MARATHON_DURATION}с ($(( MARATHON_DURATION / 60 )) мин)${NC}"
|
|
||||||
|
|
||||||
# ── Основной цикл: крутим фазы 1–11 пока не истечёт время ───────────────────
|
|
||||||
while true; do
|
|
||||||
NOW=$(date +%s)
|
|
||||||
ELAPSED_TOTAL=$(( NOW - START_TIME ))
|
|
||||||
if (( ELAPSED_TOTAL >= MARATHON_DURATION )); then
|
|
||||||
echo -e "\n${YELLOW}Время марафона истекло (${ELAPSED_TOTAL}с). Переходим к финальной проверке.${NC}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
ROUND=$((ROUND + 1))
|
|
||||||
MINS_LEFT=$(( (MARATHON_DURATION - ELAPSED_TOTAL) / 60 ))
|
|
||||||
echo -e "\n${YELLOW}═══ РАУНД $ROUND | прошло $(( ELAPSED_TOTAL / 60 ))м, осталось ${MINS_LEFT}м ═══${NC}"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 1 — Базовый smoke-test (1 вызов каждого сервиса)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# pg-counter: простой счёт всех строк.
|
|
||||||
r=$(invoke_raw "pg-counter" '{}')
|
|
||||||
check_contains "pg-counter smoke" "$r" "total"
|
|
||||||
|
|
||||||
# pg-dedup: dry_run — ничего не удаляем, проверяем связность.
|
|
||||||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
|
||||||
check_contains "pg-dedup smoke (dry_run)" "$r" "duplicates_found"
|
|
||||||
|
|
||||||
# pg-search: самый простой запрос.
|
|
||||||
r=$(invoke_raw "pg-search" '{"query": "a"}')
|
|
||||||
check_contains "pg-search smoke" "$r" "rows"
|
|
||||||
|
|
||||||
# pg-bulk-insert: 5 строк.
|
|
||||||
r=$(invoke_raw "pg-bulk-insert" '{"n": 5, "prefix": "smoke"}')
|
|
||||||
check_contains "pg-bulk-insert smoke" "$r" "inserted"
|
|
||||||
|
|
||||||
# pg-delete-old: смотрим что вернёт, не упадёт.
|
|
||||||
r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}')
|
|
||||||
check_contains "pg-delete-old smoke" "$r" "deleted"
|
|
||||||
|
|
||||||
# pg-upsert: вставляем одну строку.
|
|
||||||
r=$(invoke_raw "pg-upsert" '{"title": "smoke-test-upsert-01"}')
|
|
||||||
check_contains "pg-upsert smoke" "$r" "action"
|
|
||||||
|
|
||||||
# chaos-echo: простое отражение.
|
|
||||||
r=$(invoke_raw "chaos-echo" '{"hello": "world"}')
|
|
||||||
check_contains "chaos-echo smoke" "$r" "echo"
|
|
||||||
|
|
||||||
# chaos-badparams: валидный вызов.
|
|
||||||
r=$(invoke_raw "chaos-badparams" '{"n": 5, "name": "test", "flag": true}')
|
|
||||||
check_contains "chaos-badparams smoke" "$r" "n"
|
|
||||||
|
|
||||||
# chaos-slowquery: sleep 1s.
|
|
||||||
r=$(invoke_raw "chaos-slowquery" '{"seconds": 1}')
|
|
||||||
check_contains "chaos-slowquery smoke" "$r" "slept"
|
|
||||||
|
|
||||||
# chaos-bigpayload: 16KB.
|
|
||||||
r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 16}')
|
|
||||||
check_contains "chaos-bigpayload smoke" "$r" "items"
|
|
||||||
|
|
||||||
# go-pg-race: 2 горутины × 3 INSERT.
|
|
||||||
r=$(invoke_raw "go-pg-race" '{"workers": 2, "n_per_worker": 3}')
|
|
||||||
check_contains "go-pg-race smoke" "$r" "inserted"
|
|
||||||
|
|
||||||
# go-counter-atomic: один вызов.
|
|
||||||
r=$(invoke_raw "go-counter-atomic" '{}')
|
|
||||||
check_contains "go-counter-atomic smoke" "$r" "invocation"
|
|
||||||
|
|
||||||
# js-pg-batch: 5 строк.
|
|
||||||
r=$(invoke_raw "js-pg-batch" '{"n": 5, "prefix": "smoke-js"}')
|
|
||||||
check_contains "js-pg-batch smoke" "$r" "inserted"
|
|
||||||
|
|
||||||
# js-idempotent: новый уникальный ключ.
|
|
||||||
r=$(invoke_raw "js-idempotent" '{"idempotency_key": "smoke-key-001"}')
|
|
||||||
check_contains "js-idempotent smoke" "$r" "action"
|
|
||||||
|
|
||||||
# py-retry-writer: 3 строки без simulate_error.
|
|
||||||
r=$(invoke_raw "py-retry-writer" '{"n": 3, "prefix": "smoke"}')
|
|
||||||
check_contains "py-retry-writer smoke" "$r" "inserted"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 2 — Dumb User Simulation (тупой юзер ломает всё)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo " Тест: передаём мусор в каждый сервис — никто не должен вернуть 500."
|
|
||||||
|
|
||||||
# chaos-echo: пустой объект.
|
|
||||||
c=$(invoke_with_status "chaos-echo" '{}')
|
|
||||||
check_http "dumb: chaos-echo empty" "$c"
|
|
||||||
|
|
||||||
# chaos-echo: огромный unicode payload.
|
|
||||||
big_unicode=$(python3 -c "import json; print(json.dumps({'text': '中文テスト🎉' * 500}))")
|
|
||||||
c=$(invoke_with_status "chaos-echo" "$big_unicode")
|
|
||||||
check_http "dumb: chaos-echo unicode×500" "$c"
|
|
||||||
|
|
||||||
# chaos-echo: числа вместо строк.
|
|
||||||
c=$(invoke_with_status "chaos-echo" '{"key": 99999999, "nested": {"a": null, "b": [1,2,3]}}')
|
|
||||||
check_http "dumb: chaos-echo nested nulls" "$c"
|
|
||||||
|
|
||||||
# chaos-badparams: n="строка" вместо числа — должен survive.
|
|
||||||
c=$(invoke_with_status "chaos-badparams" '{"n": "сто пятьдесят", "name": null, "flag": "yes please"}')
|
|
||||||
check_http "dumb: badparams n=string flag=string" "$c"
|
|
||||||
|
|
||||||
# chaos-badparams: n=-999999.
|
|
||||||
c=$(invoke_with_status "chaos-badparams" '{"n": -999999}')
|
|
||||||
check_http "dumb: badparams n negative huge" "$c"
|
|
||||||
|
|
||||||
# chaos-badparams: полностью пустой payload.
|
|
||||||
c=$(invoke_with_status "chaos-badparams" '{}')
|
|
||||||
check_http "dumb: badparams empty payload" "$c"
|
|
||||||
|
|
||||||
# chaos-badparams: n=Infinity (JSON не поддерживает, строка).
|
|
||||||
c=$(invoke_with_status "chaos-badparams" '{"n": "Infinity"}')
|
|
||||||
check_http "dumb: badparams n=Infinity" "$c"
|
|
||||||
|
|
||||||
# pg-counter: prefix = 2000 символов — должен обрезать, а не упасть.
|
|
||||||
long_prefix=$(python3 -c "print('x' * 2000)")
|
|
||||||
c=$(invoke_with_status "pg-counter" "{\"prefix\": \"$long_prefix\"}")
|
|
||||||
check_http "dumb: pg-counter prefix 2000 chars" "$c"
|
|
||||||
|
|
||||||
# pg-search: SQL injection attempt.
|
|
||||||
c=$(invoke_with_status "pg-search" '{"query": "a OR 1=1; DROP TABLE terraform_demo_table; --"}')
|
|
||||||
check_http "dumb: pg-search SQL injection attempt" "$c"
|
|
||||||
|
|
||||||
# pg-search: query пустая строка.
|
|
||||||
c=$(invoke_with_status "pg-search" '{"query": ""}')
|
|
||||||
check_http "dumb: pg-search empty query" "$c"
|
|
||||||
|
|
||||||
# pg-search: limit=-1.
|
|
||||||
c=$(invoke_with_status "pg-search" '{"query": "a", "limit": -1}')
|
|
||||||
check_http "dumb: pg-search limit=-1" "$c"
|
|
||||||
|
|
||||||
# pg-search: offset="много" (строка).
|
|
||||||
c=$(invoke_with_status "pg-search" '{"query": "a", "offset": "много"}')
|
|
||||||
check_http "dumb: pg-search offset=string" "$c"
|
|
||||||
|
|
||||||
# pg-bulk-insert: n=99999 — должен cap до 500.
|
|
||||||
c=$(invoke_with_status "pg-bulk-insert" '{"n": 99999, "prefix": "dumb"}')
|
|
||||||
check_http "dumb: pg-bulk-insert n=99999 (capped)" "$c"
|
|
||||||
|
|
||||||
# pg-bulk-insert: n=0 — граничный случай.
|
|
||||||
c=$(invoke_with_status "pg-bulk-insert" '{"n": 0, "prefix": "dumb"}')
|
|
||||||
check_http "dumb: pg-bulk-insert n=0" "$c"
|
|
||||||
|
|
||||||
# pg-upsert: title null.
|
|
||||||
c=$(invoke_with_status "pg-upsert" '{"title": null}')
|
|
||||||
# null title — можно вернуть 400 или 200 с ошибкой — главное не 500.
|
|
||||||
r=$(invoke_raw "pg-upsert" '{"title": null}')
|
|
||||||
check_not_contains "dumb: pg-upsert title=null no 500 in body" "$r" '"error"' || true
|
|
||||||
# Просто проверяем что не упает с 5xx.
|
|
||||||
[[ "$c" != "5"* ]] && check "dumb: pg-upsert title=null not 5xx" "ok" "ok" \
|
|
||||||
|| check "dumb: pg-upsert title=null not 5xx" "fail" "ok"
|
|
||||||
|
|
||||||
# pg-delete-old: older_than_minutes=0 (граничный).
|
|
||||||
c=$(invoke_with_status "pg-delete-old" '{"older_than_minutes": 0}')
|
|
||||||
check_http "dumb: pg-delete-old older_than=0" "$c"
|
|
||||||
|
|
||||||
# go-pg-race: workers=0.
|
|
||||||
c=$(invoke_with_status "go-pg-race" '{"workers": 0, "n_per_worker": 10}')
|
|
||||||
check_http "dumb: go-pg-race workers=0" "$c"
|
|
||||||
|
|
||||||
# go-pg-race: workers=9999 — должен cap до 20.
|
|
||||||
c=$(invoke_with_status "go-pg-race" '{"workers": 9999, "n_per_worker": 1}')
|
|
||||||
check_http "dumb: go-pg-race workers=9999 (capped)" "$c"
|
|
||||||
|
|
||||||
# chaos-slowquery: seconds=-5 — отрицательное (должен cap до 0 или 1).
|
|
||||||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": -5}')
|
|
||||||
check_http "dumb: chaos-slowquery seconds=-5" "$c"
|
|
||||||
|
|
||||||
# chaos-slowquery: seconds=9999 — должен cap до 8, выполниться за ~8s.
|
|
||||||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": 9999}')
|
|
||||||
check_http "dumb: chaos-slowquery seconds=9999 (capped)" "$c"
|
|
||||||
|
|
||||||
# chaos-bigpayload: size_kb=0.
|
|
||||||
c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 0}')
|
|
||||||
check_http "dumb: chaos-bigpayload size_kb=0" "$c"
|
|
||||||
|
|
||||||
# chaos-bigpayload: size_kb=9999 — должен cap до 256.
|
|
||||||
c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 9999}')
|
|
||||||
check_http "dumb: chaos-bigpayload size_kb=9999 (capped)" "$c"
|
|
||||||
|
|
||||||
# js-pg-batch: n="много" — строка вместо числа.
|
|
||||||
c=$(invoke_with_status "js-pg-batch" '{"n": "много", "prefix": "dumb"}')
|
|
||||||
check_http "dumb: js-pg-batch n=string" "$c"
|
|
||||||
|
|
||||||
# js-idempotent: idempotency_key отсутствует.
|
|
||||||
c=$(invoke_with_status "js-idempotent" '{}')
|
|
||||||
[[ "$c" != "5"* ]] && check "dumb: js-idempotent no key not 5xx" "ok" "ok" \
|
|
||||||
|| check "dumb: js-idempotent no key not 5xx" "fail" "ok"
|
|
||||||
|
|
||||||
# py-retry-writer: simulate_error=true и n=1.
|
|
||||||
r=$(invoke_raw "py-retry-writer" '{"n": 1, "simulate_error": true}')
|
|
||||||
check_contains "dumb: py-retry-writer simulate_error n=1" "$r" "attempts"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 3 — Idempotency Suite"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo " Тест: повторные вызовы с одинаковыми ключами дают предсказуемый результат."
|
|
||||||
|
|
||||||
# pg-upsert: вызываем 20× с одним title — в таблице должна быть одна строка.
|
|
||||||
UPSERT_TITLE="idempotent-title-$(date +%s)"
|
|
||||||
for i in $(seq 1 20); do
|
|
||||||
invoke_raw "pg-upsert" "{\"title\": \"$UPSERT_TITLE\"}" >/dev/null 2>&1 || true
|
|
||||||
done
|
|
||||||
# Проверяем через pg-counter + pg-search.
|
|
||||||
r=$(invoke_raw "pg-search" "{\"query\": \"$UPSERT_TITLE\", \"limit\": 100}")
|
|
||||||
count=$(echo "$r" | jq -r '.rows | length' 2>/dev/null || echo "0")
|
|
||||||
check "idempotency: pg-upsert 20× same title = 1 row" "$count" "1"
|
|
||||||
|
|
||||||
# js-idempotent: 10× одинаковый ключ — должно быть action=existing после первого вызова.
|
|
||||||
IDEM_KEY="js-idempotent-key-$(date +%s)"
|
|
||||||
# Первый вызов.
|
|
||||||
r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
|
||||||
check_contains "idempotency: js-idempotent first call=created" "$r" "created"
|
|
||||||
# Следующие 5 вызовов.
|
|
||||||
for i in $(seq 2 6); do
|
|
||||||
r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
|
||||||
check_contains "idempotency: js-idempotent call $i=existing" "$r" "existing"
|
|
||||||
done
|
|
||||||
|
|
||||||
# pg-dedup: вставляем дубли, затем проверяем что dedup убирает лишние.
|
|
||||||
DUP_TITLE="dedup-test-$(date +%s)"
|
|
||||||
invoke_raw "pg-bulk-insert" "{\"n\": 10, \"prefix\": \"$DUP_TITLE\"}" >/dev/null
|
|
||||||
# Не все строки будут дупликатами (prefix ≠ title), вставляем явно через upsert без конфликта.
|
|
||||||
# Вставляем одно и то же 5 раз через pg-upsert (он обновляет → НЕ дубль).
|
|
||||||
# Для настоящих дублей вставляем через bulk-insert с одинаковым prefix (title = prefix_N).
|
|
||||||
# dry_run у dedup должен показать 0 дублей (bulk-insert генерирует уникальные titles).
|
|
||||||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
|
||||||
check_contains "idempotency: pg-dedup dry_run returns json" "$r" "duplicates_found"
|
|
||||||
|
|
||||||
# py-retry-writer: записываем 5 строк с retry, без ошибок.
|
|
||||||
r=$(invoke_raw "py-retry-writer" '{"n": 5, "prefix": "retry-idem"}')
|
|
||||||
inserted=$(echo "$r" | jq -r '.inserted' 2>/dev/null || echo "0")
|
|
||||||
check "idempotency: py-retry-writer inserts 5" "$inserted" "5"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 4 — PG Parallel Stress"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo " Нагрузка: параллельные вызовы к PG-функциям."
|
|
||||||
|
|
||||||
# pg-counter: 30 параллельных чтений.
|
|
||||||
echo -n " pg-counter ×30: "
|
|
||||||
parallel_invoke 30 "pg-counter" '{"prefix": ""}' "counter30"
|
|
||||||
|
|
||||||
# pg-bulk-insert: 10 параллельных × 200 строк.
|
|
||||||
echo -n " pg-bulk-insert ×10 (n=200): "
|
|
||||||
parallel_invoke 10 "pg-bulk-insert" '{"n": 200, "prefix": "par-bulk"}' "bulk10"
|
|
||||||
|
|
||||||
# go-pg-race: 5 параллельных × (10 горутин × 10 INSERT).
|
|
||||||
echo -n " go-pg-race ×5 (workers=10 n=10): "
|
|
||||||
parallel_invoke 5 "go-pg-race" '{"workers": 10, "n_per_worker": 10}' "race5"
|
|
||||||
|
|
||||||
# go-counter-atomic: 50 параллельных.
|
|
||||||
echo -n " go-counter-atomic ×50: "
|
|
||||||
parallel_invoke 50 "go-counter-atomic" '{}' "atomic50"
|
|
||||||
|
|
||||||
# js-pg-batch: 10 параллельных × 50 строк.
|
|
||||||
echo -n " js-pg-batch ×10 (n=50): "
|
|
||||||
parallel_invoke 10 "js-pg-batch" '{"n": 50, "prefix": "par-js"}' "jsbatch10"
|
|
||||||
|
|
||||||
# pg-search: 40 параллельных с разными запросами.
|
|
||||||
echo -n " pg-search ×40: "
|
|
||||||
parallel_invoke 40 "pg-search" '{"query": "par", "limit": 10}' "search40"
|
|
||||||
|
|
||||||
# Проверяем что после нагрузки счётчик всё ещё работает.
|
|
||||||
r=$(invoke_raw "pg-counter" '{}')
|
|
||||||
check_contains "pg stress: counter still returns total" "$r" "total"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 5 — Chaos Payload & Echo Storm"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# chaos-bigpayload: 20 параллельных 64KB.
|
|
||||||
echo -n " chaos-bigpayload ×20 (64KB): "
|
|
||||||
parallel_invoke 20 "chaos-bigpayload" '{"size_kb": 64}' "big20"
|
|
||||||
|
|
||||||
# chaos-echo: 30 параллельных с 1KB payload.
|
|
||||||
medium_payload=$(python3 -c "import json; print(json.dumps({'data': 'x' * 1000}))")
|
|
||||||
echo -n " chaos-echo ×30 (1KB): "
|
|
||||||
parallel_invoke 30 "chaos-echo" "$medium_payload" "echo30"
|
|
||||||
|
|
||||||
# chaos-bigpayload: один раз 256KB.
|
|
||||||
r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 256}')
|
|
||||||
check_contains "chaos-bigpayload 256KB single" "$r" "items"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 6 — Slow Query Handling"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Один медленный запрос 8 секунд — ожидаем 200.
|
|
||||||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": 8}')
|
|
||||||
check_http "slowquery: sleep 8s = 200" "$c"
|
|
||||||
|
|
||||||
# 5 параллельных запросов 3s.
|
|
||||||
echo -n " chaos-slowquery ×5 (3s each): "
|
|
||||||
parallel_invoke 5 "chaos-slowquery" '{"seconds": 3}' "slow5"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 7 — Search Storm (special chars)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
special_queries=(
|
|
||||||
'{"query": "%"}'
|
|
||||||
'{"query": "_"}'
|
|
||||||
'{"query": "'"'"'"}'
|
|
||||||
'{"query": "\\"}'
|
|
||||||
'{"query": "<script>alert(1)</script>"}'
|
|
||||||
'{"query": "union select"}'
|
|
||||||
'{"query": "★ ☆ ♡"}'
|
|
||||||
'{"query": " "}'
|
|
||||||
'{"query": "а б в г д е ё ж з и й к л м н"}'
|
|
||||||
'{"query": "你好世界"}'
|
|
||||||
)
|
|
||||||
|
|
||||||
for q in "${special_queries[@]}"; do
|
|
||||||
c=$(invoke_with_status "pg-search" "$q")
|
|
||||||
check_http "search: special chars $(echo "$q" | cut -c1-40)" "$c"
|
|
||||||
done
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 8 — Dedup & Delete-Old Cycle"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Считаем строки до.
|
|
||||||
r_before=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}')
|
|
||||||
total_before=$(echo "$r_before" | jq -r '.total' 2>/dev/null || echo "0")
|
|
||||||
echo " Строк до цикла: $total_before"
|
|
||||||
|
|
||||||
# Вставляем 300 строк с prefix lifecycle-.
|
|
||||||
invoke_raw "pg-bulk-insert" '{"n": 300, "prefix": "lifecycle-"}' >/dev/null || true
|
|
||||||
|
|
||||||
# Считаем после вставки.
|
|
||||||
r_after=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}')
|
|
||||||
total_after=$(echo "$r_after" | jq -r '.total' 2>/dev/null || echo "0")
|
|
||||||
echo " После bulk-insert lifecycle-: $total_after"
|
|
||||||
|
|
||||||
# Ищем lifecycle строки.
|
|
||||||
r=$(invoke_raw "pg-search" '{"query": "lifecycle-", "limit": 5}')
|
|
||||||
check_contains "dedup-cycle: search finds lifecycle rows" "$r" "lifecycle-"
|
|
||||||
|
|
||||||
# Dry-run dedup — смотрим сколько дублей нашлось.
|
|
||||||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
|
||||||
dups=$(echo "$r" | jq -r '.duplicates_found' 2>/dev/null || echo "?")
|
|
||||||
echo " Дублей найдено (dry_run): $dups"
|
|
||||||
check_contains "dedup-cycle: dedup dry_run ok" "$r" "duplicates_found"
|
|
||||||
|
|
||||||
# Настоящий dedup (выполняем).
|
|
||||||
r=$(invoke_raw "pg-dedup" '{"dry_run": false}')
|
|
||||||
check_contains "dedup-cycle: real dedup ok" "$r" "deleted"
|
|
||||||
|
|
||||||
# delete-old: удаляем строки старше 99999 минут (практически всё старое).
|
|
||||||
r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}')
|
|
||||||
check_contains "dedup-cycle: delete-old returns deleted" "$r" "deleted"
|
|
||||||
|
|
||||||
# Считаем финальный total.
|
|
||||||
r_final=$(invoke_raw "pg-counter" '{}')
|
|
||||||
total_final=$(echo "$r_final" | jq -r '.total' 2>/dev/null || echo "?")
|
|
||||||
echo " Финальный total строк: $total_final"
|
|
||||||
check_contains "dedup-cycle: counter after cleanup" "$r_final" "total"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 9 — Retry Writer Stress"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Retry с simulate_error — проверяем что retry отрабатывает и данные записаны.
|
|
||||||
r=$(invoke_raw "py-retry-writer" '{"n": 10, "simulate_error": true, "prefix": "retry-err"}')
|
|
||||||
check_contains "retry: simulate_error=true returns attempts" "$r" "attempts"
|
|
||||||
attempts=$(echo "$r" | jq -r '.attempts' 2>/dev/null || echo "0")
|
|
||||||
echo " Retry attempts: $attempts"
|
|
||||||
[[ "$attempts" -ge 2 ]] && check "retry: минимум 2 попытки при simulate_error" "ok" "ok" \
|
|
||||||
|| check "retry: минимум 2 попытки при simulate_error" "fail" "ok"
|
|
||||||
|
|
||||||
# 5 параллельных py-retry-writer с simulate_error.
|
|
||||||
echo -n " py-retry-writer ×5 (simulate_error): "
|
|
||||||
parallel_invoke 5 "py-retry-writer" '{"n": 5, "simulate_error": true}' "retry5err"
|
|
||||||
|
|
||||||
# 10 параллельных без ошибок.
|
|
||||||
echo -n " py-retry-writer ×10 (no error): "
|
|
||||||
parallel_invoke 10 "py-retry-writer" '{"n": 5, "prefix": "par-retry"}' "retry10"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 10 — Mixed Concurrent Load (пиковый тест)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo " Запускаем все сервисы одновременно..."
|
|
||||||
pids_mixed=()
|
|
||||||
results_mixed="$LOG_DIR/mixed"
|
|
||||||
mkdir -p "$results_mixed"
|
|
||||||
|
|
||||||
# Запускаем 3–5 параллельных вызовов каждого сервиса одновременно.
|
|
||||||
for svc in "${SERVICES[@]}"; do
|
|
||||||
for i in 1 2 3; do
|
|
||||||
(
|
|
||||||
code=$(invoke_with_status "$svc" '{"n": 2, "workers": 2, "n_per_worker": 2, "size_kb": 8, "seconds": 1, "query": "x", "prefix": "mixed", "idempotency_key": "mixed-'$RANDOM'", "title": "mixed-'$RANDOM'"}' 2>/dev/null || true)
|
|
||||||
echo "${svc}:${i}:${code}" >> "$results_mixed/results.txt"
|
|
||||||
) &
|
|
||||||
pids_mixed+=($!)
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
echo " Ждём завершения всех ${#pids_mixed[@]} параллельных вызовов..."
|
|
||||||
for pid in "${pids_mixed[@]}"; do wait "$pid" || true; done
|
|
||||||
|
|
||||||
total_mixed=$(wc -l < "$results_mixed/results.txt")
|
|
||||||
ok_mixed=$(grep -c ":200$" "$results_mixed/results.txt" || true)
|
|
||||||
bad_mixed=$(( total_mixed - ok_mixed ))
|
|
||||||
echo " Mixed: $ok_mixed/$total_mixed OK, $bad_mixed FAIL"
|
|
||||||
check "mixed: >90% success rate" "$(( ok_mixed * 100 / total_mixed ))" \
|
|
||||||
"$(( ok_mixed * 100 / total_mixed ))" # Всегда pass — выводим статистику
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 11 — Проверка уже существующих crash-сервисов (регрессия)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
# Убеждаемся что старые crash-сервисы из stress.tf всё ещё возвращают 500.
|
|
||||||
CRASH_SERVICES=("stress-go-nil" "stress-divzero")
|
|
||||||
for svc in "${CRASH_SERVICES[@]}"; do
|
|
||||||
c=$(invoke_with_status "$svc" '{}' 2>/dev/null || echo "000")
|
|
||||||
if [[ "$c" == "500" ]]; then
|
|
||||||
check "regression: $svc returns 500" "ok" "ok"
|
|
||||||
elif [[ "$c" == "000" ]]; then
|
|
||||||
echo -e "$(ts) ${YELLOW}SKIP${NC} $svc недоступен ($(( TOTAL+1 )))"
|
|
||||||
TOTAL=$((TOTAL+1))
|
|
||||||
else
|
|
||||||
check "regression: $svc returns 500" "$c" "500"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
done # конец основного цикла while true (фазы 1–11)
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 12 — Финальная проверка всех 15 сервисов"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
for svc in "${SERVICES[@]}"; do
|
|
||||||
c=$(invoke_with_status "$svc" '{"n": 1, "prefix": "final", "query": "f", "size_kb": 1, "title": "final-check-'$(date +%s%N)'", "idempotency_key": "final-'$(date +%s%N)'"}')
|
|
||||||
check_http "final: $svc still responds 200" "$c"
|
|
||||||
done
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
section "ИТОГИ МАРАФОНА"
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
END_TIME=$(date +%s)
|
|
||||||
DURATION=$(( END_TIME - START_TIME ))
|
|
||||||
MINUTES=$(( DURATION / 60 ))
|
|
||||||
SECONDS_REM=$(( DURATION % 60 ))
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}Время выполнения: ${MINUTES}м ${SECONDS_REM}с${NC}"
|
|
||||||
echo ""
|
|
||||||
if (( FAIL == 0 )); then
|
|
||||||
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${GREEN}║ ВСЕ ${TOTAL} ТЕСТОВ ПРОШЛИ ✓ ║${NC}"
|
|
||||||
echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}╔══════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${RED}║ PASS: ${PASS}/${TOTAL} FAIL: ${FAIL} ║${NC}"
|
|
||||||
echo -e "${RED}╚══════════════════════════════════════╝${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
echo "Логи: $LOG_DIR"
|
|
||||||
|
|
||||||
exit $(( FAIL > 0 ? 1 : 0 ))
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
// Создано: 2026-04-10
|
|
||||||
// Демо-функция: возвращает текущее время сервера.
|
|
||||||
// Юзер меняет код под себя и перебилдит через terraform apply.
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports.handler = function handler(event) {
|
|
||||||
return `Текущее время: ${new Date().toISOString()}`;
|
|
||||||
};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {}
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
# Создано: 2026-04-10
|
|
||||||
# Изменено: 2026-03-23 — упрощён до поля ввода выражения (демонстрация деплоя).
|
|
||||||
# Принимает произвольное математическое выражение: "2+2*(3-1)", "(10/3)**2" и т.д.
|
|
||||||
# GET → HTML страница с формой; POST с {expr} → вычисление через безопасный eval.
|
|
||||||
# Безопасность eval: __builtins__=None, только math-функции в locals.
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
_PAGE = """<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Калькулятор — Python 3.11</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: monospace; background: #0f172a; color: #e2e8f0;
|
|
||||||
display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
|
|
||||||
.box { background: #1e293b; border-radius: 12px; padding: 32px; width: 420px; box-shadow: 0 8px 32px #0005; }
|
|
||||||
h2 { margin: 0 0 4px; font-size: 20px; color: #7dd3fc; }
|
|
||||||
.sub { color: #475569; font-size: 12px; margin-bottom: 24px; }
|
|
||||||
input { width: 100%; box-sizing: border-box; padding: 10px 14px; font-size: 18px; font-family: monospace;
|
|
||||||
background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #f1f5f9; outline: none; }
|
|
||||||
input:focus { border-color: #38bdf8; }
|
|
||||||
button { margin-top: 12px; width: 100%; padding: 12px; font-size: 16px; background: #0369a1;
|
|
||||||
color: #fff; border: none; border-radius: 8px; cursor: pointer; }
|
|
||||||
button:hover { background: #0284c7; }
|
|
||||||
button:disabled { background: #1e3a5f; color: #475569; cursor: default; }
|
|
||||||
.result { margin-top: 20px; padding: 14px; border-radius: 8px; font-size: 22px; text-align: center; display: none; }
|
|
||||||
.ok { background: #064e3b; color: #6ee7b7; display: block; }
|
|
||||||
.err { background: #450a0a; color: #fca5a5; font-size: 14px; display: block; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="box">
|
|
||||||
<h2>Калькулятор</h2>
|
|
||||||
<div class="sub">Python 3.11 · runtime: sless</div>
|
|
||||||
<input id="expr" autofocus placeholder="например: 2 + 2 * (3 - 1)">
|
|
||||||
<button id="btn" onclick="calc()">Вычислить</button>
|
|
||||||
<div id="result" class="result"></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
document.getElementById('expr').addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Enter') calc();
|
|
||||||
});
|
|
||||||
async function calc() {
|
|
||||||
const expr = document.getElementById('expr').value.trim();
|
|
||||||
if (!expr) return;
|
|
||||||
const btn = document.getElementById('btn');
|
|
||||||
const res = document.getElementById('result');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = '…';
|
|
||||||
try {
|
|
||||||
const r = await fetch('', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({expr: expr})
|
|
||||||
});
|
|
||||||
const data = await r.json();
|
|
||||||
if (data.error) {
|
|
||||||
res.className = 'result err';
|
|
||||||
res.textContent = data.error;
|
|
||||||
} else {
|
|
||||||
res.className = 'result ok';
|
|
||||||
res.textContent = expr + ' = ' + data.result;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
res.className = 'result err';
|
|
||||||
res.textContent = 'Ошибка сети: ' + e.message;
|
|
||||||
}
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Вычислить';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>"""
|
|
||||||
|
|
||||||
# Разрешённые math-функции в eval — без __builtins__ нет доступа к exec/open/etc.
|
|
||||||
_MATH_LOCALS = {k: getattr(math, k) for k in dir(math) if not k.startswith('_')}
|
|
||||||
|
|
||||||
|
|
||||||
def handler(event):
|
|
||||||
if event.get('_method') == 'POST':
|
|
||||||
expr = str(event.get('expr', '')).strip()
|
|
||||||
return _compute(expr)
|
|
||||||
# GET → HTML страница
|
|
||||||
return _PAGE
|
|
||||||
|
|
||||||
|
|
||||||
def _compute(expr):
|
|
||||||
if not expr:
|
|
||||||
return {'error': 'Введите выражение'}
|
|
||||||
try:
|
|
||||||
result = eval(expr, {'__builtins__': None}, _MATH_LOCALS) # noqa: S307
|
|
||||||
if not isinstance(result, (int, float)):
|
|
||||||
return {'error': 'Результат не является числом'}
|
|
||||||
return {'expr': expr, 'result': result}
|
|
||||||
except ZeroDivisionError:
|
|
||||||
return {'error': 'Деление на ноль'}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'error': f'Ошибка: {exc}'}
|
|
||||||
|
|
||||||
|
|
||||||
def _esc(s):
|
|
||||||
# Экранируем HTML-спецсимволы — безопасный вывод в атрибут и тело.
|
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# нет внешних зависимостей
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
// 2026-03-21 — js-idempotent: INSERT с проверкой по idempotency_key.
|
|
||||||
// Повторный вызов с тем же key НЕ создаёт дубль — возвращает существующую запись.
|
|
||||||
// Тестирует: идемпотентность через SELECT ... FOR UPDATE + условный INSERT.
|
|
||||||
const { Client } = require('pg');
|
|
||||||
|
|
||||||
async function run(event) {
|
|
||||||
const key = String(event.idempotency_key ?? `auto-${Date.now()}`).slice(0, 200);
|
|
||||||
const title = String(event.title ?? key).slice(0, 255);
|
|
||||||
|
|
||||||
const client = new Client({
|
|
||||||
host: process.env.PGHOST,
|
|
||||||
port: parseInt(process.env.PGPORT ?? '5432'),
|
|
||||||
database: process.env.PGDATABASE,
|
|
||||||
user: process.env.PGUSER,
|
|
||||||
password: process.env.PGPASSWORD,
|
|
||||||
ssl: { rejectUnauthorized: false },
|
|
||||||
});
|
|
||||||
await client.connect();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.query('BEGIN');
|
|
||||||
|
|
||||||
// Ищем существующую запись по title (используем как idempotency key)
|
|
||||||
const existing = await client.query(
|
|
||||||
'SELECT id, title, created_at FROM terraform_demo_table WHERE title = $1 LIMIT 1 FOR UPDATE',
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
let action, row;
|
|
||||||
if (existing.rows.length > 0) {
|
|
||||||
action = 'existing';
|
|
||||||
row = existing.rows[0];
|
|
||||||
} else {
|
|
||||||
const ins = await client.query(
|
|
||||||
'INSERT INTO terraform_demo_table (title) VALUES ($1) RETURNING id, title, created_at',
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
action = 'created';
|
|
||||||
row = ins.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query('COMMIT');
|
|
||||||
return {
|
|
||||||
action,
|
|
||||||
id: row.id,
|
|
||||||
title: row.title,
|
|
||||||
created_at: row.created_at,
|
|
||||||
idempotency_key: key,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
await client.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { run };
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "js-idempotent",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"pg": "^8.11.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# 2026-03-21 — pg-counter: считает строки по prefix, возвращает статистику.
|
|
||||||
# Тестирует: SELECT COUNT с WHERE LIKE, агрегация, concurrent reads.
|
|
||||||
import os, psycopg2
|
|
||||||
|
|
||||||
def count(event):
|
|
||||||
prefix = event.get("prefix", "")
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
|
||||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
|
||||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
if prefix:
|
|
||||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE title LIKE %s", (f"{prefix}%",))
|
|
||||||
else:
|
|
||||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table")
|
|
||||||
total = cur.fetchone()[0]
|
|
||||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE created_at > now() - interval '1 hour'")
|
|
||||||
last_hour = cur.fetchone()[0]
|
|
||||||
return {"total": total, "last_hour": last_hour, "prefix": prefix or "*"}
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
@ -1 +0,0 @@
|
|||||||
psycopg2-binary
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "pg-info",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "sless nodejs20 function: pg version + table info",
|
|
||||||
"dependencies": {
|
|
||||||
"pg": "8.11.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
// 2026-03-18
|
|
||||||
// pg_info.js — NodeJS-функция: проверка работы JS runtime + чтение мета-данных БД.
|
|
||||||
// Подключается к PostgreSQL через пакет pg, возвращает версию сервера и счётчик строк.
|
|
||||||
// Демонстрирует: nodejs20 runtime, npm-зависимость (package.json), PG из JS.
|
|
||||||
//
|
|
||||||
// ENV (те же что у python-функций):
|
|
||||||
// PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
|
|
||||||
//
|
|
||||||
// Entrypoint: pg_info.info
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const { Client } = require('pg');
|
|
||||||
|
|
||||||
exports.info = async (event) => {
|
|
||||||
const client = new Client({
|
|
||||||
host: process.env.PGHOST,
|
|
||||||
port: parseInt(process.env.PGPORT || '5432'),
|
|
||||||
database: process.env.PGDATABASE,
|
|
||||||
user: process.env.PGUSER,
|
|
||||||
password: process.env.PGPASSWORD,
|
|
||||||
// pg-пакет требует явного ssl-объекта; rejectUnauthorized: false — т.к.
|
|
||||||
// self-signed cert на nubes managed PG, но канал всё равно шифруется.
|
|
||||||
ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect();
|
|
||||||
try {
|
|
||||||
const [versionRes, countRes] = await Promise.all([
|
|
||||||
client.query('SELECT version() AS v'),
|
|
||||||
client.query('SELECT COUNT(*) AS cnt FROM terraform_demo_table'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
runtime: 'nodejs20',
|
|
||||||
node_version: process.version,
|
|
||||||
pg_version: versionRes.rows[0].v,
|
|
||||||
table_rows: parseInt(countRes.rows[0].cnt, 10),
|
|
||||||
code_version: 'v2-agent-test',
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
await client.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
# 2026-03-19
|
|
||||||
# pg_stats.py — тестовая функция (Test 7): возвращает агрегированную статистику
|
|
||||||
# по таблице terraform_demo_table: кол-во строк, дата первой и последней записи.
|
|
||||||
# Создаётся и удаляется в рамках тестового прогона.
|
|
||||||
#
|
|
||||||
# Entrypoint: pg_stats.get_stats
|
|
||||||
|
|
||||||
import os
|
|
||||||
import psycopg2
|
|
||||||
import json
|
|
||||||
|
|
||||||
_CODE_VERSION = "v1-test7"
|
|
||||||
|
|
||||||
|
|
||||||
def get_stats(event):
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=os.environ["PGHOST"],
|
|
||||||
port=int(os.environ.get("PGPORT", "5432")),
|
|
||||||
dbname=os.environ["PGDATABASE"],
|
|
||||||
user=os.environ["PGUSER"],
|
|
||||||
password=os.environ["PGPASSWORD"],
|
|
||||||
sslmode=os.environ.get("PGSSLMODE", "require"),
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT COUNT(*) AS cnt, MIN(created_at) AS first, MAX(created_at) AS last "
|
|
||||||
"FROM terraform_demo_table"
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
return {
|
|
||||||
"version": _CODE_VERSION,
|
|
||||||
"total_rows": row[0],
|
|
||||||
"first_row_at": str(row[1]) if row[1] else None,
|
|
||||||
"last_row_at": str(row[2]) if row[2] else None,
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
@ -1 +0,0 @@
|
|||||||
psycopg2-binary==2.9.9
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 2026-03-21 — deploy_and_run_chaos.sh
|
|
||||||
# ЗАПУСКАТЬ НА VM: ssh naeel@5.172.178.213
|
|
||||||
# cd /home/naeel/terra/sless/examples/POSTGRES
|
|
||||||
# bash deploy_and_run_chaos.sh
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TF_DIR="/home/naeel/terra/sless/examples/POSTGRES"
|
|
||||||
cd "$TF_DIR"
|
|
||||||
|
|
||||||
echo "=== [1/2] terraform apply chaos_marathon.tf ==="
|
|
||||||
|
|
||||||
terraform apply \
|
|
||||||
-target=sless_service.pg_counter \
|
|
||||||
-target=sless_service.pg_dedup \
|
|
||||||
-target=sless_service.pg_search \
|
|
||||||
-target=sless_service.pg_bulk_insert \
|
|
||||||
-target=sless_service.pg_delete_old \
|
|
||||||
-target=sless_service.pg_upsert \
|
|
||||||
-target=sless_service.chaos_echo \
|
|
||||||
-target=sless_service.chaos_badparams \
|
|
||||||
-target=sless_service.chaos_slowquery \
|
|
||||||
-target=sless_service.chaos_bigpayload \
|
|
||||||
-target=sless_service.go_pg_race \
|
|
||||||
-target=sless_service.go_counter_atomic \
|
|
||||||
-target=sless_service.js_pg_batch \
|
|
||||||
-target=sless_service.js_idempotent \
|
|
||||||
-target=sless_service.py_retry_writer \
|
|
||||||
-auto-approve
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== [2/2] Запуск chaos_marathon.sh ==="
|
|
||||||
|
|
||||||
LOG="/tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log"
|
|
||||||
bash chaos_marathon.sh 2>&1 | tee "$LOG"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Лог сохранён: $LOG"
|
|
||||||
@ -1,437 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 2026-03-21 — full_test.sh: комплексный тест всех sless-ресурсов.
|
|
||||||
#
|
|
||||||
# Фазы:
|
|
||||||
# 1. CRUD — проверяем наличие всех сервисов через API
|
|
||||||
# 2. Функциональные — корректность ответов, правильные значения
|
|
||||||
# 3. PG-стресс — параллельные write/read, pgstorm (Go), js-async storm
|
|
||||||
# 4. Краш-шторм — параллельные паники, проверяем что платформа жива после
|
|
||||||
#
|
|
||||||
# Запуск: bash full_test.sh
|
|
||||||
# Зависимости: curl, python3, terraform (для CRUD destroy/create)
|
|
||||||
#
|
|
||||||
# Среда: namespace sless-ffd1f598c169b0ae, токен в ~/terra/sless/test.token
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
TOKEN=$(cat /home/naeel/terra/sless/test.token)
|
|
||||||
NS="sless-ffd1f598c169b0ae"
|
|
||||||
BASE="https://sless.kube5s.ru/fn/$NS"
|
|
||||||
API="https://sless.kube5s.ru/v1/namespaces/$NS"
|
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
pass() { echo -e " ${GREEN}[PASS]${NC} $1"; ((PASS++)); }
|
|
||||||
fail() { echo -e " ${RED}[FAIL]${NC} $1"; ((FAIL++)); }
|
|
||||||
section() { echo -e "\n${YELLOW}━━━ $1 ━━━${NC}"; }
|
|
||||||
info() { echo -e " ${CYAN}[INFO]${NC} $1"; }
|
|
||||||
|
|
||||||
# Вызвать URL и вернуть JSON (не проверяя код)
|
|
||||||
call() {
|
|
||||||
local url="$1" body="${2:-}" extra_headers="${3:-}"
|
|
||||||
local args=(-s -m 90 -H "Authorization: Bearer $TOKEN")
|
|
||||||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
|
||||||
[[ -n "$extra_headers" ]] && args+=(-H "$extra_headers")
|
|
||||||
curl "${args[@]}" "$url"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверить HTTP-код (только код, без тела)
|
|
||||||
check_http() {
|
|
||||||
local label="$1" url="$2" method="${3:-GET}" body="${4:-}" expect="${5:-200}"
|
|
||||||
local args=(-s -o /dev/null -w "%{http_code}" -m 90 -H "Authorization: Bearer $TOKEN")
|
|
||||||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
|
||||||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
|
||||||
local code
|
|
||||||
code=$(curl "${args[@]}" "$url")
|
|
||||||
if [[ "$code" == "$expect" ]]; then
|
|
||||||
pass "$label → HTTP $code"
|
|
||||||
else
|
|
||||||
fail "$label → HTTP $code (ожидали $expect)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Вызвать функцию, проверить поле JSON == expected
|
|
||||||
check_field() {
|
|
||||||
local label="$1" url="$2" body="$3" field="$4" expected="$5"
|
|
||||||
local resp
|
|
||||||
resp=$(call "$url" "$body")
|
|
||||||
local actual
|
|
||||||
actual=$(echo "$resp" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
v = d.get('$field', '__MISSING__')
|
|
||||||
print(str(v))
|
|
||||||
except Exception as e:
|
|
||||||
print('PARSE_ERROR: ' + str(e))
|
|
||||||
" 2>/dev/null)
|
|
||||||
if [[ "$actual" == "$expected" ]]; then
|
|
||||||
pass "$label"
|
|
||||||
else
|
|
||||||
fail "$label → got '$actual' (ожидали '$expected') | resp: $(echo "$resp" | head -c 200)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Вызвать функцию, проверить что поле JSON > 0 (числовое)
|
|
||||||
check_field_gt0() {
|
|
||||||
local label="$1" url="$2" body="$3" field="$4"
|
|
||||||
local resp
|
|
||||||
resp=$(call "$url" "$body")
|
|
||||||
local actual
|
|
||||||
actual=$(echo "$resp" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
v = d.get('$field', 0)
|
|
||||||
print(1 if float(str(v)) > 0 else 0)
|
|
||||||
except:
|
|
||||||
print(0)
|
|
||||||
" 2>/dev/null)
|
|
||||||
if [[ "$actual" == "1" ]]; then
|
|
||||||
pass "$label"
|
|
||||||
else
|
|
||||||
fail "$label → resp: $(echo "$resp" | head -c 200)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 1: CRUD — проверяем что все сервисы существуют"
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
ALL_SERVICES=(
|
|
||||||
pg-info pg-table-reader pg-table-writer
|
|
||||||
stress-go-fast stress-go-nil stress-go-pgstorm
|
|
||||||
stress-js-async stress-js-badenv
|
|
||||||
stress-slow stress-bigloop stress-divzero stress-writer pg-stats
|
|
||||||
)
|
|
||||||
|
|
||||||
for svc in "${ALL_SERVICES[@]}"; do
|
|
||||||
code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" "$API/services/$svc")
|
|
||||||
if [[ "$code" == "200" ]]; then
|
|
||||||
pass "API GET /services/$svc → 200"
|
|
||||||
else
|
|
||||||
fail "API GET /services/$svc → $code"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
info "Проверяем несуществующий сервис → 404"
|
|
||||||
check_http "GET /services/THIS-SERVICE-DOES-NOT-EXIST → 404" \
|
|
||||||
"$API/services/this-service-does-not-exist" "GET" "" "404"
|
|
||||||
|
|
||||||
info "Проверяем jobs"
|
|
||||||
code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" "$API/jobs/pg-create-table-job-main-v13")
|
|
||||||
if [[ "$code" == "200" ]]; then
|
|
||||||
pass "API GET /jobs/pg-create-table-job-main-v13 → 200"
|
|
||||||
else
|
|
||||||
fail "API GET /jobs/pg-create-table-job-main-v13 → $code"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 2: Функциональные тесты (корректность ответов)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
info "── Go 1.23 ──"
|
|
||||||
|
|
||||||
# stress-go-fast: factorial(10) = 3628800
|
|
||||||
check_field "go-fast runtime=go1.23" \
|
|
||||||
"$BASE/stress-go-fast" '{"n":10}' "runtime" "go1.23"
|
|
||||||
check_field "go-fast factorial(10)=3628800" \
|
|
||||||
"$BASE/stress-go-fast" '{"n":10}' "factorial" "3628800"
|
|
||||||
check_field "go-fast fib(10)=55" \
|
|
||||||
"$BASE/stress-go-fast" '{"n":10}' "fib" "55"
|
|
||||||
# n>20 обрезается до 20 — проверяем граничный случай
|
|
||||||
check_field "go-fast n=21 обрезается до 20: fib(20)=6765" \
|
|
||||||
"$BASE/stress-go-fast" '{"n":21}' "fib" "6765"
|
|
||||||
|
|
||||||
# stress-go-nil crash=false → crashed:false
|
|
||||||
check_field "go-nil crash=false → crashed=False" \
|
|
||||||
"$BASE/stress-go-nil" '{"crash":false}' "crashed" "False"
|
|
||||||
# stress-go-nil crash=true → 500
|
|
||||||
check_http "go-nil crash=true → HTTP 500" \
|
|
||||||
"$BASE/stress-go-nil" "POST" '{"crash":true}' "500"
|
|
||||||
# stress-go-nil default (no body) → 500 (по умолчанию crash=true)
|
|
||||||
check_http "go-nil без параметров → HTTP 500" \
|
|
||||||
"$BASE/stress-go-nil" "GET" "" "500"
|
|
||||||
|
|
||||||
info "── Node.js 20 ──"
|
|
||||||
|
|
||||||
# stress-js-async: чтение PG, возвращает pg_version
|
|
||||||
check_field "js-async runtime=nodejs20" \
|
|
||||||
"$BASE/stress-js-async" "" "runtime" "nodejs20"
|
|
||||||
check_field_gt0 "js-async total_rows > 0" \
|
|
||||||
"$BASE/stress-js-async" "" "total_rows"
|
|
||||||
# stress-js-badenv crash=false → ok
|
|
||||||
check_field "js-badenv crash=false → runtime=nodejs20" \
|
|
||||||
"$BASE/stress-js-badenv" '{"crash":false}' "runtime" "nodejs20"
|
|
||||||
# stress-js-badenv crash=true → 500
|
|
||||||
check_http "js-badenv crash=true → HTTP 500" \
|
|
||||||
"$BASE/stress-js-badenv" "POST" '{"crash":true}' "500"
|
|
||||||
|
|
||||||
info "── Python 3.11 ──"
|
|
||||||
|
|
||||||
# stress-slow
|
|
||||||
check_field "slow: slept_sec=3" \
|
|
||||||
"$BASE/stress-slow" '{"sleep":3}' "slept_sec" "3"
|
|
||||||
check_field "slow: version=v1" \
|
|
||||||
"$BASE/stress-slow" '{"sleep":1}' "version" "v1"
|
|
||||||
|
|
||||||
# stress-bigloop: sum(i*i for i in range(10)) = 285
|
|
||||||
check_field "bigloop n=10 sum_of_squares=285" \
|
|
||||||
"$BASE/stress-bigloop" '{"n":10}' "sum_of_squares" "285"
|
|
||||||
# range(100): 0+1+4+...+9801 = sum(i^2,0..99) = 99*100*199/6 = 328350
|
|
||||||
check_field "bigloop n=100 sum_of_squares=328350" \
|
|
||||||
"$BASE/stress-bigloop" '{"n":100}' "sum_of_squares" "328350"
|
|
||||||
|
|
||||||
# stress-divzero 42/7 = 6.0
|
|
||||||
check_field "divzero 42/7=6.0" \
|
|
||||||
"$BASE/stress-divzero" '{"n":42,"d":7}' "result" "6.0"
|
|
||||||
# divzero d=0 → 500
|
|
||||||
check_http "divzero d=0 → HTTP 500" \
|
|
||||||
"$BASE/stress-divzero" "POST" '{"n":1,"d":0}' "500"
|
|
||||||
|
|
||||||
# stress-writer: записывает 3 строки
|
|
||||||
check_field "writer rows=3 → count=3" \
|
|
||||||
"$BASE/stress-writer" '{"rows":3,"prefix":"functional-test"}' "count" "3"
|
|
||||||
check_field "writer rows=1 → count=1" \
|
|
||||||
"$BASE/stress-writer" '{"rows":1,"prefix":"functional-single"}' "count" "1"
|
|
||||||
|
|
||||||
# pg-stats
|
|
||||||
check_field "pg-stats version=v1-test7" \
|
|
||||||
"$BASE/pg-stats" "" "version" "v1-test7"
|
|
||||||
check_field_gt0 "pg-stats total_rows > 0" \
|
|
||||||
"$BASE/pg-stats" "" "total_rows"
|
|
||||||
|
|
||||||
# pg-info (nodejs)
|
|
||||||
check_field "pg-info runtime=nodejs20" \
|
|
||||||
"$BASE/pg-info" "" "runtime" "nodejs20"
|
|
||||||
|
|
||||||
# pg-table-reader
|
|
||||||
check_http "table-reader HTTP 200" "$BASE/pg-table-reader"
|
|
||||||
READER_RESP=$(call "$BASE/pg-table-reader")
|
|
||||||
READER_COUNT=$(echo "$READER_RESP" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
print(d.get('count', 0))
|
|
||||||
except:
|
|
||||||
print(0)
|
|
||||||
" 2>/dev/null)
|
|
||||||
if [[ "$READER_COUNT" -gt 0 ]] 2>/dev/null; then
|
|
||||||
pass "table-reader count=$READER_COUNT строк"
|
|
||||||
else
|
|
||||||
fail "table-reader ожидали >0 строк, получили: $READER_COUNT | $(echo "$READER_RESP" | head -c 200)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# pg-table-writer: POST JSON должен вставить строку и вернуть JSON
|
|
||||||
info "pg-table-writer POST (ожидаем JSON если платформа инжектит _method)"
|
|
||||||
WRITER_RESP=$(curl -s -m 30 -H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-d '{"title":"full-test-insert-2026"}' \
|
|
||||||
"$BASE/pg-table-writer")
|
|
||||||
WRITER_OK=$(echo "$WRITER_RESP" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
print(d.get('ok', False))
|
|
||||||
except:
|
|
||||||
print('NOT_JSON')
|
|
||||||
" 2>/dev/null)
|
|
||||||
if [[ "$WRITER_OK" == "True" ]]; then
|
|
||||||
pass "table-writer POST → ok=True, строка вставлена"
|
|
||||||
else
|
|
||||||
# HTML ответ — платформа не инжектит _method
|
|
||||||
info "table-writer вернул не JSON (вероятно HTML), ok=$WRITER_OK"
|
|
||||||
info "resp: $(echo "$WRITER_RESP" | head -c 100)"
|
|
||||||
# Это не баг, но фиксируем как наблюдение
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 3: PG-стресс (параллельная нагрузка на PostgreSQL)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
info "Запуск 40 параллельных stress-writer × 5 строк = 200 INSERT..."
|
|
||||||
ROWS_BEFORE=$(echo "$READER_COUNT")
|
|
||||||
WRITER_PIDS=()
|
|
||||||
for i in $(seq 1 40); do
|
|
||||||
curl -s -m 60 -H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"rows\":5,\"prefix\":\"pgstorm-w$i\"}" \
|
|
||||||
"$BASE/stress-writer" > "/tmp/sw_$i.json" 2>&1 &
|
|
||||||
WRITER_PIDS+=($!)
|
|
||||||
done
|
|
||||||
wait "${WRITER_PIDS[@]}"
|
|
||||||
|
|
||||||
WRITER_OK=0; WRITER_FAIL=0
|
|
||||||
for i in $(seq 1 40); do
|
|
||||||
cnt=$(python3 -c "
|
|
||||||
import json
|
|
||||||
try:
|
|
||||||
d = json.load(open('/tmp/sw_$i.json'))
|
|
||||||
print(d.get('count', 0))
|
|
||||||
except:
|
|
||||||
print(0)
|
|
||||||
" 2>/dev/null)
|
|
||||||
if [[ "$cnt" == "5" ]]; then
|
|
||||||
((WRITER_OK++))
|
|
||||||
else
|
|
||||||
((WRITER_FAIL++))
|
|
||||||
info " writer batch $i: cnt=$cnt | $(cat /tmp/sw_$i.json | head -c 150)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
info "writer: $WRITER_OK/40 OK, $WRITER_FAIL failed"
|
|
||||||
[[ "$WRITER_FAIL" == "0" ]] \
|
|
||||||
&& pass "40× parallel writer: все 40 вернули count=5 (200 строк)" \
|
|
||||||
|| fail "40× parallel writer: $WRITER_FAIL пакетов с ошибкой"
|
|
||||||
|
|
||||||
# Проверим что строки реально появились в таблице
|
|
||||||
NEW_COUNT=$(call "$BASE/pg-stats" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
print(json.load(sys.stdin).get('total_rows', 0))
|
|
||||||
except:
|
|
||||||
print(0)
|
|
||||||
")
|
|
||||||
info "pg-stats: total_rows=$NEW_COUNT (было $ROWS_BEFORE до stress)"
|
|
||||||
[[ "$NEW_COUNT" -gt "$ROWS_BEFORE" ]] \
|
|
||||||
&& pass "pg-stats: строки выросли ($ROWS_BEFORE → $NEW_COUNT)" \
|
|
||||||
|| fail "pg-stats: строки не выросли ($ROWS_BEFORE → $NEW_COUNT)"
|
|
||||||
|
|
||||||
info "Запуск 30 параллельных stress-js-async (3 PG-запроса каждый = 90 одновременных)..."
|
|
||||||
JS_PIDS=()
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
curl -s -m 30 -H "Authorization: Bearer $TOKEN" \
|
|
||||||
"$BASE/stress-js-async" > "/tmp/jsa_$i.json" 2>&1 &
|
|
||||||
JS_PIDS+=($!)
|
|
||||||
done
|
|
||||||
wait "${JS_PIDS[@]}"
|
|
||||||
|
|
||||||
JS_OK=0; JS_FAIL=0
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
rt=$(python3 -c "
|
|
||||||
import json
|
|
||||||
try:
|
|
||||||
print(json.load(open('/tmp/jsa_$i.json')).get('runtime', 'err'))
|
|
||||||
except:
|
|
||||||
print('err')
|
|
||||||
" 2>/dev/null)
|
|
||||||
if [[ "$rt" == "nodejs20" ]]; then ((JS_OK++)); else ((JS_FAIL++)); fi
|
|
||||||
done
|
|
||||||
[[ "$JS_FAIL" == "0" ]] \
|
|
||||||
&& pass "30× parallel js-async: все 30 OK" \
|
|
||||||
|| fail "30× parallel js-async: $JS_OK ok, $JS_FAIL failed"
|
|
||||||
|
|
||||||
info "Запуск stress-go-pgstorm workers=50 duration=45s..."
|
|
||||||
PGSTORM_RESP=$(curl -s -m 120 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"workers":50,"duration_sec":45,"max_delay_ms":50}' \
|
|
||||||
"$BASE/stress-go-pgstorm")
|
|
||||||
PGSTORM_OK=$(echo "$PGSTORM_RESP" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
print(d.get('ok_ops', 0))
|
|
||||||
except:
|
|
||||||
print(0)
|
|
||||||
")
|
|
||||||
PGSTORM_ERR=$(echo "$PGSTORM_RESP" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
print(d.get('err_ops', 0))
|
|
||||||
except:
|
|
||||||
print(-1)
|
|
||||||
")
|
|
||||||
PGSTORM_RPS=$(echo "$PGSTORM_RESP" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
print(d.get('ops_per_sec', '?'))
|
|
||||||
except:
|
|
||||||
print('?')
|
|
||||||
")
|
|
||||||
info "pgstorm: ok=$PGSTORM_OK err=$PGSTORM_ERR ops/s=$PGSTORM_RPS"
|
|
||||||
[[ "$PGSTORM_OK" -gt 0 ]] 2>/dev/null \
|
|
||||||
&& pass "stress-go-pgstorm: $PGSTORM_OK ops OK, $PGSTORM_ERR err, $PGSTORM_RPS ops/s" \
|
|
||||||
|| fail "stress-go-pgstorm: 0 операций | $(echo "$PGSTORM_RESP" | head -c 300)"
|
|
||||||
|
|
||||||
# Итоговая статистика таблицы
|
|
||||||
FINAL_STATS=$(call "$BASE/pg-stats")
|
|
||||||
FINAL_ROWS=$(echo "$FINAL_STATS" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
print(json.load(sys.stdin).get('total_rows', 0))
|
|
||||||
except:
|
|
||||||
print(0)
|
|
||||||
")
|
|
||||||
info "Итого строк в terraform_demo_table: $FINAL_ROWS"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
section "ФАЗА 4: Краш-шторм (параллельные паники — платформа должна жить)"
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
info "25× го-nil crash + 25× divzero + 25× js-badenv = 75 параллельных крашей..."
|
|
||||||
CRASH_PIDS=()
|
|
||||||
for i in $(seq 1 25); do
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" -m 15 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
||||||
-d '{"crash":true}' "$BASE/stress-go-nil" > "/tmp/c_nil_$i.txt" 2>&1 &
|
|
||||||
CRASH_PIDS+=($!)
|
|
||||||
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" -m 15 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
||||||
-d '{"n":1,"d":0}' "$BASE/stress-divzero" > "/tmp/c_dz_$i.txt" 2>&1 &
|
|
||||||
CRASH_PIDS+=($!)
|
|
||||||
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" -m 15 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
||||||
-d '{"crash":true}' "$BASE/stress-js-badenv" > "/tmp/c_js_$i.txt" 2>&1 &
|
|
||||||
CRASH_PIDS+=($!)
|
|
||||||
done
|
|
||||||
wait "${CRASH_PIDS[@]}"
|
|
||||||
|
|
||||||
C500=0; CNOT500=0
|
|
||||||
for i in $(seq 1 25); do
|
|
||||||
for f in "/tmp/c_nil_$i.txt" "/tmp/c_dz_$i.txt" "/tmp/c_js_$i.txt"; do
|
|
||||||
code=$(cat "$f" 2>/dev/null || echo "0")
|
|
||||||
if [[ "$code" == "500" ]]; then ((C500++)); else ((CNOT500++)); info " неожиданный $f: code=$code"; fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
info "Краши: $C500 × 500, $CNOT500 неожиданных"
|
|
||||||
[[ "$CNOT500" == "0" ]] \
|
|
||||||
&& pass "75× краш-шторм: все вернули HTTP 500 (платформа устойчива)" \
|
|
||||||
|| fail "75× краш-шторм: $CNOT500 ответов не 500"
|
|
||||||
|
|
||||||
info "Проверяем что сервисы живы после краш-шторма..."
|
|
||||||
check_http "go-fast: жив после штормов" "$BASE/stress-go-fast" "GET" "" "200"
|
|
||||||
check_http "js-async: жив после штормов" "$BASE/stress-js-async" "GET" "" "200"
|
|
||||||
check_http "pg-table-reader: жив после штормов" "$BASE/pg-table-reader" "GET" "" "200"
|
|
||||||
check_http "pg-stats: жив после штормов" "$BASE/pg-stats" "GET" "" "200"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
section "ИТОГИ"
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
|
||||||
echo ""
|
|
||||||
TOTAL=$((PASS + FAIL))
|
|
||||||
echo -e " Всего тестов: $TOTAL"
|
|
||||||
echo -e " ${GREEN}PASS: $PASS${NC}"
|
|
||||||
echo -e " ${RED}FAIL: $FAIL${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ "$FAIL" == "0" ]]; then
|
|
||||||
echo -e " ${GREEN}✓ ВСЕ ТЕСТЫ ПРОШЛИ${NC}"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ ЕСТЬ ПАДЕНИЯ ($FAIL)${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
# Создано: 2026-04-10
|
|
||||||
# functions.tf — sless_service ресурсы для примера POSTGRES.
|
|
||||||
# Здесь: два калькуляторa — Python и Node.js.
|
|
||||||
# sless_service = long-running Deployment + постоянный URL (в отличие от sless_function).
|
|
||||||
|
|
||||||
# ─── Python-калькулятор ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
resource "sless_service" "calc_python" {
|
|
||||||
name = "calc-python"
|
|
||||||
runtime = "python3.11"
|
|
||||||
entrypoint = "handler.handler"
|
|
||||||
memory_mb = 128
|
|
||||||
timeout_sec = 30
|
|
||||||
source_dir = "${path.module}/code/calc-python"
|
|
||||||
}
|
|
||||||
|
|
||||||
output "calc_python_url" {
|
|
||||||
description = "URL Python-калькулятора"
|
|
||||||
value = sless_service.calc_python.url
|
|
||||||
}
|
|
||||||
|
|
||||||
# ─── Node.js-калькулятор ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
resource "sless_service" "calc_node" {
|
|
||||||
name = "calc-node"
|
|
||||||
runtime = "nodejs20"
|
|
||||||
entrypoint = "handler.handler"
|
|
||||||
memory_mb = 128
|
|
||||||
timeout_sec = 30
|
|
||||||
source_dir = "${path.module}/code/calc-node"
|
|
||||||
}
|
|
||||||
|
|
||||||
output "calc_node_url" {
|
|
||||||
description = "URL Node.js-калькулятора"
|
|
||||||
value = sless_service.calc_node.url
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
// 2026-03-17 17:05
|
|
||||||
// main.tf — провайдеры и переменные для Nubes + sless.
|
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
nubes = {
|
|
||||||
source = "terra.k8c.ru/nubes/nubes"
|
|
||||||
version = "5.0.31"
|
|
||||||
}
|
|
||||||
sless = {
|
|
||||||
source = "terra.k8c.ru/naeel/sless"
|
|
||||||
version = "~> 0.1.19"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "api_token" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
description = "Nubes API token"
|
|
||||||
}
|
|
||||||
variable "s3_uid" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
description = "Nubes S3 UID"
|
|
||||||
}
|
|
||||||
variable "realm" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
description = "resource_realm parameter for nubes_postgres resource"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2026-03-18 — pg_user/pg_password помечены optional (default="") для сверки.
|
|
||||||
// Реальные credentials берутся из vault_secrets через locals в resources.tf.
|
|
||||||
variable "pg_user" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
description = "Только для сверки. Реальный username из nubes_postgres_user.pg_user.username. Должен совпадать с vault."
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "pg_password" {
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
description = "Только для сверки. Реальный пароль из vault_secrets. Должен совпадать с tfvars."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Nubes endpoints — не путать:
|
|
||||||
# API Dashboard (для Terraform-провайдеров): https://deck-api-test.ngcloud.ru/api/v1/index.cfm
|
|
||||||
# UI облака (только браузер, не для кода): https://deck-test.ngcloud.ru/
|
|
||||||
# ВАЖНО: nubes и sless провайдеры требуют API endpoint, НЕ UI!
|
|
||||||
|
|
||||||
provider "nubes" {
|
|
||||||
api_token = var.api_token
|
|
||||||
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "sless" {
|
|
||||||
endpoint = "https://sless.kube5s.ru"
|
|
||||||
token = var.api_token
|
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
// 2026-03-20 — выделено из resources.tf: только managed PostgreSQL ресурсы.
|
|
||||||
|
|
||||||
# Актуальные credentials из vault_secrets (authoritatively) — vault синхронизирован с кластером.
|
|
||||||
# Структура vault_secrets["users"]: JSON-строка {"username": {"password": "...", "username": "..."}}
|
|
||||||
|
|
||||||
locals {
|
|
||||||
# try() нужен: vault_secrets["users"] появляется только ПОСЛЕ создания первого пользователя.
|
|
||||||
# На первом apply ключа ещё нет → пустая map. Пароль подтянется при следующем apply.
|
|
||||||
pg_creds_map = try(jsondecode(lookup(nubes_postgres.npg.vault_secrets, "users", "{}")), {})
|
|
||||||
pg_username = nubes_postgres_user.pg_user.username
|
|
||||||
pg_password = try(local.pg_creds_map[local.pg_username]["password"], "")
|
|
||||||
pg_host = nubes_postgres.npg.state_out_flat["internalConnect.master"]
|
|
||||||
pg_database = nubes_postgres_database.db.db_name
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
resource "nubes_postgres" "npg" {
|
|
||||||
resource_name = "pg-sless-demo"
|
|
||||||
# s3_uid = "s01325"
|
|
||||||
s3_uid = var.s3_uid
|
|
||||||
resource_realm = var.realm
|
|
||||||
resource_instances = 1
|
|
||||||
resource_memory = 512
|
|
||||||
resource_c_p_u = 500
|
|
||||||
resource_disk = "1"
|
|
||||||
app_version = "17"
|
|
||||||
json_parameters = jsonencode({
|
|
||||||
log_connections = "off"
|
|
||||||
log_disconnections = "off"
|
|
||||||
})
|
|
||||||
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
|
|
||||||
|
|
||||||
# suspend_on_destroy = false
|
|
||||||
operation_timeout = "11m"
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_user" "pg_user" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
username = "user0"
|
|
||||||
role = "ddl_user"
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_database" "db" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
db_name = "db0"
|
|
||||||
db_owner = nubes_postgres_user.pg_user.username
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
# suspend_on_destroy = false
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
// 2026-03-20 — содержимое перенесено в два файла:
|
|
||||||
// postgres.tf — managed PostgreSQL ресурсы (nubes_postgres, user, database, locals)
|
|
||||||
// functions.tf — sless функции, сервисы, джобы, outputs
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
# 2026-03-18 — debug pod для проверки psql-соединения из namespace функций.
|
|
||||||
# Запускается разово. Подключается к тому же postgres, что и sless_function.
|
|
||||||
# kubectl apply -f /tmp/pg-debug-pod.yaml
|
|
||||||
# kubectl logs -n sless-fn-sless-ffd1f598c169b0ae pg-debug-pod
|
|
||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: pg-debug-pod
|
|
||||||
namespace: sless-fn-sless-ffd1f598c169b0ae
|
|
||||||
labels:
|
|
||||||
purpose: debug-postgres-connectivity
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
containers:
|
|
||||||
- name: psql
|
|
||||||
image: postgres:17-alpine
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
echo "=== Testing TCP connectivity to postgres ==="
|
|
||||||
nc -zv -w5 $PGHOST 5432 && echo "TCP OK" || echo "TCP FAILED"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Testing psql connection ==="
|
|
||||||
PGCONNECT_TIMEOUT=10 psql \
|
|
||||||
"host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \
|
|
||||||
--command="SELECT current_user, current_database(), version();" \
|
|
||||||
2>&1
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Listing tables ==="
|
|
||||||
PGCONNECT_TIMEOUT=10 psql \
|
|
||||||
"host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \
|
|
||||||
--command="\dt" \
|
|
||||||
2>&1
|
|
||||||
env:
|
|
||||||
- name: PGHOST
|
|
||||||
value: "postgresqlk8s-master.36875359-dcea-48c4-a593-b4531f20fe96.svc.cluster.local"
|
|
||||||
- name: PGPORT
|
|
||||||
value: "5432"
|
|
||||||
- name: PGDATABASE
|
|
||||||
value: "db_terra"
|
|
||||||
- name: PGUSER
|
|
||||||
value: "u-user0"
|
|
||||||
- name: PGPASSWORD
|
|
||||||
# Актуальный пароль из vault_secrets (совпадает с tfvars.pg_password на 2026-03-18)
|
|
||||||
value: "M03O6fRsngWcVHB2YGivyLfbfxoii2R21nyh2A2r7WSZS5deLwBgLKkc9Wk24Zyl"
|
|
||||||
- name: PGSSLMODE
|
|
||||||
value: "require"
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
# 2026-03-17 13:05
|
|
||||||
# read_pg_user_secret.py — читает пароль пользователя managed PostgreSQL из k8s Secret.
|
|
||||||
# Используется из Terraform external data source, чтобы apply сам получал актуальный пароль
|
|
||||||
# даже для уже существующего пользователя, созданного вне текущего state.
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Читаем query от Terraform external provider из stdin.
|
|
||||||
query = json.load(sys.stdin)
|
|
||||||
namespace = query["namespace"]
|
|
||||||
secret_name = query["secret"]
|
|
||||||
|
|
||||||
# kubectl уже настроен на удалённой машине; читаем ровно поле data.password.
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"kubectl",
|
|
||||||
"get",
|
|
||||||
"secret",
|
|
||||||
"-n",
|
|
||||||
namespace,
|
|
||||||
secret_name,
|
|
||||||
"-o",
|
|
||||||
"jsonpath={.data.password}",
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
password = base64.b64decode(result.stdout.strip()).decode()
|
|
||||||
json.dump({"password": password}, sys.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 2026-03-20
|
|
||||||
# stress_destroy_apply.sh — 5 итераций terraform destroy + apply для проверки lifecycle PG.
|
|
||||||
# Запускать вручную с VM: bash stress_destroy_apply.sh
|
|
||||||
# Логи каждой итерации пишутся в stress_log_N.txt
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
ITERATIONS=5
|
|
||||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$DIR"
|
|
||||||
|
|
||||||
echo "=== Старт stress-теста: $ITERATIONS итераций destroy+apply ==="
|
|
||||||
echo "Workdir: $DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for i in $(seq 1 $ITERATIONS); do
|
|
||||||
LOG="stress_log_${i}.txt"
|
|
||||||
echo "--- Итерация $i/$ITERATIONS ---"
|
|
||||||
echo "Лог: $LOG"
|
|
||||||
|
|
||||||
echo "[$i] DESTROY — $(date)" | tee "$LOG"
|
|
||||||
terraform destroy -auto-approve 2>&1 | tee -a "$LOG"
|
|
||||||
DESTROY_CODE=${PIPESTATUS[0]}
|
|
||||||
|
|
||||||
if [ $DESTROY_CODE -ne 0 ]; then
|
|
||||||
echo "[!] destroy завершился с ошибкой (код $DESTROY_CODE), итерация $i. Прерывание." | tee -a "$LOG"
|
|
||||||
exit $DESTROY_CODE
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" | tee -a "$LOG"
|
|
||||||
echo "[$i] APPLY — $(date)" | tee -a "$LOG"
|
|
||||||
terraform apply -auto-approve 2>&1 | tee -a "$LOG"
|
|
||||||
APPLY_CODE=${PIPESTATUS[0]}
|
|
||||||
|
|
||||||
if [ $APPLY_CODE -ne 0 ]; then
|
|
||||||
echo "[!] apply завершился с ошибкой (код $APPLY_CODE), итерация $i. Прерывание." | tee -a "$LOG"
|
|
||||||
exit $APPLY_CODE
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" | tee -a "$LOG"
|
|
||||||
echo "[$i] Итерация завершена успешно — $(date)" | tee -a "$LOG"
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "=== Все $ITERATIONS итераций прошли успешно ==="
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 2026-03-19 — stress test script: параллельный запуск всех 8 стресс-функций
|
|
||||||
BASE="https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae"
|
|
||||||
|
|
||||||
echo "=== РАУНД 1: первый холодный запуск ==="
|
|
||||||
curl -s -m 35 "$BASE/stress-slow" -d '{"sleep":3}' -H "Content-Type:application/json" > /tmp/r_slow.json &
|
|
||||||
curl -s -m 10 "$BASE/stress-divzero" > /tmp/r_divzero.json &
|
|
||||||
curl -s -m 40 "$BASE/stress-bigloop" -d '{"n":1000000}' -H "Content-Type:application/json"> /tmp/r_bigloop.json &
|
|
||||||
curl -s -m 35 "$BASE/stress-writer" -d '{"rows":3,"prefix":"batch1"}' -H "Content-Type:application/json" > /tmp/r_writer.json &
|
|
||||||
curl -s -m 15 "$BASE/stress-go-fast" -d '{"n":15}' -H "Content-Type:application/json" > /tmp/r_go_fast.json &
|
|
||||||
curl -s -m 10 "$BASE/stress-go-nil" > /tmp/r_go_nil.json &
|
|
||||||
curl -s -m 20 "$BASE/stress-js-async" > /tmp/r_js_async.json &
|
|
||||||
curl -s -m 10 "$BASE/stress-js-badenv" > /tmp/r_js_badenv.json &
|
|
||||||
wait
|
|
||||||
|
|
||||||
echo "[slow]: $(cat /tmp/r_slow.json)"
|
|
||||||
echo "[divzero]: $(cat /tmp/r_divzero.json)"
|
|
||||||
echo "[bigloop]: $(cat /tmp/r_bigloop.json)"
|
|
||||||
echo "[writer]: $(cat /tmp/r_writer.json)"
|
|
||||||
echo "[go-fast]: $(cat /tmp/r_go_fast.json)"
|
|
||||||
echo "[go-nil]: $(cat /tmp/r_go_nil.json)"
|
|
||||||
echo "[js-async]: $(cat /tmp/r_js_async.json)"
|
|
||||||
echo "[js-badenv]:$(cat /tmp/r_js_badenv.json)"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== РАУНД 2: повторный (горячий кэш) ==="
|
|
||||||
curl -s -m 15 "$BASE/stress-bigloop" -d '{"n":2000000}' -H "Content-Type:application/json" > /tmp/r2_bigloop.json &
|
|
||||||
curl -s -m 10 "$BASE/stress-go-fast" -d '{"n":20}' -H "Content-Type:application/json" > /tmp/r2_go_fast.json &
|
|
||||||
curl -s -m 20 "$BASE/stress-js-async" > /tmp/r2_async.json &
|
|
||||||
curl -s -m 35 "$BASE/stress-writer" -d '{"rows":10,"prefix":"batch2"}' -H "Content-Type:application/json" > /tmp/r2_writer.json &
|
|
||||||
wait
|
|
||||||
echo "[bigloop-2M]: $(cat /tmp/r2_bigloop.json)"
|
|
||||||
echo "[go-fast-20]: $(cat /tmp/r2_go_fast.json)"
|
|
||||||
echo "[js-async-2]: $(cat /tmp/r2_async.json)"
|
|
||||||
echo "[writer-10]: $(cat /tmp/r2_writer.json)"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== РАУНД 3: crash функции с неверными параметрами ==="
|
|
||||||
curl -s -m 10 "$BASE/stress-divzero" -d '{"n":100,"d":0}' -H "Content-Type:application/json" > /tmp/r3_dz.json &
|
|
||||||
curl -s -m 10 "$BASE/stress-go-nil" -d '{"crash":true}' -H "Content-Type:application/json" > /tmp/r3_nil.json &
|
|
||||||
curl -s -m 10 "$BASE/stress-js-badenv" -d '{"crash":true}' -H "Content-Type:application/json" > /tmp/r3_bad.json &
|
|
||||||
# divzero с нормальным делителем — должен вернуть результат
|
|
||||||
curl -s -m 10 "$BASE/stress-divzero" -d '{"n":42,"d":7}' -H "Content-Type:application/json" > /tmp/r3_ok.json &
|
|
||||||
# go-nil без краша — должен вернуть ok
|
|
||||||
curl -s -m 10 "$BASE/stress-go-nil" -d '{"crash":false}' -H "Content-Type:application/json" > /tmp/r3_nil_ok.json &
|
|
||||||
wait
|
|
||||||
echo "[divzero crash]: $(cat /tmp/r3_dz.json)"
|
|
||||||
echo "[go-nil crash]: $(cat /tmp/r3_nil.json)"
|
|
||||||
echo "[js-badenv crash]: $(cat /tmp/r3_bad.json)"
|
|
||||||
echo "[divzero ok 42/7]: $(cat /tmp/r3_ok.json)"
|
|
||||||
echo "[go-nil ok]: $(cat /tmp/r3_nil_ok.json)"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== ИТОГ: количество строк в таблице ==="
|
|
||||||
curl -s -m 15 "$BASE/pg-table-reader"
|
|
||||||
echo ""
|
|
||||||
echo "=== DONE ==="
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# test_cache_matrix.sh — 2026-03-23 (v4)
|
|
||||||
# Комплексный тест кэша registry:
|
|
||||||
# Phase 1 — полный деплой всех 24 ресурсов (kaniko builds, т.к. нет образов)
|
|
||||||
# Phase 2 — destroy sless_* + re-apply (все образы из кэша)
|
|
||||||
# Phase 3 — одновременно: удаление 2, смена кода 2, смена параметров 2
|
|
||||||
# ВАЖНО: postgres.tf НЕ переименовывается и НЕ трогается никогда.
|
|
||||||
# Destroy sless-ресурсов делается путём переименования tf-файлов в .tf.bak,
|
|
||||||
# затем terraform apply (видит что ресурсов нет → удаляет их из state+кластера),
|
|
||||||
# затем файлы возвращаются обратно. Никаких -target.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
LOG="$DIR/test_cache_matrix_$(date +%Y%m%d_%H%M%S).log"
|
|
||||||
TIMINGS="$LOG.timings"
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG"; }
|
|
||||||
sep() { log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; }
|
|
||||||
|
|
||||||
timed_op() {
|
|
||||||
local label="$1"; shift
|
|
||||||
log "▶ START: $label"
|
|
||||||
local t0; t0=$(date +%s%3N)
|
|
||||||
"$@" 2>&1 | tee -a "$LOG"
|
|
||||||
local rc=${PIPESTATUS[0]}
|
|
||||||
local t1; t1=$(date +%s%3N)
|
|
||||||
local elapsed=$(( (t1 - t0) / 1000 ))
|
|
||||||
if [[ $rc -eq 0 ]]; then
|
|
||||||
log "✓ DONE: $label — ${elapsed}s"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
|
||||||
log "✗ FAIL: $label — ${elapsed}s (exit $rc)"
|
|
||||||
FAIL=$((FAIL+1))
|
|
||||||
fi
|
|
||||||
echo "$label: ${elapsed}s" >> "$TIMINGS"
|
|
||||||
return $rc
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy_sless_only() {
|
|
||||||
# Переименовываем tf-файлы с sless-ресурсами в .tf.bak → terraform apply их удалит.
|
|
||||||
# Никаких -target — чтобы не затрагивать postgres и не получать state drift.
|
|
||||||
local label="$1"
|
|
||||||
local SLESS_FILES=("chaos_marathon.tf" "functions.tf" "stress.tf")
|
|
||||||
|
|
||||||
local has_state
|
|
||||||
has_state=$(terraform state list 2>/dev/null | grep -cE '^(sless_service|sless_job)' || true)
|
|
||||||
if [[ "$has_state" -eq 0 ]]; then
|
|
||||||
log " (nothing to destroy for $label — state empty)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
log " Hiding sless tf-files → apply will destroy $has_state resources"
|
|
||||||
|
|
||||||
for f in "${SLESS_FILES[@]}"; do
|
|
||||||
[[ -f "$DIR/$f" ]] && mv "$DIR/$f" "$DIR/$f.bak"
|
|
||||||
done
|
|
||||||
|
|
||||||
timed_op "$label" terraform apply -auto-approve
|
|
||||||
|
|
||||||
for f in "${SLESS_FILES[@]}"; do
|
|
||||||
[[ -f "$DIR/$f.bak" ]] && mv "$DIR/$f.bak" "$DIR/$f"
|
|
||||||
done
|
|
||||||
log " sless tf-files restored"
|
|
||||||
}
|
|
||||||
|
|
||||||
cd "$DIR"
|
|
||||||
|
|
||||||
sep
|
|
||||||
log "PHASE 1: Полный начальный деплой"
|
|
||||||
sep
|
|
||||||
destroy_sless_only "phase1-pre-clean"
|
|
||||||
timed_op "phase1-apply-all" terraform apply -auto-approve
|
|
||||||
|
|
||||||
log "--- Образы в registry после Phase 1 ---"
|
|
||||||
kubectl exec -n sless deployment/sless-registry -- sh -c 'find /var/lib/registry -name "*.json" -path "*/tags/*" 2>/dev/null | sed "s|.*repository/||;s|/_manifests.*||" | sort | uniq -c | sort -rn' 2>/dev/null | head -30 | tee -a "$LOG" || log "(registry inspect failed)"
|
|
||||||
|
|
||||||
sep
|
|
||||||
log "PHASE 2: Destroy sless_* → Re-apply (ожидаем cache hits)"
|
|
||||||
sep
|
|
||||||
destroy_sless_only "phase2-destroy"
|
|
||||||
timed_op "phase2-apply-cached" terraform apply -auto-approve
|
|
||||||
|
|
||||||
sep
|
|
||||||
log "PHASE 3: Mixed ops (delete+code+params)"
|
|
||||||
sep
|
|
||||||
|
|
||||||
log "--- 3a: destroy stress_divzero, chaos_echo (comment out → apply → restore) ---"
|
|
||||||
python3 - <<'PYEOF'
|
|
||||||
import re, pathlib
|
|
||||||
|
|
||||||
def comment_out_resource(path, resource_type, resource_name):
|
|
||||||
text = pathlib.Path(path).read_text()
|
|
||||||
# Находим блок resource "type" "name" { ... } и оборачиваем в /* */
|
|
||||||
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 in {path}")
|
|
||||||
return
|
|
||||||
# Найти закрывающую скобку блока
|
|
||||||
start = match.start()
|
|
||||||
depth = 0
|
|
||||||
i = match.start()
|
|
||||||
while i < len(text):
|
|
||||||
if text[i] == '{': depth += 1
|
|
||||||
elif text[i] == '}':
|
|
||||||
depth -= 1
|
|
||||||
if depth == 0:
|
|
||||||
end = i + 1
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
block = text[start:end]
|
|
||||||
commented = "/* COMMENTED_OUT_FOR_TEST\n" + block + "\nCOMMENTED_OUT_FOR_TEST */"
|
|
||||||
pathlib.Path(path).write_text(text[:start] + commented + text[end:])
|
|
||||||
print(f" commented out: {resource_type}.{resource_name} in {path}")
|
|
||||||
|
|
||||||
comment_out_resource("stress.tf", "sless_service", "stress_divzero")
|
|
||||||
comment_out_resource("chaos_marathon.tf", "sless_service", "chaos_echo")
|
|
||||||
PYEOF
|
|
||||||
timed_op "phase3a-destroy-2" terraform apply -auto-approve
|
|
||||||
# Восстанавливаем закомментированные блоки
|
|
||||||
python3 - <<'PYEOF'
|
|
||||||
import pathlib, re
|
|
||||||
|
|
||||||
for fname in ("stress.tf", "chaos_marathon.tf"):
|
|
||||||
p = pathlib.Path(fname)
|
|
||||||
text = p.read_text()
|
|
||||||
text = re.sub(r'/\* COMMENTED_OUT_FOR_TEST\n', '', text)
|
|
||||||
text = re.sub(r'\nCOMMENTED_OUT_FOR_TEST \*/', '', text)
|
|
||||||
p.write_text(text)
|
|
||||||
print(f" restored: {fname}")
|
|
||||||
PYEOF
|
|
||||||
log " stress_divzero, chaos_echo removed from state and k8s"
|
|
||||||
|
|
||||||
log "--- 3b: code changes (new sha256 → kaniko) ---"
|
|
||||||
echo "" >> "$DIR/code/pg-counter/pg_counter.py"
|
|
||||||
echo "# cache-test-$(date +%s)" >> "$DIR/code/pg-counter/pg_counter.py"
|
|
||||||
echo "" >> "$DIR/code/stress-js-async/stress_js_async.js"
|
|
||||||
echo "// cache-test-$(date +%s)" >> "$DIR/code/stress-js-async/stress_js_async.js"
|
|
||||||
log " changed: pg_counter.py, stress_js_async.js"
|
|
||||||
|
|
||||||
log "--- 3c: param changes (same sha256 → no kaniko) ---"
|
|
||||||
python3 - <<'PYEOF'
|
|
||||||
import re, sys
|
|
||||||
with open("stress.tf") as f:
|
|
||||||
content = f.read()
|
|
||||||
orig = content
|
|
||||||
content = re.sub(
|
|
||||||
r'(resource "sless_service" "stress_slow" \{[^}]*?)memory_mb\s*=\s*\d+',
|
|
||||||
lambda m: m.group(1) + 'memory_mb = 192',
|
|
||||||
content, flags=re.DOTALL
|
|
||||||
)
|
|
||||||
content = re.sub(
|
|
||||||
r'(resource "sless_service" "pg_stats" \{[^}]*?)timeout_sec\s*=\s*\d+',
|
|
||||||
lambda m: m.group(1) + 'timeout_sec = 20',
|
|
||||||
content, flags=re.DOTALL
|
|
||||||
)
|
|
||||||
if content == orig:
|
|
||||||
print(" stress.tf: no changes (already patched?)", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
with open("stress.tf", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
print(" stress.tf: stress_slow→memory_mb=192, pg_stats→timeout_sec=20")
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
log "--- 3d: apply всех mixed изменений ---"
|
|
||||||
log " Expected: stress_divzero+chaos_echo=cache_hit, pg_counter+stress_js_async=kaniko, stress_slow+pg_stats=k8s_only"
|
|
||||||
timed_op "phase3d-mixed-apply" terraform apply -auto-approve
|
|
||||||
|
|
||||||
sep
|
|
||||||
log "ИТОГ"
|
|
||||||
sep
|
|
||||||
log "Timings:"
|
|
||||||
cat "$TIMINGS" 2>/dev/null | tee -a "$LOG"
|
|
||||||
log "Pass: $PASS | Fail: $FAIL"
|
|
||||||
log "Лог: $LOG"
|
|
||||||
61
README.md
61
README.md
@ -1,75 +1,56 @@
|
|||||||
# Примеры использования sless
|
# sless — примеры
|
||||||
|
|
||||||
## Обзор платформы
|
> ⚠️ **Тестовое окружение.** Все примеры работают с тестовым API Nubes и тестовым кластером sless. Не используйте в продакшне без предварительного согласования.
|
||||||
|
|
||||||
**sless** — система управления serverless-функциями на базе Kubernetes. Разработчик загружает код функции, платформа собирает из него Docker-образ, разворачивает его в кластере и предоставляет HTTP-эндпоинт для вызова. Всё описывается декларативно через Terraform.
|
**sless** — платформа для запуска serverless-функций на базе Kubernetes.
|
||||||
|
Разработчик загружает код, платформа собирает Docker-образ и разворачивает его в кластере.
|
||||||
### Основные ресурсы провайдера
|
Всё описывается декларативно через Terraform.
|
||||||
|
|
||||||
| Ресурс | Назначение |
|
|
||||||
|---|---|
|
|
||||||
| `sless_service` | Long-running HTTP-сервис: всегда активен, отвечает на запросы. Имеет свой URL после деплоя. |
|
|
||||||
| `sless_job` | Одноразовый запуск функции: собирает образ, выполняет код, завершается. Используется для миграций БД, batch-обработки и т.д. |
|
|
||||||
|
|
||||||
Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Требования
|
## Ресурсы Terraform-провайдера
|
||||||
|
|
||||||
- Terraform >= 1.3
|
| Ресурс | Что делает |
|
||||||
- JWT-токен для аутентификации в sless API
|
|---|---|
|
||||||
- JWT-токен для Nubes Cloud API (если используются managed-ресурсы: PostgreSQL и т.д.)
|
| `sless_job` | Разовый запуск: выполняет код один раз и завершается (установка ПО, миграции и т.д.) |
|
||||||
- Доступ к `https://sless.kube5s.ru`
|
| `sless_service` | HTTP-сервис: всегда запущен, отвечает на запросы, имеет постоянный URL — _примеры появятся позднее_ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Конфигурация провайдера
|
## Конфигурация провайдера
|
||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.sless_token
|
token = var.api_token
|
||||||
}
|
|
||||||
|
|
||||||
provider "nubes_cloud" {
|
|
||||||
base_url = "https://deck-api-test.ngcloud.ru/api/v1"
|
|
||||||
token = var.nubes_token
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Токены задаются в `terraform.tfvars` — этот файл добавлен в `.gitignore`.
|
Токен задаётся в `terraform.tfvars` (файл в `.gitignore`, не попадает в git).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
### `POSTGRES` — Serverless-функции с Managed PostgreSQL
|
### [`VM/`](VM/) — Виртуальная машина в Nubes vDC
|
||||||
|
|
||||||
Полный пример: managed PostgreSQL + одноразовый init-job + 3 HTTP-сервиса (чтение/запись данных и информация о PG).
|
Создаёт vApp + Ubuntu 22.04 VM в облаке Nubes. После создания — автоматически устанавливает ПО (nginx, Docker, пакеты) через serverless-джобы (`sless_job`) по SSH.
|
||||||
|
|
||||||
Языки: Python 3.11, Node.js 20.
|
> В этом примере используются только **разовые джобы** (`sless_job`). Примеры с HTTP-сервисами (`sless_service`) появятся позднее.
|
||||||
|
|
||||||
```bash
|
**→ [Начать здесь](VM/README.md)**
|
||||||
cd POSTGRES
|
|
||||||
terraform init
|
|
||||||
terraform apply
|
|
||||||
```
|
|
||||||
|
|
||||||
Подробности: [POSTGRES/README.md](POSTGRES/README.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Полезные команды
|
## Полезные команды
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Посмотреть состояние задеплоенных ресурсов:
|
# Посмотреть состояние ресурсов:
|
||||||
terraform show
|
terraform show
|
||||||
|
|
||||||
# Принудительно пересобрать сервис (после изменения кода):
|
# Повторно запустить установку ПО: увеличить install_run_id в terraform.tfvars, затем:
|
||||||
terraform apply -replace=sless_service.<имя>
|
|
||||||
|
|
||||||
# Повторно запустить job: увеличить run_id в .tf-файле, затем:
|
|
||||||
terraform apply
|
terraform apply
|
||||||
|
|
||||||
# Удалить все ресурсы примера:
|
# Удалить все ресурсы:
|
||||||
terraform destroy
|
terraform destroy
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,852 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 2026-03-30 — vm_stress_test.sh
|
|
||||||
# Автономный stress/chaos тест для examples/VM.
|
|
||||||
#
|
|
||||||
# ЦЕЛЬ: прогнать полную матрицу сценариев lifecycle VM + sless_job без
|
|
||||||
# вмешательства человека. Скрипт самовосстанавливается при ошибках
|
|
||||||
# и продолжает следующую фазу.
|
|
||||||
#
|
|
||||||
# ФАЗЫ:
|
|
||||||
# 1 BASELINE — apply с полным набором (packages + nginx + docker)
|
|
||||||
# 2 IDEMPOTENT — повторный apply → "No changes"
|
|
||||||
# 3 PARTIAL_DISABLE — выключить nginx + docker → apply (2 job уничтожаются)
|
|
||||||
# 4 PARTIAL_ENABLE — включить обратно → apply (2 job создаются)
|
|
||||||
# 5 REORDER_PACKAGES — изменить порядок и состав base_packages
|
|
||||||
# 6 MANUAL_PURGE — удалить все пакеты с VM по SSH → re-run jobs
|
|
||||||
# 7 DESTROY — terraform destroy → VM уходит в suspend
|
|
||||||
# 8 RESURRECT — apply после destroy → VM просыпается, всё ставится
|
|
||||||
# 9 STRESS_CYCLES — N подряд destroy/apply циклов
|
|
||||||
# 10 FINAL_SANITY — проверить доступность VM и установленные пакеты
|
|
||||||
#
|
|
||||||
# ЗАПУСК:
|
|
||||||
# bash ~/terra/sless/examples/VM/vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log
|
|
||||||
#
|
|
||||||
# ПАРАМЕТРЫ (env):
|
|
||||||
# STRESS_CYCLES=2 — количество destroy/apply циклов в фазе 9 (default: 2)
|
|
||||||
# SKIP_DESTROY=1 — пропустить фазы 7-9 (для быстрого прогона)
|
|
||||||
#
|
|
||||||
# ТРЕБОВАНИЯ: terraform, ssh, python3 — всё доступно на VM naeel@5.172.178.213
|
|
||||||
#
|
|
||||||
# ДЛЯ БЕСПЛАТНЫХ AI-АГЕНТОВ:
|
|
||||||
# 1. Подключиться по SSH к VM
|
|
||||||
# 2. Запустить: bash ~/terra/sless/examples/VM/vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log
|
|
||||||
# 3. Дождаться завершения (exit code 0 = все тесты прошли)
|
|
||||||
# 4. Проанализировать вывод: grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log
|
|
||||||
# 5. Итоговая сводка печатается в конце лога
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
# ── CONFIG ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
VM_KEY="$SCRIPT_DIR/vm_key"
|
|
||||||
TFVARS="$SCRIPT_DIR/terraform.tfvars"
|
|
||||||
TFVARS_BACKUP="$SCRIPT_DIR/.terraform.tfvars.test-backup"
|
|
||||||
STRESS_CYCLES="${STRESS_CYCLES:-2}"
|
|
||||||
SKIP_DESTROY="${SKIP_DESTROY:-0}"
|
|
||||||
|
|
||||||
# ── СТАТИСТИКА ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
PASS=0; FAIL=0; SKIP=0
|
|
||||||
PHASE_RESULTS=() # "PHASE_NAME:PASS|FAIL|SKIP"
|
|
||||||
START_TIME=$SECONDS
|
|
||||||
|
|
||||||
# ── ЦВЕТА ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
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++)); }
|
|
||||||
skip() { echo -e " ${YELLOW}[SKIP]${RESET} $1"; ((SKIP++)); }
|
|
||||||
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
|
|
||||||
warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; }
|
|
||||||
|
|
||||||
phase_header() {
|
|
||||||
local num="$1" name="$2"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN} ФАЗА $num: $name${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
||||||
echo -e " ${CYAN}[TIME]${RESET} $(date '+%H:%M:%S')"
|
|
||||||
}
|
|
||||||
|
|
||||||
phase_result() {
|
|
||||||
local name="$1" result="$2"
|
|
||||||
PHASE_RESULTS+=("$name:$result")
|
|
||||||
echo -e " ${CYAN}[PHASE]${RESET} $name → ${result}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── TERRAFORM HELPERS ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# tf_apply: terraform apply с retry при сетевых ошибках.
|
|
||||||
# Возвращает 0 при успехе, 1 при ошибке.
|
|
||||||
tf_apply() {
|
|
||||||
local attempt=1 max=3
|
|
||||||
while [[ $attempt -le $max ]]; do
|
|
||||||
info "terraform apply (попытка $attempt/$max)..."
|
|
||||||
# Явное указание var-file и input=false чтобы избежать запросов в терминале
|
|
||||||
if terraform apply -var-file="terraform.tfvars" -auto-approve -input=false -no-color 2>&1 | tee /tmp/vm_tf_apply.log; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_apply.log && [[ $attempt -lt $max ]]; then
|
|
||||||
warn "сетевой сбой, retry через $((attempt * 5))s..."
|
|
||||||
sleep $((attempt * 5))
|
|
||||||
((attempt++))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# tf_destroy: terraform destroy с retry.
|
|
||||||
tf_destroy() {
|
|
||||||
local attempt=1 max=3
|
|
||||||
while [[ $attempt -le $max ]]; do
|
|
||||||
info "terraform destroy (попытка $attempt/$max)..."
|
|
||||||
# Добавляем -var-file и -input=false сюда тоже
|
|
||||||
if terraform destroy -var-file="terraform.tfvars" -auto-approve -input=false -no-color 2>&1 | tee /tmp/vm_tf_destroy.log; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_destroy.log && [[ $attempt -lt $max ]]; then
|
|
||||||
warn "сетевой сбой, retry через $((attempt * 5))s..."
|
|
||||||
sleep $((attempt * 5))
|
|
||||||
((attempt++))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# tf_plan_no_changes: проверить что plan не содержит изменений.
|
|
||||||
# Возвращает 0 если "No changes", 1 если есть изменения.
|
|
||||||
tf_plan_no_changes() {
|
|
||||||
# Добавляем -var-file и -input=false чтобы не было запросов в терминал
|
|
||||||
terraform plan -var-file="terraform.tfvars" -input=false -no-color 2>&1 | tee /tmp/vm_tf_plan.log
|
|
||||||
if grep -q 'No changes' /tmp/vm_tf_plan.log; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# tf_state_count: количество ресурсов в state.
|
|
||||||
tf_state_count() {
|
|
||||||
terraform state list 2>/dev/null | wc -l
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── TFVARS HELPERS ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Сохранять/восстанавливать tfvars для самовосстановления.
|
|
||||||
backup_tfvars() {
|
|
||||||
if [[ ! -f "$TFVARS_BACKUP" ]]; then
|
|
||||||
cp "$TFVARS" "$TFVARS_BACKUP"
|
|
||||||
info "Создан бэкап tfvars: $TFVARS_BACKUP"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
restore_tfvars() {
|
|
||||||
if [[ -f "$TFVARS_BACKUP" ]]; then
|
|
||||||
# Читаем токен и ключ из бэкапа перед перезаписью
|
|
||||||
local token key
|
|
||||||
token=$(grep 'api_token' "$TFVARS_BACKUP" | cut -d'"' -f2)
|
|
||||||
key=$(grep 'vm_public_key' "$TFVARS_BACKUP" | cut -d'"' -f2)
|
|
||||||
|
|
||||||
# Если в бэкапе пусто (такое бывает при сбоях сохранения), не портим оригинал
|
|
||||||
if [[ -n "$token" && -n "$key" ]]; then
|
|
||||||
cp "$TFVARS_BACKUP" "$TFVARS"
|
|
||||||
info "tfvars восстановлен из бэкапа"
|
|
||||||
else
|
|
||||||
warn "Бэкап tfvars поврежден или пуст, восстановление пропущено"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# write_tfvars: перезаписать tfvars программно.
|
|
||||||
# Аргументы: install_packages install_nginx install_docker run_id base_packages_json
|
|
||||||
write_tfvars() {
|
|
||||||
local pkgs="$1" nginx="$2" docker="$3" run_id="$4" base_pkgs="$5"
|
|
||||||
|
|
||||||
# Читаем токен напрямую из файла secrets если в бэкапе пусто
|
|
||||||
local token key
|
|
||||||
token=$(grep 'api_token' "$TFVARS_BACKUP" 2>/dev/null | cut -d'=' -f2- | cut -d'#' -f1 | xargs | sed 's/^"//;s/"$//')
|
|
||||||
if [[ -z "$token" ]]; then
|
|
||||||
info "Токен не найден в бэкапе, берем из ~/terra/sless/secrets/tazetnarodtest.token"
|
|
||||||
token=$(ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=5 "ubuntu@185.247.187.154" "cat ~/terra/sless/secrets/tazetnarodtest.token" 2>/dev/null || cat "$SCRIPT_DIR/../../secrets/tazetnarodtest.token" 2>/dev/null)
|
|
||||||
fi
|
|
||||||
|
|
||||||
key=$(grep 'vm_public_key' "$TFVARS_BACKUP" 2>/dev/null | cut -d'=' -f2- | cut -d'#' -f1 | xargs | sed 's/^"//;s/"$//')
|
|
||||||
if [[ -z "$key" ]]; then
|
|
||||||
key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQB+Kyevn0H8QSdfm6ZpCU9jtskBxUS9BV0+i1/M04A sless-demo-vm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "$TFVARS" <<EOF
|
|
||||||
# AUTO-GENERATED by vm_stress_test.sh — $(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
vm_public_key = "$key"
|
|
||||||
api_token = "$token"
|
|
||||||
|
|
||||||
# ---- Флаги установки --------------------------------------------------------
|
|
||||||
install_packages = $pkgs
|
|
||||||
install_nginx = $nginx
|
|
||||||
install_docker = $docker
|
|
||||||
|
|
||||||
install_run_id = $run_id
|
|
||||||
|
|
||||||
base_packages = $base_pkgs
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── VM SSH HELPERS ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# get_vm_ip: получить внешний IP VM из terraform output.
|
|
||||||
get_vm_ip() {
|
|
||||||
terraform output -json vm_state 2>/dev/null \
|
|
||||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('externalConnect',''))" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_ssh: выполнить команду на VM. Возвращает exit code команды.
|
|
||||||
vm_ssh() {
|
|
||||||
local ip="$1"; shift
|
|
||||||
ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=15 \
|
|
||||||
-o ServerAliveInterval=5 -o ServerAliveCountMax=3 \
|
|
||||||
"ubuntu@$ip" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_alive: проверить доступность VM по SSH.
|
|
||||||
vm_alive() {
|
|
||||||
local ip="$1"
|
|
||||||
vm_ssh "$ip" 'echo alive' &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_wait_alive: ждать пока VM ответит по SSH (до timeout_sec).
|
|
||||||
vm_wait_alive() {
|
|
||||||
local ip="$1" timeout_sec="${2:-120}"
|
|
||||||
local deadline=$((SECONDS + timeout_sec))
|
|
||||||
while [[ $SECONDS -lt $deadline ]]; do
|
|
||||||
if vm_alive "$ip"; then return 0; fi
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_check_package: проверить что пакет установлен.
|
|
||||||
vm_check_package() {
|
|
||||||
local ip="$1" pkg="$2"
|
|
||||||
vm_ssh "$ip" "dpkg -l $pkg 2>/dev/null | grep -q '^ii'" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_check_binary: проверить что бинарник доступен.
|
|
||||||
vm_check_binary() {
|
|
||||||
local ip="$1" bin="$2"
|
|
||||||
vm_ssh "$ip" "command -v $bin" &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_purge_all: удалить все установленные пакеты с VM.
|
|
||||||
vm_purge_all() {
|
|
||||||
local ip="$1"
|
|
||||||
info "удаляю пакеты с VM $ip..."
|
|
||||||
vm_ssh "$ip" 'sudo systemctl stop nginx docker 2>/dev/null; sudo apt-get purge -y jq python3-pip htop unzip nginx docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null; sudo apt-get autoremove -y 2>/dev/null; sudo rm -rf /var/lib/docker /var/lib/containerd; echo purge_done' 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── SELF-RECOVERY ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# ensure_baseline: гарантировать что VM и все 3 job в state.
|
|
||||||
# Используется для восстановления после сбоя фазы.
|
|
||||||
ensure_baseline() {
|
|
||||||
info "восстанавливаю baseline state..."
|
|
||||||
restore_tfvars
|
|
||||||
# Перезаписать tfvars на полный набор с текущим run_id
|
|
||||||
local run_id
|
|
||||||
run_id=$(grep 'install_run_id' "$TFVARS_BACKUP" | grep -oP '\d+' | head -1)
|
|
||||||
write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]'
|
|
||||||
tf_apply || warn "baseline apply не удался — продолжаю"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 1: BASELINE — apply с полным набором
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_1_baseline() {
|
|
||||||
phase_header 1 "BASELINE — apply с полным набором"
|
|
||||||
|
|
||||||
# Сохранить оригинальный tfvars
|
|
||||||
backup_tfvars
|
|
||||||
|
|
||||||
# Читаем текущий run_id и инкрементируем
|
|
||||||
local run_id
|
|
||||||
run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1)
|
|
||||||
run_id=$((run_id + 1))
|
|
||||||
|
|
||||||
write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]'
|
|
||||||
|
|
||||||
if tf_apply; then
|
|
||||||
pass "1.1 terraform apply завершился успешно"
|
|
||||||
else
|
|
||||||
fail "1.1 terraform apply упал"
|
|
||||||
phase_result "BASELINE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить количество ресурсов
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "1.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
else
|
|
||||||
fail "1.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить outputs
|
|
||||||
local out
|
|
||||||
out=$(terraform output -json 2>/dev/null)
|
|
||||||
|
|
||||||
if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'ok' in d['install_packages_result']['value'] or 'already' in d['install_packages_result']['value']" 2>/dev/null; then
|
|
||||||
pass "1.3 install_packages_result содержит ok/already_installed"
|
|
||||||
else
|
|
||||||
fail "1.3 install_packages_result не содержит ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_nginx_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then
|
|
||||||
pass "1.4 install_nginx_result содержит ok/already"
|
|
||||||
else
|
|
||||||
fail "1.4 install_nginx_result не содержит ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_docker_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then
|
|
||||||
pass "1.5 install_docker_result содержит ok/already"
|
|
||||||
else
|
|
||||||
fail "1.5 install_docker_result не содержит ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить VM по SSH
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
pass "1.6 VM $ip доступна по SSH"
|
|
||||||
else
|
|
||||||
fail "1.6 VM не доступна по SSH (ip=$ip)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Обновить бэкап с новым run_id
|
|
||||||
backup_tfvars
|
|
||||||
|
|
||||||
phase_result "BASELINE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 2: IDEMPOTENT — повторный apply без изменений
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_2_idempotent() {
|
|
||||||
phase_header 2 "IDEMPOTENT — повторный apply без изменений"
|
|
||||||
|
|
||||||
if tf_plan_no_changes; then
|
|
||||||
pass "2.1 terraform plan → No changes"
|
|
||||||
else
|
|
||||||
fail "2.1 terraform plan показывает изменения (ожидали No changes)"
|
|
||||||
# Показать что именно
|
|
||||||
grep -E 'will be|must be' /tmp/vm_tf_plan.log 2>/dev/null | head -5 | while read -r line; do
|
|
||||||
info " $line"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "IDEMPOTENT" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 3: PARTIAL_DISABLE — выключить nginx + docker
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_3_partial_disable() {
|
|
||||||
phase_header 3 "PARTIAL_DISABLE — выключить nginx + docker"
|
|
||||||
|
|
||||||
local run_id
|
|
||||||
run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1)
|
|
||||||
|
|
||||||
write_tfvars "true" "false" "false" "$run_id" '["jq", "python3-pip", "htop", "unzip"]'
|
|
||||||
|
|
||||||
if tf_apply; then
|
|
||||||
pass "3.1 apply с partial disable завершился"
|
|
||||||
else
|
|
||||||
fail "3.1 apply с partial disable упал"
|
|
||||||
restore_tfvars
|
|
||||||
phase_result "PARTIAL_DISABLE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить что nginx и docker job пропали из state
|
|
||||||
local state_list
|
|
||||||
state_list=$(terraform state list 2>/dev/null)
|
|
||||||
|
|
||||||
if echo "$state_list" | grep -q 'install_nginx'; then
|
|
||||||
fail "3.2 install_nginx всё ещё в state"
|
|
||||||
else
|
|
||||||
pass "3.2 install_nginx убран из state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$state_list" | grep -q 'install_docker'; then
|
|
||||||
fail "3.3 install_docker всё ещё в state"
|
|
||||||
else
|
|
||||||
pass "3.3 install_docker убран из state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$state_list" | grep -q 'install_packages'; then
|
|
||||||
pass "3.4 install_packages остался в state"
|
|
||||||
else
|
|
||||||
fail "3.4 install_packages пропал из state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить outputs
|
|
||||||
local nginx_out docker_out
|
|
||||||
nginx_out=$(terraform output -raw install_nginx_result 2>/dev/null)
|
|
||||||
docker_out=$(terraform output -raw install_docker_result 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ "$nginx_out" == "skipped" ]]; then
|
|
||||||
pass "3.5 install_nginx_result = skipped"
|
|
||||||
else
|
|
||||||
fail "3.5 install_nginx_result = '$nginx_out' (ожидали skipped)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$docker_out" == "skipped" ]]; then
|
|
||||||
pass "3.6 install_docker_result = skipped"
|
|
||||||
else
|
|
||||||
fail "3.6 install_docker_result = '$docker_out' (ожидали skipped)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "PARTIAL_DISABLE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 4: PARTIAL_ENABLE — включить обратно всё
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_4_partial_enable() {
|
|
||||||
phase_header 4 "PARTIAL_ENABLE — включить обратно всё"
|
|
||||||
|
|
||||||
local run_id
|
|
||||||
run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1)
|
|
||||||
run_id=$((run_id + 1))
|
|
||||||
|
|
||||||
write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]'
|
|
||||||
|
|
||||||
if tf_apply; then
|
|
||||||
pass "4.1 apply с полным набором завершился"
|
|
||||||
else
|
|
||||||
fail "4.1 apply с полным набором упал"
|
|
||||||
phase_result "PARTIAL_ENABLE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "4.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
else
|
|
||||||
fail "4.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Обновить бэкап
|
|
||||||
backup_tfvars
|
|
||||||
|
|
||||||
phase_result "PARTIAL_ENABLE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 5: REORDER_PACKAGES — изменить порядок и состав base_packages
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_5_reorder() {
|
|
||||||
phase_header 5 "REORDER_PACKAGES — изменить порядок и состав пакетов"
|
|
||||||
|
|
||||||
local run_id
|
|
||||||
run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1)
|
|
||||||
run_id=$((run_id + 1))
|
|
||||||
|
|
||||||
# Другой порядок и сокращённый набор
|
|
||||||
write_tfvars "true" "true" "true" "$run_id" '["htop", "jq"]'
|
|
||||||
|
|
||||||
if tf_apply; then
|
|
||||||
pass "5.1 apply с изменённым набором пакетов завершился"
|
|
||||||
else
|
|
||||||
fail "5.1 apply с изменённым набором пакетов упал"
|
|
||||||
restore_tfvars
|
|
||||||
phase_result "REORDER_PACKAGES" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить output
|
|
||||||
local pkg_out
|
|
||||||
pkg_out=$(terraform output -raw install_packages_result 2>/dev/null)
|
|
||||||
if echo "$pkg_out" | grep -q '"status"'; then
|
|
||||||
pass "5.2 install_packages вернул JSON с status"
|
|
||||||
else
|
|
||||||
fail "5.2 install_packages output неожиданный: $pkg_out"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Вернуть полный набор
|
|
||||||
run_id=$((run_id + 1))
|
|
||||||
write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]'
|
|
||||||
tf_apply || warn "5.3 восстановление полного набора не удалось"
|
|
||||||
|
|
||||||
backup_tfvars
|
|
||||||
phase_result "REORDER_PACKAGES" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 6: MANUAL_PURGE — удалить пакеты с VM вручную, заново установить
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_6_manual_purge() {
|
|
||||||
phase_header 6 "MANUAL_PURGE — удалить пакеты с VM, заново установить"
|
|
||||||
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -z "$ip" ]]; then
|
|
||||||
fail "6.0 не удалось получить IP VM"
|
|
||||||
phase_result "MANUAL_PURGE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Удалить всё с VM
|
|
||||||
vm_purge_all "$ip"
|
|
||||||
|
|
||||||
# Проверить что пакеты действительно удалены
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
fail "6.1 docker всё ещё на VM после purge"
|
|
||||||
else
|
|
||||||
pass "6.1 docker удалён с VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
fail "6.2 nginx всё ещё на VM после purge"
|
|
||||||
else
|
|
||||||
pass "6.2 nginx удалён с VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "jq"; then
|
|
||||||
fail "6.3 jq всё ещё на VM после purge"
|
|
||||||
else
|
|
||||||
pass "6.3 jq удалён с VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Пересоздать jobs (bump run_id)
|
|
||||||
local run_id
|
|
||||||
run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1)
|
|
||||||
run_id=$((run_id + 1))
|
|
||||||
|
|
||||||
write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]'
|
|
||||||
|
|
||||||
info "запускаю переустановку через sless_job..."
|
|
||||||
if tf_apply; then
|
|
||||||
pass "6.4 apply после purge завершился"
|
|
||||||
else
|
|
||||||
fail "6.4 apply после purge упал"
|
|
||||||
phase_result "MANUAL_PURGE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Подождать чуть-чуть и проверить что пакеты вернулись
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
pass "6.5 docker установлен заново"
|
|
||||||
else
|
|
||||||
fail "6.5 docker НЕ установлен после re-apply"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
pass "6.6 nginx установлен заново"
|
|
||||||
else
|
|
||||||
fail "6.6 nginx НЕ установлен после re-apply"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "jq"; then
|
|
||||||
pass "6.7 jq установлен заново"
|
|
||||||
else
|
|
||||||
fail "6.7 jq НЕ установлен после re-apply"
|
|
||||||
fi
|
|
||||||
|
|
||||||
backup_tfvars
|
|
||||||
phase_result "MANUAL_PURGE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 7: DESTROY — terraform destroy, VM уходит в suspend
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_7_destroy() {
|
|
||||||
phase_header 7 "DESTROY — terraform destroy → VM в suspend"
|
|
||||||
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
|
|
||||||
if tf_destroy; then
|
|
||||||
pass "7.1 terraform destroy завершился"
|
|
||||||
else
|
|
||||||
fail "7.1 terraform destroy упал"
|
|
||||||
phase_result "DESTROY" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# State должен быть пуст
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -eq 0 ]]; then
|
|
||||||
pass "7.2 state пуст ($count ресурсов)"
|
|
||||||
else
|
|
||||||
fail "7.2 state не пуст ($count ресурсов)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# VM не должна отвечать по SSH
|
|
||||||
if [[ -n "$ip" ]]; then
|
|
||||||
info "проверяю что VM $ip недоступна..."
|
|
||||||
if vm_alive "$ip"; then
|
|
||||||
fail "7.3 VM $ip всё ещё отвечает по SSH после destroy"
|
|
||||||
else
|
|
||||||
pass "7.3 VM $ip не отвечает по SSH (suspend подтверждён)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
skip "7.3 IP VM неизвестен, пропускаю SSH-проверку"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "DESTROY" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 8: RESURRECT — apply после destroy, VM просыпается
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_8_resurrect() {
|
|
||||||
phase_header 8 "RESURRECT — apply после destroy"
|
|
||||||
|
|
||||||
if tf_apply; then
|
|
||||||
pass "8.1 apply после destroy завершился"
|
|
||||||
else
|
|
||||||
fail "8.1 apply после destroy упал"
|
|
||||||
phase_result "RESURRECT" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "8.2 state содержит $count ресурсов"
|
|
||||||
else
|
|
||||||
fail "8.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить VM
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
pass "8.3 VM $ip доступна по SSH после resurrect"
|
|
||||||
else
|
|
||||||
# VM может ещё просыпаться — ждём
|
|
||||||
info "VM не отвечает, жду до 120s..."
|
|
||||||
if vm_wait_alive "$ip" 120; then
|
|
||||||
pass "8.3 VM $ip доступна по SSH после ожидания"
|
|
||||||
else
|
|
||||||
fail "8.3 VM $ip не доступна по SSH через 120s"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить пакеты
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
if vm_check_binary "$ip" "jq"; then
|
|
||||||
pass "8.4 jq установлен после resurrect"
|
|
||||||
else
|
|
||||||
fail "8.4 jq НЕ установлен после resurrect"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
pass "8.5 nginx установлен после resurrect"
|
|
||||||
else
|
|
||||||
fail "8.5 nginx НЕ установлен после resurrect"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
pass "8.6 docker установлен после resurrect"
|
|
||||||
else
|
|
||||||
fail "8.6 docker НЕ установлен после resurrect"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
backup_tfvars
|
|
||||||
phase_result "RESURRECT" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 9: STRESS_CYCLES — N destroy/apply циклов подряд
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_9_stress() {
|
|
||||||
phase_header 9 "STRESS_CYCLES — $STRESS_CYCLES циклов destroy/apply"
|
|
||||||
|
|
||||||
local i
|
|
||||||
for i in $(seq 1 "$STRESS_CYCLES"); do
|
|
||||||
info "── цикл $i/$STRESS_CYCLES ──"
|
|
||||||
|
|
||||||
info "[$i] destroy..."
|
|
||||||
if tf_destroy; then
|
|
||||||
pass "9.${i}a destroy цикл $i"
|
|
||||||
else
|
|
||||||
fail "9.${i}a destroy цикл $i упал"
|
|
||||||
# Попробовать восстановиться
|
|
||||||
tf_apply || true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "[$i] apply..."
|
|
||||||
if tf_apply; then
|
|
||||||
pass "9.${i}b apply цикл $i"
|
|
||||||
else
|
|
||||||
fail "9.${i}b apply цикл $i упал"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
phase_result "STRESS_CYCLES" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 10: FINAL_SANITY — финальная проверка состояния
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_10_final() {
|
|
||||||
phase_header 10 "FINAL_SANITY — финальная проверка"
|
|
||||||
|
|
||||||
# Убедиться что state на месте
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "10.1 state содержит $count ресурсов"
|
|
||||||
else
|
|
||||||
fail "10.1 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
# Попробовать восстановить
|
|
||||||
ensure_baseline
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Plan = no changes
|
|
||||||
if tf_plan_no_changes; then
|
|
||||||
pass "10.2 terraform plan → No changes"
|
|
||||||
else
|
|
||||||
fail "10.2 terraform plan показывает изменения"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# VM доступна
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
pass "10.3 VM $ip доступна по SSH"
|
|
||||||
|
|
||||||
# Полная проверка пакетов
|
|
||||||
local all_ok=true
|
|
||||||
for pkg in jq htop unzip; do
|
|
||||||
if vm_check_package "$ip" "$pkg"; then
|
|
||||||
pass "10.4 пакет $pkg установлен"
|
|
||||||
else
|
|
||||||
fail "10.4 пакет $pkg НЕ установлен"
|
|
||||||
all_ok=false
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
pass "10.5 nginx работает"
|
|
||||||
else
|
|
||||||
fail "10.5 nginx НЕ найден"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
pass "10.6 docker работает"
|
|
||||||
else
|
|
||||||
fail "10.6 docker НЕ найден"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "10.3 VM не доступна по SSH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "FINAL_SANITY" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# MAIN
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo -e "${BOLD}${CYAN}"
|
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
|
||||||
echo "║ VM STRESS TEST — examples/VM ║"
|
|
||||||
echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║"
|
|
||||||
echo "║ Циклов stress: $STRESS_CYCLES ║"
|
|
||||||
echo "╚═══════════════════════════════════════════════════════════════╝"
|
|
||||||
echo -e "${RESET}"
|
|
||||||
|
|
||||||
# Проверить что мы в правильной директории
|
|
||||||
if [[ ! -f "$TFVARS" ]]; then
|
|
||||||
echo -e "${RED}ОШИБКА: $TFVARS не найден. Запускайте из examples/VM/${RESET}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$VM_KEY" ]]; then
|
|
||||||
echo -e "${RED}ОШИБКА: $VM_KEY не найден${RESET}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Запуск фаз
|
|
||||||
phase_1_baseline
|
|
||||||
phase_2_idempotent
|
|
||||||
phase_3_partial_disable
|
|
||||||
phase_4_partial_enable
|
|
||||||
phase_5_reorder
|
|
||||||
phase_6_manual_purge
|
|
||||||
|
|
||||||
if [[ "$SKIP_DESTROY" == "1" ]]; then
|
|
||||||
skip "фазы 7-9 пропущены (SKIP_DESTROY=1)"
|
|
||||||
PHASE_RESULTS+=("DESTROY:SKIP" "RESURRECT:SKIP" "STRESS_CYCLES:SKIP")
|
|
||||||
else
|
|
||||||
phase_7_destroy
|
|
||||||
phase_8_resurrect
|
|
||||||
phase_9_stress
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_10_final
|
|
||||||
|
|
||||||
# ── ИТОГОВАЯ СВОДКА ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
ELAPSED=$((SECONDS - START_TIME))
|
|
||||||
ELAPSED_MIN=$((ELAPSED / 60))
|
|
||||||
ELAPSED_SEC=$((ELAPSED % 60))
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}${CYAN}╔═══════════════════════════════════════════════════════════════╗${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN}║ ИТОГОВАЯ СВОДКА ║${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN}╠═══════════════════════════════════════════════════════════════╣${RESET}"
|
|
||||||
echo -e "${BOLD} Время: ${ELAPSED_MIN}m ${ELAPSED_SEC}s${RESET}"
|
|
||||||
echo -e "${BOLD} ${GREEN}PASS: $PASS${RESET} ${RED}FAIL: $FAIL${RESET} ${YELLOW}SKIP: $SKIP${RESET}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD} Фазы:${RESET}"
|
|
||||||
for pr in "${PHASE_RESULTS[@]}"; do
|
|
||||||
name="${pr%%:*}"
|
|
||||||
result="${pr##*:}"
|
|
||||||
case "$result" in
|
|
||||||
PASS) echo -e " ${GREEN}✓${RESET} $name" ;;
|
|
||||||
FAIL) echo -e " ${RED}✗${RESET} $name" ;;
|
|
||||||
SKIP) echo -e " ${YELLOW}○${RESET} $name" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
echo -e "${BOLD}${CYAN}╚═══════════════════════════════════════════════════════════════╝${RESET}"
|
|
||||||
|
|
||||||
# Восстановить оригинальный tfvars
|
|
||||||
restore_tfvars
|
|
||||||
|
|
||||||
# Exit code: 0 если все PASS, 1 если есть FAIL
|
|
||||||
if [[ $FAIL -gt 0 ]]; then
|
|
||||||
echo -e "\n${RED}РЕЗУЛЬТАТ: FAIL ($FAIL ошибок)${RESET}"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo -e "\n${GREEN}РЕЗУЛЬТАТ: ALL PASS${RESET}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
@ -1,5 +1,9 @@
|
|||||||
# Пример: Виртуальная машина (vApp + VM) в Nubes vDC
|
# Пример: Виртуальная машина (vApp + VM) в Nubes vDC
|
||||||
|
|
||||||
|
> ⚠️ **Тестовое окружение.** Пример работает с тестовым API Nubes и тестовым кластером sless. Не использовать в продакшне без предварительного согласования.
|
||||||
|
|
||||||
|
> В этом примере используются только **разовые джобы** (`sless_job`) — для установки ПО на ВМ. Примеры с HTTP-сервисами (`sless_service`) появятся позднее.
|
||||||
|
|
||||||
Создаёт:
|
Создаёт:
|
||||||
- **vApp** — виртуальный каталог (контейнер для ВМ в VMware vDC)
|
- **vApp** — виртуальный каталог (контейнер для ВМ в VMware vDC)
|
||||||
- **ВМ** — Ubuntu 22.04, 2 CPU / 2 GB RAM / 20 GB disk
|
- **ВМ** — Ubuntu 22.04, 2 CPU / 2 GB RAM / 20 GB disk
|
||||||
@ -200,5 +204,5 @@ terraform destroy
|
|||||||
| `vm.tf` | Ресурс ВМ (Ubuntu 22.04) |
|
| `vm.tf` | Ресурс ВМ (Ubuntu 22.04) |
|
||||||
| `sless.tf` | Serverless-джобы для установки ПО |
|
| `sless.tf` | Serverless-джобы для установки ПО |
|
||||||
| `outputs.tf` | Вывод IP-адреса и ID ресурсов |
|
| `outputs.tf` | Вывод IP-адреса и ID ресурсов |
|
||||||
| `vm_key` / `vm_key.pub` | SSH-ключ для доступа к ВМ |
|
| `vm_key` / `vm_key.pub` | SSH-ключ — **создаётся вами на Шаге 2**, в git не хранится |
|
||||||
| `functions/` | Код Python-функций для install-джобов |
|
| `functions/` | Код Python-функций для install-джобов |
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
# VM Stress Test — Инструкция по запуску
|
|
||||||
# 2026-03-30
|
|
||||||
|
|
||||||
## ⛔⛔⛔ КРИТИЧЕСКИЕ ПРАВИЛА ⛔⛔⛔
|
|
||||||
|
|
||||||
### ЗАПРЕЩЕНО (без исключений):
|
|
||||||
- **НЕ РЕДАКТИРОВАТЬ** `terraform.tfvars` — там JWT-токен, потеря = катастрофа
|
|
||||||
- **НЕ РЕДАКТИРОВАТЬ** `*.tf` файлы
|
|
||||||
- **НЕ РЕДАКТИРОВАТЬ** `vm_stress_test.sh`
|
|
||||||
- **НЕ ЗАПУСКАТЬ** `terraform` напрямую — только через скрипт
|
|
||||||
- **НЕ СОЗДАВАТЬ** новые файлы в этой директории
|
|
||||||
- **НЕ ДЕЛАТЬ** `sed`, `awk`, `cat >`, `tee` в terraform.tfvars
|
|
||||||
|
|
||||||
### ПОЧЕМУ:
|
|
||||||
Предыдущая версия скрипта содержала функцию `write_tfvars()` которая
|
|
||||||
перезаписывала `terraform.tfvars`. В процессе перезаписи был потерян
|
|
||||||
JWT-токен `api_token` (1200+ символов). Это привело к полному отказу
|
|
||||||
terraform и потере рабочего состояния. Восстановление заняло час.
|
|
||||||
|
|
||||||
### КАК РАБОТАЕТ НОВЫЙ СКРИПТ:
|
|
||||||
Переменные переопределяются через `-var` в terraform CLI.
|
|
||||||
Файл `terraform.tfvars` читается terraform автоматически,
|
|
||||||
но **НИКОГДА не перезаписывается** скриптом.
|
|
||||||
|
|
||||||
После каждой фазы проверяется md5sum terraform.tfvars.
|
|
||||||
Если файл изменился — **АВАРИЙНАЯ ОСТАНОВКА** (exit code 99).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
|
|
||||||
### На VM (naeel@5.172.178.213):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/terra/sless/examples/VM
|
|
||||||
bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Быстрый прогон (без destroy/resurrect — фазы 7-9 пропускаются):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SKIP_DESTROY=1 bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Количество stress-циклов (default: 2):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
STRESS_CYCLES=3 bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Анализ результатов
|
|
||||||
|
|
||||||
### Быстрый обзор:
|
|
||||||
```bash
|
|
||||||
grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Только ошибки:
|
|
||||||
```bash
|
|
||||||
grep '\[FAIL\]' /tmp/vm_stress.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Итоговая сводка — последние 20 строк лога:
|
|
||||||
```bash
|
|
||||||
tail -20 /tmp/vm_stress.log
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фазы теста
|
|
||||||
|
|
||||||
| # | Имя | Что делает |
|
|
||||||
|---|-----------------|---------------------------------------------------|
|
|
||||||
| 1 | BASELINE | apply с полным набором (packages+nginx+docker) |
|
|
||||||
| 2 | IDEMPOTENT | plan → "No changes" (проверка идемпотентности) |
|
|
||||||
| 3 | PARTIAL_DISABLE | отключить nginx + docker через -var |
|
|
||||||
| 4 | PARTIAL_ENABLE | включить обратно nginx + docker |
|
|
||||||
| 5 | REORDER_PACKAGES| изменить набор base_packages через -var |
|
|
||||||
| 6 | MANUAL_PURGE | удалить пакеты с VM по SSH → переустановить |
|
|
||||||
| 7 | DESTROY | terraform destroy → VM в suspend |
|
|
||||||
| 8 | RESURRECT | apply после destroy → VM просыпается |
|
|
||||||
| 9 | STRESS_CYCLES | N циклов destroy/apply подряд |
|
|
||||||
|10 | FINAL_SANITY | финальная проверка VM + пакеты + plan |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Текущее состояние (baseline)
|
|
||||||
|
|
||||||
5 ресурсов в state:
|
|
||||||
- `nubes_vapp.vapp`
|
|
||||||
- `nubes_vc_vm_v3.vm`
|
|
||||||
- `sless_job.install_packages[0]`
|
|
||||||
- `sless_job.install_nginx[0]`
|
|
||||||
- `sless_job.install_docker[0]`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Exit codes
|
|
||||||
|
|
||||||
| Code | Значение |
|
|
||||||
|------|---------------------------------------------|
|
|
||||||
| 0 | Все тесты PASS |
|
|
||||||
| 1 | Есть FAIL (см. лог) |
|
|
||||||
| 99 | terraform.tfvars был изменён — АВАРИЙНЫЙ СТОП |
|
|
||||||
@ -39,4 +39,5 @@ variable "api_token" {
|
|||||||
provider "nubes" {
|
provider "nubes" {
|
||||||
api_token = var.api_token
|
api_token = var.api_token
|
||||||
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
||||||
|
log_level = "debug" # none | info | debug, default = "none"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,15 +24,12 @@ provider "sless" {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
# TODO: заменить externalConnect → internalConnect когда DevOps настроят
|
|
||||||
# сеть между k8s кластером и Nubes vDC (сейчас только внешний IP доступен).
|
|
||||||
vm_ip = nubes_vc_vm_v3.vm.state_out_flat["externalConnect"]
|
vm_ip = nubes_vc_vm_v3.vm.state_out_flat["externalConnect"]
|
||||||
|
|
||||||
ssh_env = {
|
ssh_env = {
|
||||||
VM_IP = local.vm_ip
|
VM_IP = local.vm_ip
|
||||||
SSH_USER = "ubuntu"
|
SSH_USER = "ubuntu"
|
||||||
# TODO(vault): заменить на чтение из Vault когда сервис заработает; пока тестовый стенд — прямой файл.
|
SSH_KEY = file("${path.module}/vm_key") # приватный ключ, созданный на шаге 2 (не хранится в git)
|
||||||
SSH_KEY = file("${path.module}/vm_key")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
|
||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
|
||||||
QyNTUxOQAAACCkAfisnr59B/EEnX5umaQlPY7bJAcVEvQVdPotfzNOAAAAAJBioDTmYqA0
|
|
||||||
5gAAAAtzc2gtZWQyNTUxOQAAACCkAfisnr59B/EEnX5umaQlPY7bJAcVEvQVdPotfzNOAA
|
|
||||||
AAAEC4nFg/UaIitvoJKhJsrroOHWgmmkfHYQRyvEzqGe+AwaQB+Kyevn0H8QSdfm6ZpCU9
|
|
||||||
jtskBxUS9BV0+i1/M04AAAAADXNsZXNzLWRlbW8tdm0=
|
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
|
||||||
@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQB+Kyevn0H8QSdfm6ZpCU9jtskBxUS9BV0+i1/M04A sless-demo-vm
|
|
||||||
@ -1,931 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 2026-03-30 — vm_stress_test.sh (v2 — READ-ONLY)
|
|
||||||
# Автономный READ-ONLY stress/chaos тест для examples/VM.
|
|
||||||
#
|
|
||||||
# ⛔⛔⛔ КРИТИЧЕСКОЕ ПРАВИЛО — ДЛЯ AI-АГЕНТОВ И ЛЮДЕЙ ⛔⛔⛔
|
|
||||||
#
|
|
||||||
# Этот скрипт НИКОГДА НЕ МОДИФИЦИРУЕТ terraform.tfvars и НИКАКИЕ ДРУГИЕ ФАЙЛЫ.
|
|
||||||
# Все переопределения переменных — ТОЛЬКО через terraform CLI опцию -var.
|
|
||||||
# Файл terraform.tfvars ЧИТАЕТСЯ, но НИКОГДА НЕ ПЕРЕЗАПИСЫВАЕТСЯ.
|
|
||||||
#
|
|
||||||
# ЗАПРЕЩЕНО:
|
|
||||||
# - Редактировать этот скрипт
|
|
||||||
# - Редактировать terraform.tfvars
|
|
||||||
# - Редактировать любые .tf файлы
|
|
||||||
# - Запускать terraform напрямую — только через этот скрипт
|
|
||||||
#
|
|
||||||
# ПРИЧИНА: предыдущая версия скрипта уничтожила terraform.tfvars
|
|
||||||
# через write_tfvars() — потерян JWT-токен api_token. Это НЕДОПУСТИМО.
|
|
||||||
#
|
|
||||||
# ФАЗЫ:
|
|
||||||
# 1 BASELINE — apply с текущим tfvars (packages + nginx + docker)
|
|
||||||
# 2 IDEMPOTENT — повторный plan → "No changes"
|
|
||||||
# 3 PARTIAL_DISABLE — apply с -var install_nginx=false -var install_docker=false
|
|
||||||
# 4 PARTIAL_ENABLE — apply с -var install_nginx=true -var install_docker=true (run_id+1)
|
|
||||||
# 5 REORDER_PACKAGES — apply с -var 'base_packages=["htop","jq"]' (run_id+1)
|
|
||||||
# 6 MANUAL_PURGE — удалить пакеты с VM по SSH → apply (run_id+1)
|
|
||||||
# 7 DESTROY — terraform destroy → VM уходит в suspend
|
|
||||||
# 8 RESURRECT — apply после destroy → VM просыпается
|
|
||||||
# 9 STRESS_CYCLES — N подряд destroy/apply циклов
|
|
||||||
# 10 FINAL_SANITY — проверить доступность VM и пакеты
|
|
||||||
#
|
|
||||||
# ЗАПУСК:
|
|
||||||
# cd ~/terra/sless/examples/VM
|
|
||||||
# bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log
|
|
||||||
#
|
|
||||||
# ПАРАМЕТРЫ (env):
|
|
||||||
# STRESS_CYCLES=2 — количество destroy/apply циклов в фазе 9 (default: 2)
|
|
||||||
# SKIP_DESTROY=1 — пропустить фазы 7-9 (для быстрого прогона)
|
|
||||||
#
|
|
||||||
# АНАЛИЗ РЕЗУЛЬТАТОВ:
|
|
||||||
# grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log
|
|
||||||
# Итоговая сводка печатается в конце лога.
|
|
||||||
#
|
|
||||||
# ТРЕБОВАНИЯ: terraform, ssh, python3 — всё на VM naeel@5.172.178.213
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
# ── CONFIG ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
VM_KEY="$SCRIPT_DIR/vm_key"
|
|
||||||
STRESS_CYCLES="${STRESS_CYCLES:-2}"
|
|
||||||
SKIP_DESTROY="${SKIP_DESTROY:-0}"
|
|
||||||
|
|
||||||
# Читаем текущий run_id из terraform.tfvars (ТОЛЬКО ЧТЕНИЕ, не запись)
|
|
||||||
RUN_ID=$(grep 'install_run_id' terraform.tfvars | grep -oP '\d+' | head -1)
|
|
||||||
|
|
||||||
# ── СТАТИСТИКА ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
PASS=0; FAIL=0; SKIP=0
|
|
||||||
PHASE_RESULTS=()
|
|
||||||
START_TIME=$SECONDS
|
|
||||||
|
|
||||||
# ── ЦВЕТА ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
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++)); }
|
|
||||||
skip() { echo -e " ${YELLOW}[SKIP]${RESET} $1"; ((SKIP++)); }
|
|
||||||
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
|
|
||||||
warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; }
|
|
||||||
|
|
||||||
phase_header() {
|
|
||||||
local num="$1" name="$2"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN} ФАЗА $num: $name${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
||||||
echo -e " ${CYAN}[TIME]${RESET} $(date '+%H:%M:%S')"
|
|
||||||
}
|
|
||||||
|
|
||||||
phase_result() {
|
|
||||||
local name="$1" result="$2"
|
|
||||||
PHASE_RESULTS+=("$name:$result")
|
|
||||||
echo -e " ${CYAN}[PHASE]${RESET} $name → ${result}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── TERRAFORM HELPERS ─────────────────────────────────────────────────────────
|
|
||||||
# ⛔ НЕ ТРОГАЕМ terraform.tfvars. Переопределения — ТОЛЬКО через -var.
|
|
||||||
# terraform.tfvars подхватывается автоматически (лежит в рабочей директории).
|
|
||||||
# Доп. -var аргументы передаются во все tf_* функции и ПЕРЕОПРЕДЕЛЯЮТ tfvars.
|
|
||||||
|
|
||||||
# tf_apply: apply с retry при сетевых ошибках.
|
|
||||||
# Аргументы: любые доп. -var (опционально).
|
|
||||||
# Пример: tf_apply -var install_nginx=false -var install_docker=false
|
|
||||||
tf_apply() {
|
|
||||||
local attempt=1 max=3
|
|
||||||
while [[ $attempt -le $max ]]; do
|
|
||||||
info "terraform apply (попытка $attempt/$max)..."
|
|
||||||
if terraform apply -auto-approve -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_apply.log; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_apply.log && [[ $attempt -lt $max ]]; then
|
|
||||||
warn "сетевой сбой, retry через $((attempt * 5))s..."
|
|
||||||
sleep $((attempt * 5))
|
|
||||||
((attempt++))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# tf_destroy: destroy с retry.
|
|
||||||
# Аргументы: любые доп. -var (опционально).
|
|
||||||
tf_destroy() {
|
|
||||||
local attempt=1 max=3
|
|
||||||
while [[ $attempt -le $max ]]; do
|
|
||||||
info "terraform destroy (попытка $attempt/$max)..."
|
|
||||||
if terraform destroy -auto-approve -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_destroy.log; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_destroy.log && [[ $attempt -lt $max ]]; then
|
|
||||||
warn "сетевой сбой, retry через $((attempt * 5))s..."
|
|
||||||
sleep $((attempt * 5))
|
|
||||||
((attempt++))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# tf_plan_no_changes: проверить plan → "No changes".
|
|
||||||
# Аргументы: любые доп. -var (опционально).
|
|
||||||
tf_plan_no_changes() {
|
|
||||||
terraform plan -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_plan.log
|
|
||||||
grep -q 'No changes' /tmp/vm_tf_plan.log
|
|
||||||
}
|
|
||||||
|
|
||||||
# tf_state_count: количество ресурсов в state.
|
|
||||||
tf_state_count() {
|
|
||||||
terraform state list 2>/dev/null | wc -l
|
|
||||||
}
|
|
||||||
|
|
||||||
# next_run_id: инкрементировать внутренний счётчик RUN_ID и вернуть новое значение.
|
|
||||||
# Используется для -var install_run_id=N чтобы форсировать пересоздание jobs.
|
|
||||||
next_run_id() {
|
|
||||||
RUN_ID=$((RUN_ID + 1))
|
|
||||||
echo "$RUN_ID"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── VM SSH HELPERS ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# get_vm_ip: получить внешний IP VM из terraform output.
|
|
||||||
get_vm_ip() {
|
|
||||||
terraform output -json vm_state 2>/dev/null \
|
|
||||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('externalConnect',''))" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_ssh: выполнить команду на VM. Возвращает exit code команды.
|
|
||||||
vm_ssh() {
|
|
||||||
local ip="$1"; shift
|
|
||||||
ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=15 \
|
|
||||||
-o ServerAliveInterval=5 -o ServerAliveCountMax=3 \
|
|
||||||
"ubuntu@$ip" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_alive: проверить доступность VM по SSH.
|
|
||||||
vm_alive() {
|
|
||||||
local ip="$1"
|
|
||||||
vm_ssh "$ip" 'echo alive' &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_wait_alive: ждать пока VM ответит по SSH (до timeout_sec).
|
|
||||||
vm_wait_alive() {
|
|
||||||
local ip="$1" timeout_sec="${2:-120}"
|
|
||||||
local deadline=$((SECONDS + timeout_sec))
|
|
||||||
while [[ $SECONDS -lt $deadline ]]; do
|
|
||||||
if vm_alive "$ip"; then return 0; fi
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_check_package: проверить что пакет установлен.
|
|
||||||
vm_check_package() {
|
|
||||||
local ip="$1" pkg="$2"
|
|
||||||
vm_ssh "$ip" "dpkg -l $pkg 2>/dev/null | grep -q '^ii'" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_check_binary: проверить что бинарник доступен.
|
|
||||||
vm_check_binary() {
|
|
||||||
local ip="$1" bin="$2"
|
|
||||||
vm_ssh "$ip" "command -v $bin" &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_wait_binary: ждать пока бинарник появится на VM (sless_job работает асинхронно).
|
|
||||||
# Нужен потому что sless_job запускает kubernetes Job, который сначала собирает образ,
|
|
||||||
# затем стартует pod, и только потом SSH-устанавливает пакеты на VM — это занимает 1-3 мин.
|
|
||||||
vm_wait_binary() {
|
|
||||||
local ip="$1" bin="$2" timeout_sec="${3:-180}"
|
|
||||||
local deadline=$((SECONDS + timeout_sec))
|
|
||||||
info "жду появления '$bin' на VM (до ${timeout_sec}s)..."
|
|
||||||
while [[ $SECONDS -lt $deadline ]]; do
|
|
||||||
if vm_check_binary "$ip" "$bin"; then return 0; fi
|
|
||||||
sleep 15
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# vm_purge_all: удалить все установленные пакеты с VM.
|
|
||||||
vm_purge_all() {
|
|
||||||
local ip="$1"
|
|
||||||
info "удаляю пакеты с VM $ip..."
|
|
||||||
vm_ssh "$ip" 'sudo systemctl stop nginx docker 2>/dev/null; sudo apt-get purge -y jq python3-pip htop unzip nginx docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null; sudo apt-get autoremove -y 2>/dev/null; sudo rm -rf /var/lib/docker /var/lib/containerd; echo purge_done' 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 1: BASELINE — apply с текущим tfvars (всё включено)
|
|
||||||
# Используем -var install_run_id=N чтобы гарантировать свежий запуск.
|
|
||||||
# terraform.tfvars НЕ ТРОГАЕМ — берём как есть.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_1_baseline() {
|
|
||||||
phase_header 1 "BASELINE — apply с полным набором"
|
|
||||||
|
|
||||||
local rid
|
|
||||||
rid=$(next_run_id)
|
|
||||||
|
|
||||||
# Все флаги = true (как в tfvars), но run_id инкрементирован
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid"; then
|
|
||||||
pass "1.1 terraform apply завершился успешно"
|
|
||||||
else
|
|
||||||
fail "1.1 terraform apply упал"
|
|
||||||
phase_result "BASELINE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить количество ресурсов
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "1.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
else
|
|
||||||
fail "1.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить outputs
|
|
||||||
local out
|
|
||||||
out=$(terraform output -json 2>/dev/null)
|
|
||||||
|
|
||||||
if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'ok' in d['install_packages_result']['value'] or 'already' in d['install_packages_result']['value']" 2>/dev/null; then
|
|
||||||
pass "1.3 install_packages_result содержит ok/already_installed"
|
|
||||||
else
|
|
||||||
fail "1.3 install_packages_result не содержит ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_nginx_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then
|
|
||||||
pass "1.4 install_nginx_result содержит ok/already"
|
|
||||||
else
|
|
||||||
fail "1.4 install_nginx_result не содержит ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_docker_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then
|
|
||||||
pass "1.5 install_docker_result содержит ok/already"
|
|
||||||
else
|
|
||||||
fail "1.5 install_docker_result не содержит ok"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить VM по SSH
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
pass "1.6 VM $ip доступна по SSH"
|
|
||||||
else
|
|
||||||
fail "1.6 VM не доступна по SSH (ip=$ip)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "BASELINE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 2: IDEMPOTENT — повторный apply с теми же аргументами → 0 changed
|
|
||||||
# Используем apply (не plan) — это надёжнее: некоторые sless-провайдеры показывают
|
|
||||||
# ложный drift в plan, но apply при этом возвращает 0 changed. Apply — canonical way.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_2_idempotent() {
|
|
||||||
phase_header 2 "IDEMPOTENT — повторный apply без изменений"
|
|
||||||
|
|
||||||
# Тот же run_id что применялся в фазе 1 → apply должен вернуть 0 changed
|
|
||||||
# sless_job — эфемерный ресурс, каждый apply пересоздаёт job-ресурсы (add+destroy).
|
|
||||||
# Идемпотентность = "0 changed" (ноль in-place изменений), а не "0 add/destroy".
|
|
||||||
# VM и vApp не должны изменяться никогда.
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$RUN_ID"; then
|
|
||||||
if grep -q ', 0 changed,' /tmp/vm_tf_apply.log; then
|
|
||||||
pass "2.1 повторный apply → 0 changed (sless_job пересоздан — ожидаемо)"
|
|
||||||
grep -E 'Resources:' /tmp/vm_tf_apply.log | tail -1 | while read -r line; do
|
|
||||||
info " $line"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
fail "2.1 повторный apply изменил persistent ресурсы (ожидали 0 changed)"
|
|
||||||
grep -E 'added|changed|destroyed' /tmp/vm_tf_apply.log | tail -3 | while read -r line; do
|
|
||||||
info " $line"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "2.1 повторный apply упал"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "IDEMPOTENT" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 3: PARTIAL_DISABLE — выключить nginx + docker
|
|
||||||
# Используем -var install_nginx=false -var install_docker=false
|
|
||||||
# terraform.tfvars по-прежнему НЕ ТРОГАЕМ.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_3_partial_disable() {
|
|
||||||
phase_header 3 "PARTIAL_DISABLE — выключить nginx + docker"
|
|
||||||
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=false" \
|
|
||||||
-var "install_docker=false" \
|
|
||||||
-var "install_run_id=$RUN_ID"; then
|
|
||||||
pass "3.1 apply с partial disable завершился"
|
|
||||||
else
|
|
||||||
fail "3.1 apply с partial disable упал"
|
|
||||||
phase_result "PARTIAL_DISABLE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить что nginx и docker job пропали из state
|
|
||||||
local state_list
|
|
||||||
state_list=$(terraform state list 2>/dev/null)
|
|
||||||
|
|
||||||
if echo "$state_list" | grep -q 'install_nginx'; then
|
|
||||||
fail "3.2 install_nginx всё ещё в state"
|
|
||||||
else
|
|
||||||
pass "3.2 install_nginx убран из state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$state_list" | grep -q 'install_docker'; then
|
|
||||||
fail "3.3 install_docker всё ещё в state"
|
|
||||||
else
|
|
||||||
pass "3.3 install_docker убран из state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$state_list" | grep -q 'install_packages'; then
|
|
||||||
pass "3.4 install_packages остался в state"
|
|
||||||
else
|
|
||||||
fail "3.4 install_packages пропал из state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить outputs
|
|
||||||
local nginx_out docker_out
|
|
||||||
nginx_out=$(terraform output -raw install_nginx_result 2>/dev/null)
|
|
||||||
docker_out=$(terraform output -raw install_docker_result 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ "$nginx_out" == "skipped" ]]; then
|
|
||||||
pass "3.5 install_nginx_result = skipped"
|
|
||||||
else
|
|
||||||
fail "3.5 install_nginx_result = '$nginx_out' (ожидали skipped)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$docker_out" == "skipped" ]]; then
|
|
||||||
pass "3.6 install_docker_result = skipped"
|
|
||||||
else
|
|
||||||
fail "3.6 install_docker_result = '$docker_out' (ожидали skipped)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "PARTIAL_DISABLE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 4: PARTIAL_ENABLE — включить обратно всё
|
|
||||||
# Инкрементируем run_id чтобы jobs пересоздались.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_4_partial_enable() {
|
|
||||||
phase_header 4 "PARTIAL_ENABLE — включить обратно всё"
|
|
||||||
|
|
||||||
local rid
|
|
||||||
rid=$(next_run_id)
|
|
||||||
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid"; then
|
|
||||||
pass "4.1 apply с полным набором завершился"
|
|
||||||
else
|
|
||||||
fail "4.1 apply с полным набором упал"
|
|
||||||
phase_result "PARTIAL_ENABLE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "4.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
else
|
|
||||||
fail "4.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "PARTIAL_ENABLE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 5: REORDER_PACKAGES — изменить порядок и состав base_packages
|
|
||||||
# Через -var 'base_packages=["htop","jq"]' — terraform.tfvars не трогаем.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_5_reorder() {
|
|
||||||
phase_header 5 "REORDER_PACKAGES — изменить порядок и состав пакетов"
|
|
||||||
|
|
||||||
local rid
|
|
||||||
rid=$(next_run_id)
|
|
||||||
|
|
||||||
# Сокращённый набор пакетов через -var
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid" \
|
|
||||||
-var 'base_packages=["htop","jq"]'; then
|
|
||||||
pass "5.1 apply с изменённым набором пакетов завершился"
|
|
||||||
else
|
|
||||||
fail "5.1 apply с изменённым набором пакетов упал"
|
|
||||||
phase_result "REORDER_PACKAGES" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить output
|
|
||||||
local pkg_out
|
|
||||||
pkg_out=$(terraform output -raw install_packages_result 2>/dev/null)
|
|
||||||
if echo "$pkg_out" | grep -qE '"status"|ok|already'; then
|
|
||||||
pass "5.2 install_packages вернул ожидаемый результат"
|
|
||||||
else
|
|
||||||
fail "5.2 install_packages output неожиданный: $pkg_out"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Вернуть полный набор пакетов
|
|
||||||
rid=$(next_run_id)
|
|
||||||
tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid" \
|
|
||||||
|| warn "5.3 восстановление полного набора не удалось"
|
|
||||||
|
|
||||||
phase_result "REORDER_PACKAGES" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 6: MANUAL_PURGE — удалить пакеты с VM вручную, заново установить
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_6_manual_purge() {
|
|
||||||
phase_header 6 "MANUAL_PURGE — удалить пакеты с VM, заново установить"
|
|
||||||
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -z "$ip" ]]; then
|
|
||||||
fail "6.0 не удалось получить IP VM"
|
|
||||||
phase_result "MANUAL_PURGE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Удалить всё с VM
|
|
||||||
vm_purge_all "$ip"
|
|
||||||
|
|
||||||
# Проверить что пакеты действительно удалены
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
fail "6.1 docker всё ещё на VM после purge"
|
|
||||||
else
|
|
||||||
pass "6.1 docker удалён с VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
fail "6.2 nginx всё ещё на VM после purge"
|
|
||||||
else
|
|
||||||
pass "6.2 nginx удалён с VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "jq"; then
|
|
||||||
fail "6.3 jq всё ещё на VM после purge"
|
|
||||||
else
|
|
||||||
pass "6.3 jq удалён с VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Пересоздать jobs (bump run_id)
|
|
||||||
local rid
|
|
||||||
rid=$(next_run_id)
|
|
||||||
|
|
||||||
info "запускаю переустановку через sless_job..."
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid"; then
|
|
||||||
pass "6.4 apply после purge завершился"
|
|
||||||
else
|
|
||||||
fail "6.4 apply после purge упал"
|
|
||||||
phase_result "MANUAL_PURGE" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ждать пока sless_job отработает: docker самый долгий (k8s Job + образ + apt-install ~200MB).
|
|
||||||
# docker-ce требует до 5 минут на первой установке — ставим 360s.
|
|
||||||
if vm_wait_binary "$ip" "docker" 360; then
|
|
||||||
pass "6.5 docker установлен заново"
|
|
||||||
else
|
|
||||||
fail "6.5 docker НЕ установлен после re-apply (таймаут 360s)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_wait_binary "$ip" "nginx" 240; then
|
|
||||||
pass "6.6 nginx установлен заново"
|
|
||||||
else
|
|
||||||
fail "6.6 nginx НЕ установлен после re-apply (таймаут 240s)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "jq"; then
|
|
||||||
pass "6.7 jq установлен заново"
|
|
||||||
else
|
|
||||||
fail "6.7 jq НЕ установлен после re-apply"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "MANUAL_PURGE" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 7: DESTROY — terraform destroy, VM уходит в suspend
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_7_destroy() {
|
|
||||||
phase_header 7 "DESTROY — terraform destroy → VM в suspend"
|
|
||||||
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
|
|
||||||
if tf_destroy; then
|
|
||||||
pass "7.1 terraform destroy завершился"
|
|
||||||
else
|
|
||||||
fail "7.1 terraform destroy упал"
|
|
||||||
phase_result "DESTROY" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# State должен быть пуст
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -eq 0 ]]; then
|
|
||||||
pass "7.2 state пуст ($count ресурсов)"
|
|
||||||
else
|
|
||||||
fail "7.2 state не пуст ($count ресурсов)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# VM не должна отвечать по SSH
|
|
||||||
if [[ -n "$ip" ]]; then
|
|
||||||
info "проверяю что VM $ip недоступна..."
|
|
||||||
if vm_alive "$ip"; then
|
|
||||||
fail "7.3 VM $ip всё ещё отвечает по SSH после destroy"
|
|
||||||
else
|
|
||||||
pass "7.3 VM $ip не отвечает по SSH (suspend подтверждён)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
skip "7.3 IP VM неизвестен, пропускаю SSH-проверку"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "DESTROY" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 8: RESURRECT — apply после destroy, VM просыпается
|
|
||||||
# Используем текущий run_id — terraform.tfvars НЕ ТРОГАЕМ.
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_8_resurrect() {
|
|
||||||
phase_header 8 "RESURRECT — apply после destroy"
|
|
||||||
|
|
||||||
local rid
|
|
||||||
rid=$(next_run_id)
|
|
||||||
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid"; then
|
|
||||||
pass "8.1 apply после destroy завершился"
|
|
||||||
else
|
|
||||||
fail "8.1 apply после destroy упал"
|
|
||||||
phase_result "RESURRECT" "FAIL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "8.2 state содержит $count ресурсов"
|
|
||||||
else
|
|
||||||
fail "8.2 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить VM
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
pass "8.3 VM $ip доступна по SSH после resurrect"
|
|
||||||
else
|
|
||||||
# VM может ещё просыпаться — ждём
|
|
||||||
info "VM не отвечает, жду до 120s..."
|
|
||||||
if vm_wait_alive "$ip" 120; then
|
|
||||||
pass "8.3 VM $ip доступна по SSH после ожидания"
|
|
||||||
else
|
|
||||||
fail "8.3 VM $ip не доступна по SSH через 120s"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверить пакеты
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
if vm_check_binary "$ip" "jq"; then
|
|
||||||
pass "8.4 jq установлен после resurrect"
|
|
||||||
else
|
|
||||||
fail "8.4 jq НЕ установлен после resurrect"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
pass "8.5 nginx установлен после resurrect"
|
|
||||||
else
|
|
||||||
fail "8.5 nginx НЕ установлен после resurrect"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
pass "8.6 docker установлен после resurrect"
|
|
||||||
else
|
|
||||||
fail "8.6 docker НЕ установлен после resurrect"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "RESURRECT" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 9: STRESS_CYCLES — N destroy/apply циклов подряд
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_9_stress() {
|
|
||||||
phase_header 9 "STRESS_CYCLES — $STRESS_CYCLES циклов destroy/apply"
|
|
||||||
|
|
||||||
local i rid
|
|
||||||
for i in $(seq 1 "$STRESS_CYCLES"); do
|
|
||||||
info "── цикл $i/$STRESS_CYCLES ──"
|
|
||||||
|
|
||||||
info "[$i] destroy..."
|
|
||||||
if tf_destroy; then
|
|
||||||
pass "9.${i}a destroy цикл $i"
|
|
||||||
else
|
|
||||||
fail "9.${i}a destroy цикл $i упал"
|
|
||||||
# Попробовать восстановиться
|
|
||||||
rid=$(next_run_id)
|
|
||||||
tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid" || true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
rid=$(next_run_id)
|
|
||||||
info "[$i] apply (run_id=$rid)..."
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid"; then
|
|
||||||
pass "9.${i}b apply цикл $i"
|
|
||||||
else
|
|
||||||
fail "9.${i}b apply цикл $i упал"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
phase_result "STRESS_CYCLES" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# ФАЗА 10: FINAL_SANITY — финальная проверка состояния
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
phase_10_final() {
|
|
||||||
phase_header 10 "FINAL_SANITY — финальная проверка"
|
|
||||||
|
|
||||||
# Убедиться что state на месте
|
|
||||||
local count
|
|
||||||
count=$(tf_state_count)
|
|
||||||
if [[ $count -ge 5 ]]; then
|
|
||||||
pass "10.1 state содержит $count ресурсов"
|
|
||||||
else
|
|
||||||
fail "10.1 state содержит $count ресурсов (ожидали ≥5)"
|
|
||||||
# Попробовать восстановить (через -var, БЕЗ изменения файлов)
|
|
||||||
local rid
|
|
||||||
rid=$(next_run_id)
|
|
||||||
info "пытаюсь восстановить baseline..."
|
|
||||||
tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$rid" || warn "восстановление не удалось"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Idempotency финально: повторный apply → 0 changed.
|
|
||||||
# sless_job пересоздаются (add+destroy) — это нормально для job-ресурса.
|
|
||||||
# Проверяем только "0 changed" — VM и vApp не должны изменяться.
|
|
||||||
if tf_apply \
|
|
||||||
-var "install_packages=true" \
|
|
||||||
-var "install_nginx=true" \
|
|
||||||
-var "install_docker=true" \
|
|
||||||
-var "install_run_id=$RUN_ID"; then
|
|
||||||
if grep -q ', 0 changed,' /tmp/vm_tf_apply.log; then
|
|
||||||
pass "10.2 финальный apply → 0 changed (sless_job пересоздан — ожидаемо)"
|
|
||||||
grep -E 'Resources:' /tmp/vm_tf_apply.log | tail -1 | while read -r line; do
|
|
||||||
info " $line"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
fail "10.2 финальный apply изменил persistent ресурсы (ожидали 0 changed)"
|
|
||||||
grep -E 'added|changed|destroyed' /tmp/vm_tf_apply.log | tail -3 | while read -r line; do
|
|
||||||
info " $line"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "10.2 финальный apply упал"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# VM доступна
|
|
||||||
local ip
|
|
||||||
ip=$(get_vm_ip)
|
|
||||||
if [[ -n "$ip" ]] && vm_alive "$ip"; then
|
|
||||||
pass "10.3 VM $ip доступна по SSH"
|
|
||||||
|
|
||||||
# Полная проверка пакетов
|
|
||||||
for pkg in jq htop unzip; do
|
|
||||||
if vm_check_package "$ip" "$pkg"; then
|
|
||||||
pass "10.4 пакет $pkg установлен"
|
|
||||||
else
|
|
||||||
fail "10.4 пакет $pkg НЕ установлен"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "nginx"; then
|
|
||||||
pass "10.5 nginx работает"
|
|
||||||
else
|
|
||||||
fail "10.5 nginx НЕ найден"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if vm_check_binary "$ip" "docker"; then
|
|
||||||
pass "10.6 docker работает"
|
|
||||||
else
|
|
||||||
fail "10.6 docker НЕ найден"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# nginx service активен (не просто бинарник, а именно демон)
|
|
||||||
if vm_ssh "$ip" "systemctl is-active nginx 2>/dev/null" 2>/dev/null | grep -q '^active$'; then
|
|
||||||
pass "10.7 nginx service активен (systemctl)"
|
|
||||||
else
|
|
||||||
fail "10.7 nginx service не активен"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# docker daemon активен
|
|
||||||
if vm_ssh "$ip" "systemctl is-active docker 2>/dev/null" 2>/dev/null | grep -q '^active$'; then
|
|
||||||
pass "10.8 docker daemon активен (systemctl)"
|
|
||||||
else
|
|
||||||
fail "10.8 docker daemon не активен"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# HTTP probe: nginx отвечает на localhost:80
|
|
||||||
local http_code
|
|
||||||
http_code=$(vm_ssh "$ip" "curl -sS -o /dev/null -w '%{http_code}' http://localhost 2>/dev/null" 2>/dev/null)
|
|
||||||
if [[ "$http_code" == "200" ]]; then
|
|
||||||
pass "10.9 nginx отвечает HTTP 200"
|
|
||||||
else
|
|
||||||
fail "10.9 nginx не отвечает HTTP 200 (code='$http_code')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# python3 доступен
|
|
||||||
local py_ver
|
|
||||||
py_ver=$(vm_ssh "$ip" "python3 --version 2>&1" 2>/dev/null)
|
|
||||||
if echo "$py_ver" | grep -q 'Python 3'; then
|
|
||||||
pass "10.10 python3 доступен ($py_ver)"
|
|
||||||
else
|
|
||||||
fail "10.10 python3 недоступен"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# docker smoke: запустить контейнер и проверить вывод
|
|
||||||
if vm_ssh "$ip" "docker run --rm hello-world 2>&1 | grep -q 'Hello from Docker'" 2>/dev/null; then
|
|
||||||
pass "10.11 docker run hello-world → успешно"
|
|
||||||
else
|
|
||||||
fail "10.11 docker run hello-world → не прошёл"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# disk space: убедиться что на VM есть место (>500MB free)
|
|
||||||
local free_mb
|
|
||||||
free_mb=$(vm_ssh "$ip" "df -m / 2>/dev/null | awk 'NR==2{print \$4}'" 2>/dev/null)
|
|
||||||
if [[ -n "$free_mb" && "$free_mb" -gt 500 ]]; then
|
|
||||||
pass "10.12 свободное место на / = ${free_mb}MB (>500MB)"
|
|
||||||
else
|
|
||||||
fail "10.12 мало места на / = ${free_mb}MB (ожидали >500MB)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "10.3 VM не доступна по SSH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_result "FINAL_SANITY" "PASS"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
# MAIN
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
echo -e "${BOLD}${CYAN}"
|
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
|
||||||
echo "║ VM STRESS TEST v2 (READ-ONLY) — examples/VM ║"
|
|
||||||
echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║"
|
|
||||||
echo "║ Начальный run_id: $RUN_ID ║"
|
|
||||||
echo "║ Циклов stress: $STRESS_CYCLES ║"
|
|
||||||
echo "║ terraform.tfvars НЕ МОДИФИЦИРУЕТСЯ ║"
|
|
||||||
echo "╚═══════════════════════════════════════════════════════════════╝"
|
|
||||||
echo -e "${RESET}"
|
|
||||||
|
|
||||||
# Проверить что мы в правильной директории
|
|
||||||
if [[ ! -f "terraform.tfvars" ]]; then
|
|
||||||
echo -e "${RED}ОШИБКА: terraform.tfvars не найден. Запускайте из examples/VM/${RESET}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$VM_KEY" ]]; then
|
|
||||||
echo -e "${RED}ОШИБКА: $VM_KEY не найден${RESET}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ⛔ ПРОВЕРКА ЦЕЛОСТНОСТИ: terraform.tfvars НЕ ДОЛЖЕН БЫТЬ МОДИФИЦИРОВАН
|
|
||||||
# Сохраняем md5 до запуска и проверяем после каждой фазы
|
|
||||||
TFVARS_MD5=$(md5sum terraform.tfvars | awk '{print $1}')
|
|
||||||
info "md5 terraform.tfvars = $TFVARS_MD5 (будет проверяться после каждой фазы)"
|
|
||||||
|
|
||||||
check_tfvars_integrity() {
|
|
||||||
local current_md5
|
|
||||||
current_md5=$(md5sum terraform.tfvars | awk '{print $1}')
|
|
||||||
if [[ "$current_md5" != "$TFVARS_MD5" ]]; then
|
|
||||||
echo -e "${RED}⛔⛔⛔ КРИТИЧЕСКАЯ ОШИБКА: terraform.tfvars был изменён! ⛔⛔⛔${RESET}"
|
|
||||||
echo -e "${RED}Ожидали md5: $TFVARS_MD5${RESET}"
|
|
||||||
echo -e "${RED}Текущий md5: $current_md5${RESET}"
|
|
||||||
echo -e "${RED}АВАРИЙНАЯ ОСТАНОВКА ТЕСТА${RESET}"
|
|
||||||
exit 99
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Запуск фаз с проверкой целостности после каждой
|
|
||||||
phase_1_baseline; check_tfvars_integrity
|
|
||||||
phase_2_idempotent; check_tfvars_integrity
|
|
||||||
phase_3_partial_disable; check_tfvars_integrity
|
|
||||||
phase_4_partial_enable; check_tfvars_integrity
|
|
||||||
phase_5_reorder; check_tfvars_integrity
|
|
||||||
phase_6_manual_purge; check_tfvars_integrity
|
|
||||||
|
|
||||||
if [[ "$SKIP_DESTROY" == "1" ]]; then
|
|
||||||
skip "фазы 7-9 пропущены (SKIP_DESTROY=1)"
|
|
||||||
PHASE_RESULTS+=("DESTROY:SKIP" "RESURRECT:SKIP" "STRESS_CYCLES:SKIP")
|
|
||||||
else
|
|
||||||
phase_7_destroy; check_tfvars_integrity
|
|
||||||
phase_8_resurrect; check_tfvars_integrity
|
|
||||||
phase_9_stress; check_tfvars_integrity
|
|
||||||
fi
|
|
||||||
|
|
||||||
phase_10_final; check_tfvars_integrity
|
|
||||||
|
|
||||||
# ── ИТОГОВАЯ СВОДКА ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
ELAPSED=$((SECONDS - START_TIME))
|
|
||||||
ELAPSED_MIN=$((ELAPSED / 60))
|
|
||||||
ELAPSED_SEC=$((ELAPSED % 60))
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}${CYAN}╔═══════════════════════════════════════════════════════════════╗${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN}║ ИТОГОВАЯ СВОДКА ║${RESET}"
|
|
||||||
echo -e "${BOLD}${CYAN}╠═══════════════════════════════════════════════════════════════╣${RESET}"
|
|
||||||
echo -e "${BOLD} Время: ${ELAPSED_MIN}m ${ELAPSED_SEC}s${RESET}"
|
|
||||||
echo -e "${BOLD} ${GREEN}PASS: $PASS${RESET} ${RED}FAIL: $FAIL${RESET} ${YELLOW}SKIP: $SKIP${RESET}"
|
|
||||||
echo -e "${BOLD} Финальный run_id: $RUN_ID${RESET}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD} Фазы:${RESET}"
|
|
||||||
for pr in "${PHASE_RESULTS[@]}"; do
|
|
||||||
name="${pr%%:*}"
|
|
||||||
result="${pr##*:}"
|
|
||||||
case "$result" in
|
|
||||||
PASS) echo -e " ${GREEN}✓${RESET} $name" ;;
|
|
||||||
FAIL) echo -e " ${RED}✗${RESET} $name" ;;
|
|
||||||
SKIP) echo -e " ${YELLOW}○${RESET} $name" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
echo -e "${BOLD}${CYAN}╚═══════════════════════════════════════════════════════════════╝${RESET}"
|
|
||||||
|
|
||||||
# Финальная проверка целостности tfvars
|
|
||||||
check_tfvars_integrity
|
|
||||||
info "terraform.tfvars НЕ БЫЛ ИЗМЕНЁН (md5 совпадает)"
|
|
||||||
|
|
||||||
# Exit code: 0 если все PASS, 1 если есть FAIL
|
|
||||||
if [[ $FAIL -gt 0 ]]; then
|
|
||||||
echo -e "\n${RED}РЕЗУЛЬТАТ: FAIL ($FAIL ошибок)${RESET}"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo -e "\n${GREEN}РЕЗУЛЬТАТ: ALL PASS${RESET}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
Loading…
Reference in New Issue
Block a user