From 4e59d79884d09949df536999b5e945d02321f9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 7 Mar 2026 10:30:50 +0400 Subject: [PATCH] feat: add pg-query example + upload.go supports requirements.txt - examples/pg-query/handler.py: Python function querying PostgreSQL invocations table - examples/pg-query/requirements.txt: psycopg2-binary==2.9.9 - examples/pg-query/main.tf: terraform config for sless_function + sless_trigger - internal/api/handler/upload.go: generateDockerfile() now accepts hasRequirements bool - scans zip for requirements.txt at upload time - adds RUN pip install --no-cache-dir to Dockerfile when requirements.txt present - doc/progress.md: updated status for pg-query e2e task --- doc/progress.md | 2 +- examples/pg-query/handler.py | 39 ++++++++++++++++++ examples/pg-query/main.tf | 64 ++++++++++++++++++++++++++++++ examples/pg-query/requirements.txt | 1 + internal/api/handler/upload.go | 21 +++++++++- 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 examples/pg-query/handler.py create mode 100644 examples/pg-query/main.tf create mode 100644 examples/pg-query/requirements.txt diff --git a/doc/progress.md b/doc/progress.md index 4a2e3dc..1e959ad 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -79,7 +79,7 @@ | # | Компонент | Статус | Заметки | |---|-----------|--------|---------| -| 1 | terraform apply e2e — функция с postgres | ⏳ | функция делает SELECT к postgres.sless.svc | +| 1 | terraform apply e2e — функция с postgres | 🔄 В процессе | examples/pg-query/{handler.py,requirements.txt,main.tf} созданы; upload.go поддерживает requirements.txt; поднять среду + terraform apply | | 2 | HTTP trigger e2e тест | ⏳ | Service+Ingress через TriggerController | | 3 | deployments/k8s/operator.yaml | ⏳ | Deployment оператора в кластере | | 4 | Перенос ресурсов sless в nubes провайдер | ⏳ | после e2e тестирования | diff --git a/examples/pg-query/handler.py b/examples/pg-query/handler.py new file mode 100644 index 0000000..8007e2a --- /dev/null +++ b/examples/pg-query/handler.py @@ -0,0 +1,39 @@ +# 2026-03-07 +# handler.py — пример serverless функции, работающей с PostgreSQL. +# Подключается к postgres.sless.svc.cluster.local:5432 (внутри кластера). +# DSN берётся из env переменной PG_DSN (задаётся через env_vars ресурса sless_function). +# handle(event) — принимает JSON event, возвращает список записей из таблицы invocations. +import os +import json + +def handle(event): + # psycopg2 входит в python:3.11-slim через pip install ниже, + # либо нужно добавить в Dockerfile — для этого примера устанавливаем через requirements.txt. + try: + import psycopg2 + import psycopg2.extras + except ImportError: + return {"error": "psycopg2 not installed — add requirements.txt with psycopg2-binary"} + + dsn = os.environ.get("PG_DSN", "") + if not dsn: + return {"error": "PG_DSN env variable is not set"} + + limit = event.get("limit", 5) + + conn = psycopg2.connect(dsn) + try: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT id, namespace, function_name, status, started_at FROM invocations ORDER BY started_at DESC LIMIT %s", (limit,)) + rows = cur.fetchall() + # RealDictCursor возвращает объекты, сериализуем вручную + result = [] + for row in rows: + r = dict(row) + # datetime → str + if r.get("started_at"): + r["started_at"] = str(r["started_at"]) + result.append(r) + return {"invocations": result, "count": len(result)} + finally: + conn.close() diff --git a/examples/pg-query/main.tf b/examples/pg-query/main.tf new file mode 100644 index 0000000..a545b2f --- /dev/null +++ b/examples/pg-query/main.tf @@ -0,0 +1,64 @@ +# 2026-03-07 +# main.tf — e2e тест: создать serverless функцию, которая читает из PostgreSQL. +# +# Использование: +# 1. Убедиться что оператор запущен локально: source hack/local.env && go run main.go +# 2. zip handler.py + requirements.txt: +# zip handler.zip handler.py requirements.txt +# 3. terraform init && terraform apply +# 4. После apply (ждёт ~2 мин пока kaniko соберёт образ): +# curl -s -X POST -d '{}' +# +# Функция подключается к postgres.sless.svc.cluster.local:5432 изнутри кластера. +# PG_DSN задаётся как env_var ресурса sless_function. + +terraform { + required_providers { + sless = { + source = "terra.k8c.ru/naeel/sless" + version = "~> 0.1" + } + } +} + +provider "sless" { + endpoint = "http://localhost:9090" + token = "dev-token-change-me" +} + +resource "sless_function" "pg_query" { + namespace = "default" + name = "pg-query" + runtime = "python3.11" + entrypoint = "handler.handle" + memory_mb = 128 + timeout_sec = 30 + + # DSN для подключения к postgres внутри кластера + # Хост: postgres.sless.svc.cluster.local (Service в namespace sless) + env_vars = { + PG_DSN = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" + } + + code_path = "${path.module}/handler.zip" + code_hash = filemd5("${path.module}/handler.zip") +} + +resource "sless_trigger" "pg_query_http" { + namespace = "default" + name = "pg-query-http" + type = "http" + function = sless_function.pg_query.name +} + +output "function_phase" { + value = sless_function.pg_query.phase +} + +output "function_image" { + value = sless_function.pg_query.image_ref +} + +output "trigger_url" { + value = sless_trigger.pg_query_http.url +} diff --git a/examples/pg-query/requirements.txt b/examples/pg-query/requirements.txt new file mode 100644 index 0000000..58ab769 --- /dev/null +++ b/examples/pg-query/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary==2.9.9 diff --git a/internal/api/handler/upload.go b/internal/api/handler/upload.go index 64520cd..95244e6 100644 --- a/internal/api/handler/upload.go +++ b/internal/api/handler/upload.go @@ -40,12 +40,18 @@ func runtimeBaseImage(runtime string) (string, error) { // generateDockerfile генерирует Dockerfile для kaniko. // Базовый образ содержит HTTP-обёртку (server.py). // Пользовательский код копируется в /app/function/ поверх базового образа. -func generateDockerfile(runtime string) ([]byte, error) { +// Если в zip есть requirements.txt — добавляем pip install (для python runtime). +func generateDockerfile(runtime string, hasRequirements bool) ([]byte, error) { baseImage, err := runtimeBaseImage(runtime) if err != nil { return nil, err } content := fmt.Sprintf("FROM %s\nCOPY . /app/function/\n", baseImage) + // pip install только для python runtime и только если requirements.txt есть в zip. + // Делаем это ПОСЛЕ COPY чтобы воспользоваться кешем слоёв Docker при повторных сборках. + if hasRequirements && runtime == "python3.11" { + content += "RUN pip install --no-cache-dir -r /app/function/requirements.txt\n" + } return []byte(content), nil } @@ -145,8 +151,19 @@ func (h *Handler) UploadCode(w http.ResponseWriter, r *http.Request) { return } + // Проверяем есть ли requirements.txt в zip (для python runtime — pip install) + hasRequirements := false + if zr, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData))); err == nil { + for _, f := range zr.File { + if f.Name == "requirements.txt" { + hasRequirements = true + break + } + } + } + // Генерируем Dockerfile под runtime функции - dockerfileContent, err := generateDockerfile(fn.Spec.Runtime) + dockerfileContent, err := generateDockerfile(fn.Spec.Runtime, hasRequirements) if err != nil { writeJSON(w, http.StatusBadRequest, errResp(err.Error())) return