diff --git a/POSTGRES/README.md b/POSTGRES/README.md new file mode 100644 index 0000000..ea5d451 --- /dev/null +++ b/POSTGRES/README.md @@ -0,0 +1,100 @@ +# 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 в определённых сценариях. diff --git a/POSTGRES/code/pg-info/pg_info.js b/POSTGRES/code/pg-info/pg_info.js index e08df17..9468076 100644 --- a/POSTGRES/code/pg-info/pg_info.js +++ b/POSTGRES/code/pg-info/pg_info.js @@ -36,6 +36,7 @@ exports.info = async (event) => { 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(); diff --git a/POSTGRES/code/pg-stats/pg_stats.py b/POSTGRES/code/pg-stats/pg_stats.py new file mode 100644 index 0000000..225f41b --- /dev/null +++ b/POSTGRES/code/pg-stats/pg_stats.py @@ -0,0 +1,38 @@ +# 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() diff --git a/TNAR/code/table-rw/requirements.txt b/POSTGRES/code/pg-stats/requirements.txt similarity index 100% rename from TNAR/code/table-rw/requirements.txt rename to POSTGRES/code/pg-stats/requirements.txt diff --git a/POSTGRES/code/stress-bigloop/requirements.txt b/POSTGRES/code/stress-bigloop/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/POSTGRES/code/stress-bigloop/stress_bigloop.py b/POSTGRES/code/stress-bigloop/stress_bigloop.py new file mode 100644 index 0000000..c9ce935 --- /dev/null +++ b/POSTGRES/code/stress-bigloop/stress_bigloop.py @@ -0,0 +1,20 @@ +# 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/POSTGRES/code/stress-divzero/requirements.txt b/POSTGRES/code/stress-divzero/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/POSTGRES/code/stress-divzero/stress_divzero.py b/POSTGRES/code/stress-divzero/stress_divzero.py new file mode 100644 index 0000000..86746e5 --- /dev/null +++ b/POSTGRES/code/stress-divzero/stress_divzero.py @@ -0,0 +1,13 @@ +# 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/POSTGRES/code/stress-go-fast/handler.go b/POSTGRES/code/stress-go-fast/handler.go new file mode 100644 index 0000000..cd5ac46 --- /dev/null +++ b/POSTGRES/code/stress-go-fast/handler.go @@ -0,0 +1,43 @@ +package handler +// 2026-03-19 +// handler.go — быстрая Go функция: факториал + числа Фибоначчи. +// Проверяет Go runtime под лёгкой нагрузкой и корректность JSON-ответа. +// Entrypoint: handler.Handle +package handler + +import "fmt" + +func factorial(n int) uint64 { + if n <= 1 { + return 1 + } + return uint64(n) * factorial(n-1) +} + +func fib(n int) int { + if n <= 1 { + return n + } + a, b := 0, 1 + for i := 2; i <= n; i++ { + a, b = b, a+b + } + return b +} + +func Handle(event map[string]interface{}) interface{} { + n := 10 + if v, ok := event["n"].(float64); ok { + n = int(v) + if n > 20 { + n = 20 + } + } + return map[string]interface{}{ + "runtime": "go1.23", + "version": "v1", + "n": n, + "factorial": fmt.Sprintf("%d", factorial(n)), + "fib": fib(n), + } +} diff --git a/POSTGRES/code/stress-go-nil/handler.go b/POSTGRES/code/stress-go-nil/handler.go new file mode 100644 index 0000000..0ee25f6 --- /dev/null +++ b/POSTGRES/code/stress-go-nil/handler.go @@ -0,0 +1,21 @@ +// 2026-03-19 +// handler.go — намеренный nil pointer dereference в Go. +// Проверяет что Go runtime recover() перехватывает панику и платформа возвращает 500. +// Entrypoint: handler.Handle +package handler + +func Handle(event map[string]interface{}) interface{} { + crash := true + if v, ok := event["crash"].(bool); ok { + crash = v + } + if crash { + var p *string + _ = *p // panic: намеренный nil pointer для stress-теста + } + return map[string]interface{}{ + "runtime": "go1.23", + "version": "v1", + "crashed": false, + } +} diff --git a/POSTGRES/code/stress-go-pgstorm/handler.go b/POSTGRES/code/stress-go-pgstorm/handler.go new file mode 100644 index 0000000..edf4668 --- /dev/null +++ b/POSTGRES/code/stress-go-pgstorm/handler.go @@ -0,0 +1,148 @@ +// 2026-03-19 +// handler.go — Go стресс-тест PostgreSQL через pgxpool. +// Запускает N горутин (default 100), каждая в цикле duration_sec (default 600) +// долбит PG попеременно: INSERT / SELECT COUNT / SELECT MAX с случайными задержками. +// Цель: проверить Go runtime под конкурентной нагрузкой и устойчивость PG connection pool. +// Entrypoint: handler.Handle +package handler + +import ( + "context" + "fmt" + "math/rand" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// pgDSN собирает DSN из env vars (PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE). +func pgDSN() string { + host := os.Getenv("PGHOST") + port := os.Getenv("PGPORT") + if port == "" { + port = "5432" + } + db := os.Getenv("PGDATABASE") + user := os.Getenv("PGUSER") + pass := os.Getenv("PGPASSWORD") + sslmode := os.Getenv("PGSSLMODE") + if sslmode == "" { + sslmode = "require" + } + return fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s", + host, port, db, user, pass, sslmode) +} + +// worker — одна горутина: чередует INSERT/COUNT/MAX с случайной задержкой до maxDelayMs. +// При ошибке инкрементирует errOps и продолжает (не паникует). +func worker(ctx context.Context, pool *pgxpool.Pool, workerID int, maxDelayMs int, okOps, errOps *int64) { + rng := rand.New(rand.NewSource(time.Now().UnixNano() + int64(workerID))) + op := 0 + for { + select { + case <-ctx.Done(): + return + default: + } + + // Случайная задержка перед следующей операцией: 0..maxDelayMs мс + delay := rng.Intn(maxDelayMs + 1) + time.Sleep(time.Duration(delay) * time.Millisecond) + + var err error + switch op % 3 { + case 0: // INSERT + title := fmt.Sprintf("pgstorm-w%d-%d", workerID, time.Now().UnixNano()) + _, err = pool.Exec(ctx, + "INSERT INTO terraform_demo_table (title) VALUES ($1)", title) + case 1: // SELECT COUNT + var count int64 + err = pool.QueryRow(ctx, + "SELECT COUNT(*) FROM terraform_demo_table").Scan(&count) + case 2: // SELECT MAX id + var maxID *int64 + err = pool.QueryRow(ctx, + "SELECT MAX(id) FROM terraform_demo_table").Scan(&maxID) + } + + if err != nil && ctx.Err() == nil { + atomic.AddInt64(errOps, 1) + } else if err == nil { + atomic.AddInt64(okOps, 1) + } + op++ + } +} + +func Handle(event map[string]interface{}) interface{} { + // Параметры из event (все опциональны — разумные defaults) + workers := 100 + if v, ok := event["workers"].(float64); ok && v > 0 && v <= 500 { + workers = int(v) + } + durationSec := 600 + if v, ok := event["duration_sec"].(float64); ok && v > 0 && v <= 3600 { + durationSec = int(v) + } + maxDelayMs := 300 + if v, ok := event["max_delay_ms"].(float64); ok && v >= 0 && v <= 5000 { + maxDelayMs = int(v) + } + + // Инициализация pgxpool — единый pool на всю функцию, MaxConns ограничен + // чтобы не перегрузить managed PG при большом числе горутин. + poolCfg, err := pgxpool.ParseConfig(pgDSN()) + if err != nil { + return map[string]interface{}{"error": fmt.Sprintf("parse dsn: %v", err)} + } + maxConns := 20 + if workers < 20 { + maxConns = workers + } + poolCfg.MaxConns = int32(maxConns) + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(durationSec)*time.Second) + defer cancel() + + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) + if err != nil { + return map[string]interface{}{"error": fmt.Sprintf("connect pool: %v", err)} + } + defer pool.Close() + + var okOps, errOps int64 + startTime := time.Now() + + var wg sync.WaitGroup + for i := 0; i < workers; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + worker(ctx, pool, id, maxDelayMs, &okOps, &errOps) + }(i) + } + wg.Wait() + + elapsed := time.Since(startTime).Seconds() + total := okOps + errOps + opsPerSec := 0.0 + if elapsed > 0 { + opsPerSec = float64(total) / elapsed + } + + return map[string]interface{}{ + "runtime": "go1.23", + "version": "v1", + "workers": workers, + "duration_sec": durationSec, + "max_delay_ms": maxDelayMs, + "elapsed_sec": fmt.Sprintf("%.1f", elapsed), + "total_ops": total, + "ok_ops": okOps, + "err_ops": errOps, + "ops_per_sec": fmt.Sprintf("%.1f", opsPerSec), + } +} diff --git a/POSTGRES/code/stress-js-async/package.json b/POSTGRES/code/stress-js-async/package.json new file mode 100644 index 0000000..554d7bc --- /dev/null +++ b/POSTGRES/code/stress-js-async/package.json @@ -0,0 +1,7 @@ +{ + "name": "stress-js-async", + "version": "1.0.0", + "dependencies": { + "pg": "^8.11.0" + } +} diff --git a/POSTGRES/code/stress-js-async/stress_js_async.js b/POSTGRES/code/stress-js-async/stress_js_async.js new file mode 100644 index 0000000..022c2b3 --- /dev/null +++ b/POSTGRES/code/stress-js-async/stress_js_async.js @@ -0,0 +1,37 @@ +// 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/POSTGRES/code/stress-js-badenv/package.json b/POSTGRES/code/stress-js-badenv/package.json new file mode 100644 index 0000000..1b3892c --- /dev/null +++ b/POSTGRES/code/stress-js-badenv/package.json @@ -0,0 +1,5 @@ +{ + "name": "stress-js-badenv", + "version": "1.0.0", + "dependencies": {} +} diff --git a/POSTGRES/code/stress-js-badenv/stress_js_badenv.js b/POSTGRES/code/stress-js-badenv/stress_js_badenv.js new file mode 100644 index 0000000..7169fd8 --- /dev/null +++ b/POSTGRES/code/stress-js-badenv/stress_js_badenv.js @@ -0,0 +1,17 @@ +// 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/POSTGRES/code/stress-slow/requirements.txt b/POSTGRES/code/stress-slow/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/POSTGRES/code/stress-slow/stress_slow.py b/POSTGRES/code/stress-slow/stress_slow.py new file mode 100644 index 0000000..ba54397 --- /dev/null +++ b/POSTGRES/code/stress-slow/stress_slow.py @@ -0,0 +1,18 @@ +# 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/POSTGRES/code/stress-writer/requirements.txt b/POSTGRES/code/stress-writer/requirements.txt new file mode 100644 index 0000000..58ab769 --- /dev/null +++ b/POSTGRES/code/stress-writer/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary==2.9.9 diff --git a/POSTGRES/code/stress-writer/stress_writer.py b/POSTGRES/code/stress-writer/stress_writer.py new file mode 100644 index 0000000..7eda871 --- /dev/null +++ b/POSTGRES/code/stress-writer/stress_writer.py @@ -0,0 +1,39 @@ +# 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/POSTGRES/code/table-rw/table_rw.py b/POSTGRES/code/table-rw/table_rw.py index 9558739..d142e34 100644 --- a/POSTGRES/code/table-rw/table_rw.py +++ b/POSTGRES/code/table-rw/table_rw.py @@ -1,13 +1,16 @@ -# 2026-03-19 +# 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( @@ -29,7 +32,7 @@ def list_rows(event): "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)} + return {"rows": rows, "count": len(rows), "version": _CODE_VERSION, "host": socket.gethostname()} finally: conn.close() diff --git a/POSTGRES/funcs_list.py b/POSTGRES/funcs_list.py deleted file mode 100644 index bc17fdc..0000000 --- a/POSTGRES/funcs_list.py +++ /dev/null @@ -1,129 +0,0 @@ -# 2026-03-18 (обновлено: фильтрация SLESS_EXCLUDE, читаемый вывод через "#"-ключ) -# funcs_list.py — HTTP-функция: список всех пользовательских функций с их статусами. -# Вызывает внутренний REST API оператора (ClusterIP, без TLS). -# Объединяет данные функций и триггеров в один ответ; скрывает служебные функции. -# -# 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 имена функций, которые не надо показывать -# Пример: "funcs,event-writer,event-monitor,event-cleaner" -# -# Формат вывода: JSON-объект, где каждая функция содержит поле "#" — краткий комментарий. -# При pretty-print (python3 -m json.tool) выглядит как читаемый список с аннотациями. - -import os -import requests - - -def _short_comment(fn, http_triggers, cron_triggers): - """Генерирует однострочный комментарий-описание функции по её метаданным.""" - phase = fn.get("phase", "") - runtime = fn.get("runtime", "") - - if http_triggers: - active_str = "активна" if http_triggers[0].get("active") else "неактивна" - return f"HTTP endpoint ({runtime}) — {phase}, {active_str}" - elif cron_triggers: - schedule = cron_triggers[0].get("schedule", "?") - active_str = "активна" if cron_triggers[0].get("active") else "неактивна" - return f"Cron '{schedule}' ({runtime}) — {phase}, {active_str}" - 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("/") - - # Имена функций, которые не должны присутствовать в выводе. - # Включает саму себя ("funcs") и служебные функции других примеров. - exclude = { - n.strip() - for n in os.environ.get("SLESS_EXCLUDE", "").split(",") - if n.strip() - } - - headers = {"Authorization": f"Bearer {token}"} - - fns_resp = requests.get( - f"{api_url}/v1/namespaces/{namespace}/functions", - headers=headers, - timeout=10, - ) - fns_resp.raise_for_status() - - trs_resp = requests.get( - f"{api_url}/v1/namespaces/{namespace}/triggers", - headers=headers, - timeout=10, - ) - trs_resp.raise_for_status() - - # Индекс триггеров по имени функции - triggers_by_fn = {} - for tr in trs_resp.json(): - fn_name = tr.get("function") or tr.get("functionRef") - if fn_name: - triggers_by_fn.setdefault(fn_name, []).append(tr) - - result = [] - for fn in fns_resp.json(): - name = fn["name"] - if name in exclude: - continue - - http_triggers = [ - t for t in triggers_by_fn.get(name, []) if t.get("type") == "http" - ] - cron_triggers = [ - t for t in triggers_by_fn.get(name, []) if t.get("type") == "cron" - ] - is_active = any( - t.get("enabled", True) and t.get("active", False) - for t in triggers_by_fn.get(name, []) - ) - - entry = { - # "#" — первый ключ: служит визуальным комментарием при pretty-print - "#": _short_comment(fn, http_triggers, cron_triggers), - "name": name, - "runtime": fn.get("runtime"), - "phase": fn.get("phase"), - "active": is_active, - } - - # URL вычисляем из SLESS_EXTERNAL_URL если задан — state может хранить старый домен - if http_triggers: - if ext_url: - entry["url"] = f"{ext_url}/fn/{namespace}/{name}" - else: - entry["url"] = http_triggers[0].get("url", "") - - if cron_triggers: - entry["cron"] = cron_triggers[0].get("schedule", "") - - if fn.get("message"): - entry["message"] = fn["message"] - - # created_at и last_built_at — доступны после обновления оператора до v0.1.32+ - if fn.get("created_at"): - entry["created_at"] = fn["created_at"] - if fn.get("last_built_at"): - entry["last_built_at"] = fn["last_built_at"] - - result.append(entry) - - # Сортировка: активные вверх, затем по имени - result.sort(key=lambda f: (not f["active"], f["name"])) - - return { - "namespace": namespace, - "count": len(result), - "functions": result, - } - diff --git a/POSTGRES/functions.tf b/POSTGRES/functions.tf new file mode 100644 index 0000000..e2f553c --- /dev/null +++ b/POSTGRES/functions.tf @@ -0,0 +1,105 @@ +// 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/POSTGRES/luceUNDnode.tf b/POSTGRES/luceUNDnode.tf deleted file mode 100644 index 75c0658..0000000 --- a/POSTGRES/luceUNDnode.tf +++ /dev/null @@ -1,73 +0,0 @@ - -# resource "nubes_lucee" "app1" { -# # Lucee-приложение, зависит от Postgres -# resource_name = "lucy_teststand_0" -# # resource_realm = "k8s-3.ext.nubes.ru" -# resource_realm = nubes_postgres.db2.resource_realm -# # resource_realm = "k8s-4-sandbox-nubes-ru" -# domain = "web-test-stand" - -# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testlucee" - -# json_env = jsonencode({ -# # 🔗 Настройки Data Source 'testds' для Lucee (Application.cfc) -# testds_class = "org.postgresql.Driver" # 📂 Драйвер БД -# testds_bundleName = "org.postgresql.jdbc" # 📦 Имя бандла JDBC -# testds_bundleVersion = "42.6.0" # 🔢 Версия драйвера -# testds_connectionString = "jdbc:postgresql://${nubes_postgres.db2.state_out_flat["internalConnect.master"]}:5432/postgres?sslmode=require" # 🚀 Строка подключения -# testds_username = nubes_postgres_user.db2_user.username # 👤 Логин -# testds_password = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"] # 🔑 Пароль -# testds_connectionLimit = "5" # 🚦 Лимит соединений -# testds_liveTimeout = "15" # ⏳ Таймаут жизни -# testds_validate = "false" # ✅ Валидация при запросе -# }) - -# resource_c_p_u = 300 -# resource_memory = 512 -# resource_instances = 1 -# app_version = "5.4" - -# depends_on = [nubes_postgres.db2] -# } - -# resource "nubes_nodejs" "app3" { -# # NodeJS демо, работающий с тем же Postgres. -# resource_name = "node_01" -# resource_realm = nubes_postgres.db2.resource_realm -# domain = "node07" -# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testnode.git" -# health_path = "/healthz" -# app_version = "23" - -# json_env = jsonencode({ -# # Переменные подключения к Postgres. -# PGHOST = nubes_postgres.db2.state_out_flat["internalConnect.master"] -# PGPORT = "5432" -# PGUSER = nubes_postgres_user.db2_user.username -# PGPASSWORD = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"] -# PGDATABASE = nubes_postgres_database.db2_app.db_name -# PGSSLMODE = "require" -# DATABASE_URL = format( -# "postgresql://%s:%s@%s:5432/%s?sslmode=require", -# nubes_postgres_user.db2_user.username, -# jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"], -# nubes_postgres.db2.state_out_flat["internalConnect.master"], -# nubes_postgres_database.db2_app.db_name -# ) -# }) - -# resource_c_p_u = 300 -# resource_memory = 256 -# resource_instances = 1 - -# depends_on = [nubes_postgres.db2] -# } - -# output "pg_vault_secrets" { -# value = nubes_postgres.db2.vault_secrets -# sensitive = true -# } - -# terraform output -json pg_vault_secrets - - diff --git a/POSTGRES/main.tf b/POSTGRES/main.tf index ae97e59..d47cd26 100644 --- a/POSTGRES/main.tf +++ b/POSTGRES/main.tf @@ -8,7 +8,7 @@ terraform { } sless = { source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" + version = "~> 0.1.19" } } } @@ -45,6 +45,10 @@ variable "pg_password" { 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" diff --git a/POSTGRES/postgres.tf b/POSTGRES/postgres.tf new file mode 100644 index 0000000..0315443 --- /dev/null +++ b/POSTGRES/postgres.tf @@ -0,0 +1,58 @@ +// 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 +} diff --git a/POSTGRES/resources.tf b/POSTGRES/resources.tf index 4041415..cd662b8 100644 --- a/POSTGRES/resources.tf +++ b/POSTGRES/resources.tf @@ -1,196 +1,3 @@ -// 2026-03-18 — добавлены locals для извлечения credentials из vault_secrets (без хардкода). -// Для сверки хардкод остаётся в terraform.tfvars на этапе разработки. -// sless_function и sless_job закомментированы — сначала проверяется сетевое соединение. - -# Актуальные credentials из vault_secrets (authoritatively) — vault синхронизирован с кластером. -# Структура vault_secrets["users"]: JSON-строка {"username": {"password": "...", "username": "..."}} -locals { - pg_creds_map = jsondecode(nubes_postgres.npg.vault_secrets["users"]) - pg_username = nubes_postgres_user.pg_user.username - pg_password = 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 = "teststand-pg-2" - # 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 = "u-user0" - role = "ddl_user" - adopt_existing_on_create = true -} - -resource "nubes_postgres_database" "db" { - postgres_id = nubes_postgres.npg.id - db_name = "db_terra" - db_owner = nubes_postgres_user.pg_user.username - adopt_existing_on_create = true - # suspend_on_destroy = false -} - -# Служебная функция выполняет SQL-операторы из event_json. -# Credentials берутся из locals (vault_secrets) — без хардкода. -# Для сверки хардкод остаётся в terraform.tfvars. -resource "sless_function" "postgres_sql_runner_create_table" { - name = "pg-create-table-runner" - runtime = "python3.11" - entrypoint = "sql_runner.run_sql" - 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" - # Для сверки (должно совпадать с vault): - # PGUSER = var.pg_user - # PGPASSWORD = var.pg_password - } - - source_dir = "${path.module}/code/sql-runner" -} - -resource "sless_job" "postgres_table_init_job" { - name = "pg-create-table-job-main-v13" - function = sless_function.postgres_sql_runner_create_table.name - wait_timeout_sec = 180 - run_id = 13 - - 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] -} - -# HTTP-функция на NodeJS: возвращает версию PG-сервера и счётчик строк в таблице. -# Единственная функция примера на nodejs20 — проверка что JS runtime работает. -# Доступна по URL: https://sless.kube5s.ru/fn//pg-info -resource "sless_function" "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_trigger" "pg_info_http" { - name = "pg-info-http" - type = "http" - function = sless_function.pg_info.name - enabled = true -} - -# HTTP-функции чтения и записи строк terraform_demo_table — в одном файле table_rw.py. -# list_rows (GET) — читает все строки; add_row (POST {title}) — вставляет строку. -# Доступны по URL: https://sless.kube5s.ru/fn//pg-table-reader -# https://sless.kube5s.ru/fn//pg-table-writer -resource "sless_function" "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] -} - -resource "sless_trigger" "postgres_table_reader_http" { - name = "pg-table-reader-http" - type = "http" - function = sless_function.postgres_table_reader.name - enabled = true -} - -output "table_reader_url" { - value = sless_trigger.postgres_table_reader_http.url -} - -resource "sless_function" "postgres_table_writer" { - name = "pg-table-writer" - runtime = "python3.11" - entrypoint = "table_rw.add_row" - 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] -} - -resource "sless_trigger" "postgres_table_writer_http" { - name = "pg-table-writer-http" - type = "http" - function = sless_function.postgres_table_writer.name - enabled = true -} - -output "table_writer_url" { - value = sless_trigger.postgres_table_writer_http.url -} - +// 2026-03-20 — содержимое перенесено в два файла: +// postgres.tf — managed PostgreSQL ресурсы (nubes_postgres, user, database, locals) +// functions.tf — sless функции, сервисы, джобы, outputs diff --git a/POSTGRES/stress_destroy_apply.sh.disabled b/POSTGRES/stress_destroy_apply.sh.disabled new file mode 100755 index 0000000..76bb6ca --- /dev/null +++ b/POSTGRES/stress_destroy_apply.sh.disabled @@ -0,0 +1,46 @@ +#!/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 итераций прошли успешно ===" diff --git a/POSTGRES/stress_test.sh b/POSTGRES/stress_test.sh new file mode 100644 index 0000000..c695a37 --- /dev/null +++ b/POSTGRES/stress_test.sh @@ -0,0 +1,57 @@ +#!/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 ===" diff --git a/README.md b/README.md index 1fd1bf3..96dfaae 100644 --- a/README.md +++ b/README.md @@ -8,187 +8,68 @@ | Ресурс | Назначение | |---|---| -| `sless_function` | Описывает функцию: язык, точку входа, лимиты, переменные окружения. При создании загружает код и запускает его сборку в образ. Сама по себе недоступна снаружи — нужен триггер или задание. | -| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. | -| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. | +| `sless_service` | Long-running HTTP-сервис: всегда активен, отвечает на запросы. Имеет свой URL после деплоя. | +| `sless_job` | Одноразовый запуск функции: собирает образ, выполняет код, завершается. Используется для миграций БД, batch-обработки и т.д. | -Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless.kube5s.ru/fn//<имя-функции>`. +Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`. --- ## Требования -- Terraform >= 1.0 +- Terraform >= 1.3 - JWT-токен для аутентификации в sless API +- JWT-токен для Nubes Cloud API (если используются managed-ресурсы: PostgreSQL и т.д.) - Доступ к `https://sless.kube5s.ru` ## Конфигурация провайдера -Во всех примерах файл `main.tf` содержит блок провайдера. Токен передаётся через переменную, значение которой задаётся в `terraform.tfvars`: - ```hcl provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" + endpoint = "https://sless.kube5s.ru" + token = var.sless_token +} + +provider "nubes_cloud" { + base_url = "https://deck-api-test.ngcloud.ru/api/v1" + token = var.nubes_token } ``` -Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`. - -> **Перед запуском любого примера** откройте файл `terraform.tfvars` в директории примера и впишите свой токен Nubes API: -> ```hcl -> token = "ваш токен Nubes API" -> ``` -> Токен выдаётся в личном кабинете Nubes. Файл `terraform.tfvars` добавлен в `.gitignore` — он не попадёт в репозиторий. +> Токены задаются в `terraform.tfvars` — этот файл добавлен в `.gitignore`. --- ## Примеры -### `hello-node` — минимальный пример на Node.js +### `POSTGRES` — Serverless-функции с Managed PostgreSQL -Две независимые функции: HTTP-функция, возвращающая приветствие, и одноразовое задание, суммирующее набор чисел. Хорошая отправная точка для знакомства с платформой. +Полный пример: managed PostgreSQL + одноразовый init-job + 3 HTTP-сервиса (чтение/запись данных и информация о PG). + +Языки: Python 3.11, Node.js 20. ```bash -cd hello-node +cd POSTGRES terraform init -terraform apply -auto-approve - -# Вызов HTTP-функции с передачей имени: -curl -s -X POST https://sless.kube5s.ru/fn//hello-http \ - -H 'Content-Type: application/json' -d '{"name":"World"}' - -# Результат задания: -terraform output job_message +terraform apply ``` ---- - -### `hello-go` — минимальный пример на Go 1.23 - -Аналог `hello-node`, но на Go. Демонстрирует поддержку Go-рантайма: HTTP-функция и одноразовое задание. Код пользователя оформляется как пакет `handler` с функцией `Handle(event)`. - -```bash -cd hello-go -terraform init -terraform apply -auto-approve - -terraform output job_message -terraform output trigger_url -``` - ---- - -### `pg-list-python` — выборка данных из PostgreSQL (Python) - -Минимальный пример работы с базой данных: одна HTTP-функция читает список записей из таблицы PostgreSQL и возвращает их в JSON. Таблица с тестовыми данными создаётся автоматически при первом вызове. Нет заданий, нет инициализации — только функция и триггер. - -**Переменные:** - -| Переменная | Описание | Значение по умолчанию | -|---|---|---| -| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` | - -```bash -cd pg-list-python -terraform init -terraform apply -auto-approve - -# URL функции выводится после применения: -terraform output catalog_url - -# Запрос к функции: -curl -s $(terraform output -raw catalog_url) -``` - ---- - -### `simple-python` — одноразовое задание передаёт данные в HTTP-функцию (Python) - -При `apply` выполняется задание, которое фиксирует текущее время. Результат передаётся в HTTP-функцию через переменные окружения и отображается при каждом запросе. - -```bash -cd simple-python -terraform init -terraform apply -auto-approve - -terraform output job_result -curl -s https://sless.kube5s.ru/fn//simple-py-time-display -``` - ---- - -### `simple-node` — то же самое на Node.js 20 - -```bash -cd simple-node -terraform init -terraform apply -auto-approve - -terraform output job_result -curl -s https://sless.kube5s.ru/fn//simple-node-time-display -``` - ---- - -### `notes-python` — CRUD API на Python с PostgreSQL - -Полноценное приложение: инициализация схемы базы данных через задания, CRUD-функция для работы с записями, отдельная функция для получения списка. - -**Переменные:** - -| Переменная | Описание | Значение по умолчанию | -|---|---|---| -| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` | - -```bash -cd notes-python -terraform init -terraform apply -auto-approve - -# Статус инициализации базы данных: -terraform output db_init_table_status -terraform output db_init_index_status - -# Создать запись: -curl -s -X POST "$(terraform output -raw notes_url)/add?title=Hello&body=World" - -# Получить список записей: -curl -s $(terraform output -raw notes_list_url) - -# Обновить запись (id из предыдущего ответа): -curl -s -X POST "$(terraform output -raw notes_url)/update?id=1&title=Updated&body=New+body" - -# Удалить запись: -curl -s -X POST "$(terraform output -raw notes_url)/delete?id=1" -``` +Подробности: [POSTGRES/README.md](POSTGRES/README.md) --- ## Полезные команды ```bash -# Посмотреть текущее состояние задеплоенных ресурсов: +# Посмотреть состояние задеплоенных ресурсов: terraform show -# Принудительно пересобрать функцию (например, после изменения кода): -terraform apply -replace=sless_function.<имя> -auto-approve +# Принудительно пересобрать сервис (после изменения кода): +terraform apply -replace=sless_service.<имя> -# Повторно запустить задание: увеличить значение run_id в .tf-файле, затем: -terraform apply -auto-approve +# Повторно запустить job: увеличить run_id в .tf-файле, затем: +terraform apply # Удалить все ресурсы примера: -terraform destroy -auto-approve -``` - -## Структура примера - -``` -<пример>/ -├── main.tf — конфигурация провайдера -├── *.tf — ресурсы: функции, триггеры, задания -├── variables.tf — входные переменные -├── terraform.tfvars — значения переменных (не коммитится в git) -└── code/ — исходный код функций +terraform destroy ``` diff --git a/TNAR/code/funcs-list/funcs_list.py b/TNAR/code/funcs-list/funcs_list.py deleted file mode 100644 index 68157d2..0000000 --- a/TNAR/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/TNAR/code/funcs-list/requirements.txt b/TNAR/code/funcs-list/requirements.txt deleted file mode 100644 index 2c24336..0000000 --- a/TNAR/code/funcs-list/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.31.0 diff --git a/TNAR/code/pg-info/package.json b/TNAR/code/pg-info/package.json deleted file mode 100644 index 5e6394d..0000000 --- a/TNAR/code/pg-info/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "pg-info", - "version": "1.0.0", - "description": "sless nodejs20 function: pg version + table info", - "dependencies": { - "pg": "8.11.0" - } -} \ No newline at end of file diff --git a/TNAR/code/pg-info/pg_info.js b/TNAR/code/pg-info/pg_info.js deleted file mode 100644 index e08df17..0000000 --- a/TNAR/code/pg-info/pg_info.js +++ /dev/null @@ -1,43 +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), - }; - } finally { - await client.end(); - } -}; diff --git a/TNAR/code/sql-runner/requirements.txt b/TNAR/code/sql-runner/requirements.txt deleted file mode 100644 index 56ae88a..0000000 --- a/TNAR/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/TNAR/code/sql-runner/sql_runner.py b/TNAR/code/sql-runner/sql_runner.py deleted file mode 100644 index cca9e03..0000000 --- a/TNAR/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/TNAR/code/table-rw/table_rw.py b/TNAR/code/table-rw/table_rw.py deleted file mode 100644 index 9558739..0000000 --- a/TNAR/code/table-rw/table_rw.py +++ /dev/null @@ -1,130 +0,0 @@ -# 2026-03-19 -# 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 psycopg2 -import psycopg2.extras - - -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)} - 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/TNAR/funcs_list.py b/TNAR/funcs_list.py deleted file mode 100644 index bc17fdc..0000000 --- a/TNAR/funcs_list.py +++ /dev/null @@ -1,129 +0,0 @@ -# 2026-03-18 (обновлено: фильтрация SLESS_EXCLUDE, читаемый вывод через "#"-ключ) -# funcs_list.py — HTTP-функция: список всех пользовательских функций с их статусами. -# Вызывает внутренний REST API оператора (ClusterIP, без TLS). -# Объединяет данные функций и триггеров в один ответ; скрывает служебные функции. -# -# 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 имена функций, которые не надо показывать -# Пример: "funcs,event-writer,event-monitor,event-cleaner" -# -# Формат вывода: JSON-объект, где каждая функция содержит поле "#" — краткий комментарий. -# При pretty-print (python3 -m json.tool) выглядит как читаемый список с аннотациями. - -import os -import requests - - -def _short_comment(fn, http_triggers, cron_triggers): - """Генерирует однострочный комментарий-описание функции по её метаданным.""" - phase = fn.get("phase", "") - runtime = fn.get("runtime", "") - - if http_triggers: - active_str = "активна" if http_triggers[0].get("active") else "неактивна" - return f"HTTP endpoint ({runtime}) — {phase}, {active_str}" - elif cron_triggers: - schedule = cron_triggers[0].get("schedule", "?") - active_str = "активна" if cron_triggers[0].get("active") else "неактивна" - return f"Cron '{schedule}' ({runtime}) — {phase}, {active_str}" - 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("/") - - # Имена функций, которые не должны присутствовать в выводе. - # Включает саму себя ("funcs") и служебные функции других примеров. - exclude = { - n.strip() - for n in os.environ.get("SLESS_EXCLUDE", "").split(",") - if n.strip() - } - - headers = {"Authorization": f"Bearer {token}"} - - fns_resp = requests.get( - f"{api_url}/v1/namespaces/{namespace}/functions", - headers=headers, - timeout=10, - ) - fns_resp.raise_for_status() - - trs_resp = requests.get( - f"{api_url}/v1/namespaces/{namespace}/triggers", - headers=headers, - timeout=10, - ) - trs_resp.raise_for_status() - - # Индекс триггеров по имени функции - triggers_by_fn = {} - for tr in trs_resp.json(): - fn_name = tr.get("function") or tr.get("functionRef") - if fn_name: - triggers_by_fn.setdefault(fn_name, []).append(tr) - - result = [] - for fn in fns_resp.json(): - name = fn["name"] - if name in exclude: - continue - - http_triggers = [ - t for t in triggers_by_fn.get(name, []) if t.get("type") == "http" - ] - cron_triggers = [ - t for t in triggers_by_fn.get(name, []) if t.get("type") == "cron" - ] - is_active = any( - t.get("enabled", True) and t.get("active", False) - for t in triggers_by_fn.get(name, []) - ) - - entry = { - # "#" — первый ключ: служит визуальным комментарием при pretty-print - "#": _short_comment(fn, http_triggers, cron_triggers), - "name": name, - "runtime": fn.get("runtime"), - "phase": fn.get("phase"), - "active": is_active, - } - - # URL вычисляем из SLESS_EXTERNAL_URL если задан — state может хранить старый домен - if http_triggers: - if ext_url: - entry["url"] = f"{ext_url}/fn/{namespace}/{name}" - else: - entry["url"] = http_triggers[0].get("url", "") - - if cron_triggers: - entry["cron"] = cron_triggers[0].get("schedule", "") - - if fn.get("message"): - entry["message"] = fn["message"] - - # created_at и last_built_at — доступны после обновления оператора до v0.1.32+ - if fn.get("created_at"): - entry["created_at"] = fn["created_at"] - if fn.get("last_built_at"): - entry["last_built_at"] = fn["last_built_at"] - - result.append(entry) - - # Сортировка: активные вверх, затем по имени - result.sort(key=lambda f: (not f["active"], f["name"])) - - return { - "namespace": namespace, - "count": len(result), - "functions": result, - } - diff --git a/TNAR/luceUNDnode.tf b/TNAR/luceUNDnode.tf deleted file mode 100644 index 75c0658..0000000 --- a/TNAR/luceUNDnode.tf +++ /dev/null @@ -1,73 +0,0 @@ - -# resource "nubes_lucee" "app1" { -# # Lucee-приложение, зависит от Postgres -# resource_name = "lucy_teststand_0" -# # resource_realm = "k8s-3.ext.nubes.ru" -# resource_realm = nubes_postgres.db2.resource_realm -# # resource_realm = "k8s-4-sandbox-nubes-ru" -# domain = "web-test-stand" - -# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testlucee" - -# json_env = jsonencode({ -# # 🔗 Настройки Data Source 'testds' для Lucee (Application.cfc) -# testds_class = "org.postgresql.Driver" # 📂 Драйвер БД -# testds_bundleName = "org.postgresql.jdbc" # 📦 Имя бандла JDBC -# testds_bundleVersion = "42.6.0" # 🔢 Версия драйвера -# testds_connectionString = "jdbc:postgresql://${nubes_postgres.db2.state_out_flat["internalConnect.master"]}:5432/postgres?sslmode=require" # 🚀 Строка подключения -# testds_username = nubes_postgres_user.db2_user.username # 👤 Логин -# testds_password = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"] # 🔑 Пароль -# testds_connectionLimit = "5" # 🚦 Лимит соединений -# testds_liveTimeout = "15" # ⏳ Таймаут жизни -# testds_validate = "false" # ✅ Валидация при запросе -# }) - -# resource_c_p_u = 300 -# resource_memory = 512 -# resource_instances = 1 -# app_version = "5.4" - -# depends_on = [nubes_postgres.db2] -# } - -# resource "nubes_nodejs" "app3" { -# # NodeJS демо, работающий с тем же Postgres. -# resource_name = "node_01" -# resource_realm = nubes_postgres.db2.resource_realm -# domain = "node07" -# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testnode.git" -# health_path = "/healthz" -# app_version = "23" - -# json_env = jsonencode({ -# # Переменные подключения к Postgres. -# PGHOST = nubes_postgres.db2.state_out_flat["internalConnect.master"] -# PGPORT = "5432" -# PGUSER = nubes_postgres_user.db2_user.username -# PGPASSWORD = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"] -# PGDATABASE = nubes_postgres_database.db2_app.db_name -# PGSSLMODE = "require" -# DATABASE_URL = format( -# "postgresql://%s:%s@%s:5432/%s?sslmode=require", -# nubes_postgres_user.db2_user.username, -# jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"], -# nubes_postgres.db2.state_out_flat["internalConnect.master"], -# nubes_postgres_database.db2_app.db_name -# ) -# }) - -# resource_c_p_u = 300 -# resource_memory = 256 -# resource_instances = 1 - -# depends_on = [nubes_postgres.db2] -# } - -# output "pg_vault_secrets" { -# value = nubes_postgres.db2.vault_secrets -# sensitive = true -# } - -# terraform output -json pg_vault_secrets - - diff --git a/TNAR/main.tf b/TNAR/main.tf deleted file mode 100644 index ae97e59..0000000 --- a/TNAR/main.tf +++ /dev/null @@ -1,58 +0,0 @@ -// 2026-03-17 17:05 -// main.tf — провайдеры и переменные для Nubes + sless. -terraform { - required_providers { - nubes = { - source = "terra.k8c.ru/nubes/nubes" - version = "5.0.19" - } - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -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." -} - -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" -} - diff --git a/TNAR/resources.tf b/TNAR/resources.tf deleted file mode 100644 index cae3c49..0000000 --- a/TNAR/resources.tf +++ /dev/null @@ -1,196 +0,0 @@ -// 2026-03-18 — добавлены locals для извлечения credentials из vault_secrets (без хардкода). -// Для сверки хардкод остаётся в terraform.tfvars на этапе разработки. -// sless_function и sless_job закомментированы — сначала проверяется сетевое соединение. - -# Актуальные credentials из vault_secrets (authoritatively) — vault синхронизирован с кластером. -# Структура vault_secrets["users"]: JSON-строка {"username": {"password": "...", "username": "..."}} -locals { - pg_creds_map = jsondecode(nubes_postgres.npg.vault_secrets["users"]) - pg_username = nubes_postgres_user.pg_user.username - pg_password = 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 = "testnarod-pg-0" - # 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 = "u-user0" - role = "ddl_user" - adopt_existing_on_create = true -} - -resource "nubes_postgres_database" "db" { - postgres_id = nubes_postgres.npg.id - db_name = "db_terra" - db_owner = nubes_postgres_user.pg_user.username - adopt_existing_on_create = true - # suspend_on_destroy = false -} - -# Служебная функция выполняет SQL-операторы из event_json. -# Credentials берутся из locals (vault_secrets) — без хардкода. -# Для сверки хардкод остаётся в terraform.tfvars. -resource "sless_function" "postgres_sql_runner_create_table" { - name = "pg-create-table-runner" - runtime = "python3.11" - entrypoint = "sql_runner.run_sql" - 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" - # Для сверки (должно совпадать с vault): - # PGUSER = var.pg_user - # PGPASSWORD = var.pg_password - } - - source_dir = "${path.module}/code/sql-runner" -} - -resource "sless_job" "postgres_table_init_job" { - name = "pg-create-table-job-main-v13" - function = sless_function.postgres_sql_runner_create_table.name - wait_timeout_sec = 180 - run_id = 13 - - 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] -} - -# HTTP-функция на NodeJS: возвращает версию PG-сервера и счётчик строк в таблице. -# Единственная функция примера на nodejs20 — проверка что JS runtime работает. -# Доступна по URL: https://sless.kube5s.ru/fn//pg-info -resource "sless_function" "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_trigger" "pg_info_http" { - name = "pg-info-http" - type = "http" - function = sless_function.pg_info.name - enabled = true -} - -# HTTP-функции чтения и записи строк terraform_demo_table — в одном файле table_rw.py. -# list_rows (GET) — читает все строки; add_row (POST {title}) — вставляет строку. -# Доступны по URL: https://sless.kube5s.ru/fn//pg-table-reader -# https://sless.kube5s.ru/fn//pg-table-writer -resource "sless_function" "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] -} - -resource "sless_trigger" "postgres_table_reader_http" { - name = "pg-table-reader-http" - type = "http" - function = sless_function.postgres_table_reader.name - enabled = true -} - -output "table_reader_url" { - value = sless_trigger.postgres_table_reader_http.url -} - -resource "sless_function" "postgres_table_writer" { - name = "pg-table-writer" - runtime = "python3.11" - entrypoint = "table_rw.add_row" - 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] -} - -resource "sless_trigger" "postgres_table_writer_http" { - name = "pg-table-writer-http" - type = "http" - function = sless_function.postgres_table_writer.name - enabled = true -} - -output "table_writer_url" { - value = sless_trigger.postgres_table_writer_http.url -} - diff --git a/TNAR/scripts/pg-debug-pod.yaml b/TNAR/scripts/pg-debug-pod.yaml deleted file mode 100644 index 652ed5b..0000000 --- a/TNAR/scripts/pg-debug-pod.yaml +++ /dev/null @@ -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" diff --git a/TNAR/scripts/read_pg_user_secret.py b/TNAR/scripts/read_pg_user_secret.py deleted file mode 100644 index b0735f3..0000000 --- a/TNAR/scripts/read_pg_user_secret.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/demo-event-log/event-cleaner/code/handler.py b/demo-event-log/event-cleaner/code/handler.py deleted file mode 100644 index 0fcb629..0000000 --- a/demo-event-log/event-cleaner/code/handler.py +++ /dev/null @@ -1,45 +0,0 @@ -# Изменено: 2026-03-14 -# Функция event-cleaner: удаляет N самых старых строк из таблицы events. -# Вызывается через HTTP POST из Node-RED (который слушает RabbitMQ). -# Env: POSTGRES_DSN — строка подключения к PostgreSQL. - -import os -import json -import psycopg2 - -def handle(request): - """Удаляет N старейших строк из таблицы events.""" - dsn = os.environ["POSTGRES_DSN"] - - body = {} - if request.get_data(): - try: - body = json.loads(request.get_data()) - except Exception: - pass - - # Количество строк для удаления — из тела запроса или дефолт 10 - delete_n = int(body.get("delete_n", 10)) - # Защита от случайного удаления слишком большого количества строк - delete_n = min(delete_n, 100) - - conn = psycopg2.connect(dsn) - try: - with conn.cursor() as cur: - cur.execute(""" - DELETE FROM events - WHERE id IN ( - SELECT id FROM events ORDER BY created_at ASC LIMIT %s - ) - """, (delete_n,)) - deleted = cur.rowcount - cur.execute("SELECT COUNT(*) FROM events") - remaining = cur.fetchone()[0] - conn.commit() - return json.dumps({ - "ok": True, - "deleted": deleted, - "remaining": remaining - }), 200, {"Content-Type": "application/json"} - finally: - conn.close() diff --git a/demo-event-log/event-monitor/code/handler.py b/demo-event-log/event-monitor/code/handler.py deleted file mode 100644 index d95dbfe..0000000 --- a/demo-event-log/event-monitor/code/handler.py +++ /dev/null @@ -1,47 +0,0 @@ -# Изменено: 2026-03-14 -# Функция event-monitor: считает строки в events. -# Если больше 50 — публикует сообщение в RabbitMQ queue "cleanup-needed". -# Запускается по cron (каждую минуту). -# Env: -# POSTGRES_DSN — строка подключения к PostgreSQL -# RABBITMQ_URL — amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/ - -import os -import json -import psycopg2 -import pika - -THRESHOLD = 50 - -def handle(request): - """Мониторит таблицу events. При переполнении шлёт в RabbitMQ.""" - dsn = os.environ["POSTGRES_DSN"] - rabbit_url = os.environ["RABBITMQ_URL"] - - conn = psycopg2.connect(dsn) - try: - with conn.cursor() as cur: - # Считаем количество событий - cur.execute("SELECT COUNT(*) FROM events") - count = cur.fetchone()[0] - finally: - conn.close() - - result = {"count": count, "threshold": THRESHOLD, "action": "none"} - - if count > THRESHOLD: - # Публикуем в очередь — event-cleaner получит и удалит старые строки - params = pika.URLParameters(rabbit_url) - connection = pika.BlockingConnection(params) - channel = connection.channel() - channel.queue_declare(queue="cleanup-needed", durable=True) - channel.basic_publish( - exchange="", - routing_key="cleanup-needed", - body=json.dumps({"count": count, "delete_n": 10}), - properties=pika.BasicProperties(delivery_mode=2) # persistent - ) - connection.close() - result["action"] = "cleanup_requested" - - return json.dumps(result), 200, {"Content-Type": "application/json"} diff --git a/demo-event-log/event-writer/code/handler.py b/demo-event-log/event-writer/code/handler.py deleted file mode 100644 index e4fcae8..0000000 --- a/demo-event-log/event-writer/code/handler.py +++ /dev/null @@ -1,49 +0,0 @@ -# Изменено: 2026-03-14 -# Функция event-writer: принимает HTTP POST, пишет одну строку в таблицу events. -# Таблица создаётся автоматически при первом запуске. -# Env: POSTGRES_DSN — строка подключения к PostgreSQL. - -import os -import json -import psycopg2 -from datetime import datetime, timezone - -def handle(request): - """Записывает одно событие в таблицу events.""" - dsn = os.environ["POSTGRES_DSN"] - - body = {} - if request.get_data(): - try: - body = json.loads(request.get_data()) - except Exception: - pass - - source = body.get("source", "node-red") - message = body.get("message", "ping") - - conn = psycopg2.connect(dsn) - try: - with conn.cursor() as cur: - # Создаём таблицу если нет — безопасно вызывать при каждом запросе - cur.execute(""" - CREATE TABLE IF NOT EXISTS events ( - id SERIAL PRIMARY KEY, - source VARCHAR(100) NOT NULL DEFAULT 'unknown', - message TEXT NOT NULL DEFAULT '', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ) - """) - cur.execute( - "INSERT INTO events (source, message) VALUES (%s, %s) RETURNING id, created_at", - (source, message) - ) - row = cur.fetchone() - conn.commit() - return json.dumps({ - "ok": True, - "id": row[0], - "created_at": row[1].isoformat() - }), 200, {"Content-Type": "application/json"} - finally: - conn.close() diff --git a/demo-managed-functions/README.md b/demo-managed-functions/README.md deleted file mode 100644 index 1fd1bf3..0000000 --- a/demo-managed-functions/README.md +++ /dev/null @@ -1,194 +0,0 @@ -# Примеры использования sless - -## Обзор платформы - -**sless** — система управления serverless-функциями на базе Kubernetes. Разработчик загружает код функции, платформа собирает из него Docker-образ, разворачивает его в кластере и предоставляет HTTP-эндпоинт для вызова. Всё описывается декларативно через Terraform. - -### Основные ресурсы провайдера - -| Ресурс | Назначение | -|---|---| -| `sless_function` | Описывает функцию: язык, точку входа, лимиты, переменные окружения. При создании загружает код и запускает его сборку в образ. Сама по себе недоступна снаружи — нужен триггер или задание. | -| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. | -| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. | - -Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless.kube5s.ru/fn//<имя-функции>`. - ---- - -## Требования - -- Terraform >= 1.0 -- JWT-токен для аутентификации в sless API -- Доступ к `https://sless.kube5s.ru` - -## Конфигурация провайдера - -Во всех примерах файл `main.tf` содержит блок провайдера. Токен передаётся через переменную, значение которой задаётся в `terraform.tfvars`: - -```hcl -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} -``` - -Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`. - -> **Перед запуском любого примера** откройте файл `terraform.tfvars` в директории примера и впишите свой токен Nubes API: -> ```hcl -> token = "ваш токен Nubes API" -> ``` -> Токен выдаётся в личном кабинете Nubes. Файл `terraform.tfvars` добавлен в `.gitignore` — он не попадёт в репозиторий. - ---- - -## Примеры - -### `hello-node` — минимальный пример на Node.js - -Две независимые функции: HTTP-функция, возвращающая приветствие, и одноразовое задание, суммирующее набор чисел. Хорошая отправная точка для знакомства с платформой. - -```bash -cd hello-node -terraform init -terraform apply -auto-approve - -# Вызов HTTP-функции с передачей имени: -curl -s -X POST https://sless.kube5s.ru/fn//hello-http \ - -H 'Content-Type: application/json' -d '{"name":"World"}' - -# Результат задания: -terraform output job_message -``` - ---- - -### `hello-go` — минимальный пример на Go 1.23 - -Аналог `hello-node`, но на Go. Демонстрирует поддержку Go-рантайма: HTTP-функция и одноразовое задание. Код пользователя оформляется как пакет `handler` с функцией `Handle(event)`. - -```bash -cd hello-go -terraform init -terraform apply -auto-approve - -terraform output job_message -terraform output trigger_url -``` - ---- - -### `pg-list-python` — выборка данных из PostgreSQL (Python) - -Минимальный пример работы с базой данных: одна HTTP-функция читает список записей из таблицы PostgreSQL и возвращает их в JSON. Таблица с тестовыми данными создаётся автоматически при первом вызове. Нет заданий, нет инициализации — только функция и триггер. - -**Переменные:** - -| Переменная | Описание | Значение по умолчанию | -|---|---|---| -| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` | - -```bash -cd pg-list-python -terraform init -terraform apply -auto-approve - -# URL функции выводится после применения: -terraform output catalog_url - -# Запрос к функции: -curl -s $(terraform output -raw catalog_url) -``` - ---- - -### `simple-python` — одноразовое задание передаёт данные в HTTP-функцию (Python) - -При `apply` выполняется задание, которое фиксирует текущее время. Результат передаётся в HTTP-функцию через переменные окружения и отображается при каждом запросе. - -```bash -cd simple-python -terraform init -terraform apply -auto-approve - -terraform output job_result -curl -s https://sless.kube5s.ru/fn//simple-py-time-display -``` - ---- - -### `simple-node` — то же самое на Node.js 20 - -```bash -cd simple-node -terraform init -terraform apply -auto-approve - -terraform output job_result -curl -s https://sless.kube5s.ru/fn//simple-node-time-display -``` - ---- - -### `notes-python` — CRUD API на Python с PostgreSQL - -Полноценное приложение: инициализация схемы базы данных через задания, CRUD-функция для работы с записями, отдельная функция для получения списка. - -**Переменные:** - -| Переменная | Описание | Значение по умолчанию | -|---|---|---| -| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` | - -```bash -cd notes-python -terraform init -terraform apply -auto-approve - -# Статус инициализации базы данных: -terraform output db_init_table_status -terraform output db_init_index_status - -# Создать запись: -curl -s -X POST "$(terraform output -raw notes_url)/add?title=Hello&body=World" - -# Получить список записей: -curl -s $(terraform output -raw notes_list_url) - -# Обновить запись (id из предыдущего ответа): -curl -s -X POST "$(terraform output -raw notes_url)/update?id=1&title=Updated&body=New+body" - -# Удалить запись: -curl -s -X POST "$(terraform output -raw notes_url)/delete?id=1" -``` - ---- - -## Полезные команды - -```bash -# Посмотреть текущее состояние задеплоенных ресурсов: -terraform show - -# Принудительно пересобрать функцию (например, после изменения кода): -terraform apply -replace=sless_function.<имя> -auto-approve - -# Повторно запустить задание: увеличить значение run_id в .tf-файле, затем: -terraform apply -auto-approve - -# Удалить все ресурсы примера: -terraform destroy -auto-approve -``` - -## Структура примера - -``` -<пример>/ -├── main.tf — конфигурация провайдера -├── *.tf — ресурсы: функции, триггеры, задания -├── variables.tf — входные переменные -├── terraform.tfvars — значения переменных (не коммитится в git) -└── code/ — исходный код функций -``` diff --git a/demo-managed-functions/code/event-cleaner/event_cleaner_handler.py b/demo-managed-functions/code/event-cleaner/event_cleaner_handler.py deleted file mode 100644 index 4b2bfcd..0000000 --- a/demo-managed-functions/code/event-cleaner/event_cleaner_handler.py +++ /dev/null @@ -1,26 +0,0 @@ -# Изменено: 2026-03-14 -# event-cleaner: HTTP функция для демонстрации контролируемого изменения логики. -# Работает без внешних пакетов. - -import json -from datetime import datetime, timezone - - -def event_cleaner_handle(event_cleaner_event): - """Принимает delete_n и возвращает подтверждение обработки.""" - - event_cleaner_payload = event_cleaner_event if isinstance(event_cleaner_event, dict) else {} - - event_cleaner_delete_n = int(event_cleaner_payload.get("delete_n", 10)) - event_cleaner_delete_n = max(1, min(event_cleaner_delete_n, 100)) - - event_cleaner_generated_at = datetime.now(timezone.utc).isoformat() - # Здесь intentionally имитируем очистку, чтобы показать реакцию на входные параметры. - return json.dumps( - { - "ok": True, - "accepted_delete_n": event_cleaner_delete_n, - "status": "simulated-cleanup", - "generated_at": event_cleaner_generated_at, - } - ), 200, {"Content-Type": "application/json"} diff --git a/demo-managed-functions/code/event-monitor/event_monitor_handler.py b/demo-managed-functions/code/event-monitor/event_monitor_handler.py deleted file mode 100644 index 2027cdb..0000000 --- a/demo-managed-functions/code/event-monitor/event_monitor_handler.py +++ /dev/null @@ -1,25 +0,0 @@ -# Изменено: 2026-03-14 -# event-monitor: cron-функция для демонстрации расписания и управления кодом. -# Работает без внешних библиотек и возвращает диагностический JSON. - -import json -import os -from datetime import datetime, timezone - - -EVENT_MONITOR_THRESHOLD = 50 - - -def event_monitor_handle(event_monitor_event): - """Отдаёт heartbeat для cron-запуска и видимой проверки после apply.""" - event_monitor_rabbitmq_url = os.environ.get("RABBITMQ_URL", "not-set") - event_monitor_generated_at = datetime.now(timezone.utc).isoformat() - return json.dumps( - { - "ok": True, - "monitor": "alive", - "threshold": EVENT_MONITOR_THRESHOLD, - "rabbitmq_configured": event_monitor_rabbitmq_url != "not-set", - "generated_at": event_monitor_generated_at, - } - ), 200, {"Content-Type": "application/json"} diff --git a/demo-managed-functions/code/event-writer/event_writer_handler.py b/demo-managed-functions/code/event-writer/event_writer_handler.py deleted file mode 100644 index ed84b2f..0000000 --- a/demo-managed-functions/code/event-writer/event_writer_handler.py +++ /dev/null @@ -1,28 +0,0 @@ -# Изменено: 2026-03-14 -# event-writer: простая HTTP функция без внешних зависимостей. -# Правка поля response_tag в коде сразу видна в ответе после terraform apply. - -import json -import os -from datetime import datetime, timezone - - -def event_writer_handle(event_writer_event): - """Возвращает полезный JSON-ответ для визуальной проверки выката кода.""" - writer_default_message = os.environ.get("DEFAULT_MESSAGE", "writer-default") - - writer_payload = event_writer_event if isinstance(event_writer_event, dict) else {} - - writer_message_value = writer_payload.get("message", writer_default_message) - writer_source_name = writer_payload.get("source", "event-writer") - writer_generated_at = datetime.now(timezone.utc).isoformat() - - # response_tag удобно менять для демонстрации hot-update кода через terraform apply. - writer_response_tag = "writer-v2" - return { - "ok": True, - "source": writer_source_name, - "message": writer_message_value, - "response_tag": writer_response_tag, - "generated_at": writer_generated_at, - } diff --git a/demo-managed-functions/function.tf b/demo-managed-functions/function.tf deleted file mode 100644 index 48b5877..0000000 --- a/demo-managed-functions/function.tf +++ /dev/null @@ -1,77 +0,0 @@ -# 2026-03-14 -# function.tf — ресурсы managed serverless функций для демонстрации. -# Пользователь правит код в code/* и запускает terraform apply — провайдер сам пересобирает и выкатывает функции. - -resource "sless_function" "event_writer" { - name = "event-writer" - runtime = "python3.11" - entrypoint = "event_writer_handler.event_writer_handle" - memory_mb = 128 - timeout_sec = 20 - - source_dir = "${path.module}/code/event-writer" - - env_vars = { - POSTGRES_DSN = var.pg_dsn - DEFAULT_MESSAGE = var.writer_message - } -} - -resource "sless_trigger" "event_writer_http" { - name = "event-writer-http" - type = "http" - function = sless_function.event_writer.name - enabled = true -} - -resource "sless_function" "event_monitor" { - name = "event-monitor" - runtime = "python3.11" - entrypoint = "event_monitor_handler.event_monitor_handle" - memory_mb = 128 - timeout_sec = 20 - - source_dir = "${path.module}/code/event-monitor" - - env_vars = { - POSTGRES_DSN = var.pg_dsn - RABBITMQ_URL = var.rabbitmq_url - } -} - -resource "sless_trigger" "event_monitor_cron" { - name = "event-monitor-cron" - type = "cron" - function = sless_function.event_monitor.name - enabled = true - schedule = "*/1 * * * *" -} - -resource "sless_function" "event_cleaner" { - name = "event-cleaner" - runtime = "python3.11" - entrypoint = "event_cleaner_handler.event_cleaner_handle" - memory_mb = 128 - timeout_sec = 20 - - source_dir = "${path.module}/code/event-cleaner" - - env_vars = { - POSTGRES_DSN = var.pg_dsn - } -} - -resource "sless_trigger" "event_cleaner_http" { - name = "event-cleaner-http" - type = "http" - function = sless_function.event_cleaner.name - enabled = true -} - -output "event_writer_url" { - value = sless_trigger.event_writer_http.url -} - -output "event_cleaner_url" { - value = sless_trigger.event_cleaner_http.url -} diff --git a/demo-managed-functions/main.tf b/demo-managed-functions/main.tf deleted file mode 100644 index 10fd047..0000000 --- a/demo-managed-functions/main.tf +++ /dev/null @@ -1,18 +0,0 @@ -# 2026-03-14 -# Terraform demo: managed serverless functions. -# Здесь управляем ТОЛЬКО функциями/триггерами, внешние сервисы считаем уже поднятыми. - -terraform { - required_providers { - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -provider "sless" { - endpoint = var.sless_endpoint - token = var.token - nubes_endpoint = var.nubes_endpoint -} diff --git a/demo-managed-functions/terraform.tfvars.example b/demo-managed-functions/terraform.tfvars.example deleted file mode 100644 index db09390..0000000 --- a/demo-managed-functions/terraform.tfvars.example +++ /dev/null @@ -1,11 +0,0 @@ -# 2026-03-14 -# Скопируй в terraform.tfvars и подставь актуальный токен. - -token = "PUT_TOKEN_HERE" -sless_endpoint = "https://sless-api.kube5s.ru" -nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" - -pg_dsn = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" -rabbitmq_url = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/" - -writer_message = "writer-default-v1" diff --git a/demo-managed-functions/variables.tf b/demo-managed-functions/variables.tf deleted file mode 100644 index 6012dff..0000000 --- a/demo-managed-functions/variables.tf +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-03-14 -# Переменные для demo-managed-functions. - -variable "token" { - description = "JWT токен API" - type = string - sensitive = true -} - -variable "sless_endpoint" { - description = "Endpoint sless API" - type = string - default = "https://sless.kube5s.ru" -} - -variable "nubes_endpoint" { - description = "Nubes endpoint (нужен провайдеру)" - type = string - default = "https://deck-api-test.ngcloud.ru/api/v1" -} - -variable "pg_dsn" { - description = "PostgreSQL DSN для функций" - type = string - default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" -} - -variable "rabbitmq_url" { - description = "RabbitMQ URL для функций" - type = string - default = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/" -} - -variable "writer_message" { - description = "Сообщение по умолчанию, которое пишет event-writer" - type = string - default = "writer-default-v1" -} diff --git a/hello-go/code/greeting.go b/hello-go/code/greeting.go deleted file mode 100644 index a32106e..0000000 --- a/hello-go/code/greeting.go +++ /dev/null @@ -1,29 +0,0 @@ -// Изменено: 2026-03-11 -// greeting.go — пример Go функции: возвращает приветствие. -// -// ТРЕБОВАНИЕ РАНТАЙМА: пакет должен называться handler, точка входа — Handle. -// Вся бизнес-логика — в отдельных функциях с нормальными именами. - -package handler - -import "fmt" - -// buildGreeting — формирует текст приветствия для указанного имени гостя. -func buildGreeting(guestName string) string { - return fmt.Sprintf("Hello, %s! (Go 1.23)", guestName) -} - -// Handle — точка входа, вызывается рантаймом на каждый запрос/событие. -// Не переименовывать: это жёсткий контракт рантайма (server.go вызывает handler.Handle). -func Handle(event map[string]interface{}) interface{} { - guestName, ok := event["name"].(string) - if !ok || guestName == "" { - guestName = "world" - } - - return map[string]interface{}{ - "message": buildGreeting(guestName), - "runtime": "go1.23", - "event": event, - } -} diff --git a/hello-go/http.tf b/hello-go/http.tf deleted file mode 100644 index d24bff0..0000000 --- a/hello-go/http.tf +++ /dev/null @@ -1,23 +0,0 @@ -# 2026-03-11 -# http.tf — HTTP-функция на Go: принимает запрос, возвращает приветствие. - -resource "sless_function" "hello_go_http" { - name = "hello-go-http" - runtime = "go1.23" - entrypoint = "handler.Handle" - memory_mb = 128 - timeout_sec = 60 - - source_dir = "${path.module}/code" -} - -resource "sless_trigger" "hello_go_http" { - name = "hello-go-http-trigger" - type = "http" - function = sless_function.hello_go_http.name - enabled = true -} - -output "trigger_url" { - value = sless_trigger.hello_go_http.url -} diff --git a/hello-go/job.tf b/hello-go/job.tf deleted file mode 100644 index 9a70769..0000000 --- a/hello-go/job.tf +++ /dev/null @@ -1,28 +0,0 @@ -# 2026-03-11 -# job.tf — одноразовый запуск Go функции с event payload. - -resource "sless_function" "hello_go_job" { - name = "hello-go-job" - runtime = "go1.23" - entrypoint = "handler.Handle" - memory_mb = 128 - timeout_sec = 60 - - source_dir = "${path.module}/code" -} - -resource "sless_job" "hello_go_run" { - name = "hello-go-run" - function = sless_function.hello_go_job.name - event_json = jsonencode({ name = "Go" }) - wait_timeout_sec = 300 - run_id = 1 -} - -output "job_phase" { - value = sless_job.hello_go_run.phase -} - -output "job_message" { - value = sless_job.hello_go_run.message -} diff --git a/hello-go/main.tf b/hello-go/main.tf deleted file mode 100644 index ef1580e..0000000 --- a/hello-go/main.tf +++ /dev/null @@ -1,17 +0,0 @@ -# 2026-03-11 -# main.tf — провайдеры для hello-go примера. - -terraform { - required_providers { - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} diff --git a/hello-go/terraform.tfvars.change b/hello-go/terraform.tfvars.change deleted file mode 100644 index 6daebb5..0000000 --- a/hello-go/terraform.tfvars.change +++ /dev/null @@ -1 +0,0 @@ -token = "замени на Nubes API token" и переименуй файл в terraform.tfvars diff --git a/hello-go/variables.tf b/hello-go/variables.tf deleted file mode 100644 index 538fc2b..0000000 --- a/hello-go/variables.tf +++ /dev/null @@ -1,10 +0,0 @@ -# 2026-03-11 -# variables.tf — входные переменные для hello-node примера. - -# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). -# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} -variable "token" { - description = "JWT токен облака для аутентификации в sless API" - type = string - sensitive = true -} diff --git a/hello-node/code/handler-http.js b/hello-node/code/handler-http.js deleted file mode 100644 index 16bc1a1..0000000 --- a/hello-node/code/handler-http.js +++ /dev/null @@ -1,8 +0,0 @@ -// 2026-03-08 -// handler-http.js — HTTP-функция: возвращает приветствие. -// Используется с sless_trigger (постоянный эндпоинт). -exports.handle = async (event) => { - const name = event.name || 'World'; - return { message: `Hello, ${name}! HTTP !!!` }; -}; - diff --git a/hello-node/code/handler-job.js b/hello-node/code/handler-job.js deleted file mode 100644 index 23abed8..0000000 --- a/hello-node/code/handler-job.js +++ /dev/null @@ -1,11 +0,0 @@ -// 2026-03-08 -// handler-job.js — batch-функция: суммирует числа и считает среднее. -// Используется с sless_job (одноразовый запуск). -// event.numbers — массив чисел, например [1, 2, 3, 4, 5] -exports.handle = async (event) => { - const numbers = event.numbers || []; - const sum = numbers.reduce((acc, n) => acc + n, 0); - const avg = numbers.length > 0 ? sum / numbers.length : 0; - return { input: numbers, sum, avg, count: numbers.length }; -}; - diff --git a/hello-node/http.tf b/hello-node/http.tf deleted file mode 100644 index beee105..0000000 --- a/hello-node/http.tf +++ /dev/null @@ -1,24 +0,0 @@ -# 2026-03-08 / Изменено: 2026-03-09 -# http.tf — HTTP-функция: принимает запросы, возвращает приветствие. -# Код: code/handler-http.js - -resource "sless_function" "hello_http" { - name = "hello-http" - runtime = "nodejs20" - entrypoint = "handler-http.handle" - memory_mb = 128 - timeout_sec = 30 - - source_dir = "${path.module}/code" -} - -resource "sless_trigger" "hello_http" { - name = "hello-http-trigger" - type = "http" - function = sless_function.hello_http.name - enabled = true -} - -output "trigger_url" { - value = sless_trigger.hello_http.url -} diff --git a/hello-node/job.tf b/hello-node/job.tf deleted file mode 100644 index 843d02f..0000000 --- a/hello-node/job.tf +++ /dev/null @@ -1,34 +0,0 @@ -# 2026-03-08 / Изменено: 2026-03-09 -# job.tf — одноразовая функция: суммирует числа из переданного массива. -# Код: code/handler-job.js - -resource "sless_function" "hello_job" { - name = "hello-job" - runtime = "nodejs20" - entrypoint = "handler-job.handle" - memory_mb = 128 - timeout_sec = 30 - - source_dir = "${path.module}/code" -} - -# Одноразовый запуск. Для повторного запуска увеличь run_id (1→2→3...). -resource "sless_job" "hello_run" { - name = "hello-run" - function = sless_function.hello_job.name - event_json = jsonencode({ numbers = [100, 200, 300] }) - wait_timeout_sec = 600 - run_id = 9 -} - -output "job_phase" { - value = sless_job.hello_run.phase -} - -output "job_message" { - value = sless_job.hello_run.message -} - -output "job_completion_time" { - value = sless_job.hello_run.completion_time -} diff --git a/hello-node/main.tf b/hello-node/main.tf deleted file mode 100644 index 09f8a39..0000000 --- a/hello-node/main.tf +++ /dev/null @@ -1,24 +0,0 @@ -# 2026-03-11 -# main.tf — провайдеры. -# Функции и их код определены в отдельных файлах: -# http.tf — HTTP-триггер (code/handler-http.js) -# job.tf — одноразовый запуск (code/handler-job.js) -# -# nubes_endpoint — провайдер делает GET запрос для валидации токена. -# Namespace вычисляется автоматически из JWT sub: sless-{sha256[:8]} - -terraform { - required_providers { - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} - diff --git a/hello-node/terraform.tfvars.change b/hello-node/terraform.tfvars.change deleted file mode 100644 index 6daebb5..0000000 --- a/hello-node/terraform.tfvars.change +++ /dev/null @@ -1 +0,0 @@ -token = "замени на Nubes API token" и переименуй файл в terraform.tfvars diff --git a/hello-node/test_invalid.tf.disabled b/hello-node/test_invalid.tf.disabled deleted file mode 100644 index 7c5e5eb..0000000 --- a/hello-node/test_invalid.tf.disabled +++ /dev/null @@ -1,2 +0,0 @@ -# Временный файл для негативных тестов — не применяется через terraform -# Тесты запускаются вручную с временным переименованием в .tf diff --git a/hello-node/variables.tf b/hello-node/variables.tf deleted file mode 100644 index 538fc2b..0000000 --- a/hello-node/variables.tf +++ /dev/null @@ -1,10 +0,0 @@ -# 2026-03-11 -# variables.tf — входные переменные для hello-node примера. - -# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). -# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} -variable "token" { - description = "JWT токен облака для аутентификации в sless API" - type = string - sensitive = true -} diff --git a/notes-python/code/notes-list/notes_list.py b/notes-python/code/notes-list/notes_list.py deleted file mode 100644 index 4e63bf2..0000000 --- a/notes-python/code/notes-list/notes_list.py +++ /dev/null @@ -1,31 +0,0 @@ -# 2026-03-09 -# notes_list.py — чтение всех записей из таблицы notes. -# -# Назначение: отдать полный список заметок одним запросом. -# Принимает GET или POST — тело/query параметры игнорируются. -# Возвращает JSON-массив, сортировка: новые записи первые (ORDER BY created_at DESC). -# -# Пример ответа: -# [ -# {"id": 3, "title": "Hello", "body": "World", "created_at": "2026-03-09 ..."}, -# {"id": 1, "title": "First", "body": "Note", "created_at": "2026-03-08 ..."} -# ] -import os -import psycopg2 -import psycopg2.extras - - -def list_notes(event): - dsn = os.environ['PG_DSN'] - conn = psycopg2.connect(dsn) - try: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute( - "SELECT id, title, body, created_at::text FROM notes ORDER BY created_at DESC" - ) - rows = cur.fetchall() - return [dict(r) for r in rows] - except Exception as e: - return {'error': str(e)} - finally: - conn.close() diff --git a/notes-python/code/notes-list/requirements.txt b/notes-python/code/notes-list/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/notes-python/code/notes-list/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/notes-python/code/notes/notes_crud.py b/notes-python/code/notes/notes_crud.py deleted file mode 100644 index 07b2592..0000000 --- a/notes-python/code/notes/notes_crud.py +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-03-09 -# notes_crud.py — CRUD роутер для таблицы notes. -# -# Назначение: единая функция, которая обрабатывает все операции с записями. -# Роутинг осуществляется по sub-path URL (event._path), который runtime -# берёт из входящего HTTP-запроса и добавляет в event автоматически. -# -# Доступные маршруты (все POST): -# /fn/default/notes/add?title=...&body=... → создать запись -# /fn/default/notes/update?id=1&title=...&body=... → обновить запись -# /fn/default/notes/delete?id=1 → удалить запись -# -# Параметры берутся из query string (event._query) или из тела запроса (event). -# event._path и event._query добавляет Python runtime (server.py) автоматически. -import os -import psycopg2 -import psycopg2.extras - - -def crud(event): - dsn = os.environ['PG_DSN'] - # sub-path без ведущего слэша: "add", "update", "delete" - action = event.get('_path', '/').strip('/') - q = event.get('_query', {}) - - conn = psycopg2.connect(dsn) - try: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - - if action == 'add': - title = q.get('title') or event.get('title', '') - body = q.get('body') or event.get('body', '') - if not title: - return {'error': 'title is required'} - cur.execute( - "INSERT INTO notes (title, body) VALUES (%s, %s)" - " RETURNING id, title, body, created_at::text", - (title, body) - ) - row = cur.fetchone() - conn.commit() - return dict(row) - - elif action == 'update': - id_ = q.get('id') or event.get('id') - if not id_: - return {'error': 'id is required'} - title = q.get('title') or event.get('title', '') - body = q.get('body') or event.get('body', '') - cur.execute( - "UPDATE notes SET title=%s, body=%s WHERE id=%s" - " RETURNING id, title, body, created_at::text", - (title, body, int(id_)) - ) - row = cur.fetchone() - conn.commit() - return dict(row) if row else {'error': 'not found'} - - elif action == 'delete': - id_ = q.get('id') or event.get('id') - if not id_: - return {'error': 'id is required'} - cur.execute( - "DELETE FROM notes WHERE id=%s RETURNING id", - (int(id_),) - ) - row = cur.fetchone() - conn.commit() - return {'deleted': row['id']} if row else {'error': 'not found'} - - else: - return { - 'error': f'unknown action: /{action}', - 'hint': 'use /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X' - } - - except Exception as e: - conn.rollback() - return {'error': str(e)} - finally: - conn.close() diff --git a/notes-python/code/notes/requirements.txt b/notes-python/code/notes/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/notes-python/code/notes/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/notes-python/code/sql-runner/requirements.txt b/notes-python/code/sql-runner/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/notes-python/code/sql-runner/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/notes-python/code/sql-runner/sql_runner.py b/notes-python/code/sql-runner/sql_runner.py deleted file mode 100644 index fae7675..0000000 --- a/notes-python/code/sql-runner/sql_runner.py +++ /dev/null @@ -1,39 +0,0 @@ -# 2026-03-09 -# sql_runner.py — универсальный DDL/SQL исполнитель. -# -# Назначение: выполнять произвольные SQL запросы переданные через event. -# Используется ТОЛЬКО через sless_job (init.tf) — HTTP-триггера нет намеренно, -# чтобы никто снаружи не мог выполнить произвольный SQL. -# -# Входящий event: -# { -# "statements": [ -# "CREATE TABLE IF NOT EXISTS ...", -# "CREATE INDEX IF NOT EXISTS ..." -# ] -# } -# -# Все statements выполняются последовательно в одной транзакции. -# Если хотя бы один упал — транзакция откатывается целиком. -import os -import psycopg2 - - -def run_sql(event): - dsn = os.environ['PG_DSN'] - statements = event.get('statements', []) - if not statements: - return {'error': 'no statements provided'} - - conn = psycopg2.connect(dsn) - try: - cur = conn.cursor() - for sql in statements: - cur.execute(sql) - conn.commit() - return {'ok': True, 'executed': len(statements)} - except Exception as e: - conn.rollback() - return {'error': str(e)} - finally: - conn.close() diff --git a/notes-python/init.tf b/notes-python/init.tf deleted file mode 100644 index 85cf5fb..0000000 --- a/notes-python/init.tf +++ /dev/null @@ -1,44 +0,0 @@ -# 2026-03-09 -# init.tf — однократная инициализация схемы БД через sless_job. -# -# Джобы запускаются один раз при terraform apply и ждут завершения. -# Использует функцию sql_runner (без HTTP-триггера) для безопасного DDL. -# -# Порядок выполнения гарантирован через depends_on: -# 1. notes_table_init — создаём таблицу -# 2. notes_index_init — создаём индекс (требует таблицу) -# -# Для повторного запуска (например, после DROP TABLE) — увеличь run_id. -# run_id отслеживается в state: при изменении terraform перезапустит джоб. - -# Джоб создания таблицы notes. -# CREATE TABLE IF NOT EXISTS — безопасно запускать повторно, таблица не пересоздаётся. -resource "sless_job" "notes_table_init" { - name = "notes-create-table" - function = sless_function.sql_runner.name - wait_timeout_sec = 120 - run_id = 1 - - event_json = jsonencode({ - statements = [ - "CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text NOT NULL, body text, created_at timestamp DEFAULT now())" - ] - }) -} - -# Джоб создания индекса для сортировки по дате. -# depends_on гарантирует, что таблица уже создана до создания индекса. -resource "sless_job" "notes_index_init" { - depends_on = [sless_job.notes_table_init] - - name = "notes-create-index" - function = sless_function.sql_runner.name - wait_timeout_sec = 60 - run_id = 1 - - event_json = jsonencode({ - statements = [ - "CREATE INDEX IF NOT EXISTS notes_created_idx ON notes(created_at DESC)" - ] - }) -} diff --git a/notes-python/main.tf b/notes-python/main.tf deleted file mode 100644 index 21762bb..0000000 --- a/notes-python/main.tf +++ /dev/null @@ -1,27 +0,0 @@ -# 2026-03-09 -# main.tf — конфигурация terraform и провайдеров. -# -# Все ресурсы вынесены в отдельные .tf файлы по назначению: -# variables.tf — входные переменные (pg_dsn) -# sql-runner.tf — служебная DDL-функция (без HTTP-триггера) -# init.tf — однократная инициализация схемы БД -# notes.tf — CRUD функция + HTTP-триггер -# notes-list.tf — read-only функция + HTTP-триггер -# outputs.tf — URLs развёрнутых эндпоинтов - -terraform { - required_providers { - # Провайдер для управления serverless функциями через sless API - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -# sless провайдер подключается к API кластера. -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} diff --git a/notes-python/notes-list.tf b/notes-python/notes-list.tf deleted file mode 100644 index ced284b..0000000 --- a/notes-python/notes-list.tf +++ /dev/null @@ -1,22 +0,0 @@ -# 2026-03-09 -# notes-list.tf — read-only эндпоинт: возвращает все заметки, сортировка новые первые. - -resource "sless_function" "notes_list" { - name = "notes-list" - runtime = "python3.11" - entrypoint = "notes_list.list_notes" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PG_DSN = var.pg_dsn - } - - source_dir = "${path.module}/code/notes-list" -} - -resource "sless_trigger" "notes_list_http" { - name = "notes-list-http" - type = "http" - function = sless_function.notes_list.name -} diff --git a/notes-python/notes.tf b/notes-python/notes.tf deleted file mode 100644 index 7830dc3..0000000 --- a/notes-python/notes.tf +++ /dev/null @@ -1,27 +0,0 @@ -# 2026-03-09 -# notes.tf — CRUD функция для управления заметками (CREATE / UPDATE / DELETE). -# -# Маршруты (рекомендуется POST): -# /fn/default/notes/add?title=...&body=... → INSERT -# /fn/default/notes/update?id=1&title=...&body=... → UPDATE -# /fn/default/notes/delete?id=1 → DELETE - -resource "sless_function" "notes_crud" { - name = "notes" - runtime = "python3.11" - entrypoint = "notes_crud.crud" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PG_DSN = var.pg_dsn - } - - source_dir = "${path.module}/code/notes" -} - -resource "sless_trigger" "notes_crud_http" { - name = "notes-http" - type = "http" - function = sless_function.notes_crud.name -} diff --git a/notes-python/outputs.tf b/notes-python/outputs.tf deleted file mode 100644 index 4e7f6d3..0000000 --- a/notes-python/outputs.tf +++ /dev/null @@ -1,42 +0,0 @@ -# 2026-03-09 -# outputs.tf — публичные URL развёрнутых функций. -# -# После terraform apply используй эти URLs для тестирования: -# terraform output notes_url → базовый URL для CRUD -# terraform output notes_list_url → URL для получения всех записей - -# URL CRUD-функции (notes_crud). -# Базовый URL — к нему добавляй sub-path: -# POST $(terraform output -raw notes_url)/add?title=Hello&body=World -# POST $(terraform output -raw notes_url)/update?id=1&title=Updated -# POST $(terraform output -raw notes_url)/delete?id=1 -output "notes_url" { - value = sless_trigger.notes_crud_http.url - description = "CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X" -} - -# URL read-only функции (notes_list). -# Принимает GET или POST, параметры игнорирует, возвращает все записи. -output "notes_list_url" { - value = sless_trigger.notes_list_http.url - description = "Список всех записей (GET или POST)" -} - -# Статус init-джобов — показывает результат инициализации БД. -# Если phase="Succeeded" — таблица и индекс созданы успешно. -# Если phase="Failed" — смотри message, исправь и увеличь run_id в init.tf. -output "db_init_table_status" { - value = { - phase = sless_job.notes_table_init.phase - message = sless_job.notes_table_init.message - } - description = "Статус джоба создания таблицы notes" -} - -output "db_init_index_status" { - value = { - phase = sless_job.notes_index_init.phase - message = sless_job.notes_index_init.message - } - description = "Статус джоба создания индекса" -} diff --git a/notes-python/sql-runner.tf b/notes-python/sql-runner.tf deleted file mode 100644 index 29be82d..0000000 --- a/notes-python/sql-runner.tf +++ /dev/null @@ -1,20 +0,0 @@ -# 2026-03-09 -# sql-runner.tf — служебная DDL-функция для инициализации и миграций БД. -# -# ВАЖНО: эта функция не имеет HTTP-триггера — только вызов через sless_job. -# Это сделано намеренно: функция выполняет произвольный SQL, и открывать её -# наружу через HTTP было бы небезопасно. - -resource "sless_function" "sql_runner" { - name = "sql-runner" - runtime = "python3.11" - entrypoint = "sql_runner.run_sql" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PG_DSN = var.pg_dsn - } - - source_dir = "${path.module}/code/sql-runner" -} diff --git a/notes-python/terraform.tfvars.change b/notes-python/terraform.tfvars.change deleted file mode 100644 index 6daebb5..0000000 --- a/notes-python/terraform.tfvars.change +++ /dev/null @@ -1 +0,0 @@ -token = "замени на Nubes API token" и переименуй файл в terraform.tfvars diff --git a/notes-python/variables.tf b/notes-python/variables.tf deleted file mode 100644 index 0e0b3bb..0000000 --- a/notes-python/variables.tf +++ /dev/null @@ -1,23 +0,0 @@ -# 2026-03-09 (обновлён 2026-03-11) -# variables.tf — входные переменные для notes-python примера. -# -# PG_DSN передаётся во все функции через env_vars. -# Хранится как sensitive чтобы не светился в terraform output и логах. -# В продакшне — не хардкоди DSN здесь, используй TF_VAR_pg_dsn или secrets manager. - -# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). -# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} -variable "token" { - description = "JWT токен облака для аутентификации в sless API" - type = string - sensitive = true -} - -# DSN для подключения к PostgreSQL внутри кластера. -# Формат: postgres://user:password@host:port/dbname?sslmode=... -variable "pg_dsn" { - description = "PostgreSQL DSN для подключения к БД внутри кластера" - type = string - default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" - sensitive = true -} diff --git a/pg-list-python/README.md b/pg-list-python/README.md deleted file mode 100644 index 3124f95..0000000 --- a/pg-list-python/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# pg-list-python - -Python-функция, которая читает из PostgreSQL и возвращает JSON. Таблица с демо-данными создаётся автоматически при первом вызове — никакой инициализации руками. - -## Что тут есть - -``` -code/ - catalog.py — функция list_products(event): SELECT из demo_products - requirements.txt — psycopg2-binary - -function.tf — sless_function + sless_trigger + output -main.tf — конфигурация провайдера -variables.tf — token, pg_dsn -terraform.tfvars — значения переменных (не в git) -``` - -## Запуск - -**1. Впишите токен в `terraform.tfvars`:** - -```hcl -# terraform.tfvars -token = "ваш токен Nubes API" # ← заменить на реальный токен из личного кабинета Nubes -``` - -**2. Деплой:** - -```bash -terraform init -terraform apply -auto-approve - -curl -s $(terraform output -raw catalog_url) -``` - -Ответ: - -```json -{ - "products": [ - {"id": 1, "name": "Ноутбук", "price": 89999.0}, - {"id": 2, "name": "Мышь", "price": 1299.0}, - {"id": 3, "name": "Клавиатура", "price": 3499.0}, - {"id": 4, "name": "Монитор", "price": 32000.0} - ], - "count": 4 -} -``` - -## Переменные - -| Переменная | Обязательна | Описание | -|---|---|---| -| `token` | ✅ | JWT-токен облака. Задаётся в `terraform.tfvars` | -| `pg_dsn` | — | DSN подключения к PostgreSQL. Дефолт — внутрикластерный адрес | - -`terraform.tfvars` не коммитится в git. Минимальное содержимое: - -```hcl -token = "ваш токен Nubes API" -``` - -## Как работает - -``` -terraform apply - → загружает код в S3 - → kaniko собирает Docker-образ (pip install psycopg2-binary) - → Deployment поднимается в кластере - → Ingress публикует URL - -GET /fn//product-catalog - → рантайм вызывает list_products(event) - → psycopg2 подключается к PostgreSQL - → если таблица demo_products не существует — создаёт и заполняет демо-данными - → возвращает JSON со списком -``` - -Таблица `demo_products` создаётся один раз — при первом GET. Повторные вызовы просто читают данные. - -## Изменение данных - -Функция только читает. Чтобы добавить/изменить записи — подключитесь к PostgreSQL напрямую: - -```sql -INSERT INTO demo_products (name, price) VALUES ('Кабель', 499.00); -UPDATE demo_products SET price = 95000 WHERE name = 'Ноутбук'; -``` - -## Пересборка если изменили код - -```bash -terraform apply -replace=sless_function.product_catalog -auto-approve -``` - -## Удаление - -```bash -terraform destroy -auto-approve -``` - -Таблица `demo_products` в PostgreSQL **не удаляется** — только k8s-ресурсы. diff --git a/pg-list-python/code/catalog.py b/pg-list-python/code/catalog.py deleted file mode 100644 index 2095879..0000000 --- a/pg-list-python/code/catalog.py +++ /dev/null @@ -1,47 +0,0 @@ -# 2026-03-11 -# catalog.py — читает список продуктов из PostgreSQL. -# Таблица demo_products создаётся автоматически при первом вызове. -# Точка входа: list_products(event) - -import json -import os -import psycopg2 - - -def list_products(event): - dsn = os.environ["PG_DSN"] - conn = psycopg2.connect(dsn) - try: - with conn.cursor() as cur: - _ensure_table(cur) - conn.commit() - - cur.execute("SELECT id, name, price FROM demo_products ORDER BY id") - rows = cur.fetchall() - finally: - conn.close() - - products = [{"id": row[0], "name": row[1], "price": float(row[2])} for row in rows] - return {"products": products, "count": len(products)} - - -def _ensure_table(cur): - # Создаём таблицу и наполняем демо-данными — только один раз - cur.execute(""" - CREATE TABLE IF NOT EXISTS demo_products ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - price NUMERIC(10,2) NOT NULL - ) - """) - cur.execute("SELECT COUNT(*) FROM demo_products") - if cur.fetchone()[0] == 0: - cur.executemany( - "INSERT INTO demo_products (name, price) VALUES (%s, %s)", - [ - ("Ноутбук", 89999.00), - ("Мышь", 1299.00), - ("Клавиатура", 3499.00), - ("Монитор", 32000.00), - ], - ) diff --git a/pg-list-python/code/requirements.txt b/pg-list-python/code/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/pg-list-python/code/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/pg-list-python/function.tf b/pg-list-python/function.tf deleted file mode 100644 index df1f7c2..0000000 --- a/pg-list-python/function.tf +++ /dev/null @@ -1,29 +0,0 @@ -# 2026-03-11 -# function.tf — HTTP-функция: читает из PostgreSQL и возвращает список записей. -# Нет джобов, нет инициализации — только функция + HTTP триггер. - -resource "sless_function" "product_catalog" { # объявляем serverless-функцию; "product_catalog" — локальное имя в tf-state - name = "product-catalog" # имя функции в кластере; по нему формируется URL и имя k8s-объекта - runtime = "python3.11" # базовый образ рантайма; определяет как собирается и запускается код - entrypoint = "catalog.list_products" # файл.функция которую вызывает рантайм: catalog.py → def list_products(event) - memory_mb = 128 # лимит памяти пода в мегабайтах - timeout_sec = 10 # максимальное время выполнения одного запроса в секундах - - source_dir = "${path.module}/code" # директория с кодом функции; провайдер упакует её в zip и загрузит - - # DSN передаётся через env — функция не знает об инфраструктуре - env_vars = { - PG_DSN = var.pg_dsn # строка подключения к PostgreSQL; берётся из переменной (variables.tf) - } -} - -resource "sless_trigger" "product_catalog_http" { # триггер публикует функцию наружу; без него функция существует, но недоступна - name = "product-catalog-http" # имя триггера в кластере - type = "http" # тип триггера: "http" создаёт публичный URL; альтернатива — "cron" - function = sless_function.product_catalog.name # ссылка на имя функции выше; terraform гарантирует порядок создания - enabled = true # триггер активен сразу после создания -} - -output "catalog_url" { - value = sless_trigger.product_catalog_http.url # URL вида https://sless-api.../fn//; выводится после apply -} diff --git a/pg-list-python/main.tf b/pg-list-python/main.tf deleted file mode 100644 index 871034b..0000000 --- a/pg-list-python/main.tf +++ /dev/null @@ -1,17 +0,0 @@ -# 2026-03-11 -# main.tf — провайдер для pg-list-python примера. - -terraform { - required_providers { - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} diff --git a/pg-list-python/terraform.tfvars.change b/pg-list-python/terraform.tfvars.change deleted file mode 100644 index 6daebb5..0000000 --- a/pg-list-python/terraform.tfvars.change +++ /dev/null @@ -1 +0,0 @@ -token = "замени на Nubes API token" и переименуй файл в terraform.tfvars diff --git a/pg-list-python/variables.tf b/pg-list-python/variables.tf deleted file mode 100644 index ab59478..0000000 --- a/pg-list-python/variables.tf +++ /dev/null @@ -1,14 +0,0 @@ -# 2026-03-11 -# variables.tf - -variable "token" { - description = "JWT токен облака" - type = string - sensitive = true -} - -variable "pg_dsn" { - description = "DSN подключения к PostgreSQL" - type = string - default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" -} diff --git a/simple-node/code/time_display/time_display.js b/simple-node/code/time_display/time_display.js deleted file mode 100644 index a711b3e..0000000 --- a/simple-node/code/time_display/time_display.js +++ /dev/null @@ -1,20 +0,0 @@ -// Создано: 2026-03-09 -// time_display.js — HTTP-функция (постоянный Deployment + Trigger). -// Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message. -// Демонстрирует цепочку: Job вычисляет данные → Function использует их через env. - -exports.showTime = function(event) { - // JOB_TIME устанавливается terraform из статуса джоба (JSON строка) - const jobTimeRaw = process.env.JOB_TIME || '{}'; - let jobTime; - try { - const parsed = JSON.parse(jobTimeRaw); - jobTime = parsed.time || jobTimeRaw; - } catch (e) { - jobTime = jobTimeRaw; - } - return { - message: `Сервис запустился в: ${jobTime}`, - path: event._path || '/', - }; -}; diff --git a/simple-node/code/time_getter/time_getter.js b/simple-node/code/time_getter/time_getter.js deleted file mode 100644 index a750c17..0000000 --- a/simple-node/code/time_getter/time_getter.js +++ /dev/null @@ -1,10 +0,0 @@ -// Создано: 2026-03-09 -// time_getter.js — функция запускается как Job (одноразово). -// Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout -// и записывает его в sless_job.run_getter.message — оттуда terraform передаёт -// значение в sless_function.display через env_var JOB_TIME. - -exports.getTime = function(event) { - // Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib - return { time: new Date().toISOString() }; -}; diff --git a/simple-node/main.tf b/simple-node/main.tf deleted file mode 100644 index af95879..0000000 --- a/simple-node/main.tf +++ /dev/null @@ -1,31 +0,0 @@ -# Создано: 2026-03-09 -# main.tf — пример: запустить один раз скрипт при деплое и передать его результат в функцию. -# То же самое что simple-python, но на Node.js 20. -# -# Как это работает: -# 1. При «terraform apply» запускается скрипт-джоб (time_getter) -# 2. Скрипт возвращает JSON с текущим временем -# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display) -# 4. Функция отдаёт время при каждом запросе -# -# Зачем такое нужно: -# Если данные нужны функции, но считаются один раз при деплое — -# напишите логику в джоб, а результат передайте через env_vars. -# Например: получить токен, версию схемы БД, время деплоя и т.д. -# -# namespace захардкодирован внутри провайдера, здесь ничего указывать. - -terraform { - required_providers { - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} diff --git a/simple-node/outputs.tf b/simple-node/outputs.tf deleted file mode 100644 index 4d59ef6..0000000 --- a/simple-node/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -# Создано: 2026-03-09 -# outputs.tf — что выводит terraform после apply. - -# Адрес вашей функции — откройте в браузере или вставьте в curl -output "display_url" { - description = "URL функции time_display" - value = sless_trigger.display_http.url -} - -# Что вернул скрипт-джоб — именно это передано в функцию как JOB_TIME -output "job_result" { - description = "Результат выполнения скрипта time_getter" - value = sless_job.run_getter.message -} diff --git a/simple-node/terraform.tfvars.change b/simple-node/terraform.tfvars.change deleted file mode 100644 index 6daebb5..0000000 --- a/simple-node/terraform.tfvars.change +++ /dev/null @@ -1 +0,0 @@ -token = "замени на Nubes API token" и переименуй файл в terraform.tfvars diff --git a/simple-node/time-display.tf b/simple-node/time-display.tf deleted file mode 100644 index 7d29bfe..0000000 --- a/simple-node/time-display.tf +++ /dev/null @@ -1,28 +0,0 @@ -# Создано: 2026-03-09 / Изменено: 2026-03-09 -# time-display.tf — HTTP-функция, доступная по URL после apply. -# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME. - -# HTTP-функция — отвечает на запросы по URL из outputs.tf -resource "sless_function" "time_display" { - name = "simple-node-time-display" # уникальное имя в namespace - runtime = "nodejs20" - entrypoint = "time_display.showTime" # файл.функция в code/time_display/ - memory_mb = 64 - - # Передаём результат джоба в функцию через переменную окружения. - # В коде функции: process.env.JOB_TIME - env_vars = { - JOB_TIME = sless_job.run_getter.message - } - - source_dir = "${path.module}/code/time_display" - - depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции -} - -# Публикуем функцию по HTTP — URL будет в outputs.tf -resource "sless_trigger" "display_http" { - name = "simple-node-display-http" - type = "http" - function = sless_function.time_display.name -} diff --git a/simple-node/time-getter.tf b/simple-node/time-getter.tf deleted file mode 100644 index 0075410..0000000 --- a/simple-node/time-getter.tf +++ /dev/null @@ -1,27 +0,0 @@ -# Создано: 2026-03-09 / Изменено: 2026-03-09 -# time-getter.tf — скрипт который запускается ОДИН РАЗ при terraform apply. -# После запуска его результат доступен через: sless_job.run_getter.message -# Смотри time-display.tf — там этот результат передаётся в функцию. - -# Функция для скрипта — без HTTP-триггера, вызывается только через джоб ниже -resource "sless_function" "time_getter" { - name = "simple-node-time-getter" # уникальное имя в namespace - runtime = "nodejs20" - entrypoint = "time_getter.getTime" # файл.функция в code/time_getter/ - memory_mb = 128 - - source_dir = "${path.module}/code/time_getter" -} - -# Джоб — запускает функцию time_getter один раз прямо при apply. -# run_id = 1 означает «запустить». Если увеличить (2, 3...) — запустится снова. -# После завершения: sless_job.run_getter.message = то что вернула функция -resource "sless_job" "run_getter" { - name = "simple-node-getter-run" - function = sless_function.time_getter.name - run_id = 1 - wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта - event_json = "{}" # входные данные для скрипта (пусто — данные не нужны) - - depends_on = [sless_function.time_getter] -} diff --git a/simple-node/variables.tf b/simple-node/variables.tf deleted file mode 100644 index 2057582..0000000 --- a/simple-node/variables.tf +++ /dev/null @@ -1,10 +0,0 @@ -# 2026-03-11 -# variables.tf — входные переменные для simple-node примера. - -# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). -# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} -variable "token" { - description = "JWT токен облака для аутентификации в sless API" - type = string - sensitive = true -} diff --git a/simple-python/code/time_display/time_display.py b/simple-python/code/time_display/time_display.py deleted file mode 100644 index 789cd44..0000000 --- a/simple-python/code/time_display/time_display.py +++ /dev/null @@ -1,20 +0,0 @@ -# Создано: 2026-03-09 -# time_display.py — HTTP-функция (постоянный Deployment + Trigger). -# Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message. -# Демонстрирует цепочку: Job вычисляет данные → Function использует их через env. - -import json -import os - - -def show_time(event): - # JOB_TIME устанавливается terraform из статуса джоба (JSON строка) - job_time_raw = os.environ.get("JOB_TIME", "{}") - try: - job_time = json.loads(job_time_raw).get("time", job_time_raw) - except (json.JSONDecodeError, AttributeError): - job_time = job_time_raw - return { - "message": f"Сервис запустился в: {job_time}", - "path": event.get("_path", "/"), - } diff --git a/simple-python/code/time_getter/time_getter.py b/simple-python/code/time_getter/time_getter.py deleted file mode 100644 index 05c47f6..0000000 --- a/simple-python/code/time_getter/time_getter.py +++ /dev/null @@ -1,18 +0,0 @@ -# Создано: 2026-03-09 -# time_getter.py — функция запускается как Job (одноразово). -# Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout -# и записывает его в sless_job.run_getter.message — оттуда terraform передаёт -# значение в sless_function.display через env_var JOB_TIME. - -import json -from datetime import datetime, timezone - - -def get_time(event): - # Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib - return {"time": datetime.now(timezone.utc).isoformat()} - - -if __name__ == "__main__": - # Для локального тестирования без оператора - print(json.dumps(get_time({}))) diff --git a/simple-python/main.tf b/simple-python/main.tf deleted file mode 100644 index e55c11a..0000000 --- a/simple-python/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -# Создано: 2026-03-09 -# main.tf — пример: запустить один раз скрипт при деплое и передать его результат в функцию. -# -# Как это работает: -# 1. При «terraform apply» запускается скрипт-джоб (time_getter) -# 2. Скрипт возвращает JSON с текущим временем -# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display) -# 4. Функция отдаёт время при каждом запросе -# -# Зачем такое нужно: -# Если данные нужны функции, но считаются один раз при деплое — -# напишите логику в джоб, а результат передайте через env_vars. -# Например: получить токен, версию схемы БД, время деплоя и т.д. -# -# namespace захардкодирован внутри провайдера, здесь ничего указывать. - -terraform { - required_providers { - sless = { - source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.18" - } - } -} - -provider "sless" { - endpoint = "https://sless.kube5s.ru" - token = var.token - nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" -} diff --git a/simple-python/outputs.tf b/simple-python/outputs.tf deleted file mode 100644 index 4d59ef6..0000000 --- a/simple-python/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -# Создано: 2026-03-09 -# outputs.tf — что выводит terraform после apply. - -# Адрес вашей функции — откройте в браузере или вставьте в curl -output "display_url" { - description = "URL функции time_display" - value = sless_trigger.display_http.url -} - -# Что вернул скрипт-джоб — именно это передано в функцию как JOB_TIME -output "job_result" { - description = "Результат выполнения скрипта time_getter" - value = sless_job.run_getter.message -} diff --git a/simple-python/terraform.tfvars.change b/simple-python/terraform.tfvars.change deleted file mode 100644 index 6daebb5..0000000 --- a/simple-python/terraform.tfvars.change +++ /dev/null @@ -1 +0,0 @@ -token = "замени на Nubes API token" и переименуй файл в terraform.tfvars diff --git a/simple-python/time-display.tf b/simple-python/time-display.tf deleted file mode 100644 index c5db376..0000000 --- a/simple-python/time-display.tf +++ /dev/null @@ -1,28 +0,0 @@ -# Создано: 2026-03-09 / Изменено: 2026-03-09 -# time-display.tf — HTTP-функция, доступная по URL после apply. -# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME. - -# HTTP-функция — отвечает на запросы по URL из outputs.tf -resource "sless_function" "time_display" { - name = "simple-py-time-display" # уникальное имя в namespace - runtime = "python3.11" - entrypoint = "time_display.show_time" # файл.функция в code/time_display/ - memory_mb = 64 - - # Передаём результат джоба в функцию через переменную окружения. - # В коде функции: os.environ.get("JOB_TIME") - env_vars = { - JOB_TIME = sless_job.run_getter.message - } - - source_dir = "${path.module}/code/time_display" - - depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции -} - -# Публикуем функцию по HTTP — URL будет в outputs.tf -resource "sless_trigger" "display_http" { - name = "simple-py-display-http" - type = "http" - function = sless_function.time_display.name -} diff --git a/simple-python/time-getter.tf b/simple-python/time-getter.tf deleted file mode 100644 index 970697f..0000000 --- a/simple-python/time-getter.tf +++ /dev/null @@ -1,27 +0,0 @@ -# Создано: 2026-03-09 / Изменено: 2026-03-09 -# time-getter.tf — скрипт который запускается ОДИН РАЗ при terraform apply. -# После запуска его результат доступен через: sless_job.run_getter.message -# Смотри time-display.tf — там этот результат передаётся в функцию. - -# Функция для скрипта — без HTTP-триггера, вызывается только через джоб ниже -resource "sless_function" "time_getter" { - name = "simple-py-time-getter" # уникальное имя в namespace - runtime = "python3.11" - entrypoint = "time_getter.get_time" # файл.функция в code/time_getter/ - memory_mb = 128 - - source_dir = "${path.module}/code/time_getter" -} - -# Джоб — запускает функцию time_getter один раз прямо при apply. -# run_id = 1 означает «запустить». Если увеличить (2, 3...) — запустится снова. -# После завершения: sless_job.run_getter.message = то что вернула функция -resource "sless_job" "run_getter" { - name = "simple-py-getter-run" - function = sless_function.time_getter.name - run_id = 1 - wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта - event_json = "{}" # входные данные для скрипта (пусто — данные не нужны) - - depends_on = [sless_function.time_getter] -} diff --git a/simple-python/variables.tf b/simple-python/variables.tf deleted file mode 100644 index dac71bd..0000000 --- a/simple-python/variables.tf +++ /dev/null @@ -1,10 +0,0 @@ -# 2026-03-11 -# variables.tf — входные переменные для simple-python примера. - -# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). -# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} -variable "token" { - description = "JWT токен облака для аутентификации в sless API" - type = string - sensitive = true -}