diff --git a/examples/POSTGRES/chaos_marathon.tf b/examples/POSTGRES/chaos_marathon.tf deleted file mode 100644 index 88ee5a7..0000000 --- a/examples/POSTGRES/chaos_marathon.tf +++ /dev/null @@ -1,257 +0,0 @@ -// 2026-03-21 — chaos_marathon.tf: 15 новых сервисов для часового хаос-марафона. -// Два рантайма: python3.11 (9), nodejs20 (2). -// Все зависят от sless_job.postgres_table_init_job. - -# ── Python: работа с таблицей ───────────────────────────────────────────────── - -# Считает строки по prefix — тест concurrent reads + COUNT агрегации. -resource "sless_service" "pg_counter" { - name = "pg-counter" - runtime = "python3.11" - entrypoint = "pg_counter.count" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-counter" - depends_on = [sless_job.postgres_table_init_job] -} - -# DELETE дублей по title — идемпотентный, повторный вызов безопасен. -resource "sless_service" "pg_dedup" { - name = "pg-dedup" - runtime = "python3.11" - entrypoint = "pg_dedup.dedup" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-dedup" - depends_on = [sless_job.postgres_table_init_job] -} - -# Поиск по title с ILIKE + пагинация — тест спецсимволов и SQL injection safety. -resource "sless_service" "pg_search" { - name = "pg-search" - runtime = "python3.11" - entrypoint = "pg_search.search" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-search" - depends_on = [sless_job.postgres_table_init_job] -} - -# Bulk INSERT через execute_values — до 500 строк за раз. -resource "sless_service" "pg_bulk_insert" { - name = "pg-bulk-insert" - runtime = "python3.11" - entrypoint = "pg_bulk_insert.bulk_insert" - memory_mb = 256 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-bulk-insert" - depends_on = [sless_job.postgres_table_init_job] -} - -# DELETE строк старше N минут — идемпотентный. -resource "sless_service" "pg_delete_old" { - name = "pg-delete-old" - runtime = "python3.11" - entrypoint = "pg_delete_old.delete_old" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-delete-old" - depends_on = [sless_job.postgres_table_init_job] -} - -# INSERT ON CONFLICT DO UPDATE — повторный вызов с тем же title безопасен. -resource "sless_service" "pg_upsert" { - name = "pg-upsert" - runtime = "python3.11" - entrypoint = "pg_upsert.upsert" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-upsert" - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Python: chaos ───────────────────────────────────────────────────────────── - -# Echo: принимает любой ввод и отражает обратно — проверка на мусорный input. -resource "sless_service" "chaos_echo" { - name = "chaos-echo" - runtime = "python3.11" - entrypoint = "chaos_echo.echo" - memory_mb = 128 - timeout_sec = 10 - - source_dir = "${path.module}/code/chaos-echo" - depends_on = [sless_job.postgres_table_init_job] -} - -# Валидация плохих параметров — тупой юзер не может уронить сервис. -resource "sless_service" "chaos_badparams" { - name = "chaos-badparams" - runtime = "python3.11" - entrypoint = "chaos_badparams.validate" - memory_mb = 128 - timeout_sec = 10 - - source_dir = "${path.module}/code/chaos-badparams" - depends_on = [sless_job.postgres_table_init_job] -} - -# Медленный pg_sleep — тест timeout enforcement. -resource "sless_service" "chaos_slowquery" { - name = "chaos-slowquery" - runtime = "python3.11" - entrypoint = "chaos_slowquery.slowquery" - memory_mb = 128 - timeout_sec = 12 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/chaos-slowquery" - depends_on = [sless_job.postgres_table_init_job] -} - -# Большой JSON response — тест памяти и серилизации. -resource "sless_service" "chaos_bigpayload" { - name = "chaos-bigpayload" - runtime = "python3.11" - entrypoint = "chaos_bigpayload.bigpayload" - memory_mb = 256 - timeout_sec = 15 - - source_dir = "${path.module}/code/chaos-bigpayload" - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Node.js ─────────────────────────────────────────────────────────────────── - -# Bulk INSERT через параметризованный multi-value query. -resource "sless_service" "js_pg_batch" { - name = "js-pg-batch" - runtime = "nodejs20" - entrypoint = "js_pg_batch.run" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/js-pg-batch" - depends_on = [sless_job.postgres_table_init_job] -} - -# Идемпотентный INSERT — повторный вызов с тем же key = existing, не дубль. -resource "sless_service" "js_idempotent" { - name = "js-idempotent" - runtime = "nodejs20" - entrypoint = "js_idempotent.run" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/js-idempotent" - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Python: retry ───────────────────────────────────────────────────────────── - -# Запись с retry при transient PG error — тест устойчивости к сбоям. -resource "sless_service" "py_retry_writer" { - name = "py-retry-writer" - runtime = "python3.11" - entrypoint = "py_retry_writer.retry_write" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/py-retry-writer" - depends_on = [sless_job.postgres_table_init_job] -} diff --git a/examples/POSTGRES/code/chaos-badparams/chaos_badparams.py b/examples/POSTGRES/code/chaos-badparams/chaos_badparams.py deleted file mode 100644 index cdfe032..0000000 --- a/examples/POSTGRES/code/chaos-badparams/chaos_badparams.py +++ /dev/null @@ -1,40 +0,0 @@ -# 2026-03-21 — chaos-badparams: проверяет что функция не падает на мусорных входных данных. -# Принимает type=missing|wrong_type|huge|negative|zero и возвращает safe-ответ. -# Тестирует: устойчивость к "тупому юзеру" — никакого 500 на плохих входных данных. -import json - -_MAX_N = 10_000 - -def validate(event): - errors = [] - results = {} - - # n: должно быть int от 1 до MAX_N - raw_n = event.get("n") - try: - n = int(raw_n) - if n <= 0: - errors.append(f"n must be > 0, got {n}") - n = 1 - elif n > _MAX_N: - errors.append(f"n capped from {n} to {_MAX_N}") - n = _MAX_N - except (TypeError, ValueError): - errors.append(f"n is not a valid int: {repr(raw_n)}, using default 1") - n = 1 - results["n"] = n - - # name: обрезаем до 100 символов - raw_name = event.get("name", "") - if not isinstance(raw_name, str): - raw_name = str(raw_name) - errors.append("name was not a string, converted") - name = raw_name[:100] - results["name"] = name - - # flag: любое "truthy" значение - raw_flag = event.get("flag", False) - flag = raw_flag in (True, "true", "1", 1, "yes") - results["flag"] = flag - - return {"ok": len(errors) == 0, "errors": errors, "results": results} diff --git a/examples/POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py b/examples/POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py deleted file mode 100644 index 523e4d2..0000000 --- a/examples/POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py +++ /dev/null @@ -1,27 +0,0 @@ -# 2026-03-21 — chaos-bigpayload: генерирует/принимает большой JSON. -# Тестирует: большие ответы (64KB+), память рантайма. -import json, time - -def bigpayload(event): - size_kb = min(int(event.get("size_kb", 16)), 256) # cap 256KB - word = str(event.get("word", "x"))[:32] - - # Генерируем список строк нужного размера - chunk = word * 32 # ~32+ байт на запись - items = [] - total = 0 - target = size_kb * 1024 - i = 0 - while total < target: - entry = f"{chunk}-{i}" - items.append(entry) - total += len(entry) + 3 # 3 байта JSON overhead - i += 1 - - return { - "items_count": len(items), - "size_kb_approx": round(total / 1024, 1), - "first": items[0] if items else "", - "last": items[-1] if items else "", - "ts": int(time.time()), - } diff --git a/examples/POSTGRES/code/chaos-echo/chaos_echo.py b/examples/POSTGRES/code/chaos-echo/chaos_echo.py deleted file mode 100644 index 276023e..0000000 --- a/examples/POSTGRES/code/chaos-echo/chaos_echo.py +++ /dev/null @@ -1,19 +0,0 @@ -# 2026-03-21 — chaos-echo: отражает входные данные обратно. -# Тестирует: большие payload, unicode, null, вложенные структуры, спецсимволы. -# "Тупой юзер" шлёт всё что угодно — функция должна вернуть это обратно без падения. -import json - -def echo(event): - # Пытаемся сериализовать обратно — выловит непериализуемые типы - try: - size = len(json.dumps(event)) - except Exception: - size = -1 - - keys = list(event.keys()) if isinstance(event, dict) else [] - return { - "echo": event, - "keys": keys, - "size_bytes": size, - "type": type(event).__name__, - } diff --git a/examples/POSTGRES/code/chaos-slowquery/chaos_slowquery.py b/examples/POSTGRES/code/chaos-slowquery/chaos_slowquery.py deleted file mode 100644 index 6b9fb42..0000000 --- a/examples/POSTGRES/code/chaos-slowquery/chaos_slowquery.py +++ /dev/null @@ -1,19 +0,0 @@ -# 2026-03-21 — chaos-slowquery: намеренно медленный запрос через pg_sleep. -# Тестирует: timeout enforcement — платформа должна прервать запрос если > timeout_sec. -# sleep_sec cap = 8 (меньше timeout_sec=10 сервиса → успех; >10 → таймаут платформы). -import os, psycopg2 - -def slowquery(event): - sleep_sec = min(float(event.get("sleep_sec", 2.0)), 8.0) - 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 pg_sleep(%s), now()::text", (sleep_sec,)) - result = cur.fetchone() - return {"slept_sec": sleep_sec, "pg_now": result[1]} - finally: - conn.close() diff --git a/examples/POSTGRES/code/chaos-slowquery/requirements.txt b/examples/POSTGRES/code/chaos-slowquery/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/chaos-slowquery/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/funcs-list/funcs_list.py b/examples/POSTGRES/code/funcs-list/funcs_list.py deleted file mode 100644 index 68157d2..0000000 --- a/examples/POSTGRES/code/funcs-list/funcs_list.py +++ /dev/null @@ -1,94 +0,0 @@ -# 2026-03-18 (обновлено: plain text вывод; фильтрация SLESS_EXCLUDE) -# funcs_list.py — HTTP-функция: список пользовательских функций, человекочитаемый plain text. -# Вызывает внутренний REST API оператора (ClusterIP, без TLS). -# Возвращает str → python runtime отдаёт text/plain напрямую без json.dumps. -# -# Env vars: -# SLESS_API_URL — URL оператора (http://sless-operator.sless.svc.cluster.local:9090) -# SLESS_NAMESPACE — namespace пользователя (sless-{hex16}) -# SLESS_TOKEN — JWT токен для /v1/ API -# SLESS_EXTERNAL_URL — публичный базовый URL (https://sless.kube5s.ru) -# SLESS_EXCLUDE — comma-separated имена функций, которые не показывать - -import os -import requests - -SEP = "─" * 52 - - -def _comment(fn, http_trigs, cron_trigs): - phase = fn.get("phase", "?") - runtime = fn.get("runtime", "?") - if http_trigs: - active = "активна" if http_trigs[0].get("active") else "неактивна" - return f"HTTP endpoint ({runtime}) — {phase}, {active}" - elif cron_trigs: - schedule = cron_trigs[0].get("schedule", "?") - active = "активна" if cron_trigs[0].get("active") else "неактивна" - return f"Cron '{schedule}' ({runtime}) — {phase}, {active}" - else: - return f"Job/runner без триггера ({runtime}) — {phase}" - - -def list_all(event): - api_url = os.environ["SLESS_API_URL"].rstrip("/") - namespace = os.environ["SLESS_NAMESPACE"] - token = os.environ["SLESS_TOKEN"] - ext_url = os.environ.get("SLESS_EXTERNAL_URL", "").rstrip("/") - exclude = {n.strip() for n in os.environ.get("SLESS_EXCLUDE", "").split(",") if n.strip()} - - headers = {"Authorization": f"Bearer {token}"} - fns = requests.get(f"{api_url}/v1/namespaces/{namespace}/functions", headers=headers, timeout=10) - trs = requests.get(f"{api_url}/v1/namespaces/{namespace}/triggers", headers=headers, timeout=10) - fns.raise_for_status() - trs.raise_for_status() - - trig_idx = {} - for tr in trs.json(): - fn_name = tr.get("function") or tr.get("functionRef") - if fn_name: - trig_idx.setdefault(fn_name, []).append(tr) - - items = [] - for fn in fns.json(): - name = fn["name"] - if name in exclude: - continue - http_t = [t for t in trig_idx.get(name, []) if t.get("type") == "http"] - cron_t = [t for t in trig_idx.get(name, []) if t.get("type") == "cron"] - is_active = any(t.get("enabled", True) and t.get("active", False) for t in trig_idx.get(name, [])) - items.append((fn, http_t, cron_t, is_active)) - - # Сортировка: активные вверх, затем по имени - items.sort(key=lambda x: (not x[3], x[0]["name"])) - - lines = [] - for fn, http_t, cron_t, is_active in items: - name = fn["name"] - lines.append(SEP) - lines.append(f" {_comment(fn, http_t, cron_t)}") - lines.append(f" name: {name}") - lines.append(f" runtime: {fn.get('runtime', '?')}") - lines.append(f" phase: {fn.get('phase', '?')}") - lines.append(f" active: {'да' if is_active else 'нет'}") - - if http_t: - url = f"{ext_url}/fn/{namespace}/{name}" if ext_url else http_t[0].get("url", "") - lines.append(f" url: {url}") - if cron_t: - lines.append(f" cron: {cron_t[0].get('schedule', '?')}") - if fn.get("created_at"): - lines.append(f" created: {fn['created_at']}") - if fn.get("last_built_at"): - lines.append(f" built: {fn['last_built_at']}") - if fn.get("message"): - lines.append(f" message: {fn['message']}") - - lines.append(SEP) - lines.append(f" namespace: {namespace} | total: {len(items)}") - lines.append(SEP) - - # Возвращаем str — python runtime отдаст text/plain напрямую - return "\n".join(lines) + "\n" - - diff --git a/examples/POSTGRES/code/funcs-list/requirements.txt b/examples/POSTGRES/code/funcs-list/requirements.txt deleted file mode 100644 index 2c24336..0000000 --- a/examples/POSTGRES/code/funcs-list/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.31.0 diff --git a/examples/POSTGRES/code/js-pg-batch/js_pg_batch.js b/examples/POSTGRES/code/js-pg-batch/js_pg_batch.js deleted file mode 100644 index c95eb45..0000000 --- a/examples/POSTGRES/code/js-pg-batch/js_pg_batch.js +++ /dev/null @@ -1,43 +0,0 @@ -// 2026-03-21 — js-pg-batch: вставляет N строк через parameterized bulk query. -// Тестирует: async/await PG с пакетной вставкой, Node.js под нагрузкой. -const { Client } = require('pg'); - -async function run(event) { - const n = Math.min(parseInt(event.n ?? 20, 10) || 20, 200); - const prefix = String(event.prefix ?? 'js-batch').slice(0, 40); - - 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 { - const ts = Date.now(); - // Строим multi-value INSERT: INSERT INTO ... VALUES ($1), ($2), ... - const placeholders = []; - const values = []; - for (let i = 0; i < n; i++) { - placeholders.push(`($${i + 1})`); - values.push(`${prefix}-${ts}-${i}`); - } - const sql = `INSERT INTO terraform_demo_table (title) VALUES ${placeholders.join(',')} RETURNING id`; - const t0 = Date.now(); - const res = await client.query(sql, values); - const elapsed = (Date.now() - t0) / 1000; - - return { - inserted: res.rowCount, - first_id: res.rows[0]?.id ?? null, - elapsed_sec: elapsed, - }; - } finally { - await client.end(); - } -} - -module.exports = { run }; diff --git a/examples/POSTGRES/code/js-pg-batch/package.json b/examples/POSTGRES/code/js-pg-batch/package.json deleted file mode 100644 index f484622..0000000 --- a/examples/POSTGRES/code/js-pg-batch/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "js-pg-batch", - "version": "1.0.0", - "dependencies": { - "pg": "^8.11.3" - } -} diff --git a/examples/POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py b/examples/POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py deleted file mode 100644 index 555ffbe..0000000 --- a/examples/POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-03-21 — pg-bulk-insert: bulk INSERT через execute_values. -# Тестирует: большие батчи (до 500 строк), производительность, память. -import os, time, psycopg2, psycopg2.extras - -def bulk_insert(event): - try: - n = max(0, min(int(event.get("n", 50)), 500)) # cap 500, min 0 - except (TypeError, ValueError): - n = 50 - prefix = str(event.get("prefix", "bulk"))[:50] - ts = int(time.time() * 1000) - - # n=0 — граничный случай: вернуть сразу без обращения к PG. - if n == 0: - return {"inserted": 0, "first_id": None, "elapsed_sec": 0.0} - - rows = [(f"{prefix}-{ts}-{i}",) for i in range(n)] - - 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: - t0 = time.time() - with conn.cursor() as cur: - psycopg2.extras.execute_values( - cur, - "INSERT INTO terraform_demo_table (title) VALUES %s RETURNING id", - rows, - page_size=100, - ) - ids = [r[0] for r in cur.fetchall()] - conn.commit() - elapsed = round(time.time() - t0, 3) - return {"inserted": len(ids), "first_id": ids[0] if ids else None, "elapsed_sec": elapsed} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-bulk-insert/requirements.txt b/examples/POSTGRES/code/pg-bulk-insert/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-bulk-insert/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-dedup/pg_dedup.py b/examples/POSTGRES/code/pg-dedup/pg_dedup.py deleted file mode 100644 index 18cba85..0000000 --- a/examples/POSTGRES/code/pg-dedup/pg_dedup.py +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-03-21 — pg-dedup: удаляет дубликаты по title, оставляет первый (min id). -# Тестирует: DELETE с subquery, CTE, idempotency (повторный вызов безопасен). -import os, psycopg2 - -def dedup(event): - dry_run = str(event.get("dry_run", "false")).lower() in ("true", "1", "yes") - 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(*) FROM terraform_demo_table t1 - WHERE EXISTS ( - SELECT 1 FROM terraform_demo_table t2 - WHERE t2.title = t1.title AND t2.id < t1.id - ) - """) - dupes_count = cur.fetchone()[0] - - if not dry_run and dupes_count > 0: - cur.execute(""" - DELETE FROM terraform_demo_table - WHERE id NOT IN ( - SELECT MIN(id) FROM terraform_demo_table GROUP BY title - ) - """) - deleted = cur.rowcount - conn.commit() - else: - deleted = 0 - - return {"duplicates_found": dupes_count, "deleted": deleted, "dry_run": dry_run} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-dedup/requirements.txt b/examples/POSTGRES/code/pg-dedup/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-dedup/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-delete-old/pg_delete_old.py b/examples/POSTGRES/code/pg-delete-old/pg_delete_old.py deleted file mode 100644 index d9655b3..0000000 --- a/examples/POSTGRES/code/pg-delete-old/pg_delete_old.py +++ /dev/null @@ -1,33 +0,0 @@ -# 2026-03-21 — pg-delete-old: удаляет строки старше N минут (default 60). -# Тестирует: DELETE с RETURNING, идемпотентность (повторный вызов = 0 удалений если нет старых). -import os, psycopg2, psycopg2.extras - -def delete_old(event): - older_than_min = max(int(event.get("older_than_min", 60)), 1) - prefix_filter = 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(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - if prefix_filter: - cur.execute( - "DELETE FROM terraform_demo_table " - "WHERE created_at < now() - interval '1 minute' * %s " - "AND title LIKE %s RETURNING id, title", - (older_than_min, f"{prefix_filter}%"), - ) - else: - cur.execute( - "DELETE FROM terraform_demo_table " - "WHERE created_at < now() - interval '1 minute' * %s RETURNING id, title", - (older_than_min,), - ) - deleted = [dict(r) for r in cur.fetchall()] - conn.commit() - return {"deleted": len(deleted), "older_than_min": older_than_min, "sample": deleted[:5]} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-delete-old/requirements.txt b/examples/POSTGRES/code/pg-delete-old/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-delete-old/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-search/pg_search.py b/examples/POSTGRES/code/pg-search/pg_search.py deleted file mode 100644 index fa62669..0000000 --- a/examples/POSTGRES/code/pg-search/pg_search.py +++ /dev/null @@ -1,36 +0,0 @@ -# 2026-03-21 — pg-search: полнотекстовый поиск по title через ILIKE + LIMIT/OFFSET. -# Тестирует: пагинацию, спецсимволы в input (XSS, SQL injection attempt → безопасно через параметры). -import os, psycopg2, psycopg2.extras - -def search(event): - # «query» — основной параметр (user-friendly), «q» — алиас для совместимости. - query = str(event.get("query") or event.get("q") or "")[:200] - # int() может упасть если юзер прислал строку — защищаем try/except. - try: - limit = max(1, min(int(event.get("limit", 20)), 100)) - except (TypeError, ValueError): - limit = 20 - try: - offset = max(0, int(event.get("offset", 0))) - except (TypeError, ValueError): - offset = 0 - - 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(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - pattern = f"%{query}%" if query else "%" - cur.execute( - "SELECT id, title, created_at::text FROM terraform_demo_table " - "WHERE title ILIKE %s ORDER BY id DESC LIMIT %s OFFSET %s", - (pattern, limit, offset), - ) - rows = [dict(r) for r in cur.fetchall()] - cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE title ILIKE %s", (pattern,)) - total = cur.fetchone()["count"] - return {"rows": rows, "count": len(rows), "total": total, "q": query, "limit": limit, "offset": offset} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-search/requirements.txt b/examples/POSTGRES/code/pg-search/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-search/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-upsert/pg_upsert.py b/examples/POSTGRES/code/pg-upsert/pg_upsert.py deleted file mode 100644 index 7159cfc..0000000 --- a/examples/POSTGRES/code/pg-upsert/pg_upsert.py +++ /dev/null @@ -1,36 +0,0 @@ -# 2026-03-21 — pg-upsert: INSERT ... ON CONFLICT (title) DO UPDATE. -# Тестирует: идемпотентность вставки — один и тот же title можно вызывать 100 раз подряд. -# Требует уникального индекса на title — создаётся при первом вызове (CREATE UNIQUE INDEX IF NOT EXISTS). -import os, psycopg2 - -def upsert(event): - title = str(event.get("title", "upsert-default"))[:255] - payload = str(event.get("payload", ""))[:500] - - 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: - # Создаём уникальный индекс если нет — для поддержки ON CONFLICT - cur.execute( - "CREATE UNIQUE INDEX IF NOT EXISTS terraform_demo_table_title_uniq " - "ON terraform_demo_table (title)" - ) - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) " - "ON CONFLICT (title) DO UPDATE SET created_at = now() " - "RETURNING id, title, created_at::text, xmax", - (title,), - ) - row = cur.fetchone() - was_insert = row[3] == 0 # xmax=0 означает INSERT, иначе UPDATE - conn.commit() - return { - "id": row[0], "title": row[1], "created_at": row[2], - "action": "inserted" if was_insert else "updated", - } - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-upsert/requirements.txt b/examples/POSTGRES/code/pg-upsert/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-upsert/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/py-retry-writer/py_retry_writer.py b/examples/POSTGRES/code/py-retry-writer/py_retry_writer.py deleted file mode 100644 index fbc5e56..0000000 --- a/examples/POSTGRES/code/py-retry-writer/py_retry_writer.py +++ /dev/null @@ -1,54 +0,0 @@ -# 2026-03-21 — py-retry-writer: пишет N строк с retry при PG ошибке. -# Тестирует: устойчивость к transient PG errors (simulate_error=true), retry logic, -# корректный rollback при частичном сбое. -import os, time, psycopg2, random - -_MAX_RETRIES = 3 - -def retry_write(event): - n = min(int(event.get("n", 5)), 100) - prefix = str(event.get("prefix", "retry"))[:40] - # simulate_error: с вероятностью 30% кидает OperationalError на 2-й попытке - simulate = str(event.get("simulate_error", "false")).lower() in ("true", "1") - - attempt = 0 - last_err = None - - while attempt < _MAX_RETRIES: - attempt += 1 - try: - 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"), - ) - inserted = [] - try: - with conn.cursor() as cur: - for i in range(n): - # Симуляция: на первой попытке падаем с вероятностью 50% - if simulate and attempt == 1 and i == n // 2: - raise psycopg2.OperationalError("simulated transient error") - title = f"{prefix}-{int(time.time()*1000)}-{i}-a{attempt}" - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id", - (title,), - ) - inserted.append(cur.fetchone()[0]) - conn.commit() - return { - "ok": True, "inserted": len(inserted), - "attempts": attempt, "first_id": inserted[0] if inserted else None, - } - except Exception as e: - conn.rollback() - raise - finally: - conn.close() - except psycopg2.OperationalError as e: - last_err = str(e) - if attempt < _MAX_RETRIES: - time.sleep(0.3 * attempt) # exponential backoff - continue - - return {"ok": False, "attempts": attempt, "last_error": last_err} diff --git a/examples/POSTGRES/code/py-retry-writer/requirements.txt b/examples/POSTGRES/code/py-retry-writer/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/py-retry-writer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/sql-runner/requirements.txt b/examples/POSTGRES/code/sql-runner/requirements.txt deleted file mode 100644 index 56ae88a..0000000 --- a/examples/POSTGRES/code/sql-runner/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# 2026-03-17 00:00 -# requirements.txt — зависимости для функции запуска SQL. -psycopg2-binary==2.9.9 diff --git a/examples/POSTGRES/code/sql-runner/sql_runner.py b/examples/POSTGRES/code/sql-runner/sql_runner.py deleted file mode 100644 index cca9e03..0000000 --- a/examples/POSTGRES/code/sql-runner/sql_runner.py +++ /dev/null @@ -1,39 +0,0 @@ -# 2026-03-17 00:00 -# sql_runner.py — функция для выполнения SQL-операторов из входного события. -import os -import psycopg2 - - -def run_sql(event): - # Выполняет список SQL-операторов в одной транзакции для атомарной инициализации схемы. - # Параметры подключения передаются раздельно, чтобы избежать ошибок парсинга DSN при спецсимволах. - pg_host = os.environ["PGHOST"] - pg_port = os.environ.get("PGPORT", "5432") - pg_database = os.environ["PGDATABASE"] - pg_user = os.environ["PGUSER"] - pg_password = os.environ["PGPASSWORD"] - pg_sslmode = os.environ.get("PGSSLMODE", "require") - statements = event.get("statements", []) - - if not statements: - return {"error": "no statements provided"} - - connection = psycopg2.connect( - host=pg_host, - port=pg_port, - dbname=pg_database, - user=pg_user, - password=pg_password, - sslmode=pg_sslmode, - ) - try: - cursor = connection.cursor() - for statement in statements: - cursor.execute(statement) - connection.commit() - return {"ok": True, "executed": len(statements)} - except Exception as error: - connection.rollback() - return {"error": str(error)} - finally: - connection.close() diff --git a/examples/POSTGRES/code/stress-bigloop/requirements.txt b/examples/POSTGRES/code/stress-bigloop/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/POSTGRES/code/stress-bigloop/stress_bigloop.py b/examples/POSTGRES/code/stress-bigloop/stress_bigloop.py deleted file mode 100644 index c9ce935..0000000 --- a/examples/POSTGRES/code/stress-bigloop/stress_bigloop.py +++ /dev/null @@ -1,20 +0,0 @@ -# 2026-03-19 -# stress_bigloop.py — CPU-интенсивная функция: считает сумму квадратов N чисел. -# Проверяет поведение под нагрузкой (большая и средняя итерация). - -import time - -_VERSION = "v1" - - -def run(event): - n = int(event.get("n", 500_000)) - start = time.monotonic() - total = sum(i * i for i in range(n)) - elapsed = round(time.monotonic() - start, 4) - return { - "version": _VERSION, - "n": n, - "sum_of_squares": total, - "elapsed_sec": elapsed, - } diff --git a/examples/POSTGRES/code/stress-divzero/requirements.txt b/examples/POSTGRES/code/stress-divzero/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/POSTGRES/code/stress-divzero/stress_divzero.py b/examples/POSTGRES/code/stress-divzero/stress_divzero.py deleted file mode 100644 index 86746e5..0000000 --- a/examples/POSTGRES/code/stress-divzero/stress_divzero.py +++ /dev/null @@ -1,13 +0,0 @@ -# 2026-03-19 -# stress_divzero.py — намеренно делит на ноль (ZeroDivisionError). -# Проверяет: платформа перехватывает панику, возвращает HTTP 500, не роняет под. - -_VERSION = "v1" - - -def run(event): - numerator = int(event.get("n", 42)) - denominator = int(event.get("d", 0)) # по умолчанию 0 — намеренный краш - # ZeroDivisionError: проверяем что платформа обрабатывает исключения - result = numerator / denominator - return {"version": _VERSION, "result": result} diff --git a/examples/POSTGRES/code/stress-js-async/package.json b/examples/POSTGRES/code/stress-js-async/package.json deleted file mode 100644 index 554d7bc..0000000 --- a/examples/POSTGRES/code/stress-js-async/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "stress-js-async", - "version": "1.0.0", - "dependencies": { - "pg": "^8.11.0" - } -} diff --git a/examples/POSTGRES/code/stress-js-async/stress_js_async.js b/examples/POSTGRES/code/stress-js-async/stress_js_async.js deleted file mode 100644 index 022c2b3..0000000 --- a/examples/POSTGRES/code/stress-js-async/stress_js_async.js +++ /dev/null @@ -1,37 +0,0 @@ -// 2026-03-19 -// stress_js_async.js — делает 3 параллельных запроса к PG через Promise.all. -// Проверяет nodejs20 runtime под умеренной нагрузкой и async/await. -// -// Entrypoint: stress_js_async.run - -'use strict'; - -const { Client } = require('pg'); - -exports.run = 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, - ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, - }); - await client.connect(); - try { - const [ver, cnt, max] = await Promise.all([ - client.query('SELECT version() AS v'), - client.query('SELECT COUNT(*) AS cnt FROM terraform_demo_table'), - client.query('SELECT MAX(id) AS max_id FROM terraform_demo_table'), - ]); - return { - runtime: 'nodejs20', - version: 'v1', - pg_version: ver.rows[0].v.split(' ').slice(0, 2).join(' '), - total_rows: parseInt(cnt.rows[0].cnt, 10), - max_id: max.rows[0].max_id, - }; - } finally { - await client.end(); - } -}; diff --git a/examples/POSTGRES/code/stress-js-badenv/package.json b/examples/POSTGRES/code/stress-js-badenv/package.json deleted file mode 100644 index 1b3892c..0000000 --- a/examples/POSTGRES/code/stress-js-badenv/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "stress-js-badenv", - "version": "1.0.0", - "dependencies": {} -} diff --git a/examples/POSTGRES/code/stress-js-badenv/stress_js_badenv.js b/examples/POSTGRES/code/stress-js-badenv/stress_js_badenv.js deleted file mode 100644 index 7169fd8..0000000 --- a/examples/POSTGRES/code/stress-js-badenv/stress_js_badenv.js +++ /dev/null @@ -1,17 +0,0 @@ -// 2026-03-19 -// stress_js_badenv.js — читает несуществующую переменную env и падает. -// Проверяет: платформа перехватывает TypeError/undefined, возвращает 500. -// -// Entrypoint: stress_js_badenv.run - -'use strict'; - -exports.run = async (event) => { - const crash = event.crash !== false; // по умолчанию crash=true - if (crash) { - // Читаем несуществующий env, пытаемся вызвать .toUpperCase() на undefined - const val = process.env.THIS_VAR_DOES_NOT_EXIST_AT_ALL; - return { shout: val.toUpperCase() }; // TypeError: Cannot read properties of undefined - } - return { runtime: 'nodejs20', version: 'v1', crashed: false }; -}; diff --git a/examples/POSTGRES/code/stress-slow/requirements.txt b/examples/POSTGRES/code/stress-slow/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/POSTGRES/code/stress-slow/stress_slow.py b/examples/POSTGRES/code/stress-slow/stress_slow.py deleted file mode 100644 index ba54397..0000000 --- a/examples/POSTGRES/code/stress-slow/stress_slow.py +++ /dev/null @@ -1,18 +0,0 @@ -# 2026-03-19 -# stress_slow.py — долгая функция: спит N секунд (по умолчанию 8). -# Проверяет что timeout-механизм и параллельные запросы не блокируют друг друга. - -import time -import os - -_VERSION = "v1" - - -def run(event): - secs = int(event.get("sleep", 8)) - time.sleep(secs) - return { - "version": _VERSION, - "slept_sec": secs, - "pid": os.getpid(), - } diff --git a/examples/POSTGRES/code/stress-writer/requirements.txt b/examples/POSTGRES/code/stress-writer/requirements.txt deleted file mode 100644 index 58ab769..0000000 --- a/examples/POSTGRES/code/stress-writer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary==2.9.9 diff --git a/examples/POSTGRES/code/stress-writer/stress_writer.py b/examples/POSTGRES/code/stress-writer/stress_writer.py deleted file mode 100644 index 7eda871..0000000 --- a/examples/POSTGRES/code/stress-writer/stress_writer.py +++ /dev/null @@ -1,39 +0,0 @@ -# 2026-03-19 -# stress_writer.py — пишет N строк в terraform_demo_table (по умолчанию 5). -# Проверяет параллельные INSERT'ы и устойчивость соединения с PG при нагрузке. - -import os -import psycopg2 -import time - -_VERSION = "v1" - - -def run(event): - n = int(event.get("rows", 5)) - prefix = event.get("prefix", "stress") - - 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"), - ) - inserted = [] - try: - with conn.cursor() as cur: - for i in range(n): - title = f"{prefix}-{int(time.time()*1000)}-{i}" - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id", - (title,), - ) - row = cur.fetchone() - inserted.append({"id": row[0], "title": title}) - conn.commit() - finally: - conn.close() - - return {"version": _VERSION, "inserted": inserted, "count": len(inserted)} diff --git a/examples/POSTGRES/code/table-rw/requirements.txt b/examples/POSTGRES/code/table-rw/requirements.txt deleted file mode 100644 index 58ab769..0000000 --- a/examples/POSTGRES/code/table-rw/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary==2.9.9 diff --git a/examples/POSTGRES/code/table-rw/table_rw.py b/examples/POSTGRES/code/table-rw/table_rw.py deleted file mode 100644 index d142e34..0000000 --- a/examples/POSTGRES/code/table-rw/table_rw.py +++ /dev/null @@ -1,133 +0,0 @@ -# 2026-03-19 — добавлен version и hostname в ответ list_rows для тестирования обновления кода -# table_rw.py — чтение и запись строк в terraform_demo_table. -# Два entrypoint в одном файле: list_rows (JSON API) и add_row (HTML-страница + POST-обработчик). -# ENV: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE - -import os -import json -import socket -import psycopg2 -import psycopg2.extras - -_CODE_VERSION = "v2-with-hostname" - - -def _connect(): - return psycopg2.connect( - host=os.environ["PGHOST"], - port=os.environ.get("PGPORT", "5432"), - dbname=os.environ["PGDATABASE"], - user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], - sslmode=os.environ.get("PGSSLMODE", "require"), - ) - - -def list_rows(event): - # Возвращает все строки terraform_demo_table, отсортированные по убыванию created_at. - conn = _connect() - try: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute( - "SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC" - ) - rows = [dict(r) for r in cur.fetchall()] - return {"rows": rows, "count": len(rows), "version": _CODE_VERSION, "host": socket.gethostname()} - finally: - conn.close() - - -def _render_page(rows, message=""): - # HTML-страница с формой ввода и таблицей строк. - # message — статус последней операции (успех / ошибка). - rows_html = "".join( - f"{r['id']}{r['title']}{r['created_at']}" - for r in rows - ) - msg_html = f'

