chore(examples): remove stale examples, keep only POSTGRES
- Deleted: TNAR, demo-event-log, demo-managed-functions, hello-go, hello-node, k8s, notes-python, pg-list-python, simple-node, simple-python - POSTGRES: removed luceUNDnode.tf (commented-out legacy), stress_log_1.txt, funcs_list.py; disabled stress_destroy_apply.sh (PG lifecycle stress has delete_user bug); added README.md - examples/README.md: updated to reflect current state (sless_service + sless_job)
This commit is contained in:
parent
4fa1e71cf2
commit
4b04cde84b
100
POSTGRES/README.md
Normal file
100
POSTGRES/README.md
Normal file
@ -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 в определённых сценариях.
|
||||
@ -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();
|
||||
|
||||
38
POSTGRES/code/pg-stats/pg_stats.py
Normal file
38
POSTGRES/code/pg-stats/pg_stats.py
Normal file
@ -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()
|
||||
0
POSTGRES/code/stress-bigloop/requirements.txt
Normal file
0
POSTGRES/code/stress-bigloop/requirements.txt
Normal file
20
POSTGRES/code/stress-bigloop/stress_bigloop.py
Normal file
20
POSTGRES/code/stress-bigloop/stress_bigloop.py
Normal file
@ -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,
|
||||
}
|
||||
0
POSTGRES/code/stress-divzero/requirements.txt
Normal file
0
POSTGRES/code/stress-divzero/requirements.txt
Normal file
13
POSTGRES/code/stress-divzero/stress_divzero.py
Normal file
13
POSTGRES/code/stress-divzero/stress_divzero.py
Normal file
@ -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}
|
||||
43
POSTGRES/code/stress-go-fast/handler.go
Normal file
43
POSTGRES/code/stress-go-fast/handler.go
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
21
POSTGRES/code/stress-go-nil/handler.go
Normal file
21
POSTGRES/code/stress-go-nil/handler.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
148
POSTGRES/code/stress-go-pgstorm/handler.go
Normal file
148
POSTGRES/code/stress-go-pgstorm/handler.go
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
7
POSTGRES/code/stress-js-async/package.json
Normal file
7
POSTGRES/code/stress-js-async/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "stress-js-async",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"pg": "^8.11.0"
|
||||
}
|
||||
}
|
||||
37
POSTGRES/code/stress-js-async/stress_js_async.js
Normal file
37
POSTGRES/code/stress-js-async/stress_js_async.js
Normal file
@ -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();
|
||||
}
|
||||
};
|
||||
5
POSTGRES/code/stress-js-badenv/package.json
Normal file
5
POSTGRES/code/stress-js-badenv/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "stress-js-badenv",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {}
|
||||
}
|
||||
17
POSTGRES/code/stress-js-badenv/stress_js_badenv.js
Normal file
17
POSTGRES/code/stress-js-badenv/stress_js_badenv.js
Normal file
@ -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 };
|
||||
};
|
||||
0
POSTGRES/code/stress-slow/requirements.txt
Normal file
0
POSTGRES/code/stress-slow/requirements.txt
Normal file
18
POSTGRES/code/stress-slow/stress_slow.py
Normal file
18
POSTGRES/code/stress-slow/stress_slow.py
Normal file
@ -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(),
|
||||
}
|
||||
1
POSTGRES/code/stress-writer/requirements.txt
Normal file
1
POSTGRES/code/stress-writer/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary==2.9.9
|
||||
39
POSTGRES/code/stress-writer/stress_writer.py
Normal file
39
POSTGRES/code/stress-writer/stress_writer.py
Normal file
@ -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)}
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
105
POSTGRES/functions.tf
Normal file
105
POSTGRES/functions.tf
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
58
POSTGRES/postgres.tf
Normal file
58
POSTGRES/postgres.tf
Normal file
@ -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
|
||||
}
|
||||
@ -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/<namespace>/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/<namespace>/pg-table-reader
|
||||
# https://sless.kube5s.ru/fn/<namespace>/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
|
||||
|
||||
46
POSTGRES/stress_destroy_apply.sh.disabled
Executable file
46
POSTGRES/stress_destroy_apply.sh.disabled
Executable file
@ -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 итераций прошли успешно ==="
|
||||
57
POSTGRES/stress_test.sh
Normal file
57
POSTGRES/stress_test.sh
Normal file
@ -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 ==="
|
||||
169
README.md
169
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>/<имя-функции>`.
|
||||
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"
|
||||
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/<namespace>/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/<namespace>/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/<namespace>/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
|
||||
```
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
requests==2.31.0
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "pg-info",
|
||||
"version": "1.0.0",
|
||||
"description": "sless nodejs20 function: pg version + table info",
|
||||
"dependencies": {
|
||||
"pg": "8.11.0"
|
||||
}
|
||||
}
|
||||
@ -1,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();
|
||||
}
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
# 2026-03-17 00:00
|
||||
# requirements.txt — зависимости для функции запуска SQL.
|
||||
psycopg2-binary==2.9.9
|
||||
@ -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()
|
||||
@ -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"<tr><td>{r['id']}</td><td>{r['title']}</td><td>{r['created_at']}</td></tr>"
|
||||
for r in rows
|
||||
)
|
||||
msg_html = f'<p class="msg">{message}</p>' if message else ""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>pg-table-writer</title>
|
||||
<style>
|
||||
body {{ font-family: sans-serif; max-width: 700px; margin: 40px auto; background: #111; color: #eee; }}
|
||||
h1 {{ color: #7dd3fc; }}
|
||||
form {{ display: flex; gap: 8px; margin-bottom: 24px; }}
|
||||
input[type=text] {{ flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #1e1e1e; color: #eee; font-size: 15px; }}
|
||||
button {{ padding: 8px 18px; background: #2563eb; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; }}
|
||||
button:hover {{ background: #1d4ed8; }}
|
||||
table {{ width: 100%; border-collapse: collapse; }}
|
||||
th, td {{ padding: 8px 10px; border-bottom: 1px solid #333; text-align: left; }}
|
||||
th {{ color: #7dd3fc; }}
|
||||
.msg {{ color: #4ade80; margin-bottom: 12px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>pg-table-writer</h1>
|
||||
<form method="POST">
|
||||
<input type="text" name="title" placeholder="Введите строку..." autofocus required>
|
||||
<button type="submit">Добавить</button>
|
||||
</form>
|
||||
{msg_html}
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>title</th><th>created_at</th></tr></thead>
|
||||
<tbody>{rows_html}</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
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()
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
58
TNAR/main.tf
58
TNAR/main.tf
@ -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"
|
||||
}
|
||||
|
||||
@ -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/<namespace>/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/<namespace>/pg-table-reader
|
||||
# https://sless.kube5s.ru/fn/<namespace>/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
|
||||
}
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
# 2026-03-18 — debug pod для проверки psql-соединения из namespace функций.
|
||||
# Запускается разово. Подключается к тому же postgres, что и sless_function.
|
||||
# kubectl apply -f /tmp/pg-debug-pod.yaml
|
||||
# kubectl logs -n sless-fn-sless-ffd1f598c169b0ae pg-debug-pod
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pg-debug-pod
|
||||
namespace: sless-fn-sless-ffd1f598c169b0ae
|
||||
labels:
|
||||
purpose: debug-postgres-connectivity
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: psql
|
||||
image: postgres:17-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Testing TCP connectivity to postgres ==="
|
||||
nc -zv -w5 $PGHOST 5432 && echo "TCP OK" || echo "TCP FAILED"
|
||||
|
||||
echo ""
|
||||
echo "=== Testing psql connection ==="
|
||||
PGCONNECT_TIMEOUT=10 psql \
|
||||
"host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \
|
||||
--command="SELECT current_user, current_database(), version();" \
|
||||
2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Listing tables ==="
|
||||
PGCONNECT_TIMEOUT=10 psql \
|
||||
"host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \
|
||||
--command="\dt" \
|
||||
2>&1
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: "postgresqlk8s-master.36875359-dcea-48c4-a593-b4531f20fe96.svc.cluster.local"
|
||||
- name: PGPORT
|
||||
value: "5432"
|
||||
- name: PGDATABASE
|
||||
value: "db_terra"
|
||||
- name: PGUSER
|
||||
value: "u-user0"
|
||||
- name: PGPASSWORD
|
||||
# Актуальный пароль из vault_secrets (совпадает с tfvars.pg_password на 2026-03-18)
|
||||
value: "M03O6fRsngWcVHB2YGivyLfbfxoii2R21nyh2A2r7WSZS5deLwBgLKkc9Wk24Zyl"
|
||||
- name: PGSSLMODE
|
||||
value: "require"
|
||||
@ -1,40 +0,0 @@
|
||||
# 2026-03-17 13:05
|
||||
# read_pg_user_secret.py — читает пароль пользователя managed PostgreSQL из k8s Secret.
|
||||
# Используется из Terraform external data source, чтобы apply сам получал актуальный пароль
|
||||
# даже для уже существующего пользователя, созданного вне текущего state.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
# Читаем query от Terraform external provider из stdin.
|
||||
query = json.load(sys.stdin)
|
||||
namespace = query["namespace"]
|
||||
secret_name = query["secret"]
|
||||
|
||||
# kubectl уже настроен на удалённой машине; читаем ровно поле data.password.
|
||||
result = subprocess.run(
|
||||
[
|
||||
"kubectl",
|
||||
"get",
|
||||
"secret",
|
||||
"-n",
|
||||
namespace,
|
||||
secret_name,
|
||||
"-o",
|
||||
"jsonpath={.data.password}",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
password = base64.b64decode(result.stdout.strip()).decode()
|
||||
json.dump({"password": password}, sys.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,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()
|
||||
@ -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"}
|
||||
@ -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()
|
||||
@ -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/<namespace>/<имя-функции>`.
|
||||
|
||||
---
|
||||
|
||||
## Требования
|
||||
|
||||
- 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/<namespace>/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/<namespace>/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/<namespace>/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/ — исходный код функций
|
||||
```
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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,
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
@ -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"
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
|
||||
@ -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
|
||||
}
|
||||
@ -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 !!!` };
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
|
||||
@ -1,2 +0,0 @@
|
||||
# Временный файл для негативных тестов — не применяется через terraform
|
||||
# Тесты запускаются вручную с временным переименованием в .tf
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
@ -1 +0,0 @@
|
||||
psycopg2-binary
|
||||
@ -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()
|
||||
@ -1 +0,0 @@
|
||||
psycopg2-binary
|
||||
@ -1 +0,0 @@
|
||||
psycopg2-binary
|
||||
@ -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()
|
||||
@ -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)"
|
||||
]
|
||||
})
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 = "Статус джоба создания индекса"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
|
||||
@ -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
|
||||
}
|
||||
@ -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/<namespace>/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-ресурсы.
|
||||
@ -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),
|
||||
],
|
||||
)
|
||||
@ -1 +0,0 @@
|
||||
psycopg2-binary
|
||||
@ -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/<namespace>/<name>; выводится после apply
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
|
||||
@ -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"
|
||||
}
|
||||
@ -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 || '/',
|
||||
};
|
||||
};
|
||||
@ -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() };
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
|
||||
@ -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
|
||||
}
|
||||
@ -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]
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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", "/"),
|
||||
}
|
||||
@ -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({})))
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user