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**.
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
# Решения и обоснования
|
# Решения и обоснования
|
||||||
|
|
||||||
|
## 2026-03-18 — Архитектура: funcs как глобальный сервис, web-консоль
|
||||||
|
|
||||||
|
### Хранение кода функций
|
||||||
|
|
||||||
|
S3 (minio внутри кластера) хранит **два артефакта** на каждый upload:
|
||||||
|
|
||||||
|
```
|
||||||
|
functions/{namespace}/{name}/{timestamp}.zip ← ИСХОДНЫЙ КОД (zip пользователя)
|
||||||
|
contexts/{namespace}/{name}/{timestamp}.tar.gz ← BUILD CONTEXT для kaniko (zip + Dockerfile)
|
||||||
|
```
|
||||||
|
|
||||||
|
Function CRD хранит `spec.s3Key` — указывает на `contexts/...` (build context).
|
||||||
|
Из него можно восстановить путь к исходному zip:
|
||||||
|
`contexts/{ns}/{name}/{ts}.tar.gz` → `functions/{ns}/{name}/{ts}.zip`
|
||||||
|
|
||||||
|
Поэтому для отображения кода в веб-консоли **не нужно ничего менять в CRD**:
|
||||||
|
достаточно нового эндпоинта `GET /source` который читает zip из S3.
|
||||||
|
|
||||||
|
### Решение: funcs как глобальный Go сервис вместо per-user terraform
|
||||||
|
|
||||||
|
**Было:** `sless_function.funcs_list` + `sless_trigger.funcs_list_http` в `examples/POSTGRES/resources.tf`
|
||||||
|
— Для каждого пользователя terraform создавал отдельный pod функции
|
||||||
|
— Требовал `api_token`, `SLESS_NAMESPACE` как env vars в terraform
|
||||||
|
— Не масштабируется: N пользователей = N лишних pod'ов
|
||||||
|
|
||||||
|
**Стало:** `services/funcs/main.go` — один Go HTTP сервис в namespace `sless`
|
||||||
|
— Деплоится один раз через `deployments/k8s/funcs-service.yaml`
|
||||||
|
— Принимает JWT токен → извлекает `sub` → `SHA256[:8]` → namespace
|
||||||
|
— URL без токена: `/funcs/<namespace>` (namespace не секрет — виден в URL каждой функции)
|
||||||
|
— `SLESS_SERVICE_TOKEN` задаётся через `kubectl set env` (не хранится в git)
|
||||||
|
|
||||||
|
### Про будущую синхронизацию terraform-папок с кластером
|
||||||
|
|
||||||
|
Terraform уже работает по схеме: `source_dir` → zip → `POST /upload` → S3.
|
||||||
|
Обратная синхронизация (кластер → локальная папка): скачать zip из S3 → распаковать в `source_dir`.
|
||||||
|
Никаких структурных изменений не потребует. Реализовывать ПОСЛЕ web-консоли.
|
||||||
|
|
||||||
|
### Архитектура web-консоли (план, ветка feat/web-console)
|
||||||
|
|
||||||
|
**Принцип:** минимум изменений в операторе, максимум логики в `sless-funcs-service`.
|
||||||
|
|
||||||
|
**Два новых эндпоинта в операторе:**
|
||||||
|
|
||||||
|
| Метод | Путь | Что делает |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| GET | `/v1/namespaces/{ns}/functions/{name}/source` | Читает zip из S3 → JSON `[{name, content}]` |
|
||||||
|
| PATCH | `/v1/namespaces/{ns}/triggers/{name}` | `{"enabled": bool}` → обновляет Trigger CRD |
|
||||||
|
|
||||||
|
**`sless-funcs-service` — HTML режим:**
|
||||||
|
- Если запрос из браузера (`Accept: text/html`) → отдаёт HTML страницу
|
||||||
|
- Список функций — аккордеон; при раскрытии `fetch(/source)` подгружает файлы
|
||||||
|
- Подсветка синтаксиса: `highlight.js` с CDN (не требует сборки)
|
||||||
|
- Кнопки ▶ Старт / ■ Стоп → `PATCH /triggers/{name}` через fetch
|
||||||
|
- `text/plain` ответ для curl/CLI остаётся без изменений
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-03-18 — Смена sless API endpoint: sless-api.kube5s.ru → sless.kube5s.ru
|
## 2026-03-18 — Смена sless API endpoint: sless-api.kube5s.ru → sless.kube5s.ru
|
||||||
|
|
||||||
**Решение:** Оператор sless доступен по `https://sless.kube5s.ru` (не `sless-api.kube5s.ru`). Все examples, deployments и ConfigMap обновлены.
|
**Решение:** Оператор sless доступен по `https://sless.kube5s.ru` (не `sless-api.kube5s.ru`). Все examples, deployments и ConfigMap обновлены.
|
||||||
|
|||||||
+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 |
|
||||||
|
|
||||||
## Шаблон записи
|
## Шаблон записи
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+763
@@ -0,0 +1,763 @@
|
|||||||
|
# Решения и обоснования
|
||||||
|
|
||||||
|
## 2026-03-18 — Архитектура: funcs как глобальный сервис, web-консоль
|
||||||
|
|
||||||
|
### Хранение кода функций
|
||||||
|
|
||||||
|
S3 (minio внутри кластера) хранит **два артефакта** на каждый upload:
|
||||||
|
|
||||||
|
```
|
||||||
|
functions/{namespace}/{name}/{timestamp}.zip ← ИСХОДНЫЙ КОД (zip пользователя)
|
||||||
|
contexts/{namespace}/{name}/{timestamp}.tar.gz ← BUILD CONTEXT для kaniko (zip + Dockerfile)
|
||||||
|
```
|
||||||
|
|
||||||
|
Function CRD хранит `spec.s3Key` — указывает на `contexts/...` (build context).
|
||||||
|
Из него можно восстановить путь к исходному zip:
|
||||||
|
`contexts/{ns}/{name}/{ts}.tar.gz` → `functions/{ns}/{name}/{ts}.zip`
|
||||||
|
|
||||||
|
Поэтому для отображения кода в веб-консоли **не нужно ничего менять в CRD**:
|
||||||
|
достаточно нового эндпоинта `GET /source` который читает zip из S3.
|
||||||
|
|
||||||
|
### Решение: funcs как глобальный Go сервис вместо per-user terraform
|
||||||
|
|
||||||
|
**Было:** `sless_function.funcs_list` + `sless_trigger.funcs_list_http` в `examples/POSTGRES/resources.tf`
|
||||||
|
— Для каждого пользователя terraform создавал отдельный pod функции
|
||||||
|
— Требовал `api_token`, `SLESS_NAMESPACE` как env vars в terraform
|
||||||
|
— Не масштабируется: N пользователей = N лишних pod'ов
|
||||||
|
|
||||||
|
**Стало:** `services/funcs/main.go` — один Go HTTP сервис в namespace `sless`
|
||||||
|
— Деплоится один раз через `deployments/k8s/funcs-service.yaml`
|
||||||
|
— Принимает JWT токен → извлекает `sub` → `SHA256[:8]` → namespace
|
||||||
|
— URL без токена: `/funcs/<namespace>` (namespace не секрет — виден в URL каждой функции)
|
||||||
|
— `SLESS_SERVICE_TOKEN` задаётся через `kubectl set env` (не хранится в git)
|
||||||
|
|
||||||
|
### Про будущую синхронизацию terraform-папок с кластером
|
||||||
|
|
||||||
|
Terraform уже работает по схеме: `source_dir` → zip → `POST /upload` → S3.
|
||||||
|
Обратная синхронизация (кластер → локальная папка): скачать zip из S3 → распаковать в `source_dir`.
|
||||||
|
Никаких структурных изменений не потребует. Реализовывать ПОСЛЕ web-консоли.
|
||||||
|
|
||||||
|
### Архитектура web-консоли (план, ветка feat/web-console)
|
||||||
|
|
||||||
|
**Принцип:** минимум изменений в операторе, максимум логики в `sless-funcs-service`.
|
||||||
|
|
||||||
|
**Два новых эндпоинта в операторе:**
|
||||||
|
|
||||||
|
| Метод | Путь | Что делает |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| GET | `/v1/namespaces/{ns}/functions/{name}/source` | Читает zip из S3 → JSON `[{name, content}]` |
|
||||||
|
| PATCH | `/v1/namespaces/{ns}/triggers/{name}` | `{"enabled": bool}` → обновляет Trigger CRD |
|
||||||
|
|
||||||
|
**`sless-funcs-service` — HTML режим:**
|
||||||
|
- Если запрос из браузера (`Accept: text/html`) → отдаёт HTML страницу
|
||||||
|
- Список функций — аккордеон; при раскрытии `fetch(/source)` подгружает файлы
|
||||||
|
- Подсветка синтаксиса: `highlight.js` с CDN (не требует сборки)
|
||||||
|
- Кнопки ▶ Старт / ■ Стоп → `PATCH /triggers/{name}` через fetch
|
||||||
|
- `text/plain` ответ для curl/CLI остаётся без изменений
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-18 — Смена sless API endpoint: sless-api.kube5s.ru → sless.kube5s.ru
|
||||||
|
|
||||||
|
**Решение:** Оператор sless доступен по `https://sless.kube5s.ru` (не `sless-api.kube5s.ru`). Все examples, deployments и ConfigMap обновлены.
|
||||||
|
|
||||||
|
**Причина:** При пересоздании кластера DNS-запись `sless-api.kube5s.ru` не была обновлена — она указывала на IP `5.172.178.182` (старый, мёртвый кластер). Ingress нового кластера закреплён на `185.247.187.147`. Отдельная запись `sless.kube5s.ru` уже корректно указывала на `185.247.187.147`.
|
||||||
|
TLS handshake timeout возникал потому что старый IP принимал TCP:443, но не завершал TLS (nginx жив, бэкенд мёртв). Go HTTP клиент ждал системный таймаут (~90s) и повторял бесконечно.
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- `deployments/k8s/operator.yaml`: `EXTERNAL_URL`, `INGRESS_HOST`, ingress host → `sless.kube5s.ru`
|
||||||
|
- ConfigMap `sless-operator-config` в кластере: `EXTERNAL_URL` обновлён через `kubectl patch`
|
||||||
|
- Ingress `sless-operator` в кластере: host + TLS secret → `sless.kube5s.ru`
|
||||||
|
- Все `examples/**/main.tf`: `endpoint = "https://sless.kube5s.ru"`
|
||||||
|
|
||||||
|
**Правило:** При пересоздании кластера — первым делом проверять соответствие DNS → ingress IP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-17 — Разделение prod/test endpoint'ов в examples/
|
||||||
|
|
||||||
|
**Решение:** Все `examples/` ОБЯЗАНЫ использовать `deck-api-test.ngcloud.ru` для обоих провайдеров: `nubes` и `sless` (`nubes_endpoint`). Продовый `deck-api.ngcloud.ru` — только для реальных клиентов.
|
||||||
|
|
||||||
|
**Причина:** Смешивание prod и test endpoint'ов в одном `terraform apply` приводит к тому что ресурсы создаются в разных средах. `sless_function`/`sless_job` могут получать данные (PGHOST, credentials) из прода, а pod запускается в тест-кластере — и не может достучаться до хоста.
|
||||||
|
|
||||||
|
**Правило для `main.tf` в examples:**
|
||||||
|
```hcl
|
||||||
|
provider "nubes" {
|
||||||
|
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
||||||
|
}
|
||||||
|
provider "sless" {
|
||||||
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-17 — terraform apply только на удалённом сервере
|
||||||
|
|
||||||
|
**Решение:** `terraform init/plan/apply/destroy` для `examples/` — исключительно через SSH на сервере `naeel@5.172.178.213`. Локальный запуск запрещён.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
1. Провайдер `terra.k8c.ru/naeel/sless` кэширован только на удалённом сервере
|
||||||
|
2. Локальный terraform не имеет сетевого доступа к k8s кластеру и внутренним кластерным адресам (например PGHOST вида `*.svc.cluster.local`)
|
||||||
|
3. Случайный локальный запуск с prod токенами может затронуть боевую среду
|
||||||
|
|
||||||
|
**Как запускать:**
|
||||||
|
```bash
|
||||||
|
# Сначала синхронизировать изменения:
|
||||||
|
rsync -av -e "ssh -i /home/naeel/remote_dev/common/id_ed25519.txt" \
|
||||||
|
/home/naeel/remote_dev/sless/examples/<example>/ \
|
||||||
|
naeel@5.172.178.213:/home/naeel/terra/sless/examples/<example>/
|
||||||
|
|
||||||
|
# Затем запускать на remote:
|
||||||
|
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-06 — Отдельная репа для сервиса
|
||||||
|
|
||||||
|
**Решение:** Serverless service в отдельной репе, не вместе с Terraform provider.
|
||||||
|
|
||||||
|
**Причина:** Разные зоны ответственности, разные релизы, потенциально разные команды.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-06 — Один бинарник для v1
|
||||||
|
|
||||||
|
**Решение:** Один Go бинарник вместо микросервисов.
|
||||||
|
|
||||||
|
**Причина:** Нагрузки изначально нет. Проще деплоить, проще отлаживать. Разделим при необходимости.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-06 — Аутентификация через облачный токен
|
||||||
|
|
||||||
|
**Решение:** Использовать Bearer token облака, без Keycloak.
|
||||||
|
|
||||||
|
**Причина:** Terraform provider уже работает с токенами облака. Keycloak — лишняя зависимость для v1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-06 — S3 облачный, остальное в кубере
|
||||||
|
|
||||||
|
**Решение:** S3 (Ceph) использовать облачный (`ceph.tst.nubes.ru`), PostgreSQL/Redis — в кластере.
|
||||||
|
|
||||||
|
**Причина:** S3 имеет внешний доступ и уже готов. Для PostgreSQL/Redis сетевого связывания с облаком пока нет — настраивается через devops облака.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-06 — Текущий кластер для разработки
|
||||||
|
|
||||||
|
**Решение:** Использовать существующий k8s кластер (namespace `sless`), потом перенести на новый.
|
||||||
|
|
||||||
|
**Причина:** Новый кластер ещё не готов. Изоляция через namespace — безопасно для существующих сервисов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-06 — RabbitMQ откладываем
|
||||||
|
|
||||||
|
**Решение:** В v1 только HTTP и Cron триггеры. RabbitMQ/event triggers — в v2.
|
||||||
|
|
||||||
|
**Причина:** Упрощение первой итерации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — DockerHub вместо внутреннего registry
|
||||||
|
|
||||||
|
**Решение:** Образы функций и runtime базовые образы публикуются на DockerHub (user `naeel`).
|
||||||
|
|
||||||
|
**Причина:** Namespace `registry` в кластере — это Apache NiFi Registry (NOT Docker). Отдельный Docker registry не поднят. DockerHub доступен и достаточен для разработки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — Terraform провайдер sless — отдельный модуль в той же репе
|
||||||
|
|
||||||
|
**Решение:** `terraform/provider/` — независимый Go-модуль внутри репы `sless`.
|
||||||
|
|
||||||
|
**Причина:** Удобно держать рядом с кодом оператора во время разработки.
|
||||||
|
|
||||||
|
**Важно:** `provider "sless"` и `provider "nubes"` — это **два отдельных независимых провайдера**. Объединять их нельзя:
|
||||||
|
- разные зоны ответственности (`nubes` — облачная инфраструктура, `sless` — serverless функции)
|
||||||
|
- разные релизные циклы
|
||||||
|
- разные команды в будущем
|
||||||
|
|
||||||
|
Пользователь использует оба провайдера вместе в одном `.tf` файле, но это не означает что они должны быть одним бинарником.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — WaitReady в Terraform провайдере при создании функции
|
||||||
|
|
||||||
|
**Решение:** После `UploadCode` провайдер ждёт `phase=Ready` (polling каждые 5 сек, таймаут 5 мин).
|
||||||
|
|
||||||
|
**Причина:** Kaniko-сборка занимает ~1 минуту. Без ожидания `terraform apply` завершился бы с `phase=Building` в state, что неверно отображало бы реальное состояние ресурса.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — code_hash для детектирования изменений кода функции
|
||||||
|
|
||||||
|
**Решение:** Атрибут `code_hash` в `sless_function` — пользователь задаёт через `filemd5("./handler.zip")`. Изменение hash → провайдер перезагружает zip и запускает пересборку.
|
||||||
|
|
||||||
|
**Причина:** Terraform не отслеживает содержимое файлов автоматически. Это стандартный паттерн (аналогично `aws_lambda_function.source_code_hash`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — Scale-to-zero откладываем до v2
|
||||||
|
|
||||||
|
**Решение:** В v1 функции работают как Deployment с постоянно живым подом (always-on). Scale-to-zero — в v2 через KEDA HTTP Add-on.
|
||||||
|
|
||||||
|
**Причина:** Scale-to-zero меняет архитектуру контроллера и routing. Для MVP это несоразмерная сложность. Пользователь может управлять ресурсами вручную через `replicas = 0/1/N` (планируется в v1.1).
|
||||||
|
|
||||||
|
**v2 план:** Заменить Deployment на `HTTPScaledObject` (KEDA), минимальные реплики = 0. KEDA буферизует запросы во время cold start (~1-3 сек).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — replicas как ручное управление масштабом (TODO v1.1)
|
||||||
|
|
||||||
|
**Решение:** Добавить поле `replicas *int32` в `FunctionSpec`. Пользователь задаёт через Terraform: `replicas = 0` (выключить), `replicas = 1` (включить), `replicas = N` (масштабировать).
|
||||||
|
|
||||||
|
**Причина:** Без этого функция жрёт ресурсы 24/7 даже если не нужна. Это минимальный механизм контроля потребления до реализации scale-to-zero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — PostgreSQL опционален для базового Function Hosting
|
||||||
|
|
||||||
|
**Решение:** Postgres нужен только для логов вызовов (`invocations`). Для базового деплоя функций — не нужен. Оператор работает без него (просто не пишет логи).
|
||||||
|
|
||||||
|
**Минимальные зависимости для production:** k8s кластер + S3 + Docker registry + Ingress.
|
||||||
|
|
||||||
|
## 2026-03-07 — Версионированные теги для runtime образов (не :latest)
|
||||||
|
|
||||||
|
**Решение:** Runtime базовые образы (`sless-runtime-python3.11`, `sless-runtime-nodejs20`) и образ оператора (`sless-operator`) тегируются по схеме `v<major>.<minor>.<patch>`. `:latest` не используется.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
- `:latest` приводит к непредсказуемому поведению: kaniko может взять старый кешированный образ, pod не перезапускается если `imagePullPolicy: IfNotPresent`.
|
||||||
|
- Версионированные теги дают явный контроль: при изменении runtime нужно обновить тег в `upload.go` → это принудительно пересобирает все функции с новым базовым образом.
|
||||||
|
- Аудит и откат: можно пинить конкретную версию runtime.
|
||||||
|
|
||||||
|
**Соглашение:**
|
||||||
|
- Runtime образы: `naeel/sless-runtime-{lang}:v{версия}` (например `v0.1.0`)
|
||||||
|
- Оператор: `naeel/sless-operator:v{версия}`
|
||||||
|
- При изменении runtime — инкрементировать минорную версию образа и обновить константу в `upload.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — nodejs20 как второй поддерживаемый runtime
|
||||||
|
|
||||||
|
**Решение:** Добавлен nodejs20 runtime (`node:20-alpine` base, `server.js` HTTP wrapper, `exports.handle(event)`).
|
||||||
|
|
||||||
|
**Причина:** Node.js — стандарт для serverless (AWS Lambda, Vercel). Покрывает JS/TypeScript аудиторию. Паттерн идентичен python3.11: runtime image → kaniko → Deployment.
|
||||||
|
|
||||||
|
**Детали реализации:**
|
||||||
|
- `runtimes/nodejs20/server.js` — `http.createServer`, динамический `require(HANDLER_PATH)`
|
||||||
|
- Зависимости через `package.json` → `npm install --omit=dev` (аналог `requirements.txt` → `pip install`)
|
||||||
|
- `entrypoint` в HCL игнорируется для Node.js (всегда `handler.js` + `exports.handle`) — TODO: поддержать произвольный entrypoint в v1.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — FunctionJob CRD: одноразовые запуски функций
|
||||||
|
|
||||||
|
**Решение:** Добавлен `FunctionJob` CRD для одноразового запуска функции с произвольным JSON-событием.
|
||||||
|
**Причина:** Нужны sync-вызовы без HTTP — для батч-обработки, миграций, крон-задач через Terraform.
|
||||||
|
**Реализация:**
|
||||||
|
- `api/v1alpha1/job_types.go` — CRD: `FunctionRef`, `EventJSON`, phases: Pending/Running/Succeeded/Failed
|
||||||
|
- `controllers/functionjob_controller.go` — создаёт k8s Job, ждёт завершения, синхронизирует статус
|
||||||
|
- `internal/api/handler/jobs.go` — REST: CreateJob/GetJob/DeleteJob
|
||||||
|
- `terraform/provider/internal/resources/job_resource.go` — ресурс `sless_job`
|
||||||
|
- Настраиваемые таймауты: `build_timeout_sec` (sless_function), `wait_timeout_sec` (sless_job)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-07 — Прокси /fn/ вместо wildcard Ingress
|
||||||
|
|
||||||
|
**Проблема:** wildcard DNS `*.fn.kube5s.ru` недоступен (провайдер не позволяет).
|
||||||
|
**Решение:** HTTP-прокси внутри оператора — маршрут `GET|POST|... /fn/{namespace}/{name}` на `sless-api.kube5s.ru`.
|
||||||
|
**Реализация:**
|
||||||
|
- `internal/api/handler/invoke.go` — форвардит запрос к `http://{fn}.sless-fn-{ns}.svc.cluster.local:8080`
|
||||||
|
- `internal/api/router.go` — `/fn/` регистрируется **до** auth middleware, публично доступен; `/v1/` — по-прежнему с Bearer токеном (gorilla `Use()`)
|
||||||
|
- `internal/config/config.go` — новое поле `ExternalURL` (env `EXTERNAL_URL`)
|
||||||
|
- `controllers/trigger_controller.go` — если `ExternalURL` задан, `Trigger.Status.URL = ExternalURL/fn/{ns}/{name}`; иначе fallback: создаёт Ingress с поддоменом (прежнее поведение)
|
||||||
|
- `deployments/k8s/operator.yaml` — `EXTERNAL_URL=https://sless-api.kube5s.ru`
|
||||||
|
|
||||||
|
**URL функции:** `https://sless-api.kube5s.ru/fn/{namespace}/{name}`
|
||||||
|
**E2E:** `curl https://sless-api.kube5s.ru/fn/default/hello-node` → `{"message":"Hello, Naeel! (nodejs20)"}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-08 — Lifecycle control: trigger.enabled + job.run_id
|
||||||
|
|
||||||
|
**Задача:** управление жизненным циклом ресурсов без удаления.
|
||||||
|
|
||||||
|
### trigger.enabled
|
||||||
|
|
||||||
|
**Проблема:** нет способа "заморозить" функцию без удаления Trigger/Function (
|
||||||
|
освобождение ресурсов под праздники, дебаггинг и т.д.).
|
||||||
|
|
||||||
|
**Решение:** `enabled bool` (по умолчанию `true`) в `TriggerSpec`.
|
||||||
|
- `enabled=false` → trigger_controller масштабирует Deployment функции до 0 реплик.
|
||||||
|
- Функция не принимает запросы, не потребляет CPU (pod не запущен).
|
||||||
|
- Изменение **не** пересоздаёт ресурс (нет RequiresReplace) — in-place через PATCH.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- `api/v1alpha1/trigger_types.go` — `Enabled bool` в TriggerSpec, `//+kubebuilder:default=true`
|
||||||
|
- `controllers/trigger_controller.go` — патчит Deployment replicas=0/1 в зависимости от Enabled
|
||||||
|
- `internal/api/handler/triggers.go` — `UpdateTrigger` handler (PATCH), поле `enabled` в request/response
|
||||||
|
- `internal/api/router.go` — `PATCH /v1/namespaces/{namespace}/triggers/{name}`
|
||||||
|
- `internal/client/client.go` — `TriggerUpdateRequest`, `UpdateTrigger()` метод
|
||||||
|
- `terraform/provider/internal/resources/trigger_resource.go` — атрибут `enabled` (Optional+Computed, default=true), реализован `Update` метод
|
||||||
|
|
||||||
|
### job.run_id
|
||||||
|
|
||||||
|
**Проблема:** нет способа создать FunctionJob "отложенным" — с явным контролем когда запускать.
|
||||||
|
Также нет механизма повторного запуска с сохранением структуры ресурса.
|
||||||
|
|
||||||
|
**Решение:** `run_id int64` (по умолчанию `0`) в `FunctionJobSpec`.
|
||||||
|
- `run_id=0` → FunctionJob создаётся в k8s, но k8s Job не запускается (phase=Skipped).
|
||||||
|
- `run_id>0` → запускает Job. Увеличение значения (1→2→3) = повторный запуск через пересоздание.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- `api/v1alpha1/job_types.go` — `RunID int64` в FunctionJobSpec, `//+kubebuilder:default=0`
|
||||||
|
- `controllers/functionjob_controller.go` — если RunID==0 → устанавливает phase=Skipped, return
|
||||||
|
- `internal/api/handler/jobs.go` — поле `run_id` в jobRequest/jobResponse
|
||||||
|
- `internal/client/client.go` — `RunID int64` в JobRequest/JobResponse
|
||||||
|
- `terraform/provider/internal/resources/job_resource.go` — атрибут `run_id` (RequiresReplace, default=0). Если run_id=0 → не ждёт завершения, phase=Skipped сразу в state.
|
||||||
|
|
||||||
|
**Версии:**
|
||||||
|
- operator: `naeel/sless-operator:v0.1.6`
|
||||||
|
- provider: `terra.k8c.ru/naeel/sless v0.1.4`
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-08 — Переключение registry с Harbor на DockerHub
|
||||||
|
|
||||||
|
**Проблема:** Harbor (`pearlharbor.registryk8s.services.ngcloud.ru`) — внешний сервис облачного провайдера. Нестабилен: `/v2/` периодически зависает на 10+ секунд или возвращает 504. Kaniko не мог завершить push образа.
|
||||||
|
|
||||||
|
**Решение:** `REGISTRY_HOST=naeel` (DockerHub namespace). Образы функций пушатся как `naeel/sless-default-{namespace}-{name}:latest`.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- `deployments/k8s/operator.yaml` — configmap `REGISTRY_HOST: "naeel"`
|
||||||
|
- Secret `sless-registry-auth` уже содержал DockerHub credentials → дополнительных изменений не потребовалось
|
||||||
|
|
||||||
|
**Компромисс:** DockerHub — публичный registry. Образы функций пользователей публично видимы. Для production нужен приватный registry (Harbor, ECR, GCR и т.д.).
|
||||||
|
|
||||||
|
**Версии:**
|
||||||
|
- operator: оператор не пересобирался, только configmap
|
||||||
|
- commit: `b69f795`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-08 — FunctionJob polling вместо Owns watch
|
||||||
|
|
||||||
|
**Проблема:** `Owns(&batchv1.Job{})` в `SetupWithManager` не работает cross-namespace. Job создаётся в `sless-fn-{ns}`, FunctionJob — в user namespace. Watch никогда не срабатывал.
|
||||||
|
|
||||||
|
**Решение:** Убрать `Owns`. В `syncJobStatus` при Running статусе возвращать `ctrl.Result{RequeueAfter: 5 * time.Second}` — контроллер сам поллит k8s Job каждые 5 сек.
|
||||||
|
|
||||||
|
**Версии:**
|
||||||
|
- operator: `naeel/sless-operator:v0.1.10`
|
||||||
|
- commit: `461ac09`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-08 — code_hash: filesha256 вместо output_md5
|
||||||
|
|
||||||
|
**Проблема:** `hashicorp/archive v2.7.x` имеет баг: `output_md5` возвращает MD5 предыдущей версии zip. `output_sha` и `output_sha256` обновляются корректно.
|
||||||
|
|
||||||
|
**Решение:** `code_hash = filesha256("${path.module}/code/handler.js")` — хэшируется исходный файл напрямую.
|
||||||
|
|
||||||
|
**Правило проекта:** В `sless_function.code_hash` всегда использовать `filesha256(source_file)`, не `archive_file.output_md5`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-08 — Rollout restart после kaniko build (imagePullPolicy + :latest)
|
||||||
|
|
||||||
|
**Проблема:** После успешной kaniko сборки pod не перезапускался — kubelet брал кешированный образ `:latest` (imagePullPolicy: IfNotPresent). Функция возвращала старый код.
|
||||||
|
|
||||||
|
**Решение:** В `ensureDeployment` при обновлении существующего Deployment проставляем аннотацию:
|
||||||
|
```go
|
||||||
|
existing.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = fn.Status.LastBuiltAt.Time.Format(time.RFC3339)
|
||||||
|
```
|
||||||
|
Значение привязано к `fn.Status.LastBuiltAt` → меняется при каждой сборке → Kubernetes делает rolling restart → свежий образ гарантированно пул-ится.
|
||||||
|
|
||||||
|
**Правило проекта:** При использовании `:latest` tag всегда явно проставлять `restartedAt` annotation при обновлении кода.
|
||||||
|
|
||||||
|
**Версия:** operator `naeel/sless-operator:v0.1.11`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-10 — LLM-валидация кода при upload (pre-build security gate)
|
||||||
|
|
||||||
|
**Решение:** Интегрировать вызов облачного LLM в pipeline upload кода. LLM анализирует исходники пользователя **до** отправки в S3 и запуска kaniko. Если код подозрительный — upload отклоняется с HTTP 400 и причиной.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
1. LLM уже развёрнут (или скоро будет) в облаке nubes.ru — его нужно загрузить реальной работой.
|
||||||
|
2. Dogfooding: облачный провайдер использует собственный сервис ИИ в своём же продукте serverless.
|
||||||
|
3. Маркетинг: "ваш код проверяется ИИ перед деплоем" — реальная продающая фича.
|
||||||
|
4. Security: защита от криптомайнеров, ботнетов, DDoS-агентов, port scanners в пользовательских функциях.
|
||||||
|
|
||||||
|
**Точка интеграции:** `internal/api/handler/upload.go` — между распаковкой zip и упаковкой tar.gz.
|
||||||
|
|
||||||
|
**Pipeline с LLM:**
|
||||||
|
```
|
||||||
|
POST /upload (zip)
|
||||||
|
→ распаковка zip
|
||||||
|
→ извлечение текстовых файлов (.py, .js, .ts, .json, .sh, .sql ...)
|
||||||
|
→ POST к облачному LLM API с исходниками + системным промптом
|
||||||
|
→ safe=true → Dockerfile + tar.gz → S3 → CRD patch (обычный путь)
|
||||||
|
→ safe=false → HTTP 400 {"error": "code validation failed: <reason>"}
|
||||||
|
→ LLM error → warning в лог, upload продолжается (soft-fail)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Режим работы: blocking + soft-fail**
|
||||||
|
- `safe=false` → upload отклоняется (HTTP 400), код не попадает в S3, сборка не начинается.
|
||||||
|
- LLM недоступен (timeout, 5xx) → upload **пропускается** (soft-fail), логируется warning.
|
||||||
|
Причина: недоступность LLM не должна ломать весь pipeline деплоя.
|
||||||
|
|
||||||
|
**Новый пакет:** `internal/validator/`
|
||||||
|
|
||||||
|
**Интерфейс:**
|
||||||
|
```go
|
||||||
|
// internal/validator/validator.go
|
||||||
|
type CodeValidator interface {
|
||||||
|
// Validate проверяет код функции перед сборкой.
|
||||||
|
// files — map[filename]content (текстовые файлы из zip).
|
||||||
|
// Возвращает (true, "") если код safe, (false, reason) если нет.
|
||||||
|
// При ошибке связи с LLM — возвращает (true, "") + логирует warning (soft-fail).
|
||||||
|
Validate(ctx context.Context, files map[string]string, runtime string) (safe bool, reason string, err error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**LLM-реализация:**
|
||||||
|
```go
|
||||||
|
// internal/validator/llm.go
|
||||||
|
type LLMValidator struct {
|
||||||
|
endpoint string // URL облачного LLM API (OpenAI-compatible)
|
||||||
|
apiKey string // токен доступа
|
||||||
|
timeout time.Duration // default: 15s
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Конфигурация (env vars):**
|
||||||
|
| Переменная | Default | Описание |
|
||||||
|
|------------|---------|----------|
|
||||||
|
| `LLM_ENABLED` | `false` | Включатель. false → NoopValidator (всегда safe) |
|
||||||
|
| `LLM_ENDPOINT` | — | URL LLM API, например `https://llm.nubes.ru/v1/chat/completions` |
|
||||||
|
| `LLM_API_KEY` | — | Bearer-токен для LLM API |
|
||||||
|
| `LLM_TIMEOUT` | `15s` | Максимальное время ожидания ответа |
|
||||||
|
|
||||||
|
`LLM_ENABLED=false` → оператор работает без LLM зависимости. По умолчанию выключено.
|
||||||
|
|
||||||
|
**Prompt-стратегия:**
|
||||||
|
|
||||||
|
Промпт НЕ хардкодится в Go — выносится в константу с возможностью override через ConfigMap.
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a security reviewer for a serverless cloud platform.
|
||||||
|
Analyze the following {runtime} code deployed as a cloud function.
|
||||||
|
|
||||||
|
Check for:
|
||||||
|
1. Cryptocurrency mining (crypto hash algorithms, pool connections, stratum protocol)
|
||||||
|
2. DDoS/botnet behavior (mass outbound HTTP/UDP, connection floods)
|
||||||
|
3. Port scanning / network reconnaissance
|
||||||
|
4. Reverse shells, backdoors, C2 communication
|
||||||
|
5. Attempts to escape container (access host filesystem, /proc, /sys)
|
||||||
|
6. Obfuscated code designed to hide malicious intent
|
||||||
|
|
||||||
|
Files:
|
||||||
|
{files_content}
|
||||||
|
|
||||||
|
Respond ONLY with valid JSON, no other text:
|
||||||
|
{"safe": true} or {"safe": false, "reason": "brief explanation"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что НЕ проверяем через LLM (не его задача):**
|
||||||
|
- Качество кода, стиль, best practices
|
||||||
|
- Уязвимости в зависимостях (это Trivy/npm audit, потом)
|
||||||
|
- Бизнес-логику пользователя
|
||||||
|
|
||||||
|
**Ограничения по размеру:**
|
||||||
|
- Суммарный размер текстовых файлов > 100KB → skip LLM (дорого, context window). Деплой проходит.
|
||||||
|
- Бинарные файлы (.pyc, .so, node_modules/) → не отправляются в LLM.
|
||||||
|
- Только расширения: `.py`, `.js`, `.ts`, `.json`, `.yaml`, `.yml`, `.txt`, `.sh`, `.sql`, `.go`.
|
||||||
|
|
||||||
|
**Встраивание в upload.go:**
|
||||||
|
```go
|
||||||
|
// После распаковки zip, до generateDockerfile
|
||||||
|
if h.Validator != nil {
|
||||||
|
files := extractTextFiles(zipData)
|
||||||
|
safe, reason, err := h.Validator.Validate(r.Context(), files, fn.Spec.Runtime)
|
||||||
|
if err != nil {
|
||||||
|
h.Log.Warn("llm validation error (soft-fail)", "err", err)
|
||||||
|
} else if !safe {
|
||||||
|
writeJSON(w, http.StatusBadRequest, errResp("code validation failed: "+reason))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Terraform provider:** Получит `status 400: code validation failed: <reason>` — пользователь видит причину в `terraform apply` output.
|
||||||
|
|
||||||
|
**Файлы для реализации:**
|
||||||
|
1. `internal/validator/validator.go` — интерфейс CodeValidator + NoopValidator
|
||||||
|
2. `internal/validator/llm.go` — LLMValidator с HTTP client к OpenAI-compatible API
|
||||||
|
3. `internal/validator/extract.go` — extractTextFiles: zip → map[string]string
|
||||||
|
4. `internal/config/config.go` — добавить LLM_ENABLED, LLM_ENDPOINT, LLM_API_KEY, LLM_TIMEOUT
|
||||||
|
5. `internal/api/handler/handler.go` — добавить Validator поле
|
||||||
|
6. `internal/api/handler/upload.go` — вызов Validator между zip и tar.gz
|
||||||
|
7. `main.go` — wire: if LLM_ENABLED → LLMValidator, else → NoopValidator
|
||||||
|
|
||||||
|
**Компромиссы:**
|
||||||
|
- +5-15 секунд к каждому деплою (зависит от скорости LLM).
|
||||||
|
- False positives: пользователь получит 400 с причиной, может обратиться в support.
|
||||||
|
- Soft-fail при недоступности LLM: security degraded, но деплой работает.
|
||||||
|
- Prompt не идеален: LLM не ловит всё. Это дополнительный слой, не единственный.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Два провайдера: sless и nubes — нельзя объединять
|
||||||
|
|
||||||
|
**Решение:** Провайдеры `sless` и `nubes` — **два отдельных независимых провайдера**.
|
||||||
|
Объединять их в один бинарник нельзя.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
- Разные зоны ответственности: `nubes` — облачная инфраструктура (ВМ, сети, объектное хранилище),
|
||||||
|
`sless` — serverless функции.
|
||||||
|
- Разные релизные циклы.
|
||||||
|
- В будущем — разные команды.
|
||||||
|
|
||||||
|
Пользователь использует оба в одном `.tf` файле — это нормально, это не значит что они один бинарник.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Namespace-per-user через JWT sub → SHA256
|
||||||
|
|
||||||
|
**Решение:** Каждый пользователь облака получает отдельный k8s namespace.
|
||||||
|
Namespace вычисляется детерминированно из JWT sub.
|
||||||
|
|
||||||
|
**Алгоритм:**
|
||||||
|
```
|
||||||
|
namespace = "sless-" + hex(SHA256(JWT.sub)[:8])
|
||||||
|
```
|
||||||
|
Итоговая длина: 22 символа. Пример: `sless-cdd874dfa31ba6ca`.
|
||||||
|
|
||||||
|
**Почему SHA256, а не UUID напрямую:**
|
||||||
|
- UUID (sub) напрямую в имени namespace — раскрывает внутренний ID пользователя.
|
||||||
|
- SHA256 — необратим, namespace не позволяет восстановить sub.
|
||||||
|
|
||||||
|
**Реализация:**
|
||||||
|
- `client.SubFromJWT(token)` — декодирует JWT payload → возвращает sub
|
||||||
|
- `client.NamespaceFromSub(sub)` — SHA256(sub)[:8] → hex → "sless-{hex16}"
|
||||||
|
- Вычисляется в `provider.Configure()` до создания Client
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — EnsureNamespace как отдельный endpoint (SoC)
|
||||||
|
|
||||||
|
**Проблема:** Создание namespace было в resource-хендлерах (CreateFunction, CreateTrigger, CreateJob).
|
||||||
|
Это нарушение разделения ответственностей: ресурс должен заниматься только тем, для чего предназначен.
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
- Создан отдельный endpoint `POST /v1/namespaces/{namespace}/ensure`
|
||||||
|
- Хендлер вынесен в отдельный файл `internal/api/handler/namespace.go`
|
||||||
|
- Провайдер вызывает его **один раз** в `Configure()` до создания любых ресурсов
|
||||||
|
- `handler.go` очищен от k8s-типов (corev1, k8serrors, metav1) — только инфраструктура
|
||||||
|
|
||||||
|
**Поведение endpoint:**
|
||||||
|
- 200 OK `{"namespace": "...", "status": "exists"}` — namespace уже был
|
||||||
|
- 201 Created `{"namespace": "...", "status": "created"}` — namespace создан
|
||||||
|
- Идемпотентен: параллельные запросы не падают (IsAlreadyExists обработан)
|
||||||
|
|
||||||
|
**Кто отвечает за namespace:**
|
||||||
|
Только `EnsureNamespace`. Ни один другой хендлер namespace не трогает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — JWT validation в операторе вместо статического токена
|
||||||
|
|
||||||
|
**Проблема:** Оператор сравнивал Bearer токен со статическим `apiToken` из конфига.
|
||||||
|
JWT-токены облака не совпадали → все запросы от провайдера отклонялись с 401.
|
||||||
|
|
||||||
|
**Решение:** `internal/api/middleware/auth.go` — заменена проверка:
|
||||||
|
- Было: `token == cfg.APIToken` (строковое сравнение)
|
||||||
|
- Стало: `validateJWT(token)` — проверяет структуру JWT (3 части), наличие `sub`, срок действия `exp`
|
||||||
|
|
||||||
|
**Почему подпись не проверяется:**
|
||||||
|
Оператор находится за Ingress в закрытом кластере (trusted perimeter).
|
||||||
|
Проверка подписи требует публичный ключ issuer — усложнение без реальной пользы в данной топологии.
|
||||||
|
Подпись проверяется косвенно через `PingNubesAPI` в провайдере при `terraform init`.
|
||||||
|
|
||||||
|
**Версия:** operator v0.1.20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Валидация токена через nubes API при Configure
|
||||||
|
|
||||||
|
**Решение:** При `terraform init` / `terraform apply` провайдер пингует nubes API
|
||||||
|
для подтверждения что токен действителен.
|
||||||
|
|
||||||
|
**Реализация:** `client.PingNubesAPI(ctx, endpoint, token)`:
|
||||||
|
- `GET <nubes_endpoint>` с Bearer токеном
|
||||||
|
- 401/403 → токен отклонён → ошибка инициализации провайдера
|
||||||
|
- Ошибка соединения → ошибка инициализации
|
||||||
|
- Любой другой статус (200, 404, 500...) → токен не декларирован невалидным → OK
|
||||||
|
|
||||||
|
**Конфигурация:**
|
||||||
|
```hcl
|
||||||
|
provider "sless" {
|
||||||
|
endpoint = "https://sless-api.kube5s.ru"
|
||||||
|
token = file("./secrets/prod.token")
|
||||||
|
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Env-альтернативы: SLESS_ENDPOINT, SLESS_API_TOKEN, NUBES_ENDPOINT.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — SoC рефакторинг handler.go
|
||||||
|
|
||||||
|
**Решение:** Файл `handler.go` — чистая инфраструктура.
|
||||||
|
Бизнес-логика по доменам — в отдельных файлах одного package.
|
||||||
|
|
||||||
|
**Структура handler/ package:**
|
||||||
|
```
|
||||||
|
handler.go — Handler struct + helpers (writeJSON, errResp, pathVar, namespace)
|
||||||
|
namespace.go — EnsureNamespace (k8s namespace lifecycle)
|
||||||
|
functions.go — CRUD Function
|
||||||
|
triggers.go — CRUD Trigger
|
||||||
|
jobs.go — CRUD FunctionJob
|
||||||
|
upload.go — zip -> tar.gz -> S3 -> CRD patch
|
||||||
|
invoke.go — прокси /fn/ -> in-cluster
|
||||||
|
invocations.go — 501 stub
|
||||||
|
```
|
||||||
|
|
||||||
|
**Принцип:** каждый файл отвечает за один домен.
|
||||||
|
`handler.go` не импортирует `corev1/k8serrors/metav1` — эти зависимости только в `namespace.go`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Namespace пользователя никогда не удаляется
|
||||||
|
|
||||||
|
**Решение:** User namespace (`sless-{hex16}`) **не удаляется** ни при каких обстоятельствах.
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
- Namespace вычисляется из `JWT.sub` — неизменяемого идентификатора пользователя.
|
||||||
|
- Namespace = "home directory" пользователя в кластере: `terraform destroy` удаляет
|
||||||
|
функции/триггеры/джобы, но не сам контейнер для ресурсов.
|
||||||
|
- Удаление namespace уничтожило бы все CRD объекты пользователя.
|
||||||
|
- Повторный `terraform apply` (после destroy) нашёл бы свой ns живым — правильное поведение.
|
||||||
|
|
||||||
|
**Верификация (проверено):**
|
||||||
|
- В API нет маршрута `DELETE /v1/namespaces/{namespace}`.
|
||||||
|
- `handleDeletion` в `function_controller.go` удаляет: Deployment, Service, Ingress, kaniko Job.
|
||||||
|
- `handleTriggerDeletion` в `trigger_controller.go` удаляет: CronJob (в deployNS), Service, Ingress.
|
||||||
|
- Оба контроллера содержат явный комментарий: "Namespace sless-fn-{userNS} НЕ удаляется — он принадлежит пользователю".
|
||||||
|
- Тест: `kubectl get ns sless-cdd874dfa31ba6ca` — namespace жив через 93 минуты после `terraform destroy`.
|
||||||
|
|
||||||
|
**Оба namespace предохраняются:**
|
||||||
|
- `sless-{hex16}` — user namespace (хранит CRD объекты Function/Trigger/FunctionJob)
|
||||||
|
- `sless-fn-{hex16}` — deploy namespace (хранит Deployment/Service/Ingress/CronJob)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Builder SoC: context.go отделён от upload.go
|
||||||
|
|
||||||
|
**Проблема:** `generateDockerfile`, `runtimeBaseImage`, `zipToTarGz` жили в `handler/upload.go`.
|
||||||
|
Знание о runtime образах и структуре build context — детали **сборки**, не HTTP-хендлера.
|
||||||
|
Нарушение SoC: HTTP-файл знал о Docker, kaniko, tar.gz, zip-разборе.
|
||||||
|
|
||||||
|
**Решение:** Перенести в `internal/builder/context.go`, единственный публичный API:
|
||||||
|
```go
|
||||||
|
func PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
- `upload.go`: ~200 LOC → ~60 LOC (только HTTP: принять zip, вызвать PrepareContext, сохранить в S3)
|
||||||
|
- `context.go`: всё знание о runtime образах, zip→tar, Dockerfile генерации
|
||||||
|
|
||||||
|
**Детали реализации:**
|
||||||
|
- `zipToTarGz` принимает `*zip.Reader` вместо `[]byte` — zip парсится один раз в `PrepareContext`
|
||||||
|
- `PrepareContext` сама сканирует zip-архив (requirements.txt, package.json) — хендлер не знает об этом
|
||||||
|
- `runtimeBaseImage` возвращает ошибку для неизвестного runtime — ранний fail до kaniko
|
||||||
|
|
||||||
|
**Тесты:** 4 теста в `internal/builder/context_test.go` (python+requirements, node без package.json, unsupported runtime, Dockerfile-first в tar).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Фильтрация hop-by-hop headers в /fn/ прокси
|
||||||
|
|
||||||
|
**Проблема:** `invoke.go` пробрасывал все заголовки ответа функции клиенту, включая hop-by-hop.
|
||||||
|
`Transfer-Encoding: chunked` особенно опасен: Go `http.ResponseWriter` не умеет его воспроизводить,
|
||||||
|
клиент получал некорректное тело ответа (или ошибку framing).
|
||||||
|
|
||||||
|
**Решение:** Фильтровать по RFC 2616 §13.5.1 перед записью в `w`:
|
||||||
|
```go
|
||||||
|
var hopByHopHeaders = map[string]bool{
|
||||||
|
"Connection": true, "Keep-Alive": true, "Proxy-Authenticate": true,
|
||||||
|
"Proxy-Authorization": true, "Te": true, "Trailers": true,
|
||||||
|
"Transfer-Encoding": true, "Upgrade": true,
|
||||||
|
}
|
||||||
|
// В цикле:
|
||||||
|
if hopByHopHeaders[k] { continue }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Почему map[string]bool:** O(1) lookup, ключи в canonical form (`http.CanonicalHeaderKey`),
|
||||||
|
совпадает с форматом ключей в `http.Header` — нет нужды нормализовывать.
|
||||||
|
|
||||||
|
**Тесты:** 3 теста в `internal/api/handler/invoke_test.go`
|
||||||
|
(filtered from response, map contains all RFC2616, canonical key form).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — JWKS insertion point stub в auth.go
|
||||||
|
|
||||||
|
**Контекст:** v1 auth — `validateJWT` проверяет структуру токена (sub, exp) без проверки подписи.
|
||||||
|
Это допустимо в trusted perimeter (оператор в k8s, доступен только изнутри).
|
||||||
|
|
||||||
|
**Решение:** Добавлена `verifySignature()` как закомментированная заготовка в `auth.go`.
|
||||||
|
|
||||||
|
**v2 план (когда nubes даст JWKS endpoint):**
|
||||||
|
1. `GET {NUBES_JWKS_URL}/.well-known/jwks.json`
|
||||||
|
2. Найти ключ по `kid` из JWT header
|
||||||
|
3. Проверить подпись RS256/ES256 через `github.com/lestrrat-go/jwx/v2`
|
||||||
|
4. Добавить вызов `verifySignature(token)` в `validateJWT` после проверки структуры.
|
||||||
|
|
||||||
|
**Зачем stub:** любой агент или разработчик видит точную строку для вставки. Нет риска забыть.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — CronJob перенесён в deployNS
|
||||||
|
|
||||||
|
**Проблема:** CronJob для HTTP-триггеров создавался в `tr.Namespace` (user namespace: `sless-{hex16}`).
|
||||||
|
При применении NetworkPolicy (каждый namespace изолирован) — CronJob не мог бы дотянуться до API.
|
||||||
|
|
||||||
|
**Решение:** CronJob создаётся в `deployNS` = `"sless-fn-" + tr.Namespace`,
|
||||||
|
где живут Deployment/Service — NetworkPolicy там уже правильная.
|
||||||
|
|
||||||
|
**Затронутые места в trigger_controller.go:**
|
||||||
|
- `buildCronJob` — namespace в ObjectMeta
|
||||||
|
- `r.Client.Create` — нет изменений (namespace из объекта)
|
||||||
|
- `r.Client.Get` в reconcile — `deployNS` вместо ns
|
||||||
|
- `handleTriggerDeletion` — удаление CronJob из `deployNS`
|
||||||
|
|
||||||
|
**Дополнительно:** `curlimages/curl:latest` → `curlimages/curl:8.5.0` (pin версии).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-03-11 — Sort env vars в buildDeployment
|
||||||
|
|
||||||
|
**Проблема:** `fn.Spec.Env` — это `map[string]string`. Итерация по map в Go недетерминирована.
|
||||||
|
Каждый reconcile мог генерировать Pod spec с другим порядком env vars → лишние rollout'ы.
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```go
|
||||||
|
keys := make([]string, 0, len(fn.Spec.Env))
|
||||||
|
for k := range fn.Spec.Env { keys = append(keys, k) }
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys { envVars = append(envVars, corev1.EnvVar{Name: k, Value: fn.Spec.Env[k]}) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Тесты:** 2 теста в `controllers/function_controller_unit_test.go`
|
||||||
|
(4 env vars → алфавитный порядок после SLESS_ENTRYPOINT; пустой Env → только SLESS_ENTRYPOINT).
|
||||||
+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