{message}

' if message else "" - return f""" - - - - pg-table-writer - - - -

pg-table-writer

-
- - -
- {msg_html} - - - {rows_html} -
#titlecreated_at
- -""" - - -def add_row(event): - # GET → HTML-страница с формой и списком строк. - # POST → вставляет строку из form-поля title или JSON-поля title, - # затем возвращает обновлённую HTML-страницу. - # POST с Content-Type: application/json (curl/API) → возвращает JSON. - method = event.get("_method", "GET") - - if method == "GET": - conn = _connect() - try: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC") - rows = [dict(r) for r in cur.fetchall()] - finally: - conn.close() - return _render_page(rows) - - # POST — вставка строки - # Поле title приходит либо из JSON-тела, либо из application/x-www-form-urlencoded. - # Сервер уже распарсил JSON в event; form-данные приходят как event["body"] = "title=...". - title = event.get("title", "").strip() - if not title: - # Попытка распарсить form-encoded body (браузерная форма) - body = event.get("body", "") - if body.startswith("title="): - from urllib.parse import unquote_plus - title = unquote_plus(body[len("title="):].split("&")[0]).strip() - - if not title: - return {"ok": False, "error": "title is required"} - - conn = _connect() - try: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id, title, created_at::text", - (title,), - ) - row = dict(cur.fetchone()) - conn.commit() - - # Если запрос из браузера (form POST) — возвращаем обновлённую страницу. - # Если из curl/API — возвращаем JSON. - accept = event.get("_accept", "") - if "application/json" in accept: - return {"ok": True, "row": row} - - # Перечитываем все строки для обновлённой страницы - cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC") - rows = [dict(r) for r in cur.fetchall()] - return _render_page(rows, message=f"Добавлено: «{row['title']}»") - finally: - conn.close() diff --git a/examples/POSTGRES/functions.tf b/examples/POSTGRES/functions.tf deleted file mode 100644 index e2f553c..0000000 --- a/examples/POSTGRES/functions.tf +++ /dev/null @@ -1,105 +0,0 @@ -// 2026-03-20 (merge: sless_function + старый sless_job объединены в один self-contained sless_job) -// Теперь sless_job несёт в себе runtime/entrypoint/source_dir — не нужен отдельный sless_function. -// WaitJobDone таймаут 900s покрывает kaniko сборку (~5 мин) + выполнение SQL (~несколько сек). - -# Одноразовый запуск: собирает образ через kaniko, выполняет SQL, завершается. -# Заменяет sless_function.postgres_sql_runner_create_table + sless_job.postgres_table_init_job. -resource "sless_job" "postgres_table_init_job" { - name = "pg-create-table-job-main-v13" - runtime = "python3.11" - entrypoint = "sql_runner.run_sql" - memory_mb = 128 - timeout_sec = 30 - source_dir = "${path.module}/code/sql-runner" - wait_timeout_sec = 900 - run_id = 13 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - event_json = jsonencode({ - statements = [ - "CREATE TABLE IF NOT EXISTS terraform_demo_table (id serial PRIMARY KEY, title text NOT NULL, created_at timestamp DEFAULT now())" - ] - }) - - depends_on = [nubes_postgres_database.db] -} - -# Long-running сервис на NodeJS: возвращает версию PG-сервера и счётчик строк в таблице. -resource "sless_service" "pg_info" { - name = "pg-info" - runtime = "nodejs20" - entrypoint = "pg_info.info" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-info" - - depends_on = [sless_job.postgres_table_init_job] -} - -resource "sless_service" "postgres_table_reader" { - name = "pg-table-reader" - runtime = "python3.11" - entrypoint = "table_rw.list_rows" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/table-rw" - - depends_on = [sless_job.postgres_table_init_job] -} - -output "table_reader_url" { - value = sless_service.postgres_table_reader.url -} - -resource "sless_service" "postgres_table_writer" { - name = "pg-table-writer" - runtime = "python3.11" - entrypoint = "table_rw.add_row" - memory_mb = 256 - timeout_sec = 45 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/table-rw" - - depends_on = [sless_job.postgres_table_init_job] -} - -output "table_writer_url" { - value = sless_service.postgres_table_writer.url -} diff --git a/examples/POSTGRES/stress.tf b/examples/POSTGRES/stress.tf deleted file mode 100644 index fc3281e..0000000 --- a/examples/POSTGRES/stress.tf +++ /dev/null @@ -1,119 +0,0 @@ -// 2026-03-21 — stress.tf: все стресс-сервисы для комплексного тестирования. -// Два рантайма: nodejs20 (2), python3.11 (5). -// Все depends_on = [sless_job.postgres_table_init_job] — таблица должна существовать. - - -# ── Node.js 20 ──────────────────────────────────────────────────────────────── - -# 3 параллельных PG-запроса через Promise.all. Проверяет async/await + nodejs20. -resource "sless_service" "stress_js_async" { - name = "stress-js-async" - runtime = "nodejs20" - entrypoint = "stress_js_async.run" - memory_mb = 128 - timeout_sec = 20 - source_dir = "${path.module}/code/stress-js-async" - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - depends_on = [sless_job.postgres_table_init_job] -} - -# TypeError через undefined.toUpperCase(). Без PG. Проверяет перехват JS-ошибок. -resource "sless_service" "stress_js_badenv" { - name = "stress-js-badenv" - runtime = "nodejs20" - entrypoint = "stress_js_badenv.run" - memory_mb = 128 - timeout_sec = 10 - source_dir = "${path.module}/code/stress-js-badenv" - - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Python 3.11 ─────────────────────────────────────────────────────────────── - -# Спит N секунд. Без PG. Проверяет timeout и сосуществование долгих запросов. -resource "sless_service" "stress_slow" { - name = "stress-slow" - runtime = "python3.11" - entrypoint = "stress_slow.run" - memory_mb = 128 - timeout_sec = 35 - source_dir = "${path.module}/code/stress-slow" - - depends_on = [sless_job.postgres_table_init_job] -} - -# CPU-нагрузка: сумма квадратов N чисел. Без PG. Проверяет compute-bound задачи. -resource "sless_service" "stress_bigloop" { - name = "stress-bigloop" - runtime = "python3.11" - entrypoint = "stress_bigloop.run" - memory_mb = 256 - timeout_sec = 60 - source_dir = "${path.module}/code/stress-bigloop" - - depends_on = [sless_job.postgres_table_init_job] -} - -# ZeroDivisionError. Без PG. Проверяет перехват Python-исключений → HTTP 500. -resource "sless_service" "stress_divzero" { - name = "stress-divzero" - runtime = "python3.11" - entrypoint = "stress_divzero.run" - memory_mb = 128 - timeout_sec = 10 - source_dir = "${path.module}/code/stress-divzero" - - depends_on = [sless_job.postgres_table_init_job] -} - -# Параллельный INSERT в terraform_demo_table через psycopg2. Проверяет PG-write. -resource "sless_service" "stress_writer" { - name = "stress-writer" - runtime = "python3.11" - entrypoint = "stress_writer.run" - memory_mb = 128 - timeout_sec = 60 - source_dir = "${path.module}/code/stress-writer" - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - depends_on = [sless_job.postgres_table_init_job] -} - -# Агрегированная статистика terraform_demo_table (COUNT/MIN/MAX). Для мониторинга. -resource "sless_service" "pg_stats" { - name = "pg-stats" - runtime = "python3.11" - entrypoint = "pg_stats.get_stats" - memory_mb = 128 - timeout_sec = 15 - source_dir = "${path.module}/code/pg-stats" - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - depends_on = [sless_job.postgres_table_init_job] -} diff --git a/services/funcs/funcs-service.yaml b/services/funcs/funcs-service.yaml index 47a5d84..ed406cc 100644 --- a/services/funcs/funcs-service.yaml +++ b/services/funcs/funcs-service.yaml @@ -27,7 +27,7 @@ spec: spec: containers: - name: funcs - image: naeel/sless-funcs-service:v0.1.3 + image: naeel/sless-funcs-service:v0.1.4 ports: - containerPort: 8090 env: diff --git a/services/funcs/index.html b/services/funcs/index.html index 229289e..26586a2 100644 --- a/services/funcs/index.html +++ b/services/funcs/index.html @@ -312,6 +312,181 @@ color: #484f58; font-size: 0.9rem; } + + /* Кнопки действий на карточке функции */ + .action-btn { + flex-shrink: 0; + padding: 3px 10px; + border-radius: 5px; + border: 1px solid #30363d; + cursor: pointer; + font-size: 0.73rem; + font-weight: 500; + background: #21262d; + color: #8b949e; + transition: background 0.1s, color 0.1s; + } + .action-btn:hover { background: #30363d; color: #c9d1d9; } + .action-btn.del:hover { background: #3d1a1a; border-color: #6b1a1a; color: #f85149; } + + /* Кодовый редактор во встроенном режиме */ + .edit-area { + width: 100%; + min-height: 220px; + background: #0d1117; + color: #e6edf3; + border: none; + border-top: 1px solid #30363d; + padding: 14px 16px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.78rem; + line-height: 1.55; + resize: vertical; + outline: none; + display: block; + box-sizing: border-box; + } + .edit-actions { + display: flex; + gap: 8px; + padding: 8px 14px; + background: #161b22; + border-top: 1px solid #30363d; + } + .edit-save-btn { + padding: 5px 16px; + background: #238636; + border: 1px solid #2ea043; + color: #fff; + border-radius: 6px; + cursor: pointer; + font-size: 0.8rem; + } + .edit-save-btn:hover { background: #2ea043; } + .edit-save-btn:disabled { opacity: 0.5; cursor: not-allowed; } + .edit-cancel-btn { + padding: 5px 16px; + background: #21262d; + border: 1px solid #30363d; + color: #c9d1d9; + border-radius: 6px; + cursor: pointer; + font-size: 0.8rem; + } + .edit-status { + font-size: 0.77rem; + padding: 5px 8px; + align-self: center; + color: #8b949e; + } + .edit-status.ok { color: #3fb950; } + .edit-status.err { color: #f85149; } + + /* ---------- Модалка создания функции ---------- */ + .modal-backdrop { + display: none; + position: fixed; inset: 0; + background: rgba(0,0,0,0.65); + z-index: 100; + align-items: center; + justify-content: center; + } + .modal-backdrop.open { display: flex; } + .modal { + background: #161b22; + border: 1px solid #30363d; + border-radius: 10px; + width: min(660px, 95vw); + max-height: 90vh; + overflow-y: auto; + padding: 24px; + display: flex; + flex-direction: column; + gap: 14px; + } + .modal h2 { + font-size: 1rem; + font-weight: 600; + color: #e6edf3; + } + .modal label { + font-size: 0.82rem; + color: #8b949e; + display: block; + margin-bottom: 4px; + } + .modal input, .modal select { + width: 100%; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #e6edf3; + padding: 7px 10px; + font-size: 0.85rem; + box-sizing: border-box; + outline: none; + } + .modal input:focus, .modal select:focus { border-color: #58a6ff; } + .modal-row { display: flex; gap: 12px; } + .modal-row > div { flex: 1; } + .modal-code { + width: 100%; + min-height: 200px; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #e6edf3; + padding: 10px 12px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.78rem; + line-height: 1.55; + resize: vertical; + outline: none; + box-sizing: border-box; + } + .modal-code:focus { border-color: #58a6ff; } + .modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + } + .modal-create-btn { + padding: 7px 20px; + background: #238636; + border: 1px solid #2ea043; + color: #fff; + border-radius: 6px; + cursor: pointer; + font-size: 0.85rem; + } + .modal-create-btn:hover { background: #2ea043; } + .modal-create-btn:disabled { opacity: 0.5; cursor: not-allowed; } + .modal-cancel-btn { + padding: 7px 20px; + background: #21262d; + border: 1px solid #30363d; + color: #c9d1d9; + border-radius: 6px; + cursor: pointer; + font-size: 0.85rem; + } + .modal-err { + color: #f85149; + font-size: 0.8rem; + min-height: 18px; + } + .create-fn-btn { + margin-left: auto; + background: #238636; + border: 1px solid #2ea043; + color: #fff; + padding: 6px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 0.82rem; + transition: background 0.1s; + } + .create-fn-btn:hover { background: #2ea043; } @@ -321,12 +496,62 @@ / +

+ + +