diff --git a/doc/api/design.md b/doc/api/design.md index a61c1d7..a6b8e2b 100644 --- a/doc/api/design.md +++ b/doc/api/design.md @@ -1,5 +1,7 @@ # API Design +Последнее обновление: 2026-03-18 + ## Базовый URL ``` @@ -7,15 +9,37 @@ http://:9090/v1 ``` Локально: `http://localhost:9090/v1` -В кластере (будущее): `http://sless-operator.sless.svc.cluster.local:9090/v1` +В кластере: `http://sless-operator.sless.svc.cluster.local:9090/v1` +Публично: `https://sless.kube5s.ru/v1/...` (через Ingress) -**Реализовано** (текущий namespace-aware формат): +**Реализованные эндпоинты:** ``` -/v1/namespaces/{namespace}/functions[/{name}] -/v1/namespaces/{namespace}/functions/{name}/upload -/v1/namespaces/{namespace}/triggers[/{name}] -/v1/namespaces/{namespace}/functions/{name}/invocations +GET /v1/namespaces/{ns}/functions +POST /v1/namespaces/{ns}/functions +GET /v1/namespaces/{ns}/functions/{name} +DELETE /v1/namespaces/{ns}/functions/{name} +POST /v1/namespaces/{ns}/functions/{name}/upload +GET /v1/namespaces/{ns}/triggers +POST /v1/namespaces/{ns}/triggers +GET /v1/namespaces/{ns}/triggers/{name} +DELETE /v1/namespaces/{ns}/triggers/{name} +GET /v1/namespaces/{ns}/functions/{name}/invocations +``` + +**Запланированные эндпоинты (ветка feat/web-console):** + +``` +GET /v1/namespaces/{ns}/functions/{name}/source ← НОВЫЙ: код из S3 zip +PATCH /v1/namespaces/{ns}/triggers/{name} ← НОВЫЙ: enable/disable триггера +``` + +**Глобальный сервис funcs (не оператор):** + +``` +GET https://sless.kube5s.ru/funcs/ ← без токена, plain text / HTML +GET https://sless.kube5s.ru/funcs?token= ← с токеном +GET https://sless.kube5s.ru/health ← liveness probe ``` ## Аутентификация @@ -24,6 +48,11 @@ http://:9090/v1 Authorization: Bearer ``` +Токен — JWT от `auth-api`. Middleware в операторе: +1. Извлекает `sub` из payload (без проверки подписи — доверяет Ingress) +2. Вычисляет namespace: `SHA256(sub)[:8]` hex → `sless-{16 hex символов}` +3. Проверяет что запрошенный `{namespace}` совпадает с вычисленным + ## Ресурсы ### Functions diff --git a/doc/architecture/overview.md b/doc/architecture/overview.md index 05bd707..eea641a 100644 --- a/doc/architecture/overview.md +++ b/doc/architecture/overview.md @@ -1,6 +1,6 @@ # Архитектура системы -Последнее обновление: 2026-03-11 (v0.1.22) +Последнее обновление: 2026-03-18 (v0.1.33 + funcs-service v0.1.3) ## Общее описание @@ -13,6 +13,7 @@ Managed Serverless Functions Service для облачного провайде | Компонент | Технология | Где запущен | |-----------|-----------|-------------| | Operator (API + Controllers) | Go (controller-runtime) | Kubernetes, namespace `sless` | +| funcs-service (глобальная консоль) | Go (net/http) | Kubernetes, namespace `sless` | | PostgreSQL | PostgreSQL 16 | Kubernetes, namespace `sless` | | S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` | | Container Registry | DockerHub (`naeel/`) | внешний | @@ -25,6 +26,44 @@ Managed Serverless Functions Service для облачного провайде > Redis и RabbitMQ — отложены до v2. +## Компонент: funcs-service + +Глобальный HTTP сервис — **одна копия** на весь кластер, для всех пользователей. + +``` +User Browser / curl + └─► https://sless.kube5s.ru/funcs/ + └─► nginx Ingress (sless-funcs-ingress) + └─► sless-funcs-service:8090 (namespace sless) + └─► http://sless-operator.sless.svc.cluster.local:9090/v1/... +``` + +**Файлы:** +- `services/funcs/main.go` — логика +- `services/funcs/Dockerfile` — multi-stage Go → alpine +- `deployments/k8s/funcs-service.yaml` — Deployment + Service + Ingress + +**Env vars сервиса:** +| Переменная | Значение | +|-----------|---------| +| `SLESS_OPERATOR_URL` | `http://sless-operator.sless.svc.cluster.local:9090` | +| `SLESS_EXTERNAL_URL` | `https://sless.kube5s.ru` | +| `SLESS_EXCLUDE` | `event-writer,event-monitor,event-cleaner` | +| `SLESS_SERVICE_TOKEN` | JWT токен (задаётся через `kubectl set env`, не в git) | + +## Хранение кода функций в S3 + +``` +Terraform source_dir (локально) + └─► zip → POST /upload → builder.PrepareContext() + ├─► functions/{ns}/{name}/{ts}.zip ← ИСХОДНЫЙ КОД пользователя + └─► contexts/{ns}/{name}/{ts}.tar.gz ← BUILD CONTEXT для kaniko + └─► Function CRD: spec.s3Key = "contexts/..." + └─► контроллер → kaniko Job → Docker image +``` + +Для web-консоли: `GET /source` читает `functions/{ns}/{name}/{ts}.zip` напрямую. + ## Изоляция пользователей — Namespace per user Каждый пользователь облака получает **отдельный k8s namespace**. diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 0000000..a6b8e2b --- /dev/null +++ b/doc/design.md @@ -0,0 +1,159 @@ +# API Design + +Последнее обновление: 2026-03-18 + +## Базовый URL + +``` +http://:9090/v1 +``` + +Локально: `http://localhost:9090/v1` +В кластере: `http://sless-operator.sless.svc.cluster.local:9090/v1` +Публично: `https://sless.kube5s.ru/v1/...` (через Ingress) + +**Реализованные эндпоинты:** + +``` +GET /v1/namespaces/{ns}/functions +POST /v1/namespaces/{ns}/functions +GET /v1/namespaces/{ns}/functions/{name} +DELETE /v1/namespaces/{ns}/functions/{name} +POST /v1/namespaces/{ns}/functions/{name}/upload +GET /v1/namespaces/{ns}/triggers +POST /v1/namespaces/{ns}/triggers +GET /v1/namespaces/{ns}/triggers/{name} +DELETE /v1/namespaces/{ns}/triggers/{name} +GET /v1/namespaces/{ns}/functions/{name}/invocations +``` + +**Запланированные эндпоинты (ветка feat/web-console):** + +``` +GET /v1/namespaces/{ns}/functions/{name}/source ← НОВЫЙ: код из S3 zip +PATCH /v1/namespaces/{ns}/triggers/{name} ← НОВЫЙ: enable/disable триггера +``` + +**Глобальный сервис funcs (не оператор):** + +``` +GET https://sless.kube5s.ru/funcs/ ← без токена, plain text / HTML +GET https://sless.kube5s.ru/funcs?token= ← с токеном +GET https://sless.kube5s.ru/health ← liveness probe +``` + +## Аутентификация + +``` +Authorization: Bearer +``` + +Токен — JWT от `auth-api`. Middleware в операторе: +1. Извлекает `sub` из payload (без проверки подписи — доверяет Ingress) +2. Вычисляет namespace: `SHA256(sub)[:8]` hex → `sless-{16 hex символов}` +3. Проверяет что запрошенный `{namespace}` совпадает с вычисленным + +## Ресурсы + +### Functions + +| Метод | Путь | Описание | +|-------|------|----------| +| GET | /functions | Список функций | +| POST | /functions | Создать функцию | +| GET | /functions/{id} | Получить функцию | +| PUT | /functions/{id} | Обновить функцию | +| DELETE | /functions/{id} | Удалить функцию | + +### Versions (код функции) + +| Метод | Путь | Описание | +|-------|------|----------| +| GET | /functions/{id}/versions | Список версий | +| POST | /functions/{id}/versions | Загрузить новый код (multipart zip) | +| GET | /functions/{id}/versions/{ver} | Получить версию | +| POST | /functions/{id}/versions/{ver}/activate | Активировать версию | + +### Triggers + +| Метод | Путь | Описание | +|-------|------|----------| +| GET | /functions/{id}/triggers | Список триггеров | +| POST | /functions/{id}/triggers | Создать триггер (HTTP/Cron) | +| DELETE | /functions/{id}/triggers/{tid} | Удалить триггер | + +### Invocations (вызов и логи) + +| Метод | Путь | Описание | +|-------|------|----------| +| POST | /functions/{id}/invoke | Синхронный вызов | +| GET | /functions/{id}/invocations | История вызовов | +| GET | /functions/{id}/invocations/{iid} | Детали вызова + логи | + +## Upload endpoint + +``` +POST /v1/namespaces/{namespace}/functions/{name}/upload +Content-Type: multipart/form-data +Authorization: Bearer + +field: code = +``` + +Процесс: +1. Принимает zip (max 32MB) +2. Распаковывает zip +3. Генерирует `Dockerfile` (`FROM naeel/sless-runtime-{runtime}:latest\nCOPY . /app/function/`) +4. Перепаковывает в `tar.gz` (kaniko требует tar format) +5. Загружает в S3: `contexts/{ns}/{name}/{timestamp}.tar.gz` +6. Обновляет `fn.Spec.S3Key` → контроллер видит изменение и запускает kaniko Job + +Ответ `200 OK`: +```json +{"message": "build queued", "phase": "Pending", "s3_key": "contexts/..."} +``` + +## Поддерживаемые runtime (v1) + +- `python3.11` — реализован и протестирован +- `go1.21` — планируется +- `nodejs20` — планируется + +## Модель Function + +```json +{ + "id": "fn-uuid", + "name": "my-function", + "description": "...", + "runtime": "python3.11", + "entrypoint": "handler.handle", + "memory_mb": 128, + "timeout_sec": 30, + "env_vars": {"KEY": "value"}, + "active_version": "1", + "status": "active", + "created_at": "...", + "updated_at": "..." +} +``` + +## Модель Trigger + +```json +{ + "id": "tr-uuid", + "type": "http", + "url": "https://sless.api.ngcloud.ru/invoke/fn-uuid", + "created_at": "..." +} +``` + +```json +{ + "id": "tr-uuid", + "type": "cron", + "schedule": "0 * * * *", + "created_at": "..." +} +``` diff --git a/doc/errors/log.md b/doc/errors/log.md index 3cf7a43..e924a7f 100644 --- a/doc/errors/log.md +++ b/doc/errors/log.md @@ -2,6 +2,190 @@ > Сюда записываем проблемы с которыми столкнулись и как их решили. +--- + +## 2026-03-17 — Баг 3: PodLogOptions compile error (v0.1.31) + +**Проблема:** Оператор не компилировался. Ошибка: +``` +controllers/functionjob_controller.go: unknown field Stdout in corev1.PodLogOptions +controllers/functionjob_controller.go: unknown field Stderr in corev1.PodLogOptions +``` + +**Причина:** `corev1.PodLogOptions{}` в k8s API не имеет полей `Stdout` и `Stderr` — это поля из `corev1.ContainerState`. Код выглядел так: +```go +req := kube.CoreV1().Pods(namespace).GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{ + Stdout: true, // не существует + Stderr: true, // не существует +}) +``` + +**Решение:** Убрать несуществующие поля. `GetLogs` по умолчанию возвращает stdout+stderr без дополнительных флагов: +```go +req := kube.CoreV1().Pods(namespace).GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{}) +``` + +**Файл:** `controllers/functionjob_controller.go`, функция `getJobPodOutput` +**Версия:** исправлено в `naeel/sless-operator:v0.1.31` + +--- + +## 2026-03-17 — Главная причина провала POSTGRES example: неправильный nubes_endpoint + +### Симптом + +`terraform apply` для `examples/POSTGRES` падал с `"job failed, check pod logs"`. При этом `examples/simple-python` и `examples/hello-node` работали нормально на тест-стенде. + +### Корневая причина + +В `examples/POSTGRES/main.tf` в блоке `provider "sless"` был указан **продовый** endpoint Nubes API: + +```hcl +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" + token = var.api_token + nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" # ← ПРОД +} +``` + +При этом `provider "nubes"` уже указывал на тест: +```hcl +provider "nubes" { + api_token = var.api_token + api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm" # ← тест ✓ +} +``` + +Из-за этого `sless_function` и `sless_job` стучались в **продовый** Nubes API, где у тестового пользователя другой namespace и другой кластер. Postgres и pod запускались в контексте прода, а не тест-стенда. Поды не могли подключиться к базе. + +### Фикс + +```hcl +# examples/POSTGRES/main.tf +provider "sless" { + endpoint = "https://sless-api.kube5s.ru" + token = var.api_token + nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1" # ← тест +} +``` + +Аналогично исправлено в `examples/simple-python/main.tf`. + +### Почему долго искали + +Долгое расследование ушло на "split-brain" — API-созданные FunctionJob'ы были невидимы через `kubectl get functionjobs -A`. Это объяснялось тем что: +1. Job завершался (Failed) за ~5 секунд +2. Между завершением и опросом kubectl объект уже мог быть очищен +3. Параллельно в кластере фигурировал другой namespace (`sless-ffd1f598c169b0ae` — прод) vs тест-namespace + +На самом деле контроллер работал корректно. Проблема была исключительно в `nubes_endpoint`. + +### Урок + +**Всегда проверять**: когда в `main.tf` два провайдера (`nubes` + `sless`) — у обоих endpoint'ы должны указывать на **одну среду**. Смешивание прод/тест endpoint'ов в одном apply даёт непредсказуемые результаты. + +--- + +## 2026-03-17 — Ошибка агента: локальный запуск terraform + +**Проблема:** Агент запускал `terraform apply` **локально** (`/home/naeel/remote_dev/sless/examples/...`) вместо запуска на удалённом сервере `naeel@5.172.178.213`. + +**Почему неправильно:** +- Провайдер `terra.k8c.ru/naeel/sless` установлен только на удалённом сервере +- Локальный terraform не имеет доступа к k8s кластеру напрямую +- Потенциально затрагивал продовые API из локальной сети + +**Правило:** Все `terraform init/plan/apply/destroy` для `examples/` — только через SSH на `naeel@5.172.178.213`: +```bash +ssh -i /home/naeel/remote_dev/common/id_ed25519.txt naeel@5.172.178.213 \ + 'cd /home/naeel/terra/sless/examples/ && terraform apply -auto-approve -no-color' +``` + +--- + +## 2026-03-17 — FunctionJob всегда "job failed, check pod logs" — расследование и два бага + +### Хронология расследования + +**Симптом:** `terraform apply` на `examples/POSTGRES` завершается ошибкой: +``` +job failed: job failed, check pod logs: kubectl logs -n sless-fn-sless-ffd1f598c169b0ae -l functionjob=pg-create-table-job-main-v12 +``` +Команда из сообщения — ничего не выдаёт (под не найден). + +--- + +**Ошибка агента №1 — SQL-диагностика вместо анализа кода** + +Первая реакция — запустить диагностические поды в кластере с psycopg2 чтобы проверить подключение к PostgreSQL. Это было **неправильно**: проблема не в подключении, а в том что job не запускался вообще. Пользователь остановил — "ПРИ ЧЁМ тут SQL запросы???". + +--- + +**Ошибка агента №2 — не читал собственный код** + +Раньше читались логи оператора и kubectl describe — но не исходный код контроллеров. Пользователь указал: "мы пишем ВСЁ сами, смотри в код". После прочтения `functionjob_controller.go` и `functions.go` картина сложилась за одно чтение. + +--- + +### Баг 1 — k8s label `job-name` удалён в 1.27+ + +**Файл:** `controllers/functionjob_controller.go`, функция `getJobPodOutput` + +**Код до:** +```go +pods, err := kube.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: "job-name=" + jobName, +}) +if err != nil || len(pods.Items) == 0 { + return "completed successfully" // ← сюда всегда попадали +} +``` + +**Причина:** В k8s 1.27 лейбл `job-name` на pod-ах был deprecated, в 1.32+ удалён полностью. У нас кластер **1.34.1**. Поэтому `List` возвращал 0 подов всегда → fallback `"completed successfully"` → при `phase=Failed` финальное сообщение `"job failed, check pod logs: ..."`. + +Логи реального сбоя никогда не попадали в `status.Message` FunctionJob, поэтому **корневая ошибка Python-функции была невидима**. + +**Как нашли:** Проверили `kubectl get pod -l "batch.kubernetes.io/job-name"` — вернул поды. `kubectl get pod -l "job-name"` — не вернул. Версия кластера: `v1.34.1` (kubectl `v1.35.2`). + +**Фикс:** Использовать собственный лейбл `functionjob=`, который мы сами выставляем на PodTemplate и который работает независимо от версии k8s. + +--- + +**Также:** В строке 211 подсказка для пользователя тоже использовала устаревший лейбл: +```go +"job failed, check pod logs: kubectl logs -n " + job.Namespace + " -l job-name=" + job.Name +``` +Исправлено на `functionjob=` — теперь команда реально работает. + +--- + +### Баг 2 — Split-brain cached client при CreateFunction + +**Файл:** `internal/api/handler/functions.go`, функция `CreateFunction` + +**Проявление:** API возвращает `409 "function already exists"`, а `kubectl get function -A` функцию не видит. `terraform state rm sless_function.*` не помогает — следующий `terraform apply` снова получает 409. + +**Причина:** `h.K8s` в handler — это **cached client** controller-runtime. Цепочка: +1. Предыдущий `terraform destroy` вызвал `DELETE /functions/pg-create-table-runner` +2. Handler вызвал `h.K8s.Delete(ctx, fn)` — объект удалён из **etcd** +3. Кеш controller-runtime обновляется асинхронно (informer watch). Несколько секунд объект ещё жив в памяти оператора +4. Следующий `terraform apply` → `POST /functions` → `h.K8s.Create(ctx, fn)` → `IsAlreadyExists` (кеш ещё видит объект) +5. Код проверяет `phase == Failed` — но объект в кеше в фазе `Ready` → уходит в 409 навсегда + +`kubectl get function` шёл **мимо кеша** (прямо в k8s API) → NotFound. API handler шёл **через кеш** → AlreadyExists. Поэтому они показывали разные результаты. + +**Фикс:** При `IsAlreadyExists` делать `uncached Get` (через `client.ObjectKey` напрямую). Если объект реально не найден в etcd (`IsNotFound`) — значит кеш устарел, смело создаём заново. Если найден — возвращаем 409 как обычно (объект реально существует). + +--- + +### Итог + +| # | Баг | Файл | Строка | Тип | +|---|-----|------|--------|-----| +| 1 | `LabelSelector: "job-name="` deprecated k8s 1.27+ | `controllers/functionjob_controller.go` | 311 | Совместимость | +| 1b | Подсказка `kubectl logs -l job-name=` тоже устарела | `controllers/functionjob_controller.go` | 211 | UX | +| 2 | Cached client → perpetual 409 при CreateFunction | `internal/api/handler/functions.go` | 117-133 | Race condition | + ## Шаблон записи ``` diff --git a/doc/overview.md b/doc/overview.md new file mode 100644 index 0000000..eea641a --- /dev/null +++ b/doc/overview.md @@ -0,0 +1,204 @@ +# Архитектура системы + +Последнее обновление: 2026-03-18 (v0.1.33 + funcs-service v0.1.3) + +## Общее описание + +Managed Serverless Functions Service для облачного провайдера nubes.ru. +Пользователь загружает код через Terraform, сервис его собирает (kaniko) и запускает +по HTTP-триггеру, расписанию (cron) или вручную через one-shot Job. + +## Стек + +| Компонент | Технология | Где запущен | +|-----------|-----------|-------------| +| Operator (API + Controllers) | Go (controller-runtime) | Kubernetes, namespace `sless` | +| funcs-service (глобальная консоль) | Go (net/http) | Kubernetes, namespace `sless` | +| PostgreSQL | PostgreSQL 16 | Kubernetes, namespace `sless` | +| S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` | +| Container Registry | DockerHub (`naeel/`) | внешний | +| Builder | kaniko (k8s Job) | namespace пользователя | +| Функции (HTTP) | k8s Deployment + Service | namespace пользователя | +| Функции (one-shot) | k8s Job | namespace пользователя | +| Функции (cron) | k8s CronJob | namespace пользователя | +| Terraform Provider | Go (plugin framework v6) | localhost/CI | +| nubes API | REST (облако) | `deck-api.ngcloud.ru` | + +> Redis и RabbitMQ — отложены до v2. + +## Компонент: funcs-service + +Глобальный HTTP сервис — **одна копия** на весь кластер, для всех пользователей. + +``` +User Browser / curl + └─► https://sless.kube5s.ru/funcs/ + └─► nginx Ingress (sless-funcs-ingress) + └─► sless-funcs-service:8090 (namespace sless) + └─► http://sless-operator.sless.svc.cluster.local:9090/v1/... +``` + +**Файлы:** +- `services/funcs/main.go` — логика +- `services/funcs/Dockerfile` — multi-stage Go → alpine +- `deployments/k8s/funcs-service.yaml` — Deployment + Service + Ingress + +**Env vars сервиса:** +| Переменная | Значение | +|-----------|---------| +| `SLESS_OPERATOR_URL` | `http://sless-operator.sless.svc.cluster.local:9090` | +| `SLESS_EXTERNAL_URL` | `https://sless.kube5s.ru` | +| `SLESS_EXCLUDE` | `event-writer,event-monitor,event-cleaner` | +| `SLESS_SERVICE_TOKEN` | JWT токен (задаётся через `kubectl set env`, не в git) | + +## Хранение кода функций в S3 + +``` +Terraform source_dir (локально) + └─► zip → POST /upload → builder.PrepareContext() + ├─► functions/{ns}/{name}/{ts}.zip ← ИСХОДНЫЙ КОД пользователя + └─► contexts/{ns}/{name}/{ts}.tar.gz ← BUILD CONTEXT для kaniko + └─► Function CRD: spec.s3Key = "contexts/..." + └─► контроллер → kaniko Job → Docker image +``` + +Для web-консоли: `GET /source` читает `functions/{ns}/{name}/{ts}.zip` напрямую. + +## Изоляция пользователей — Namespace per user + +Каждый пользователь облака получает **отдельный k8s namespace**. + +``` +JWT токен (Bearer) + └─► JWT.sub (строка "0199e325-1cdf-7cda-9319-e5302a85e291") + └─► SHA256(sub) → первые 8 байт → hex → "sless-{16 hex символов}" + └─► namespace = "sless-cdd874dfa31ba6ca" +``` + +- Namespace детерминирован: один sub → всегда один namespace. +- sub не раскрывается в имени namespace (SHA256 необратим). +- Длина 22 символа — укладывается в лимит k8s (63). + +**Кто создаёт namespace:** +Terraform провайдер при Configure() вызывает POST /v1/namespaces/{ns}/ensure +**один раз**, до любых ресурсных операций. +Resource-хендлеры (Function, Trigger, Job) namespace **не создают** — это не их ответственность. + +## Аутентификация + +### Оператор (REST API) +- Bearer JWT в заголовке Authorization +- Проверяется структура JWT (3 части), наличие sub claim, срок действия exp +- Подпись **не проверяется** — trusted perimeter (оператор за Ingress) + +### Terraform Provider (при Configure) +1. Декодирует JWT → sub +2. Вычисляет namespace через SHA256 +3. Если задан nubes_endpoint — пингует nubes API (GET ) с тем же токеном + - HTTP 401/403 → ошибка инициализации провайдера + - Недоступен → ошибка инициализации +4. Вызывает POST /v1/namespaces/{ns}/ensure (создаёт namespace если нет) + +## Схема вызова + +``` +Пользователь (curl / браузер) + | + v GET|POST|... /fn/{namespace}/{name}/* +sless-api Ingress -> Operator /fn/ прокси + | + v HTTP forward -> http://{name}.{namespace}.svc.cluster.local:8080 +k8s Service -> Deployment/Pod функции +``` + +``` +terraform apply + | + v provider Configure() + 1. JWT -> sub -> namespace + 2. PingNubesAPI (валидация токена) + 3. POST /v1/namespaces/{ns}/ensure <- создаёт k8s namespace + | + +-> POST /v1/namespaces/{ns}/functions <- создаёт Function CRD + | +-> POST /upload (zip) <- загружает код -> S3 -> kaniko Job + | +-> polling phase=Ready + | + +-> POST /v1/namespaces/{ns}/triggers <- создаёт Trigger CRD + | +-> controller: Deployment + Service + (CronJob для cron) + | + +-> POST /v1/namespaces/{ns}/jobs <- создаёт FunctionJob CRD + +-> controller: k8s Job -> result в status +``` + +## Структура кода + +``` +sless/ +|-- main.go точка входа: k8s manager + REST API сервер (goroutine) +|-- internal/ +| |-- api/ +| | |-- router.go gorilla/mux: /fn/ (публичный), /v1/ (auth + middleware) +| | |-- handler/ +| | | |-- handler.go Handler struct + helpers (writeJSON, namespace(), pathVar()) +| | | |-- namespace.go EnsureNamespace (POST /v1/namespaces/{ns}/ensure) +| | | |-- functions.go CRUD Function +| | | |-- triggers.go CRUD Trigger +| | | |-- jobs.go CRUD FunctionJob +| | | |-- upload.go zip -> Dockerfile -> tar.gz -> S3 -> CRD patch +| | | |-- invoke.go прокси /fn/{ns}/{name} -> in-cluster DNS +| | | +-- invocations.go 501 stub (реализация отложена) +| | +-- middleware/ +| | |-- auth.go JWT validation (struct + sub + exp, подпись не проверяется) +| | +-- logging.go slog request logger +| |-- builder/ +| | |-- builder.go kaniko Job lifecycle (Build, JobStatus, Cleanup) +| | +-- context.go PrepareContext: zip+runtime → tar.gz+Dockerfile для kaniko +| |-- config/config.go Load() из env vars +| +-- storage/ +| |-- postgres/store.go SaveInvocation, ListInvocations, RunMigrations +| +-- s3/client.go Upload, Download, UploadContext (tar.gz для kaniko) +|-- controllers/ +| |-- function_controller.go Reconcile: Pending->Building->Ready/Failed + Deployment +| |-- trigger_controller.go Reconcile: Service+Ingress (http) / CronJob (cron) +| +-- functionjob_controller.go Reconcile: k8s Job -> Succeeded/Failed + output capture +|-- api/v1alpha1/ +| |-- function_types.go Function CRD +| |-- trigger_types.go Trigger CRD +| +-- job_types.go FunctionJob CRD +|-- deployments/k8s/ +| |-- operator.yaml Deployment + Service + Ingress +| +-- rbac.yaml ClusterRole + ClusterRoleBinding + ServiceAccount +|-- terraform/provider/ независимый Go-модуль +| +-- internal/ +| |-- client/client.go SubFromJWT, NamespaceFromSub, PingNubesAPI + CRUD +| |-- provider/provider.go Configure(): JWT->NS->ping->EnsureNamespace +| +-- resources/ +| |-- function_resource.go sless_function +| |-- trigger_resource.go sless_trigger +| +-- job_resource.go sless_job ++-- runtimes/ + |-- python3.11/ server.py + Dockerfile -> naeel/sless-runtime-python3.11:v0.1.1 + +-- nodejs20/ server.js + Dockerfile -> naeel/sless-runtime-nodejs20:v0.1.2 +``` + +## Kubernetes кластер + +Сейчас используется существующий кластер (временный). +Планируется переезд на новый кластер — манифесты переносятся без изменений. + +Ноды: +- wheel-control-plane-fm9sr — control-plane +- wheel-workers-tv4qr-r45xs — worker +- wheel-workers-tv4qr-x8xw7 — worker + +Ingress: nginx, external IP 5.172.178.182 +API endpoint: https://sless-api.kube5s.ru + +## Версии в production + +| Артефакт | Тег/Версия | +|---------|-----------| +| naeel/sless-operator | v0.1.22 | +| terra.k8c.ru/naeel/sless провайдер | v0.1.13 | +| naeel/sless-runtime-python3.11 | v0.1.1 | +| naeel/sless-runtime-nodejs20 | v0.1.2 | diff --git a/doc/progress.md b/doc/progress.md index 6ffaa99..c04ed0e 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -1,6 +1,53 @@ # Прогресс разработки -Последнее обновление: 2026-03-18 12:00 (DNS инцидент устранён, POSTGRES E2E PASS) +Последнее обновление: 2026-03-18 20:00 (funcs global service v0.1.3, ветка feat/web-console) + +## 2026-03-18 — funcs: глобальный Go сервис + ветка web-console + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 1 | Python runtime str→text/plain | ✅ | `runtimes/python3.11/server.py`: если функция возвращает `str` → `Content-Type: text/plain`. Образ `naeel/sless-runtime-python3.11:v0.1.3` | +| 2 | funcs_list.py: plain text вывод | ✅ | Переписан с JSON на plain text с `─` разделителями, фильтр `SLESS_EXCLUDE` | +| 3 | Оператор v0.1.33 | ✅ | context.go: python runtime → v0.1.3; `internal/builder/context.go` обновлён | +| 4 | `funcs` как глобальный Go сервис | ✅ | `services/funcs/main.go` — standalone Go HTTP сервер. Деплоится ОДИН РАЗ в namespace `sless`, работает для ВСЕХ пользователей. Образ `naeel/sless-funcs-service:v0.1.3` | +| 5 | URL без токена | ✅ | `https://sless.kube5s.ru/funcs/` — открывается в браузере без токена. `SLESS_SERVICE_TOKEN` задан через `kubectl set env` | +| 6 | Удалить funcs из terraform | ✅ | `examples/POSTGRES/resources.tf`: удалены `sless_function.funcs_list`, `sless_trigger.funcs_list_http`, `output "funcs_url"`, `local.user_namespace`. `terraform apply` — 2 destroyed | +| 7 | Ветка feat/web-console | ✅ | Создана от `feat/harbor-integration` на remote, запушена | +| 8 | Документация архитектуры хранения кода | ✅ | См. `doc/decisions/log.md` раздел "Хранение кода функций и web-консоль" | +| 9 | Коммиты | ✅ | `e8cd62e` (funcs Go service), `38bb494` (v0.1.3 — /funcs/namespace без токена) | + +### Текущие URL функций + +``` +https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae # список функций (без токена) +https://sless.kube5s.ru/funcs?token= # то же, через токен +https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae/pg-info +https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae/pg-table-reader +``` + +### Образы + +| Образ | Версия | Что изменилось | +|-------|--------|----------------| +| `naeel/sless-operator` | v0.1.33 | created_at, last_built_at в API; python runtime v0.1.3 | +| `naeel/sless-runtime-python3.11` | v0.1.3 | str → text/plain в server.py | +| `naeel/sless-funcs-service` | v0.1.3 | /funcs/ без токена, /health probe | + +--- + +## 2026-03-18 — Следующие шаги: web-консоль (ветка feat/web-console) + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 1 | Оператор: `GET /v1/namespaces/{ns}/functions/{name}/source` | ⏳ | Читает `functions/{ns}/{name}/{ts}.zip` из S3, возвращает JSON с файлами | +| 2 | Оператор: `PATCH /v1/namespaces/{ns}/triggers/{name}` | ⏳ | Принимает `{"enabled": true/false}`, обновляет Trigger CRD | +| 3 | `sless-funcs-service`: HTML страница | ⏳ | Если `Accept: text/html` → отдаёт HTML с JS; аккордеон файлов + кнопки ▶/■ | +| 4 | Bump оператора до v0.1.34 | ⏳ | После пп. 1-2 | +| 5 | Bump funcs-service до v0.2.0 | ⏳ | После п. 3 | + +--- + +## 2026-03-18 12:00 — DNS инцидент устранён, POSTGRES E2E PASS ## 2026-03-18 — DNS инцидент: sless-api.kube5s.ru → мёртвый кластер