diff --git a/notes-python/code/notes-list/handler.py b/notes-python/code/notes-list/handler.py new file mode 100644 index 0000000..84a0cf4 --- /dev/null +++ b/notes-python/code/notes-list/handler.py @@ -0,0 +1,22 @@ +# 2026-03-09 +# handler.py — возвращает все записи из таблицы notes. +# GET/POST /fn/default/notes-list → JSON массив записей, сортировка по created_at DESC. +import os +import psycopg2 +import psycopg2.extras + + +def handle(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/handler.py b/notes-python/code/notes/handler.py new file mode 100644 index 0000000..cce026f --- /dev/null +++ b/notes-python/code/notes/handler.py @@ -0,0 +1,74 @@ +# 2026-03-09 +# handler.py — CRUD роутер для таблицы notes. +# Роутинг по event._path (sub-path URL): +# POST /fn/default/notes/add?title=...&body=... → INSERT +# POST /fn/default/notes/update?id=1&title=... → UPDATE +# POST /fn/default/notes/delete?id=1 → DELETE +# _path и _query добавляет runtime из HTTP запроса (server.py). +import os +import psycopg2 +import psycopg2.extras + + +def handle(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/handler.py b/notes-python/code/sql-runner/handler.py new file mode 100644 index 0000000..2b39304 --- /dev/null +++ b/notes-python/code/sql-runner/handler.py @@ -0,0 +1,26 @@ +# 2026-03-09 +# handler.py — универсальный исполнитель SQL запросов. +# Принимает event.statements — массив SQL строк, выполняет последовательно. +# Используется sless_job для DDL операций (CREATE TABLE, миграции и т.д.) +import os +import psycopg2 + + +def handle(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/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/dist/notes-list.zip b/notes-python/dist/notes-list.zip new file mode 100644 index 0000000..1e3b036 Binary files /dev/null and b/notes-python/dist/notes-list.zip differ diff --git a/notes-python/dist/notes.zip b/notes-python/dist/notes.zip new file mode 100644 index 0000000..5402309 Binary files /dev/null and b/notes-python/dist/notes.zip differ diff --git a/notes-python/dist/sql-runner.zip b/notes-python/dist/sql-runner.zip new file mode 100644 index 0000000..b684fdd Binary files /dev/null and b/notes-python/dist/sql-runner.zip differ diff --git a/notes-python/init.tf b/notes-python/init.tf new file mode 100644 index 0000000..a76760f --- /dev/null +++ b/notes-python/init.tf @@ -0,0 +1,34 @@ +# 2025-06-05 +# init.tf — джобы инициализации БД: создание таблицы + индекса. +# Запускаются один раз при terraform apply. +# Для повторного запуска (например после drop) — увеличь run_id. + +resource "sless_job" "create_table" { + namespace = "default" + 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())" + ] + }) +} + +resource "sless_job" "create_index" { + depends_on = [sless_job.create_table] + + namespace = "default" + 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..5f5e71f --- /dev/null +++ b/notes-python/main.tf @@ -0,0 +1,21 @@ +# 2025-06-05 +# main.tf — terraform{} + provider для notes-python примера. +# Ресурсы вынесены в отдельные .tf файлы. + +terraform { + required_providers { + sless = { + source = "terra.k8c.ru/naeel/sless" + version = "~> 0.1.7" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.0" + } + } +} + +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..2dc4fcf --- /dev/null +++ b/notes-python/notes-list.tf @@ -0,0 +1,32 @@ +# 2025-06-05 +# notes-list.tf — функция для получения всех записей из таблицы notes. +# GET или POST /fn/default/notes-list → JSON массив всех записей, сортировка по дате (новые первые). + +data "archive_file" "notes_list" { + type = "zip" + source_dir = "${path.module}/code/notes-list" + output_path = "${path.module}/dist/notes-list.zip" +} + +resource "sless_function" "notes_list" { + namespace = "default" + name = "notes-list" + runtime = "python3.11" + entrypoint = "handler.handle" + memory_mb = 128 + timeout_sec = 30 + + env_vars = { + PG_DSN = var.pg_dsn + } + + code_path = data.archive_file.notes_list.output_path + code_hash = filesha256("${path.module}/code/notes-list/handler.py") +} + +resource "sless_trigger" "notes_list_http" { + namespace = "default" + 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..9d0cb64 --- /dev/null +++ b/notes-python/notes.tf @@ -0,0 +1,35 @@ +# 2025-06-05 +# notes.tf — CRUD функция для таблицы notes. +# Роутинг по sub-path URL: +# POST /fn/default/notes/add?title=...&body=... → INSERT записи +# POST /fn/default/notes/update?id=1&title=...&body=... → UPDATE записи +# POST /fn/default/notes/delete?id=1 → DELETE записи + +data "archive_file" "notes" { + type = "zip" + source_dir = "${path.module}/code/notes" + output_path = "${path.module}/dist/notes.zip" +} + +resource "sless_function" "notes" { + namespace = "default" + name = "notes" + runtime = "python3.11" + entrypoint = "handler.handle" + memory_mb = 128 + timeout_sec = 30 + + env_vars = { + PG_DSN = var.pg_dsn + } + + code_path = data.archive_file.notes.output_path + code_hash = filesha256("${path.module}/code/notes/handler.py") +} + +resource "sless_trigger" "notes_http" { + namespace = "default" + name = "notes-http" + type = "http" + function = sless_function.notes.name +} diff --git a/notes-python/outputs.tf b/notes-python/outputs.tf new file mode 100644 index 0000000..d14f579 --- /dev/null +++ b/notes-python/outputs.tf @@ -0,0 +1,12 @@ +# 2025-06-05 +# outputs.tf — URL эндпоинтов для notes-python примера. + +output "notes_url" { + value = sless_trigger.notes_http.url + description = "Базовый URL CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X" +} + +output "notes_list_url" { + value = sless_trigger.notes_list_http.url + description = "URL для получения всех записей (GET или POST)" +} diff --git a/notes-python/sql-runner.tf b/notes-python/sql-runner.tf new file mode 100644 index 0000000..fe5a56c --- /dev/null +++ b/notes-python/sql-runner.tf @@ -0,0 +1,26 @@ +# 2025-06-05 +# sql-runner.tf — универсальная функция для выполнения SQL запросов. +# Используется джобами для DDL операций (CREATE TABLE, индексы, миграции). +# event.statements — массив SQL строк, выполняются последовательно в одной транзакции. + +data "archive_file" "sql_runner" { + type = "zip" + source_dir = "${path.module}/code/sql-runner" + output_path = "${path.module}/dist/sql-runner.zip" +} + +resource "sless_function" "sql_runner" { + namespace = "default" + name = "sql-runner" + runtime = "python3.11" + entrypoint = "handler.handle" + memory_mb = 128 + timeout_sec = 30 + + env_vars = { + PG_DSN = var.pg_dsn + } + + code_path = data.archive_file.sql_runner.output_path + code_hash = filesha256("${path.module}/code/sql-runner/handler.py") +} diff --git a/notes-python/variables.tf b/notes-python/variables.tf new file mode 100644 index 0000000..d0ffeb4 --- /dev/null +++ b/notes-python/variables.tf @@ -0,0 +1,9 @@ +# 2025-06-05 +# variables.tf — входные переменные для notes-python примера. + +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 +}