- architecture/overview.md — актуальная архитектура: namespace-per-user, схема JWT->SHA256->namespace, структура кода, версии в production - decisions/log.md — новые решения: два провайдера нельзя объединять, namespace-per-user, EnsureNamespace как отдельный endpoint (SoC), JWT validation вместо статического токена, валидация через nubes API, handler.go SoC рефакторинг - progress.md — статус 2026-03-11 (all done), технический долг - architecture/agent-handoff-2026-03-11.md — подробный handoff для Opus: полная архитектура, lifecycle контроллеров, примеры кода, ТЗ, вопросы
644 lines
26 KiB
Markdown
644 lines
26 KiB
Markdown
# Agent Handoff — 2026-03-11
|
||
|
||
Документ для передачи контекста следующему агенту (Claude Opus).
|
||
Охватывает всё что реализовано, ключевые решения, текущее состояние кода,
|
||
технический долг и вопросы для анализа.
|
||
|
||
---
|
||
|
||
## 1. Что такое этот проект
|
||
|
||
Managed Serverless Functions Service — платформа для запуска пользовательских
|
||
функций в облаке nubes.ru.
|
||
|
||
**Аналог:** AWS Lambda, Yandex Cloud Functions, но для собственного облака.
|
||
|
||
**Цель:** пользователь пишет функцию (Python/Node.js), загружает через Terraform,
|
||
получает HTTP endpoint или триггер по расписанию. Вся инфраструктура скрыта.
|
||
|
||
**Текущий статус:** MVP работает в production кластере. Идёт итеративное улучшение.
|
||
|
||
---
|
||
|
||
## 2. Стек и инфраструктура
|
||
|
||
```
|
||
Пользователь
|
||
-> Terraform provider sless (terra.k8c.ru/naeel/sless v0.1.13)
|
||
-> REST API оператора (https://sless-api.kube5s.ru)
|
||
-> Kubernetes кластер (существующий, namespace sless)
|
||
-> S3 (Ceph, s3.msk-1.ngcloud.ru) — хранение кода
|
||
-> DockerHub (naeel/) — хранение образов функций
|
||
-> kaniko (k8s Job) — сборка Docker образов из кода
|
||
-> Deployments/Jobs/CronJobs — запуск функций
|
||
```
|
||
|
||
**Kubernetes кластер:**
|
||
- 1 control-plane + 2 workers
|
||
- Ingress nginx, external IP 5.172.178.182
|
||
- StorageClass local-path (rawfile CSI / OpenEBS)
|
||
- cert-manager, Kyverno, Cilium CNI
|
||
- kubectl: KUBECONFIG=~/.kube/wheel.conf
|
||
|
||
**Namespace оператора:** sless
|
||
**Operator image:** naeel/sless-operator:v0.1.21
|
||
**Provider version:** terra.k8c.ru/naeel/sless v0.1.13
|
||
|
||
---
|
||
|
||
## 3. Архитектура — компоненты
|
||
|
||
### Оператор (один Go бинарник)
|
||
|
||
```
|
||
main.go
|
||
|
|
||
+-- k8s manager (controller-runtime)
|
||
| +-- FunctionReconciler (функции lifecycle)
|
||
| +-- TriggerReconciler (HTTP/cron триггеры)
|
||
| +-- FunctionJobReconciler (one-shot запуски)
|
||
|
|
||
+-- REST API сервер (goroutine, :9090)
|
||
+-- /fn/{ns}/{name} — публичный прокси вызова функций (без auth)
|
||
+-- /v1/... — управление ресурсами (JWT auth)
|
||
```
|
||
|
||
#### REST API маршруты
|
||
|
||
```
|
||
POST /v1/namespaces/{ns}/ensure <- EnsureNamespace
|
||
GET /v1/namespaces/{ns}/functions <- ListFunctions
|
||
POST /v1/namespaces/{ns}/functions <- CreateFunction
|
||
GET /v1/namespaces/{ns}/functions/{name} <- GetFunction
|
||
PUT /v1/namespaces/{ns}/functions/{name} <- UpdateFunction
|
||
DELETE /v1/namespaces/{ns}/functions/{name} <- DeleteFunction
|
||
POST /v1/namespaces/{ns}/functions/{name}/upload <- UploadCode (zip)
|
||
GET /v1/namespaces/{ns}/functions/{name}/invocations <- 501 (не реализован)
|
||
GET /v1/namespaces/{ns}/triggers <- ListTriggers
|
||
POST /v1/namespaces/{ns}/triggers <- CreateTrigger
|
||
GET /v1/namespaces/{ns}/triggers/{name} <- GetTrigger
|
||
PATCH /v1/namespaces/{ns}/triggers/{name} <- UpdateTrigger (enabled)
|
||
DELETE /v1/namespaces/{ns}/triggers/{name} <- DeleteTrigger
|
||
POST /v1/namespaces/{ns}/jobs <- CreateJob
|
||
GET /v1/namespaces/{ns}/jobs/{name} <- GetJob
|
||
DELETE /v1/namespaces/{ns}/jobs/{name} <- DeleteJob
|
||
ANY /fn/{namespace}/{name}/* <- InvokeFunction (прокси)
|
||
```
|
||
|
||
#### CRD ресурсы
|
||
|
||
**Function:**
|
||
```go
|
||
FunctionSpec {
|
||
Runtime string // "python3.11" | "nodejs20"
|
||
Entrypoint string // "handler.handle" (python) / игнорируется (node)
|
||
S3Key string // contexts/{ns}/{name}/{ts}.tar.gz
|
||
S3Bucket string
|
||
MemoryMB int32
|
||
TimeoutSec int32
|
||
Env []corev1.EnvVar
|
||
}
|
||
FunctionStatus {
|
||
Phase FunctionPhase // Pending | Building | Ready | Failed
|
||
ImageRef string // naeel/sless-{ns}-{name}:{sha12}
|
||
Message string
|
||
LastBuiltAt metav1.Time
|
||
}
|
||
```
|
||
|
||
**Trigger:**
|
||
```go
|
||
TriggerSpec {
|
||
Type TriggerType // http | cron
|
||
FunctionRef string
|
||
Schedule string // cron expression
|
||
Enabled bool // false -> replicas=0 (функция не принимает запросы)
|
||
}
|
||
TriggerStatus {
|
||
Active bool
|
||
URL string // https://sless-api.kube5s.ru/fn/{ns}/{name}
|
||
LastScheduleTime *metav1.Time
|
||
}
|
||
```
|
||
|
||
**FunctionJob:**
|
||
```go
|
||
FunctionJobSpec {
|
||
FunctionRef string
|
||
EventJSON string // произвольный JSON-payload для функции
|
||
RunID int64 // 0=skip, >0=run; увеличить для повторного запуска
|
||
}
|
||
FunctionJobStatus {
|
||
Phase FunctionJobPhase // Pending | Skipped | Running | Succeeded | Failed
|
||
JobName string
|
||
StartTime *metav1.Time
|
||
CompletionTime *metav1.Time
|
||
Message string // stdout функции (результат)
|
||
}
|
||
```
|
||
|
||
### Terraform Provider
|
||
|
||
**Ресурсы:**
|
||
- `sless_function` — управление функцией (CRUD + upload + WaitReady)
|
||
- `sless_trigger` — управление триггером (CRUD + WaitGone при Delete)
|
||
- `sless_job` — one-shot запуск (Create + WaitJobDone если run_id>0)
|
||
|
||
**Provider конфигурация:**
|
||
```hcl
|
||
provider "sless" {
|
||
endpoint = "https://sless-api.kube5s.ru"
|
||
token = file("./secrets/prod.token")
|
||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||
# env alternatives: SLESS_ENDPOINT, SLESS_API_TOKEN, NUBES_ENDPOINT
|
||
}
|
||
```
|
||
|
||
**Инициализация (Configure):**
|
||
1. Читаем endpoint + token
|
||
2. SubFromJWT(token) -> sub
|
||
3. NamespaceFromSub(sub) -> namespace = "sless-{sha256(sub)[:8]hex}"
|
||
4. PingNubesAPI(nubes_endpoint, token) -> 401/403 = ошибка
|
||
5. client.New(endpoint, token, namespace)
|
||
6. c.EnsureNamespace(ctx, namespace) -> POST /v1/namespaces/{ns}/ensure
|
||
|
||
---
|
||
|
||
## 4. Ключевые архитектурные решения
|
||
|
||
### Namespace per user
|
||
|
||
Изоляция пользователей через k8s namespace:
|
||
```
|
||
namespace = "sless-" + hex(SHA256(JWT.sub)[:8])
|
||
```
|
||
- Детерминирован (один sub = один namespace всегда)
|
||
- Необратим (нельзя восстановить sub из namespace)
|
||
- Длина 22 символа (< лимита k8s 63)
|
||
- Пример реального namespace: sless-cdd874dfa31ba6ca
|
||
|
||
### Разделение ответственностей (SoC)
|
||
|
||
handler/ package намеренно разделён по файлам:
|
||
```
|
||
handler.go — только инфраструктура (Handler struct, helpers)
|
||
namespace.go — EnsureNamespace (k8s namespace lifecycle)
|
||
functions.go — CRUD Function
|
||
triggers.go — CRUD Trigger
|
||
jobs.go — CRUD FunctionJob
|
||
upload.go — код -> S3 -> kaniko
|
||
invoke.go — прокси /fn/
|
||
```
|
||
|
||
**Правило:** resource-хендлеры не создают namespace. Namespace создаётся один раз
|
||
в namespace.go через отдельный endpoint.
|
||
|
||
### JWT auth в операторе
|
||
|
||
Проверяется: структура JWT (3 части) + sub claim существует + exp не истёк.
|
||
Подпись НЕ проверяется — trusted perimeter.
|
||
|
||
### Аутентификация токена через nubes API
|
||
|
||
Токен считается валидным если nubes API не вернул 401/403.
|
||
Это происходит один раз при terraform init/apply в Configure().
|
||
|
||
### Два провайдера — нельзя объединять
|
||
|
||
`sless` и `nubes` — два отдельных Terraform провайдера.
|
||
Разные зоны ответственности, разные релизные циклы.
|
||
|
||
### DockerHub вместо registry в кластере
|
||
|
||
namespace `registry` — это Apache NiFi Registry (не Docker!).
|
||
Образы функций: `naeel/sless-{ns}-{name}:{sha12}` на DockerHub.
|
||
Компромисс: образы публичны. Для production нужен приватный registry.
|
||
|
||
### HTTP прокси /fn/ вместо wildcard DNS
|
||
|
||
У облачного провайдера нет возможности создать wildcard DNS *.fn.kube5s.ru.
|
||
Вместо этого оператор сам проксирует запросы:
|
||
```
|
||
GET https://sless-api.kube5s.ru/fn/{namespace}/{name}/path?query
|
||
-> GET http://{name}.{namespace}.svc.cluster.local:8080/path?query
|
||
```
|
||
|
||
### Kaniko сборка образов
|
||
|
||
kaniko запускается как k8s Job в namespace пользователя.
|
||
Контекст сборки — tar.gz в S3 (zip от пользователя перепаковывается).
|
||
Dockerfile генерируется автоматически из runtime (пользователь не видит).
|
||
|
||
```
|
||
POST /upload (zip)
|
||
-> распаковка zip
|
||
-> (TODO: LLM-валидация кода)
|
||
-> generateDockerfile(runtime)
|
||
-> zipToTarGz -> S3
|
||
-> Function.Spec.S3Key = новый ключ
|
||
-> контроллер видит изменение -> запускает kaniko Job
|
||
```
|
||
|
||
### WaitReady после upload
|
||
|
||
terraform apply блокируется до phase=Ready (kaniko сборка ~1 мин).
|
||
Polling каждые 5 сек, таймаут default 300 сек.
|
||
Без этого terraform state показывал бы phase=Building.
|
||
|
||
### code_hash для детектирования изменений
|
||
|
||
Атрибут `code_hash` в sless_function.
|
||
Пользователь задаёт через `filesha256("./handler.js")`.
|
||
Изменение hash -> провайдер перезагружает zip -> пересборка.
|
||
НЕ использовать output_md5 от hashicorp/archive — там баг (MD5 не обновляется).
|
||
|
||
---
|
||
|
||
## 5. Структура файлов провайдера
|
||
|
||
```
|
||
terraform/provider/
|
||
go.mod module: terraform-provider-sless
|
||
main.go запуск провайдера через plugin framework
|
||
internal/
|
||
client/client.go HTTP-клиент к REST API оператора
|
||
SubFromJWT(token) string JWT payload decode -> sub
|
||
NamespaceFromSub(sub) string SHA256[:8] -> "sless-{hex16}"
|
||
PingNubesAPI(ctx, ep, token) GET запрос к nubes API
|
||
New(endpoint, token, ns) создаёт Client
|
||
EnsureNamespace(ctx, ns) POST /v1/namespaces/{ns}/ensure
|
||
CreateFunction/GetFunction/UpdateFunction/DeleteFunction
|
||
UploadCode/UploadCodeReader
|
||
CreateTrigger/GetTrigger/UpdateTrigger/DeleteTrigger
|
||
CreateJob/GetJob/DeleteJob
|
||
WaitReady(ctx, ns, name, timeout)
|
||
WaitJobDone(ctx, ns, name, timeout)
|
||
provider/provider.go
|
||
Configure() - JWT->NS->ping->EnsureNamespace, создаёт client
|
||
resources/
|
||
function_resource.go
|
||
source_dir атрибут zipDir() в памяти, sha256 автоматически
|
||
code_path атрибут путь к готовому zip
|
||
code_hash атрибут filesha256(source_file) для детектирования
|
||
build_timeout_sec таймаут ожидания kaniko (default 300)
|
||
trigger_resource.go
|
||
enabled атрибут false -> replicas=0 in-place PATCH
|
||
job_resource.go
|
||
run_id атрибут 0=skip, >0=run, повторный запуск = увеличить
|
||
wait_timeout_sec таймаут ожидания job
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Lifecycle контроллеров
|
||
|
||
### FunctionReconciler
|
||
|
||
```
|
||
Function CRD создан -> phase=Pending
|
||
S3Key задан?
|
||
Нет -> ждём upload
|
||
Да ->
|
||
Уже Building?
|
||
Нет -> запустить kaniko Job (startBuild)
|
||
Да -> проверить статус Job (checkBuild)
|
||
succeeded? -> обновить ImageRef, S3Key аннотацию, phase=Ready
|
||
failed? -> phase=Failed
|
||
phase=Ready?
|
||
-> ensureDeployment (создать/обновить Deployment)
|
||
+ ensureRegistrySecret (скопировать DockerHub secret в NS пользователя)
|
||
Удаление (finalizer)?
|
||
-> удалить Deployment + Service + kaniko Jobs
|
||
```
|
||
|
||
**Idempotency guard:** аннотация `last-built-s3key` предотвращает повторный запуск
|
||
kaniko для одного и того же S3 ключа.
|
||
|
||
**Rollout restart:** при обновлении Deployment проставляется аннотация
|
||
`kubectl.kubernetes.io/restartedAt = fn.Status.LastBuiltAt` — гарантирует пулл
|
||
свежего образа даже при :latest теге.
|
||
|
||
### TriggerReconciler
|
||
|
||
```
|
||
Trigger CRD создан
|
||
type=http?
|
||
-> reconcileHTTP: Service + (Ingress если нет ExternalURL)
|
||
Status.URL = ExternalURL/fn/{ns}/{name} (или Ingress URL)
|
||
enabled=false? -> patch Deployment replicas=0
|
||
enabled=true? -> patch Deployment replicas=1
|
||
type=cron?
|
||
-> reconcileCron: CronJob (вызывает функцию через HTTP по расписанию)
|
||
Удаление (finalizer)?
|
||
-> handleTriggerDeletion: удалить Service + Ingress из namespace пользователя
|
||
```
|
||
|
||
### FunctionJobReconciler
|
||
|
||
```
|
||
FunctionJob CRD создан
|
||
RunID == 0? -> phase=Skipped, return
|
||
RunID > 0?
|
||
Job не создан? -> создать k8s Job
|
||
Job существует?
|
||
-> syncJobStatus: проверить Conditions Job
|
||
Succeeded? -> getJobPodOutput() -> Message = stdout, phase=Succeeded
|
||
Failed? -> phase=Failed
|
||
иначе -> RequeueAfter 5s (polling)
|
||
Удаление? -> удалить k8s Job
|
||
```
|
||
|
||
**Cross-namespace проблема:** FunctionJob в namespace пользователя, k8s Job тоже
|
||
там. Owns watch убран (не работает cross-namespace). Используется polling (RequeueAfter 5s).
|
||
|
||
---
|
||
|
||
## 7. Рантаймы функций
|
||
|
||
### python3.11
|
||
|
||
```
|
||
runtimes/python3.11/
|
||
server.py Flask-like HTTP сервер :8080
|
||
GET /health -> {"status":"ok"}
|
||
POST /* -> загружает /app/function/{HANDLER_PATH}
|
||
вызывает handler.handle(event_dict) -> response
|
||
Dockerfile FROM python:3.11-slim
|
||
COPY server.py /app/
|
||
CMD ["python", "/app/server.py"]
|
||
|
||
Публичный образ: naeel/sless-runtime-python3.11:v0.1.1
|
||
```
|
||
|
||
**Пользовательский код:** handler.py с `def handle(event): return {...}`
|
||
|
||
### nodejs20
|
||
|
||
```
|
||
runtimes/nodejs20/
|
||
server.js http.createServer :8080
|
||
GET /health -> {"status":"ok"}
|
||
POST /* -> require(HANDLER_PATH) -> exports.handle(event)
|
||
Dockerfile FROM node:20-alpine
|
||
COPY server.js /app/
|
||
CMD ["node", "/app/server.js"]
|
||
|
||
Публичный образ: naeel/sless-runtime-nodejs20:v0.1.2
|
||
```
|
||
|
||
**Пользовательский код:** handler.js с `exports.handle = async (event) => {...}`
|
||
|
||
### Dockerfile генерируется автоматически
|
||
|
||
При upload оператор генерирует Dockerfile:
|
||
```
|
||
FROM naeel/sless-runtime-{runtime}:{версия}
|
||
COPY . /app/function/
|
||
RUN pip install -r requirements.txt # если есть (python)
|
||
RUN npm install --omit=dev # если есть package.json (node)
|
||
```
|
||
|
||
Пользователь никогда не видит Dockerfile.
|
||
|
||
---
|
||
|
||
## 8. Пример Terraform конфигурации
|
||
|
||
```hcl
|
||
terraform {
|
||
required_providers {
|
||
sless = {
|
||
source = "terra.k8c.ru/naeel/sless"
|
||
version = "~> 0.1.13"
|
||
}
|
||
}
|
||
}
|
||
|
||
provider "sless" {
|
||
endpoint = "https://sless-api.kube5s.ru"
|
||
token = file("${path.module}/../../secrets/prod.token")
|
||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||
}
|
||
|
||
# HTTP функция
|
||
resource "sless_function" "hello_http" {
|
||
name = "hello-http"
|
||
runtime = "nodejs20"
|
||
|
||
source_dir = "${path.module}/code" # папка с handler.js
|
||
# ИЛИ:
|
||
# code_path = "${path.module}/handler.zip"
|
||
# code_hash = filesha256("${path.module}/code/handler.js")
|
||
|
||
memory_mb = 128
|
||
timeout_sec = 30
|
||
env_vars = {
|
||
"NODE_ENV" = "production"
|
||
}
|
||
}
|
||
|
||
# HTTP триггер
|
||
resource "sless_trigger" "hello_http" {
|
||
name = "hello-http-trigger"
|
||
function_ref = sless_function.hello_http.name
|
||
type = "http"
|
||
enabled = true
|
||
}
|
||
|
||
# Cron триггер
|
||
resource "sless_trigger" "daily" {
|
||
name = "daily-job"
|
||
function_ref = sless_function.hello_http.name
|
||
type = "cron"
|
||
schedule = "0 9 * * *"
|
||
}
|
||
|
||
# One-shot Job
|
||
resource "sless_job" "hello_run" {
|
||
name = "hello-run"
|
||
function_ref = sless_function.hello_http.name
|
||
run_id = 1 # увеличить для повторного запуска
|
||
event_json = jsonencode({"input": [100, 200, 300]})
|
||
}
|
||
|
||
output "trigger_url" {
|
||
value = sless_trigger.hello_http.trigger_url
|
||
}
|
||
output "job_result" {
|
||
value = sless_job.hello_run.job_message
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Технический долг
|
||
|
||
### Средний приоритет (реально нужно)
|
||
|
||
1. **`upload.go`: builder logic в HTTP handler**
|
||
- `generateDockerfile()`, `runtimeBaseImage()`, `zipToTarGz()` — это логика сборщика
|
||
- Должно быть в `internal/builder/` или отдельном builderconfig пакете
|
||
- HTTP handler должен только принять zip и вызвать builder.PrepareContext()
|
||
|
||
2. **`invocations.go`: 501 stub**
|
||
- PostgreSQL подключён, RunMigrations работает, таблица `invocations` создана
|
||
- Логика ListInvocations в postgres/store.go уже есть
|
||
- Осталось только подключить endpoint к store
|
||
|
||
3. **LLM-валидация кода при upload**
|
||
- Полный дизайн в `doc/decisions/log.md` (раздел 2026-03-10)
|
||
- Интерфейс `CodeValidator`, `LLMValidator`, `NoopValidator`
|
||
- Точка вставки: upload.go между распаковкой zip и tar.gz
|
||
- Soft-fail: LLM недоступен -> предупреждение, деплой продолжается
|
||
- Blocking: LLM говорит unsafe -> HTTP 400
|
||
|
||
### Низкий приоритет (v1.1 / v2)
|
||
|
||
4. **`ensureRegistrySecret` в FunctionReconciler**
|
||
- Копирует DockerHub secret в namespace пользователя
|
||
- Cross-namespace инфраструктурная операция в бизнес-контроллере
|
||
- Идеально — отдельный контроллер или admission webhook
|
||
|
||
5. **replicas field в FunctionSpec**
|
||
- Позволит пользователю `replicas=0` (выключить без удаления)
|
||
- Пока enabled/disabled только через Trigger.Enabled
|
||
|
||
6. **Scale-to-zero (KEDA)**
|
||
- Заменить Deployment на HTTPScaledObject (KEDA HTTP Add-on)
|
||
- minReplicas=0, cold start ~1-3 сек
|
||
- Требует установки KEDA в кластер
|
||
|
||
7. **Инвокации history (v2)**
|
||
- Логирование каждого вызова в PostgreSQL
|
||
- invoke.go -> SaveInvocation -> ListInvocations endpoint
|
||
|
||
8. **RabbitMQ event triggers (v2)**
|
||
- Подписка на очередь -> вызов функции
|
||
- EventDispatcher компонент
|
||
|
||
9. **Приватный Docker registry**
|
||
- Сейчас DockerHub — образы функций публичны
|
||
- Для production: Harbor / ECR / GCR
|
||
|
||
10. **Метрики в Victoria Metrics**
|
||
- Время сборки, время вызова, ошибки, фазы функций
|
||
|
||
---
|
||
|
||
## 10. Известные ограничения
|
||
|
||
| # | Ограничение | Последствие |
|
||
|---|-------------|-------------|
|
||
| 1 | DockerHub — публичный registry | Код функций в образах виден всем |
|
||
| 2 | JWT подпись не проверяется | Внутри trusted perimeter — OK, при публичном доступе — risk |
|
||
| 3 | Один бинарник (API + Controllers) | Нельзя масштабировать по отдельности |
|
||
| 4 | Функция без replicas field | Нет ручного выключения без удаления |
|
||
| 5 | namespace не удаляется при destroy | Пустой namespace остаётся в k8s |
|
||
| 6 | LLM-валидация не реализована | Код не проверяется перед деплоем |
|
||
| 7 | invocations endpoint — 501 | История вызовов недоступна |
|
||
|
||
---
|
||
|
||
## 11. Вопросы для анализа Opus
|
||
|
||
Следующему агенту предлагается ответить на:
|
||
|
||
1. **Builder SoC:** Правильно ли выносить `generateDockerfile/zipToTarGz` в `internal/builder/`?
|
||
Как это соотносится с тем что контроллер уже использует builder.Build()?
|
||
Какой интерфейс был бы оптимальным?
|
||
|
||
2. **LLM-валидация:** Дизайн в decisions/log.md — что в нём не учтено?
|
||
Как обрабатывать false positives (пользователи которые получат 400 незаслуженно)?
|
||
Нужен ли ручной override/whitelist?
|
||
|
||
3. **Namespace lifecycle:** Сейчас namespace не удаляется при `terraform destroy`.
|
||
Это намеренно (данные не теряются при случайном destroy)?
|
||
Или нужен endpoint DELETE /v1/namespaces/{ns} с принудительной очисткой?
|
||
|
||
4. **Security: JWT подпись:** Стоит ли добавить проверку подписи через JWKS URI nubes?
|
||
Это усложнит архитектуру, но даст дополнительный слой защиты.
|
||
При каком масштабе/угрозах это становится необходимым?
|
||
|
||
5. **ensureRegistrySecret:** Сейчас копирование DockerHub secret в namespace пользователя
|
||
делается в FunctionReconciler. Это admission webhook? Отдельный reconciler?
|
||
Какой паттерн правильнее для cross-namespace секретов в k8s?
|
||
|
||
6. **Единый бинарник:** При каком масштабе нагрузки оправдано разделение
|
||
API Server и Controllers на отдельные поды?
|
||
Какая метрика должна служить триггером для разделения?
|
||
|
||
---
|
||
|
||
## 12. Текущее состояние git
|
||
|
||
```
|
||
Branch: feat/namespace-per-user
|
||
Last commit: a1774e1
|
||
Message: "refactor: SoC — EnsureNamespace в namespace.go, маршрут /ensure, client.EnsureNamespace, fix secrets в .gitignore"
|
||
|
||
Файлы в коммите:
|
||
.gitignore — добавлена secrets/
|
||
examples/hello-node/main.tf — версия провайдера ~> 0.1.13
|
||
internal/api/handler/functions.go — убран вызов ensureNamespace
|
||
internal/api/handler/handler.go — убраны k8s-типы, только инфраструктура
|
||
internal/api/handler/jobs.go — убран вызов ensureNamespace
|
||
internal/api/handler/namespace.go — НОВЫЙ: EnsureNamespace хендлер
|
||
internal/api/handler/triggers.go — убран вызов ensureNamespace
|
||
internal/api/middleware/auth.go — JWT validation (sub+exp)
|
||
internal/api/router.go — маршрут /ensure добавлен
|
||
terraform/provider/internal/client/client.go — SubFromJWT, NamespaceFromSub, PingNubesAPI, EnsureNamespace
|
||
terraform/provider/internal/provider/provider.go — Configure(): JWT->NS->ping->EnsureNamespace
|
||
```
|
||
|
||
Предыдущий коммит в ветке: 5ae2ee7 (JWT auth fix, оператор v0.1.20, провайдер v0.1.12)
|
||
|
||
---
|
||
|
||
## 13. Как запустить E2E тест
|
||
|
||
Предварительно: токен в `secrets/prod.token`, KUBECONFIG=~/.kube/wheel.conf
|
||
|
||
```bash
|
||
# 1. Проверить кластер
|
||
KUBECONFIG=~/.kube/wheel.conf kubectl -n sless get pods
|
||
|
||
# 2. Проверить оператор
|
||
curl -sk https://sless-api.kube5s.ru/fn/nonexistent/ | jq .
|
||
|
||
# 3. Terraform apply
|
||
cd examples/hello-node
|
||
terraform init
|
||
TOKEN=$(cat ../../secrets/prod.token) terraform apply -auto-approve
|
||
|
||
# 4. Вызов функции
|
||
curl https://sless-api.kube5s.ru/fn/sless-cdd874dfa31ba6ca/hello-http
|
||
|
||
# 5. Cleanup
|
||
TOKEN=$(cat ../../secrets/prod.token) terraform destroy -auto-approve
|
||
```
|
||
|
||
Ожидаемый результат apply:
|
||
- Создан namespace sless-cdd874dfa31ba6ca
|
||
- 2 функции (hello-http nodejs20, hello-job nodejs20)
|
||
- 1 HTTP триггер
|
||
- 1 FunctionJob с результатом {"input":[100,200,300],"sum":600}
|
||
|
||
---
|
||
|
||
## 14. Файлы для детального чтения
|
||
|
||
Для понимания кода рекомендуется читать в порядке:
|
||
|
||
1. `api/v1alpha1/function_types.go` — что такое Function
|
||
2. `internal/api/handler/handler.go` + `namespace.go` — базовая инфраструктура
|
||
3. `internal/api/handler/functions.go` — CRUD пример
|
||
4. `internal/api/handler/upload.go` — загрузка кода (TODO: перенести builder logic)
|
||
5. `internal/api/router.go` — все маршруты
|
||
6. `internal/api/middleware/auth.go` — JWT validation
|
||
7. `controllers/function_controller.go` — основной reconcile loop
|
||
8. `internal/builder/builder.go` — kaniko Job management
|
||
9. `terraform/provider/internal/provider/provider.go` — Configure()
|
||
10. `terraform/provider/internal/client/client.go` — HTTP-клиент
|
||
11. `terraform/provider/internal/resources/function_resource.go` — terraform ресурс
|
||
12. `examples/hello-node/main.tf` — рабочий пример использования
|