docs: document all changes and web-console plan
- doc/progress.md: funcs global service v0.1.x, URLs, next steps - doc/decisions/log.md: storage architecture, funcs service decision, web-console plan - doc/api/design.md: current + planned endpoints (/source, PATCH trigger) - doc/architecture/overview.md: funcs-service component, S3 storage diagram
This commit is contained in:
+35
-6
@@ -1,5 +1,7 @@
|
|||||||
# API Design
|
# API Design
|
||||||
|
|
||||||
|
Последнее обновление: 2026-03-18
|
||||||
|
|
||||||
## Базовый URL
|
## Базовый URL
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -7,15 +9,37 @@ http://<operator-host>:9090/v1
|
|||||||
```
|
```
|
||||||
|
|
||||||
Локально: `http://localhost: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}]
|
GET /v1/namespaces/{ns}/functions
|
||||||
/v1/namespaces/{namespace}/functions/{name}/upload
|
POST /v1/namespaces/{ns}/functions
|
||||||
/v1/namespaces/{namespace}/triggers[/{name}]
|
GET /v1/namespaces/{ns}/functions/{name}
|
||||||
/v1/namespaces/{namespace}/functions/{name}/invocations
|
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/<namespace> ← без токена, plain text / HTML
|
||||||
|
GET https://sless.kube5s.ru/funcs?token=<jwt> ← с токеном
|
||||||
|
GET https://sless.kube5s.ru/health ← liveness probe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Аутентификация
|
## Аутентификация
|
||||||
@@ -24,6 +48,11 @@ http://<operator-host>:9090/v1
|
|||||||
Authorization: Bearer <cloud-token>
|
Authorization: Bearer <cloud-token>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Токен — JWT от `auth-api`. Middleware в операторе:
|
||||||
|
1. Извлекает `sub` из payload (без проверки подписи — доверяет Ingress)
|
||||||
|
2. Вычисляет namespace: `SHA256(sub)[:8]` hex → `sless-{16 hex символов}`
|
||||||
|
3. Проверяет что запрошенный `{namespace}` совпадает с вычисленным
|
||||||
|
|
||||||
## Ресурсы
|
## Ресурсы
|
||||||
|
|
||||||
### Functions
|
### Functions
|
||||||
|
|||||||
@@ -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` |
|
| Operator (API + Controllers) | Go (controller-runtime) | Kubernetes, namespace `sless` |
|
||||||
|
| funcs-service (глобальная консоль) | Go (net/http) | Kubernetes, namespace `sless` |
|
||||||
| PostgreSQL | PostgreSQL 16 | Kubernetes, namespace `sless` |
|
| PostgreSQL | PostgreSQL 16 | Kubernetes, namespace `sless` |
|
||||||
| S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` |
|
| S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` |
|
||||||
| Container Registry | DockerHub (`naeel/`) | внешний |
|
| Container Registry | DockerHub (`naeel/`) | внешний |
|
||||||
@@ -25,6 +26,44 @@ Managed Serverless Functions Service для облачного провайде
|
|||||||
|
|
||||||
> Redis и RabbitMQ — отложены до v2.
|
> Redis и RabbitMQ — отложены до v2.
|
||||||
|
|
||||||
|
## Компонент: funcs-service
|
||||||
|
|
||||||
|
Глобальный HTTP сервис — **одна копия** на весь кластер, для всех пользователей.
|
||||||
|
|
||||||
|
```
|
||||||
|
User Browser / curl
|
||||||
|
└─► https://sless.kube5s.ru/funcs/<namespace>
|
||||||
|
└─► 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
|
## Изоляция пользователей — Namespace per user
|
||||||
|
|
||||||
Каждый пользователь облака получает **отдельный k8s namespace**.
|
Каждый пользователь облака получает **отдельный k8s namespace**.
|
||||||
|
|||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
# API Design
|
||||||
|
|
||||||
|
Последнее обновление: 2026-03-18
|
||||||
|
|
||||||
|
## Базовый URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<operator-host>: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/<namespace> ← без токена, plain text / HTML
|
||||||
|
GET https://sless.kube5s.ru/funcs?token=<jwt> ← с токеном
|
||||||
|
GET https://sless.kube5s.ru/health ← liveness probe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Аутентификация
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <cloud-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Токен — 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 <token>
|
||||||
|
|
||||||
|
field: code = <zip-file>
|
||||||
|
```
|
||||||
|
|
||||||
|
Процесс:
|
||||||
|
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": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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/<example> && 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=<fj.Name>`, который мы сами выставляем на PodTemplate и который работает независимо от версии k8s.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Также:** В строке 211 подсказка для пользователя тоже использовала устаревший лейбл:
|
||||||
|
```go
|
||||||
|
"job failed, check pod logs: kubectl logs -n " + job.Namespace + " -l job-name=" + job.Name
|
||||||
|
```
|
||||||
|
Исправлено на `functionjob=<fj.Name>` — теперь команда реально работает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Баг 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 |
|
||||||
|
|
||||||
## Шаблон записи
|
## Шаблон записи
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+204
@@ -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/<namespace>
|
||||||
|
└─► 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 <nubes_endpoint>) с тем же токеном
|
||||||
|
- 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 |
|
||||||
+48
-1
@@ -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/<namespace>` — открывается в браузере без токена. `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=<jwt> # то же, через токен
|
||||||
|
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/<namespace> без токена, /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 → мёртвый кластер
|
## 2026-03-18 — DNS инцидент: sless-api.kube5s.ru → мёртвый кластер
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user