feat: notes-python CRUD example + runtime path/query forwarding
- invoke.go: forward sub-path and query string to function pods - server.js v0.1.2: add _path, _query, _method to event - server.py v0.1.1: add _path, _query, _method to event - upload.go: bump runtime versions (nodejs20:v0.1.2, python3.11:v0.1.1) - examples/notes-python: CRUD notes via sub-path routing - sql-runner: generic SQL executor for DDL jobs - notes: CRUD router (/add, /update, /delete) - notes-list: SELECT all notes - init.tf: create TABLE + INDEX on apply
This commit is contained in:
parent
7e3f45176b
commit
daf750e89d
22
notes-python/code/notes-list/handler.py
Normal file
22
notes-python/code/notes-list/handler.py
Normal file
@ -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()
|
||||
1
notes-python/code/notes-list/requirements.txt
Normal file
1
notes-python/code/notes-list/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
74
notes-python/code/notes/handler.py
Normal file
74
notes-python/code/notes/handler.py
Normal file
@ -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()
|
||||
1
notes-python/code/notes/requirements.txt
Normal file
1
notes-python/code/notes/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
26
notes-python/code/sql-runner/handler.py
Normal file
26
notes-python/code/sql-runner/handler.py
Normal file
@ -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()
|
||||
1
notes-python/code/sql-runner/requirements.txt
Normal file
1
notes-python/code/sql-runner/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
BIN
notes-python/dist/notes-list.zip
vendored
Normal file
BIN
notes-python/dist/notes-list.zip
vendored
Normal file
Binary file not shown.
BIN
notes-python/dist/notes.zip
vendored
Normal file
BIN
notes-python/dist/notes.zip
vendored
Normal file
Binary file not shown.
BIN
notes-python/dist/sql-runner.zip
vendored
Normal file
BIN
notes-python/dist/sql-runner.zip
vendored
Normal file
Binary file not shown.
34
notes-python/init.tf
Normal file
34
notes-python/init.tf
Normal file
@ -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)"
|
||||
]
|
||||
})
|
||||
}
|
||||
21
notes-python/main.tf
Normal file
21
notes-python/main.tf
Normal file
@ -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"
|
||||
}
|
||||
32
notes-python/notes-list.tf
Normal file
32
notes-python/notes-list.tf
Normal file
@ -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
|
||||
}
|
||||
35
notes-python/notes.tf
Normal file
35
notes-python/notes.tf
Normal file
@ -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
|
||||
}
|
||||
12
notes-python/outputs.tf
Normal file
12
notes-python/outputs.tf
Normal file
@ -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)"
|
||||
}
|
||||
26
notes-python/sql-runner.tf
Normal file
26
notes-python/sql-runner.tf
Normal file
@ -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")
|
||||
}
|
||||
9
notes-python/variables.tf
Normal file
9
notes-python/variables.tf
Normal file
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user