From 9293985003f44757a99ea2597ae866487ecfce0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 9 Mar 2026 17:57:29 +0400 Subject: [PATCH] init0 --- .gitignore | 42 ++++++ README.md | 141 ++++++++++++++++++ hello-node/code/handler-http.js | 8 + hello-node/code/handler-job.js | 11 ++ hello-node/http.tf | 24 +++ hello-node/job.tf | 34 +++++ hello-node/main.tf | 20 +++ hello-node/test_invalid.tf.disabled | 2 + notes-python/code/notes-list/notes_list.py | 31 ++++ notes-python/code/notes-list/requirements.txt | 1 + notes-python/code/notes/notes_crud.py | 81 ++++++++++ notes-python/code/notes/requirements.txt | 1 + notes-python/code/sql-runner/requirements.txt | 1 + notes-python/code/sql-runner/sql_runner.py | 39 +++++ notes-python/init.tf | 44 ++++++ notes-python/main.tf | 27 ++++ notes-python/notes-list.tf | 22 +++ notes-python/notes.tf | 27 ++++ notes-python/outputs.tf | 42 ++++++ notes-python/sql-runner.tf | 20 +++ notes-python/variables.tf | 15 ++ simple-node/code/time_display/time_display.js | 20 +++ simple-node/code/time_getter/time_getter.js | 10 ++ simple-node/main.tf | 30 ++++ simple-node/outputs.tf | 14 ++ simple-node/time-display.tf | 28 ++++ simple-node/time-getter.tf | 27 ++++ .../code/time_display/time_display.py | 20 +++ simple-python/code/time_getter/time_getter.py | 18 +++ simple-python/main.tf | 29 ++++ simple-python/outputs.tf | 14 ++ simple-python/time-display.tf | 28 ++++ simple-python/time-getter.tf | 27 ++++ 33 files changed, 898 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 hello-node/code/handler-http.js create mode 100644 hello-node/code/handler-job.js create mode 100644 hello-node/http.tf create mode 100644 hello-node/job.tf create mode 100644 hello-node/main.tf create mode 100644 hello-node/test_invalid.tf.disabled create mode 100644 notes-python/code/notes-list/notes_list.py create mode 100644 notes-python/code/notes-list/requirements.txt create mode 100644 notes-python/code/notes/notes_crud.py create mode 100644 notes-python/code/notes/requirements.txt create mode 100644 notes-python/code/sql-runner/requirements.txt create mode 100644 notes-python/code/sql-runner/sql_runner.py create mode 100644 notes-python/init.tf create mode 100644 notes-python/main.tf create mode 100644 notes-python/notes-list.tf create mode 100644 notes-python/notes.tf create mode 100644 notes-python/outputs.tf create mode 100644 notes-python/sql-runner.tf create mode 100644 notes-python/variables.tf create mode 100644 simple-node/code/time_display/time_display.js create mode 100644 simple-node/code/time_getter/time_getter.js create mode 100644 simple-node/main.tf create mode 100644 simple-node/outputs.tf create mode 100644 simple-node/time-display.tf create mode 100644 simple-node/time-getter.tf create mode 100644 simple-python/code/time_display/time_display.py create mode 100644 simple-python/code/time_getter/time_getter.py create mode 100644 simple-python/main.tf create mode 100644 simple-python/outputs.tf create mode 100644 simple-python/time-display.tf create mode 100644 simple-python/time-getter.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad1391c --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ + +# S3 конфиги с кредами — не коммитим +.s3cfg* + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +hack/local.env +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +terraform/provider/build/ + +# Terraform state и кэш провайдеров (не коммитим) +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +**/terraform.tfstate.*.backup +**/handler.zip + +# Локальные артефакты упаковки функций +**/dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bf4e0e --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Примеры sless + +Примеры показывают различные сценарии использования serverless функций через Terraform провайдер `terra.k8c.ru/naeel/sless`. + +## Требования + +- Terraform >= 1.0 +- Доступ к `https://sless-api.kube5s.ru` + +## Провайдер + +Во всех примерах `main.tf` содержит: + +```hcl +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" +} +``` + +--- + +## Примеры + +### `simple-python` — джоб передаёт результат в HTTP-функцию (Python) + +При `apply` запускается джоб, его вывод передаётся в HTTP-функцию через `env_vars`. + +```bash +cd simple-python +terraform init +terraform apply -auto-approve + +# Что вернул джоб (время на момент деплоя): +terraform output job_result + +# Проверить функцию: +curl -s https://sless-api.kube5s.ru/fn/default/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-api.kube5s.ru/fn/default/simple-node-time-display +``` + +--- + +### `hello-node` — минимальный пример на Node.js + +Две независимые функции: HTTP-функция (возвращает приветствие) и одноразовый джоб (суммирует числа). + +```bash +cd hello-node +terraform init +terraform apply -auto-approve + +# Проверить HTTP-функцию: +curl -s -X POST https://sless-api.kube5s.ru/fn/default/hello-http \ + -H 'Content-Type: application/json' -d '{"name":"World"}' + +# Посмотреть результат джоба: +terraform output job_message +``` + +--- + +### `notes-python` — CRUD API на Python + PostgreSQL + +Полноценное приложение: инициализация схемы БД через джобы, CRUD-функция, read-only функция для списка записей. + +**Переменные:** + +| Переменная | Описание | Дефолт | +|---|---|---| +| `pg_dsn` | DSN для подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` | + +```bash +cd notes-python +terraform init + +# Опционально — переопределить DSN: +# export TF_VAR_pg_dsn="postgres://user:pass@host:5432/db?sslmode=disable" + +terraform apply -auto-approve + +# Проверить инициализацию БД: +terraform output db_init_table_status +terraform output db_init_index_status + +# URL функций: +terraform output notes_url # CRUD +terraform output notes_list_url # список всех записей + +# Создать запись: +curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/add?title=Hello&body=World" + +# Список записей: +curl -s https://sless-api.kube5s.ru/fn/default/notes-list + +# Обновить (id из предыдущего ответа): +curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/update?id=1&title=Updated&body=New+body" + +# Удалить: +curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/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 — ресурсы (функции, триггеры, джобы) +├── outputs.tf — URLs и статусы после apply +├── variables.tf — входные переменные (если есть) +└── code/ — исходный код функций +``` diff --git a/hello-node/code/handler-http.js b/hello-node/code/handler-http.js new file mode 100644 index 0000000..16bc1a1 --- /dev/null +++ b/hello-node/code/handler-http.js @@ -0,0 +1,8 @@ +// 2026-03-08 +// handler-http.js — HTTP-функция: возвращает приветствие. +// Используется с sless_trigger (постоянный эндпоинт). +exports.handle = async (event) => { + const name = event.name || 'World'; + return { message: `Hello, ${name}! HTTP !!!` }; +}; + diff --git a/hello-node/code/handler-job.js b/hello-node/code/handler-job.js new file mode 100644 index 0000000..23abed8 --- /dev/null +++ b/hello-node/code/handler-job.js @@ -0,0 +1,11 @@ +// 2026-03-08 +// handler-job.js — batch-функция: суммирует числа и считает среднее. +// Используется с sless_job (одноразовый запуск). +// event.numbers — массив чисел, например [1, 2, 3, 4, 5] +exports.handle = async (event) => { + const numbers = event.numbers || []; + const sum = numbers.reduce((acc, n) => acc + n, 0); + const avg = numbers.length > 0 ? sum / numbers.length : 0; + return { input: numbers, sum, avg, count: numbers.length }; +}; + diff --git a/hello-node/http.tf b/hello-node/http.tf new file mode 100644 index 0000000..beee105 --- /dev/null +++ b/hello-node/http.tf @@ -0,0 +1,24 @@ +# 2026-03-08 / Изменено: 2026-03-09 +# http.tf — HTTP-функция: принимает запросы, возвращает приветствие. +# Код: code/handler-http.js + +resource "sless_function" "hello_http" { + name = "hello-http" + runtime = "nodejs20" + entrypoint = "handler-http.handle" + memory_mb = 128 + timeout_sec = 30 + + source_dir = "${path.module}/code" +} + +resource "sless_trigger" "hello_http" { + name = "hello-http-trigger" + type = "http" + function = sless_function.hello_http.name + enabled = true +} + +output "trigger_url" { + value = sless_trigger.hello_http.url +} diff --git a/hello-node/job.tf b/hello-node/job.tf new file mode 100644 index 0000000..843d02f --- /dev/null +++ b/hello-node/job.tf @@ -0,0 +1,34 @@ +# 2026-03-08 / Изменено: 2026-03-09 +# job.tf — одноразовая функция: суммирует числа из переданного массива. +# Код: code/handler-job.js + +resource "sless_function" "hello_job" { + name = "hello-job" + runtime = "nodejs20" + entrypoint = "handler-job.handle" + memory_mb = 128 + timeout_sec = 30 + + source_dir = "${path.module}/code" +} + +# Одноразовый запуск. Для повторного запуска увеличь run_id (1→2→3...). +resource "sless_job" "hello_run" { + name = "hello-run" + function = sless_function.hello_job.name + event_json = jsonencode({ numbers = [100, 200, 300] }) + wait_timeout_sec = 600 + run_id = 9 +} + +output "job_phase" { + value = sless_job.hello_run.phase +} + +output "job_message" { + value = sless_job.hello_run.message +} + +output "job_completion_time" { + value = sless_job.hello_run.completion_time +} diff --git a/hello-node/main.tf b/hello-node/main.tf new file mode 100644 index 0000000..cfb00b4 --- /dev/null +++ b/hello-node/main.tf @@ -0,0 +1,20 @@ +# 2026-03-08 +# main.tf — провайдеры. +# Функции и их код определены в отдельных файлах: +# http.tf — HTTP-триггер (code/handler-http.js) +# job.tf — одноразовый запуск (code/handler-job.js) + +terraform { + required_providers { + sless = { + source = "terra.k8c.ru/naeel/sless" + version = "~> 0.1.10" + } + } +} + +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" + token = "dev-token-change-me" +} + diff --git a/hello-node/test_invalid.tf.disabled b/hello-node/test_invalid.tf.disabled new file mode 100644 index 0000000..7c5e5eb --- /dev/null +++ b/hello-node/test_invalid.tf.disabled @@ -0,0 +1,2 @@ +# Временный файл для негативных тестов — не применяется через terraform +# Тесты запускаются вручную с временным переименованием в .tf diff --git a/notes-python/code/notes-list/notes_list.py b/notes-python/code/notes-list/notes_list.py new file mode 100644 index 0000000..4e63bf2 --- /dev/null +++ b/notes-python/code/notes-list/notes_list.py @@ -0,0 +1,31 @@ +# 2026-03-09 +# notes_list.py — чтение всех записей из таблицы notes. +# +# Назначение: отдать полный список заметок одним запросом. +# Принимает GET или POST — тело/query параметры игнорируются. +# Возвращает JSON-массив, сортировка: новые записи первые (ORDER BY created_at DESC). +# +# Пример ответа: +# [ +# {"id": 3, "title": "Hello", "body": "World", "created_at": "2026-03-09 ..."}, +# {"id": 1, "title": "First", "body": "Note", "created_at": "2026-03-08 ..."} +# ] +import os +import psycopg2 +import psycopg2.extras + + +def list_notes(event): + dsn = os.environ['PG_DSN'] + conn = psycopg2.connect(dsn) + try: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "SELECT id, title, body, created_at::text FROM notes ORDER BY created_at DESC" + ) + rows = cur.fetchall() + return [dict(r) for r in rows] + except Exception as e: + return {'error': str(e)} + finally: + conn.close() diff --git a/notes-python/code/notes-list/requirements.txt b/notes-python/code/notes-list/requirements.txt new file mode 100644 index 0000000..37ec460 --- /dev/null +++ b/notes-python/code/notes-list/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary diff --git a/notes-python/code/notes/notes_crud.py b/notes-python/code/notes/notes_crud.py new file mode 100644 index 0000000..07b2592 --- /dev/null +++ b/notes-python/code/notes/notes_crud.py @@ -0,0 +1,81 @@ +# 2026-03-09 +# notes_crud.py — CRUD роутер для таблицы notes. +# +# Назначение: единая функция, которая обрабатывает все операции с записями. +# Роутинг осуществляется по sub-path URL (event._path), который runtime +# берёт из входящего HTTP-запроса и добавляет в event автоматически. +# +# Доступные маршруты (все POST): +# /fn/default/notes/add?title=...&body=... → создать запись +# /fn/default/notes/update?id=1&title=...&body=... → обновить запись +# /fn/default/notes/delete?id=1 → удалить запись +# +# Параметры берутся из query string (event._query) или из тела запроса (event). +# event._path и event._query добавляет Python runtime (server.py) автоматически. +import os +import psycopg2 +import psycopg2.extras + + +def crud(event): + dsn = os.environ['PG_DSN'] + # sub-path без ведущего слэша: "add", "update", "delete" + action = event.get('_path', '/').strip('/') + q = event.get('_query', {}) + + conn = psycopg2.connect(dsn) + try: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + if action == 'add': + title = q.get('title') or event.get('title', '') + body = q.get('body') or event.get('body', '') + if not title: + return {'error': 'title is required'} + cur.execute( + "INSERT INTO notes (title, body) VALUES (%s, %s)" + " RETURNING id, title, body, created_at::text", + (title, body) + ) + row = cur.fetchone() + conn.commit() + return dict(row) + + elif action == 'update': + id_ = q.get('id') or event.get('id') + if not id_: + return {'error': 'id is required'} + title = q.get('title') or event.get('title', '') + body = q.get('body') or event.get('body', '') + cur.execute( + "UPDATE notes SET title=%s, body=%s WHERE id=%s" + " RETURNING id, title, body, created_at::text", + (title, body, int(id_)) + ) + row = cur.fetchone() + conn.commit() + return dict(row) if row else {'error': 'not found'} + + elif action == 'delete': + id_ = q.get('id') or event.get('id') + if not id_: + return {'error': 'id is required'} + cur.execute( + "DELETE FROM notes WHERE id=%s RETURNING id", + (int(id_),) + ) + row = cur.fetchone() + conn.commit() + return {'deleted': row['id']} if row else {'error': 'not found'} + + else: + return { + 'error': f'unknown action: /{action}', + 'hint': 'use /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X' + } + + except Exception as e: + conn.rollback() + return {'error': str(e)} + finally: + conn.close() diff --git a/notes-python/code/notes/requirements.txt b/notes-python/code/notes/requirements.txt new file mode 100644 index 0000000..37ec460 --- /dev/null +++ b/notes-python/code/notes/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary diff --git a/notes-python/code/sql-runner/requirements.txt b/notes-python/code/sql-runner/requirements.txt new file mode 100644 index 0000000..37ec460 --- /dev/null +++ b/notes-python/code/sql-runner/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary diff --git a/notes-python/code/sql-runner/sql_runner.py b/notes-python/code/sql-runner/sql_runner.py new file mode 100644 index 0000000..fae7675 --- /dev/null +++ b/notes-python/code/sql-runner/sql_runner.py @@ -0,0 +1,39 @@ +# 2026-03-09 +# sql_runner.py — универсальный DDL/SQL исполнитель. +# +# Назначение: выполнять произвольные SQL запросы переданные через event. +# Используется ТОЛЬКО через sless_job (init.tf) — HTTP-триггера нет намеренно, +# чтобы никто снаружи не мог выполнить произвольный SQL. +# +# Входящий event: +# { +# "statements": [ +# "CREATE TABLE IF NOT EXISTS ...", +# "CREATE INDEX IF NOT EXISTS ..." +# ] +# } +# +# Все statements выполняются последовательно в одной транзакции. +# Если хотя бы один упал — транзакция откатывается целиком. +import os +import psycopg2 + + +def run_sql(event): + dsn = os.environ['PG_DSN'] + statements = event.get('statements', []) + if not statements: + return {'error': 'no statements provided'} + + conn = psycopg2.connect(dsn) + try: + cur = conn.cursor() + for sql in statements: + cur.execute(sql) + conn.commit() + return {'ok': True, 'executed': len(statements)} + except Exception as e: + conn.rollback() + return {'error': str(e)} + finally: + conn.close() diff --git a/notes-python/init.tf b/notes-python/init.tf new file mode 100644 index 0000000..85cf5fb --- /dev/null +++ b/notes-python/init.tf @@ -0,0 +1,44 @@ +# 2026-03-09 +# init.tf — однократная инициализация схемы БД через sless_job. +# +# Джобы запускаются один раз при terraform apply и ждут завершения. +# Использует функцию sql_runner (без HTTP-триггера) для безопасного DDL. +# +# Порядок выполнения гарантирован через depends_on: +# 1. notes_table_init — создаём таблицу +# 2. notes_index_init — создаём индекс (требует таблицу) +# +# Для повторного запуска (например, после DROP TABLE) — увеличь run_id. +# run_id отслеживается в state: при изменении terraform перезапустит джоб. + +# Джоб создания таблицы notes. +# CREATE TABLE IF NOT EXISTS — безопасно запускать повторно, таблица не пересоздаётся. +resource "sless_job" "notes_table_init" { + name = "notes-create-table" + function = sless_function.sql_runner.name + wait_timeout_sec = 120 + run_id = 1 + + event_json = jsonencode({ + statements = [ + "CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text NOT NULL, body text, created_at timestamp DEFAULT now())" + ] + }) +} + +# Джоб создания индекса для сортировки по дате. +# depends_on гарантирует, что таблица уже создана до создания индекса. +resource "sless_job" "notes_index_init" { + depends_on = [sless_job.notes_table_init] + + name = "notes-create-index" + function = sless_function.sql_runner.name + wait_timeout_sec = 60 + run_id = 1 + + event_json = jsonencode({ + statements = [ + "CREATE INDEX IF NOT EXISTS notes_created_idx ON notes(created_at DESC)" + ] + }) +} diff --git a/notes-python/main.tf b/notes-python/main.tf new file mode 100644 index 0000000..505e104 --- /dev/null +++ b/notes-python/main.tf @@ -0,0 +1,27 @@ +# 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.10" + } + } +} + +# sless провайдер подключается к API кластера. +# В продакшне token следует передавать через TF_VAR или secrets. +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" + token = "dev-token-change-me" +} diff --git a/notes-python/notes-list.tf b/notes-python/notes-list.tf new file mode 100644 index 0000000..ced284b --- /dev/null +++ b/notes-python/notes-list.tf @@ -0,0 +1,22 @@ +# 2026-03-09 +# notes-list.tf — read-only эндпоинт: возвращает все заметки, сортировка новые первые. + +resource "sless_function" "notes_list" { + name = "notes-list" + runtime = "python3.11" + entrypoint = "notes_list.list_notes" + memory_mb = 128 + timeout_sec = 30 + + env_vars = { + PG_DSN = var.pg_dsn + } + + source_dir = "${path.module}/code/notes-list" +} + +resource "sless_trigger" "notes_list_http" { + name = "notes-list-http" + type = "http" + function = sless_function.notes_list.name +} diff --git a/notes-python/notes.tf b/notes-python/notes.tf new file mode 100644 index 0000000..7830dc3 --- /dev/null +++ b/notes-python/notes.tf @@ -0,0 +1,27 @@ +# 2026-03-09 +# notes.tf — CRUD функция для управления заметками (CREATE / UPDATE / DELETE). +# +# Маршруты (рекомендуется POST): +# /fn/default/notes/add?title=...&body=... → INSERT +# /fn/default/notes/update?id=1&title=...&body=... → UPDATE +# /fn/default/notes/delete?id=1 → DELETE + +resource "sless_function" "notes_crud" { + name = "notes" + runtime = "python3.11" + entrypoint = "notes_crud.crud" + memory_mb = 128 + timeout_sec = 30 + + env_vars = { + PG_DSN = var.pg_dsn + } + + source_dir = "${path.module}/code/notes" +} + +resource "sless_trigger" "notes_crud_http" { + name = "notes-http" + type = "http" + function = sless_function.notes_crud.name +} diff --git a/notes-python/outputs.tf b/notes-python/outputs.tf new file mode 100644 index 0000000..4e7f6d3 --- /dev/null +++ b/notes-python/outputs.tf @@ -0,0 +1,42 @@ +# 2026-03-09 +# outputs.tf — публичные URL развёрнутых функций. +# +# После terraform apply используй эти URLs для тестирования: +# terraform output notes_url → базовый URL для CRUD +# terraform output notes_list_url → URL для получения всех записей + +# URL CRUD-функции (notes_crud). +# Базовый URL — к нему добавляй sub-path: +# POST $(terraform output -raw notes_url)/add?title=Hello&body=World +# POST $(terraform output -raw notes_url)/update?id=1&title=Updated +# POST $(terraform output -raw notes_url)/delete?id=1 +output "notes_url" { + value = sless_trigger.notes_crud_http.url + description = "CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X" +} + +# URL read-only функции (notes_list). +# Принимает GET или POST, параметры игнорирует, возвращает все записи. +output "notes_list_url" { + value = sless_trigger.notes_list_http.url + description = "Список всех записей (GET или POST)" +} + +# Статус init-джобов — показывает результат инициализации БД. +# Если phase="Succeeded" — таблица и индекс созданы успешно. +# Если phase="Failed" — смотри message, исправь и увеличь run_id в init.tf. +output "db_init_table_status" { + value = { + phase = sless_job.notes_table_init.phase + message = sless_job.notes_table_init.message + } + description = "Статус джоба создания таблицы notes" +} + +output "db_init_index_status" { + value = { + phase = sless_job.notes_index_init.phase + message = sless_job.notes_index_init.message + } + description = "Статус джоба создания индекса" +} diff --git a/notes-python/sql-runner.tf b/notes-python/sql-runner.tf new file mode 100644 index 0000000..29be82d --- /dev/null +++ b/notes-python/sql-runner.tf @@ -0,0 +1,20 @@ +# 2026-03-09 +# sql-runner.tf — служебная DDL-функция для инициализации и миграций БД. +# +# ВАЖНО: эта функция не имеет HTTP-триггера — только вызов через sless_job. +# Это сделано намеренно: функция выполняет произвольный SQL, и открывать её +# наружу через HTTP было бы небезопасно. + +resource "sless_function" "sql_runner" { + name = "sql-runner" + runtime = "python3.11" + entrypoint = "sql_runner.run_sql" + memory_mb = 128 + timeout_sec = 30 + + env_vars = { + PG_DSN = var.pg_dsn + } + + source_dir = "${path.module}/code/sql-runner" +} diff --git a/notes-python/variables.tf b/notes-python/variables.tf new file mode 100644 index 0000000..d4e0af0 --- /dev/null +++ b/notes-python/variables.tf @@ -0,0 +1,15 @@ +# 2026-03-09 +# variables.tf — входные переменные для notes-python примера. +# +# PG_DSN передаётся во все функции через env_vars. +# Хранится как sensitive чтобы не светился в terraform output и логах. +# В продакшне — не хардкоди DSN здесь, используй TF_VAR_pg_dsn или secrets manager. + +# DSN для подключения к PostgreSQL внутри кластера. +# Формат: postgres://user:password@host:port/dbname?sslmode=... +variable "pg_dsn" { + description = "PostgreSQL DSN для подключения к БД внутри кластера" + type = string + default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" + sensitive = true +} diff --git a/simple-node/code/time_display/time_display.js b/simple-node/code/time_display/time_display.js new file mode 100644 index 0000000..a711b3e --- /dev/null +++ b/simple-node/code/time_display/time_display.js @@ -0,0 +1,20 @@ +// Создано: 2026-03-09 +// time_display.js — HTTP-функция (постоянный Deployment + Trigger). +// Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message. +// Демонстрирует цепочку: Job вычисляет данные → Function использует их через env. + +exports.showTime = function(event) { + // JOB_TIME устанавливается terraform из статуса джоба (JSON строка) + const jobTimeRaw = process.env.JOB_TIME || '{}'; + let jobTime; + try { + const parsed = JSON.parse(jobTimeRaw); + jobTime = parsed.time || jobTimeRaw; + } catch (e) { + jobTime = jobTimeRaw; + } + return { + message: `Сервис запустился в: ${jobTime}`, + path: event._path || '/', + }; +}; diff --git a/simple-node/code/time_getter/time_getter.js b/simple-node/code/time_getter/time_getter.js new file mode 100644 index 0000000..a750c17 --- /dev/null +++ b/simple-node/code/time_getter/time_getter.js @@ -0,0 +1,10 @@ +// Создано: 2026-03-09 +// time_getter.js — функция запускается как Job (одноразово). +// Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout +// и записывает его в sless_job.run_getter.message — оттуда terraform передаёт +// значение в sless_function.display через env_var JOB_TIME. + +exports.getTime = function(event) { + // Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib + return { time: new Date().toISOString() }; +}; diff --git a/simple-node/main.tf b/simple-node/main.tf new file mode 100644 index 0000000..9cb2843 --- /dev/null +++ b/simple-node/main.tf @@ -0,0 +1,30 @@ +# Создано: 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.10" + } + } +} + +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" + token = "dev-token-change-me" +} diff --git a/simple-node/outputs.tf b/simple-node/outputs.tf new file mode 100644 index 0000000..4d59ef6 --- /dev/null +++ b/simple-node/outputs.tf @@ -0,0 +1,14 @@ +# Создано: 2026-03-09 +# outputs.tf — что выводит terraform после apply. + +# Адрес вашей функции — откройте в браузере или вставьте в curl +output "display_url" { + description = "URL функции time_display" + value = sless_trigger.display_http.url +} + +# Что вернул скрипт-джоб — именно это передано в функцию как JOB_TIME +output "job_result" { + description = "Результат выполнения скрипта time_getter" + value = sless_job.run_getter.message +} diff --git a/simple-node/time-display.tf b/simple-node/time-display.tf new file mode 100644 index 0000000..7d29bfe --- /dev/null +++ b/simple-node/time-display.tf @@ -0,0 +1,28 @@ +# Создано: 2026-03-09 / Изменено: 2026-03-09 +# time-display.tf — HTTP-функция, доступная по URL после apply. +# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME. + +# HTTP-функция — отвечает на запросы по URL из outputs.tf +resource "sless_function" "time_display" { + name = "simple-node-time-display" # уникальное имя в namespace + runtime = "nodejs20" + entrypoint = "time_display.showTime" # файл.функция в code/time_display/ + memory_mb = 64 + + # Передаём результат джоба в функцию через переменную окружения. + # В коде функции: process.env.JOB_TIME + env_vars = { + JOB_TIME = sless_job.run_getter.message + } + + source_dir = "${path.module}/code/time_display" + + depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции +} + +# Публикуем функцию по HTTP — URL будет в outputs.tf +resource "sless_trigger" "display_http" { + name = "simple-node-display-http" + type = "http" + function = sless_function.time_display.name +} diff --git a/simple-node/time-getter.tf b/simple-node/time-getter.tf new file mode 100644 index 0000000..426bbcc --- /dev/null +++ b/simple-node/time-getter.tf @@ -0,0 +1,27 @@ +# Создано: 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 = 64 + + source_dir = "${path.module}/code/time_getter" +} + +# Джоб — запускает функцию time_getter один раз прямо при apply. +# run_id = 1 означает «запустить». Если увеличить (2, 3...) — запустится снова. +# После завершения: sless_job.run_getter.message = то что вернула функция +resource "sless_job" "run_getter" { + name = "simple-node-getter-run" + function = sless_function.time_getter.name + run_id = 1 + wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта + event_json = "{}" # входные данные для скрипта (пусто — данные не нужны) + + depends_on = [sless_function.time_getter] +} diff --git a/simple-python/code/time_display/time_display.py b/simple-python/code/time_display/time_display.py new file mode 100644 index 0000000..789cd44 --- /dev/null +++ b/simple-python/code/time_display/time_display.py @@ -0,0 +1,20 @@ +# Создано: 2026-03-09 +# time_display.py — HTTP-функция (постоянный Deployment + Trigger). +# Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message. +# Демонстрирует цепочку: Job вычисляет данные → Function использует их через env. + +import json +import os + + +def show_time(event): + # JOB_TIME устанавливается terraform из статуса джоба (JSON строка) + job_time_raw = os.environ.get("JOB_TIME", "{}") + try: + job_time = json.loads(job_time_raw).get("time", job_time_raw) + except (json.JSONDecodeError, AttributeError): + job_time = job_time_raw + return { + "message": f"Сервис запустился в: {job_time}", + "path": event.get("_path", "/"), + } diff --git a/simple-python/code/time_getter/time_getter.py b/simple-python/code/time_getter/time_getter.py new file mode 100644 index 0000000..05c47f6 --- /dev/null +++ b/simple-python/code/time_getter/time_getter.py @@ -0,0 +1,18 @@ +# Создано: 2026-03-09 +# time_getter.py — функция запускается как Job (одноразово). +# Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout +# и записывает его в sless_job.run_getter.message — оттуда terraform передаёт +# значение в sless_function.display через env_var JOB_TIME. + +import json +from datetime import datetime, timezone + + +def get_time(event): + # Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib + return {"time": datetime.now(timezone.utc).isoformat()} + + +if __name__ == "__main__": + # Для локального тестирования без оператора + print(json.dumps(get_time({}))) diff --git a/simple-python/main.tf b/simple-python/main.tf new file mode 100644 index 0000000..5032776 --- /dev/null +++ b/simple-python/main.tf @@ -0,0 +1,29 @@ +# Создано: 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.10" + } + } +} + +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" + token = "dev-token-change-me" +} diff --git a/simple-python/outputs.tf b/simple-python/outputs.tf new file mode 100644 index 0000000..4d59ef6 --- /dev/null +++ b/simple-python/outputs.tf @@ -0,0 +1,14 @@ +# Создано: 2026-03-09 +# outputs.tf — что выводит terraform после apply. + +# Адрес вашей функции — откройте в браузере или вставьте в curl +output "display_url" { + description = "URL функции time_display" + value = sless_trigger.display_http.url +} + +# Что вернул скрипт-джоб — именно это передано в функцию как JOB_TIME +output "job_result" { + description = "Результат выполнения скрипта time_getter" + value = sless_job.run_getter.message +} diff --git a/simple-python/time-display.tf b/simple-python/time-display.tf new file mode 100644 index 0000000..c5db376 --- /dev/null +++ b/simple-python/time-display.tf @@ -0,0 +1,28 @@ +# Создано: 2026-03-09 / Изменено: 2026-03-09 +# time-display.tf — HTTP-функция, доступная по URL после apply. +# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME. + +# HTTP-функция — отвечает на запросы по URL из outputs.tf +resource "sless_function" "time_display" { + name = "simple-py-time-display" # уникальное имя в namespace + runtime = "python3.11" + entrypoint = "time_display.show_time" # файл.функция в code/time_display/ + memory_mb = 64 + + # Передаём результат джоба в функцию через переменную окружения. + # В коде функции: os.environ.get("JOB_TIME") + env_vars = { + JOB_TIME = sless_job.run_getter.message + } + + source_dir = "${path.module}/code/time_display" + + depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции +} + +# Публикуем функцию по HTTP — URL будет в outputs.tf +resource "sless_trigger" "display_http" { + name = "simple-py-display-http" + type = "http" + function = sless_function.time_display.name +} diff --git a/simple-python/time-getter.tf b/simple-python/time-getter.tf new file mode 100644 index 0000000..fd57615 --- /dev/null +++ b/simple-python/time-getter.tf @@ -0,0 +1,27 @@ +# Создано: 2026-03-09 / Изменено: 2026-03-09 +# time-getter.tf — скрипт который запускается ОДИН РАЗ при terraform apply. +# После запуска его результат доступен через: sless_job.run_getter.message +# Смотри time-display.tf — там этот результат передаётся в функцию. + +# Функция для скрипта — без HTTP-триггера, вызывается только через джоб ниже +resource "sless_function" "time_getter" { + name = "simple-py-time-getter" # уникальное имя в namespace + runtime = "python3.11" + entrypoint = "time_getter.get_time" # файл.функция в code/time_getter/ + memory_mb = 64 + + source_dir = "${path.module}/code/time_getter" +} + +# Джоб — запускает функцию time_getter один раз прямо при apply. +# run_id = 1 означает «запустить». Если увеличить (2, 3...) — запустится снова. +# После завершения: sless_job.run_getter.message = то что вернула функция +resource "sless_job" "run_getter" { + name = "simple-py-getter-run" + function = sless_function.time_getter.name + run_id = 1 + wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта + event_json = "{}" # входные данные для скрипта (пусто — данные не нужны) + + depends_on = [sless_function.time_getter] +}