- agent-handoff-2026-03-10.md — GPT-5.4 code review (lifecycle issues, invocation history gap) - opus-pragmatic-review-2026-03-10.md — Opus прагматичный review для небольшого провайдера - Opus: gVisor/LLM validation — overkill для MVP, фокус на быстрые фиксы + ResourceQuota/NetworkPolicy - Обновлён progress.md с новыми документами - .gitignore — добавлен test.token
614 lines
36 KiB
Markdown
614 lines
36 KiB
Markdown
# Claude Opus 4.6: Прагматичный технический обзор sless
|
||
|
||
Дата: 2026-03-10
|
||
Автор: Claude Opus 4.6
|
||
Контекст: Полный анализ кодовой базы sless + ревью документов GPT-5.4 и Claude Sonnet 4.6
|
||
|
||
---
|
||
|
||
## Преамбула: для кого этот документ
|
||
|
||
Этот документ написан для **небольшого облачного провайдера** (nubes.ru), а не для Amazon или Yandex Cloud.
|
||
Это значит:
|
||
|
||
- Пользователей пока единицы, не тысячи.
|
||
- Сервис — одна из многих фич облака, не ключевой revenue stream.
|
||
- Команда маленькая, времени мало, каждый шаг должен давать реальный выход.
|
||
- Перфекционизм — враг. Прагматизм — друг.
|
||
|
||
Все рекомендации ниже даны с учётом этого. Я не буду предлагать gVisor, LLM-validation, image scanning и прочие production-grade вещи на текущем этапе. Подробнее — в разделе "Где я не согласен с коллегами".
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
**Общая оценка: 7/10 для текущей стадии.**
|
||
|
||
Проект **реально работает**. Это главное. Есть:
|
||
- Полный цикл: код → zip → S3 → kaniko → Docker image → Deployment → HTTP endpoint
|
||
- Два рантайма (Python 3.11, Node.js 20), оба протестированы
|
||
- Terraform provider с source_dir, WaitReady, WaitGone
|
||
- Набор примеров которые работают (hello-node, simple-node, simple-python, notes-python)
|
||
- Cron триггеры, FunctionJob для разовых запусков
|
||
- Механизмы lifecycle control (enabled, run_id)
|
||
|
||
Проект прошёл через **реальные** проблемы (бесконечные build-циклы, cross-namespace OwnerRef, cleanup bugs, registry нестабильность) и **решил** их все. Это важнее чем идеальная архитектура.
|
||
|
||
**Но есть конкретные дефекты, которые реально сломают сервис при первых же пользователях.** Ниже — что именно и в каком порядке чинить.
|
||
|
||
---
|
||
|
||
## Часть 1: Подтверждённые дефекты — что реально ломается
|
||
|
||
### 1.1 Trigger и FunctionJob не реагируют на готовность Function
|
||
|
||
**Что происходит:**
|
||
|
||
```go
|
||
// trigger_controller.go, строка ~313
|
||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||
return ctrl.NewControllerManagedBy(mgr).
|
||
For(&slessv1alpha1.Trigger{}). // Только Trigger. Нет watch на Function.
|
||
Complete(r)
|
||
}
|
||
```
|
||
|
||
Комментарий в коде (строка ~311) говорит: "настраивает watch на Function". **Это неправда** — watch на Function отсутствует.
|
||
|
||
**Когда стреляет:** Terraform создаёт Function и Trigger одновременно (или Trigger чуть раньше). Trigger проверяет Function, видит "не Ready", пишет "waiting" и **выходит без RequeueAfter**. Когда Function станет Ready — Trigger **не узнает об этом**. Trigger может зависнуть навсегда.
|
||
|
||
**Аналогичная проблема** в `functionjob_controller.go` — FunctionJob тоже не подписан на изменения Function.
|
||
|
||
**Реальный сценарий:** Пользователь делает `terraform apply` с Function + Trigger + Job. Kaniko собирает образ 1-2 минуты. Trigger создаётся за секунды. Trigger застревает в "waiting", пользователь думает что платформа сломалась.
|
||
|
||
**Почему это работает сейчас в demo:** Terraform provider делает WaitReady на Function перед созданием Trigger. Но прямое создание через kubectl или API — уязвимо.
|
||
|
||
**Как чинить:**
|
||
|
||
Вариант A (правильный): Добавить `Watches` на Function с маппингом на связанные Trigger/FunctionJob:
|
||
```go
|
||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||
return ctrl.NewControllerManagedBy(mgr).
|
||
For(&slessv1alpha1.Trigger{}).
|
||
Watches(
|
||
&slessv1alpha1.Function{},
|
||
handler.EnqueueRequestsFromMapFunc(r.findTriggersForFunction),
|
||
).
|
||
Complete(r)
|
||
}
|
||
```
|
||
|
||
Вариант B (простой, но грубый): Добавить `RequeueAfter: 15 * time.Second` когда Function не Ready. Trigger будет переопрашивать каждые 15 секунд. Грубо, но работает и реализуется за 2 строки.
|
||
|
||
**Рекомендация для небольшого провайдера:** Вариант B сейчас, Вариант A — когда появляется хотя бы 10 активных пользователей. RequeueAfter для единиц пользователей вообще не создаёт нагрузки.
|
||
|
||
**Оценка трудозатрат:** Вариант B — 15 минут. Вариант A — 1-2 часа.
|
||
|
||
---
|
||
|
||
### 1.2 Invocation history обещана, но не пишется
|
||
|
||
**Факт:** Есть таблица `invocations` в PostgreSQL. Есть метод `SaveInvocation()`. Есть API endpoint GET. Метод **нигде не вызывается**.
|
||
|
||
**Файлы:**
|
||
- `migrations/001_initial.sql` — таблица есть
|
||
- `internal/storage/postgres/store.go` — SaveInvocation реализован
|
||
- `internal/api/handler/invocations.go` — ListInvocations вызывается
|
||
- `internal/api/handler/invoke.go` — SaveInvocation **не вызывается**
|
||
|
||
**Когда стреляет:** Пользователь вызывает GET /invocations — всегда пустой список. Он думает что вызовов не было, а они были.
|
||
|
||
**Как чинить:**
|
||
|
||
В `invoke.go`, после получения ответа от функции, вызвать `SaveInvocation`:
|
||
```go
|
||
// После resp, err := httpClient.Do(proxyReq)
|
||
go func() {
|
||
_ = h.Store.SaveInvocation(context.Background(), &postgres.Invocation{
|
||
FunctionName: name,
|
||
Namespace: ns,
|
||
HTTPStatus: resp.StatusCode,
|
||
Duration: duration,
|
||
CreatedAt: time.Now(),
|
||
})
|
||
}()
|
||
```
|
||
|
||
**Важно:** Запись в горутине чтобы не замедлять HTTP response. Если Postgres упал — вызов всё равно прошёл, просто не записался. Это нормально для логов, не для критических данных.
|
||
|
||
**Альтернатива:** Если invocation history пока никому не нужна — **честно убрать endpoint** из API. Пустой API хуже отсутствующего — он вводит в заблуждение.
|
||
|
||
**Рекомендация:** Для началa убрать endpoint или поставить заглушку с 501 Not Implemented. Когда реально понадобится — тогда реализовать нормально с учётом того что писать (статус, duration, размер ответа, ошибка).
|
||
|
||
**Оценка трудозатрат:** Заглушка — 5 минут. Реализация — 1-2 часа.
|
||
|
||
---
|
||
|
||
### 1.3 UpdateFunction молча затирает spec нулевыми значениями
|
||
|
||
**Что происходит:**
|
||
|
||
```go
|
||
// functions.go, UpdateFunction
|
||
fn.Spec.Runtime = req.Runtime // если req.Runtime == "" → затрёт ""
|
||
fn.Spec.Entrypoint = req.Entrypoint // аналогично
|
||
fn.Spec.MemoryMB = req.MemoryMB // если не передал → 0, но Create требует >0
|
||
fn.Spec.TimeoutSec = req.TimeoutSec // если не передал → 0
|
||
```
|
||
|
||
**Когда стреляет:** Пользователь хочет обновить только `env_vars`. Отправляет `{"env_vars": {"DB": "..."}}`. Все остальные поля в req = zero values. MemoryMB становится 0. Runtime становится "". Deployment ломается.
|
||
|
||
**Как чинить:**
|
||
|
||
Два варианта:
|
||
1. **PATCH-семантика:** обновлять только ненулевые поля (JSON merge patch)
|
||
2. **Полная замена:** требовать все поля как в Create (PUT-семантика с валидацией)
|
||
|
||
**Рекомендация:** PUT с валидацией — проще и надёжнее. Добавить ту же проверку что в Create:
|
||
```go
|
||
if req.Runtime == "" || req.Entrypoint == "" || req.MemoryMB <= 0 {
|
||
writeJSON(w, http.StatusBadRequest, errResp("runtime, entrypoint and memory_mb are required"))
|
||
return
|
||
}
|
||
```
|
||
|
||
**Оценка трудозатрат:** 15 минут.
|
||
|
||
---
|
||
|
||
### 1.4 CronJob использует `curlimages/curl:latest` с внешнего DockerHub
|
||
|
||
```go
|
||
// trigger_controller.go, reconcileCron
|
||
Image: "curlimages/curl:latest",
|
||
```
|
||
|
||
**Проблемы:**
|
||
1. `:latest` — непредсказуемый тег (вредности этого уже была уроком на этом же проекте)
|
||
2. Внешняя зависимость — если DockerHub rate-limit или сеть мигнула, cron-триггер не работает
|
||
3. Минорная: не pinned к дайджесту
|
||
|
||
**Как чинить:** Запинить версию: `curlimages/curl:8.5.0` (или какая стабильная). Или использовать `busybox:1.36` с `wget`.
|
||
|
||
**Оценка трудозатрат:** 2 минуты.
|
||
|
||
---
|
||
|
||
## Часть 2: Несогласованность контрактов — не ломает, но путает
|
||
|
||
### 2.1 FunctionNamespacePrefix объявлен, но не используется
|
||
|
||
**Файлы:**
|
||
- `internal/config/config.go` — поле `FunctionNamespacePrefix` из env `FUNCTION_NAMESPACE_PREFIX`
|
||
- `controllers/function_controller.go` — хардкод `"sless-fn-" + fn.Namespace`
|
||
- `controllers/trigger_controller.go` — хардкод `"sless-fn-" + tr.Namespace`
|
||
- `controllers/functionjob_controller.go` — хардкод `"sless-fn-" + fj.Namespace`
|
||
- `internal/api/handler/invoke.go` — хардкод `sless-fn-%s`
|
||
|
||
Конфиг-поле создаёт иллюзию настраиваемости. Если кто-то изменит env — ничего не произойдёт.
|
||
|
||
**Рекомендация:** Удалить поле из конфига. Хардкод `sless-fn-` это нормально для v1. Если когда-нибудь понадобится менять — тогда и протащить. Сейчас мёртвый конфиг хуже хардкода.
|
||
|
||
**Оценка трудозатрат:** 10 минут (удалить поле + обновить комментарий).
|
||
|
||
---
|
||
|
||
### 2.2 TimeoutSec: есть в CRD и API, не enforcement нигде
|
||
|
||
**Файлы:**
|
||
- `api/v1alpha1/function_types.go` — поле `TimeoutSec int32`
|
||
- `internal/api/handler/functions.go` — принимается в API
|
||
- `runtimes/python3.11/server.py` — **нет** ограничения по времени
|
||
- `runtimes/nodejs20/server.js` — **нет** ограничения по времени
|
||
- `internal/api/handler/invoke.go` — `httpClient.Timeout = 30 * time.Second` (статический, не из CRD)
|
||
|
||
**Рекомендация:** **Не реализовывать** timeout enforcement сейчас. Это потребует:
|
||
- Передать TimeoutSec через env var в pod
|
||
- Реализовать в каждом runtime (process timeout, signal handling)
|
||
- Синхронизировать proxy timeout с pod timeout
|
||
|
||
Это значимая работа. Для единиц пользователей — overkill.
|
||
|
||
Вместо этого: **задокументировать** в API design что TimeoutSec сейчас informational. Когда появятся реальные long-running проблемы — тогда реализовать.
|
||
|
||
---
|
||
|
||
### 2.3 Conditions в CRD объявлены, но не ведутся
|
||
|
||
```go
|
||
// function_types.go
|
||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||
```
|
||
|
||
Код обновляет `Phase` и `Message`, но Conditions не заполняет — это мёртвый код в status.
|
||
|
||
**Рекомендация:** Не трогать. Conditions — стандартная практика k8s, они пригодятся позже. Пока Phase+Message достаточно. Главное — не строить на них зависимости в контроллере пока не заполняете.
|
||
|
||
---
|
||
|
||
### 2.4 PreWarmSeconds — объявлен, не реализован
|
||
|
||
```go
|
||
// trigger_types.go
|
||
PreWarmSeconds int32 `json:"preWarmSeconds,omitempty"`
|
||
```
|
||
|
||
**Рекомендация:** Оставить как есть. Это заготовка для scale-to-zero. Пока нет scale-to-zero — поле бесполезно, но и не мешает.
|
||
|
||
---
|
||
|
||
## Часть 3: Что реально работает хорошо
|
||
|
||
Прежде чем обсуждать проблемы, важно зафиксировать что сделано правильно. Это не "похвала", а правки в оценку рисков: если фундамент плохой — надо переделывать. Фундамент **хороший**.
|
||
|
||
### 3.1 Идемпотентность сборки
|
||
|
||
```go
|
||
builtKey := fn.Annotations["sless.kube5s.ru/last-built-s3key"]
|
||
needsBuild := fn.Spec.S3Key != "" && builtKey != fn.Spec.S3Key
|
||
```
|
||
|
||
Решение с аннотацией `last-built-s3key` как idempotency guard — **отлично**. Это решило реальную проблему (94 параллельных Job-а). Решение простое, понятное, работает. Не надо менять.
|
||
|
||
### 3.2 Версионированные теги образов
|
||
|
||
Переход от `:latest` к version-based тегам (через hash от S3 key) — правильное решение.
|
||
Это решило проблему кэширования kaniko и `imagePullPolicy: IfNotPresent`.
|
||
|
||
### 3.3 MergePatch вместо Update в upload.go
|
||
|
||
```go
|
||
patch := client.MergeFrom(fn.DeepCopy())
|
||
fn.Spec.S3Key = s3Key
|
||
h.K8s.Patch(ctx, fn, patch)
|
||
```
|
||
|
||
Правильно. Прямой `Update` ломался из-за concurrent reconcile. MergePatch — стандартное решение.
|
||
|
||
### 3.4 source_dir в Terraform provider
|
||
|
||
Убрали зависимость от `hashicorp/archive` — это устранило VPN/registry конфликт. Provider сам создаёт zip, считает SHA256. Независимость от внешних провайдеров = меньше точек отказа.
|
||
|
||
### 3.5 Cleanup через finalizer
|
||
|
||
Function, Trigger — корректная схема cleanup через finalizer. handleDeletion чистит Deployment + Service + Ingress. handleTriggerDeletion чистит CronJob/Service/Ingress. Terraform provider ждёт WaitGone.
|
||
|
||
### 3.6 Решение с cross-namespace polling
|
||
|
||
Проблема: OwnerReference кросс-неймспейсно не работают → Owns(&Job{}) бесполезен.
|
||
Решение: убрать Owns, использовать RequeueAfter polling.
|
||
Это грубо, но работает и задокументировано в коде с пояснением почему.
|
||
|
||
### 3.7 Документация ошибок
|
||
|
||
`doc/errors/log.md` — **отличная** инженерная практика. Каждая проблема с причиной, контекстом, решением. Это ценнее тестов на данном этапе, потому что помогает не наступать на те же грабли.
|
||
|
||
### 3.8 Один бинарник — правильно для текущего масштаба
|
||
|
||
Один процесс = один Deployment, один лог, одна точка мониторинга. Для 1-50 функций в кластере — это идеальное решение. Не надо распиливать.
|
||
|
||
---
|
||
|
||
## Часть 4: Где я не согласен с коллегами
|
||
|
||
### 4.1 С GPT-5.4 (agent-handoff-2026-03-10.md)
|
||
|
||
GPT-5.4 провёл качественный технический аудит. Проблемы A1-A4 выявлены точно. Приоритизация разумная. **Но:**
|
||
|
||
**Избыточный фокус на config consistency (B1).** GPT-5.4 поставил "убрать FunctionNamespacePrefix" в отдельный этап работ (Этап 4) — это работа на 10 минут, не заслуживает целого этапа. Просто удалить поле заодно при следующем коммите.
|
||
|
||
**Недооценка проблемы UpdateFunction.** GPT-5.4 упоминает "Update не может случайно испортить spec" в этапе 3 (validation), но не выделяет как отдельную проблему. Между тем это реально может сломать функцию при первом же `terraform apply` с частичным обновлением.
|
||
|
||
**В целом:** GPT-5.4 дал отличную базу. Его 5 этапов — разумный план. Я согласен с приоритизацией примерно на 80%.
|
||
|
||
### 4.2 С Claude Sonnet 4.6 (sonnet-review-of-gpt-analysis.md)
|
||
|
||
Sonnet выявил что GPT-5.4 "смотрел как на внутренний инструмент". Это верное наблюдение. **Но затем Sonnet ушёл в другую крайность — начал проектировать production security для Amazon-масштаба.**
|
||
|
||
Конкретно:
|
||
|
||
**gVisor (RuntimeClass)** — для небольшого облачного провайдера с единицами пользователей это **overkill**. gVisor:
|
||
- Требует установки на каждую ноду кластера
|
||
- Даёт 10-20% performance overhead
|
||
- Ломает некоторые syscalls (не все рантаймы работают)
|
||
- Усложняет debugging
|
||
- Решает проблему container escape, которая актуальна когда у вас **тысячи** непроверенных пользователей
|
||
|
||
**Когда нужен gVisor:** Когда сервис открыт для self-service публичной регистрации. Для invite-only или managed-клиентов небольшого провайдера — NetworkPolicy между namespace достаточно на годы.
|
||
|
||
**LLM-валидация кода** — интересная идея, но:
|
||
- False positives будут ломать developer experience
|
||
- Задержка сборки +5-15 секунд на каждый деплой
|
||
- LLM не гарантирует detection rate для malware
|
||
- Сложно тестировать и поддерживать
|
||
- Bandit/npm audit покрывают 80% реальных проблем без LLM
|
||
|
||
**Когда нужен LLM:** Когда у вас free tier с тысячами анонимных пользователей. Не сейчас.
|
||
|
||
**Image scanning (Trivy/Grype)** — полезная вещь, но:
|
||
- Сканирует base image, не пользовательский код
|
||
- Можно запустить один раз при обновлении runtime, не при каждом build
|
||
- Не требует интеграции в pipeline
|
||
|
||
**ResourceQuota per tenant** — вот это **полезно** и **просто**:
|
||
```yaml
|
||
apiVersion: v1
|
||
kind: ResourceQuota
|
||
metadata:
|
||
name: sless-quota
|
||
namespace: sless-fn-{tenant}
|
||
spec:
|
||
hard:
|
||
pods: "20"
|
||
requests.memory: 4Gi
|
||
```
|
||
Это единственная security-рекомендация Sonnet которую стоит реализовать сейчас. Защищает от fork-бомб и runaway pods без сложностей.
|
||
|
||
**NetworkPolicy** — да, стоит добавить. Но простейшую — deny inter-namespace traffic. Не сложную multi-rule систему.
|
||
|
||
**Моя оценка Sonnet:** Правильно указал на пробел в security-мышлении. Но рекомендации масштабированы для Amazon, не для nubes.ru. Из его 8-пунктного плана для текущего этапа актуальны только 2: NetworkPolicy (простая) и ResourceQuota.
|
||
|
||
---
|
||
|
||
## Часть 5: Конкретный план работ — что делать и в каком порядке
|
||
|
||
Порядок отсортирован по **отдаче на вложенное время**. Не по "правильности" или "красоте".
|
||
|
||
### Этап 0: Быстрые фиксы (1-2 часа суммарно)
|
||
|
||
Эти вещи можно и нужно сделать прямо сейчас, одним коммитом:
|
||
|
||
| # | Задача | Время | Файлы |
|
||
|---|--------|-------|-------|
|
||
| 1 | RequeueAfter: 15s когда Function не Ready в TriggerReconciler | 2 мин | `controllers/trigger_controller.go` строка ~87 |
|
||
| 2 | RequeueAfter: 15s когда Function не Ready в FunctionJobReconciler | 2 мин | `controllers/functionjob_controller.go` строка ~111 |
|
||
| 3 | Валидация в UpdateFunction (runtime, entrypoint, memory_mb required) | 15 мин | `internal/api/handler/functions.go` |
|
||
| 4 | Pinned version curl image: `curlimages/curl:8.5.0` | 2 мин | `controllers/trigger_controller.go` |
|
||
| 5 | Убрать FunctionNamespacePrefix из config.go | 10 мин | `internal/config/config.go` |
|
||
| 6 | Invocations endpoint → 501 Not Implemented ИЛИ убрать из API | 10 мин | `internal/api/handler/invocations.go`, `internal/api/router.go` |
|
||
|
||
**Почему этап 0:** Каждый из этих фиксов занимает минуты, но закрывает реальный дефект или убирает ложный контракт. Суммарно — 1 час максимум, а сервис становится значительно надёжнее.
|
||
|
||
### Этап 1: Реализация invocation history (если нужна) — 2-4 часа
|
||
|
||
**Только если** есть реальный use case для истории вызовов (биллинг, дебаг, мониторинг).
|
||
|
||
Если нет — **пропустить**. Пустой endpoint хуже отсутствующего, но 501 из этапа 0 честно говорит "не реализовано".
|
||
|
||
Если нужна:
|
||
|
||
1. Добавить SaveInvocation в invoke.go (асинхронно, в горутине)
|
||
2. Добавить запись результата в FunctionJobReconciler при завершении Job
|
||
3. Протестировать через curl + GET /invocations
|
||
4. Обновить doc/api/design.md
|
||
|
||
**Файлы:**
|
||
- `internal/api/handler/invoke.go`
|
||
- `controllers/functionjob_controller.go`
|
||
- `internal/storage/postgres/store.go`
|
||
- `internal/api/handler/invocations.go`
|
||
|
||
### Этап 2: ResourceQuota + NetworkPolicy — 1-2 часа
|
||
|
||
Минимальная изоляция tenantов. Даже для единиц пользователей это разумная гигиена.
|
||
|
||
### Этап 2.5: LLM-валидация кода при upload — 3-4 часа
|
||
|
||
**Обновлено:** Перенесено из "когда-нибудь потом" в активный план.
|
||
**Причина:** LLM уже в облаке nubes.ru — это dogfooding + маркетинг + реальная загрузка сервиса работой.
|
||
|
||
Детали дизайна: `doc/decisions/log.md` → "2026-03-10 — LLM-валидация кода при upload".
|
||
|
||
**Суть:** Новый пакет `internal/validator/`. Между распаковкой zip и отправкой в S3 — вызов LLM API.
|
||
Safe=false → HTTP 400. LLM недоступен → soft-fail (warning, деплой проходит).
|
||
Выключается через `LLM_ENABLED=false` (default).
|
||
|
||
**Файлы:**
|
||
1. `internal/validator/validator.go` — интерфейс + NoopValidator
|
||
2. `internal/validator/llm.go` — LLM HTTP client
|
||
3. `internal/validator/extract.go` — извлечение текстовых файлов из zip
|
||
4. `internal/config/config.go` — LLM_* env vars
|
||
5. `internal/api/handler/upload.go` — точка вызова
|
||
6. `main.go` — wire
|
||
|
||
**ResourceQuota:**
|
||
- `controllers/function_controller.go` → при создании namespace `sless-fn-*` создавать ResourceQuota
|
||
- Лимиты фиксированные для v1: pods=20, memory=4Gi, cpu=4
|
||
|
||
**NetworkPolicy:**
|
||
- `controllers/function_controller.go` → при создании namespace `sless-fn-*` создавать deny-all NetworkPolicy
|
||
- Разрешить: egress к DNS (kube-dns), egress к интернету (если нужно), ingress от оператора
|
||
|
||
**Файлы для создания:**
|
||
- Шаблон в контроллере, не отдельный файл
|
||
|
||
### Этап 3: Watch на Function для Trigger и FunctionJob — 2-3 часа
|
||
|
||
Замена RequeueAfter polling на правильный event-driven watch.
|
||
|
||
**Почему не в этапе 0:** RequeueAfter 15s уже закроет проблему для единиц пользователей. Правильный watch нужен когда функций станет сотни и polling начнёт создавать лишнюю нагрузку.
|
||
|
||
**Реализация:**
|
||
```go
|
||
// trigger_controller.go
|
||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||
return ctrl.NewControllerManagedBy(mgr).
|
||
For(&slessv1alpha1.Trigger{}).
|
||
Watches(
|
||
&slessv1alpha1.Function{},
|
||
handler.EnqueueRequestsFromMapFunc(r.findTriggersForFunction),
|
||
).
|
||
Complete(r)
|
||
}
|
||
|
||
func (r *TriggerReconciler) findTriggersForFunction(ctx context.Context, obj client.Object) []reconcile.Request {
|
||
fn := obj.(*slessv1alpha1.Function)
|
||
var triggers slessv1alpha1.TriggerList
|
||
_ = r.List(ctx, &triggers, client.InNamespace(fn.Namespace))
|
||
var requests []reconcile.Request
|
||
for _, tr := range triggers.Items {
|
||
if tr.Spec.FunctionRef == fn.Name {
|
||
requests = append(requests, reconcile.Request{
|
||
NamespacedName: client.ObjectKeyFromObject(&tr),
|
||
})
|
||
}
|
||
}
|
||
return requests
|
||
}
|
||
```
|
||
|
||
Аналогично для FunctionJobReconciler.
|
||
|
||
### Этап 4: Минимальные тесты — 3-4 часа
|
||
|
||
**Не гнаться за coverage.** Покрыть только то что реально ломалось.
|
||
|
||
Тесты через envtest (уже есть bootstrap в `suite_test.go`):
|
||
|
||
1. **Trigger ожидает Function Ready:** Create Function (Pending) → Create Trigger → проверить что Trigger в "waiting" → перевести Function в Ready → проверить что Trigger стал Active
|
||
2. **Cleanup при удалении:** Create Function → Create Deployment в sless-fn- → Delete Function → проверить что Deployment/Service/Ingress удалены
|
||
3. **FunctionJob не запускается при RunID=0:** Create FunctionJob(RunID=0) → проверить phase=Skipped
|
||
|
||
**Не нужны тесты на:**
|
||
- Авторизацию (один статический токен, тестировать нечего)
|
||
- S3 upload (внешняя зависимость, лучше E2E)
|
||
- Kaniko build (внешняя зависимость)
|
||
|
||
### Этап 5 (v2): Auth по токену облака — неопределённые сроки
|
||
|
||
Это зависит от инфраструктуры nubes.ru:
|
||
- Нужен endpoint для валидации токена → namespace identity
|
||
- API router вытаскивает namespace из identity, а не из URL
|
||
- Обратная совместимость: текущие Terraform-конфиги должны работать
|
||
|
||
**Не делать пока не будет:**
|
||
1. Auth-сервиса/endpoint nubes.ru для валидации токенов
|
||
2. Хотя бы 3-5 реальных пользователей, которым нужна изоляция
|
||
|
||
### Что делать когда-нибудь потом (v2+)
|
||
|
||
Эти вещи **не нужны** для текущего масштаба. Помечаю чтобы не забыть, но не тратить время:
|
||
|
||
| Тема | Когда реально нужно | Почему не сейчас |
|
||
|------|---------------------|------------------|
|
||
| Scale-to-zero (KEDA) | Когда функций >50 и оплата за ресурсы болит | Меняет архитектуру routing, сложно |
|
||
| gVisor/Kata | Когда открыт self-service signup | Overkill для invite-only |
|
||
| LLM code validation | **Перенесено в активный план (Этап 2.5)** | Dogfooding облачного LLM |
|
||
| Image scanning (Trivy) | Когда security-аудит требуется | Можно сканировать base images отдельно |
|
||
| Go runtime | Когда есть запрос от пользователей | Python + Node покрывают 90% use cases |
|
||
| Replicas в FunctionSpec | Когда пользователи жалуются на потребление | Пока 1 replica = 1 функция, просто |
|
||
| Разделение API/Operator | Когда пользователей >100 или нужен HA API | Один бинарник проще |
|
||
| RabbitMQ event triggers | Когда есть реальный event-driven use case | HTTP + Cron покрывают MVP |
|
||
| S3/Registry cleanup | Когда 100+ сборок и диск заканчивается | Ручная чистка пока достаточна |
|
||
| Versions API (rollback) | Когда пользователи просят откат | Текущая модель: re-upload = новая версия |
|
||
|
||
---
|
||
|
||
## Часть 6: Структурные наблюдения
|
||
|
||
### 6.1 Operator namespace hardcode
|
||
|
||
`main.go` хардкодит `"sless"` как OperatorNamespace:
|
||
```go
|
||
OperatorNamespace: "sless",
|
||
```
|
||
|
||
Это нормально. Namespace оператора не должен быть динамическим — это deployment-level решение. Если переносим в другой namespace, меняем одну строку. Не проблема.
|
||
|
||
### 6.2 Ошибки проглатываются через `_ = r.Status().Update(...)`
|
||
|
||
Встречается в нескольких местах:
|
||
```go
|
||
_ = r.Status().Update(ctx, tr)
|
||
```
|
||
|
||
Это осознанный trade-off: если status update упал, reconcile продолжится, и следующий Requeue перезапишет. Для status-only обновлений это OK. Главное не делать так для spec-изменений.
|
||
|
||
### 6.3 handleDeletion не чистит S3 artifacts и registry images
|
||
|
||
При удалении Function чистятся: Deployment, Service, Ingress.
|
||
Не чистятся: S3 build context, Docker image в registry.
|
||
|
||
**Для текущего масштаба это не проблема.** При 100+ функциях — начнёт накапливаться мусор. Простое решение — периодический cron-скрипт для чистки orphaned объектов, не встроенная логика в контроллер.
|
||
|
||
### 6.4 migrations/001_initial.sql читается из файла на диске
|
||
|
||
```go
|
||
migrationSQL, err := os.ReadFile("migrations/001_initial.sql")
|
||
```
|
||
|
||
Это означает что бинарник зависит от наличия файла в текущей директории. Работает с `go run` и если Docker COPY включает migrations/. Не самый robust подход, но для одного файла — приемлемо.
|
||
|
||
**Когда станет проблемой:** Когда будет 5+ миграций. Тогда стоит встроить через `go:embed` или использовать migrate-библиотеку.
|
||
|
||
### 6.5 API doc/api/design.md частично устарел
|
||
|
||
- Versions API описан, но не реализован
|
||
- Invocations API описан как рабочий, но не записывает
|
||
- Runtime list: `go1.21` помечен как "планируется", но решение отложено
|
||
|
||
**Рекомендация:** Обновить design.md после этапа 0, зафиксировав что реально работает в v1.
|
||
|
||
---
|
||
|
||
## Часть 7: Итоговая оценка
|
||
|
||
### Что делает этот проект хорошим MVP
|
||
|
||
1. **Реально работает end-to-end.** terraform apply → функция доступна по HTTPS.
|
||
2. **Прошёл через реальные проблемы** и решил их все (не застрял на happy path).
|
||
3. **Документация ошибок** лучше чем в большинстве проектов.
|
||
4. **Terraform provider** полностью функционален с source_dir, WaitReady, lifecycle control.
|
||
5. **Архитектура не over-engineered** — один бинарник, простые контроллеры, minimal dependencies.
|
||
|
||
### Что нужно для промышленной эксплуатации (небольшой провайдер)
|
||
|
||
1. **Этап 0** — быстрые фиксы (RequeueAfter, validation, pin versions) — **обязательно перед любыми пользователями**
|
||
2. **ResourceQuota + NetworkPolicy** — минимальная изоляция
|
||
3. **Auth по облачному токену** — когда инфраструктура nubes.ru будет готова
|
||
|
||
### Чего НЕ нужно делать
|
||
|
||
- Не распиливать на микросервисы
|
||
- Не внедрять gVisor/Kata
|
||
- Не интегрировать LLM-валидацию кода
|
||
- Не делать scale-to-zero
|
||
- Не добавлять Go runtime (пока нет запроса)
|
||
- Не строить versioning/rollback (пока нет запроса)
|
||
- Не менять gorilla/mux на что-то "модное"
|
||
- Не добавлять Conditions-логику в контроллеры
|
||
|
||
---
|
||
|
||
## Файловая карта: что где менять
|
||
|
||
### Этап 0 (быстрые фиксы)
|
||
|
||
| Файл | Изменение |
|
||
|------|-----------|
|
||
| `controllers/trigger_controller.go` ~87 | Добавить `return ctrl.Result{RequeueAfter: 15 * time.Second}, nil` вместо `return ctrl.Result{}, nil` |
|
||
| `controllers/functionjob_controller.go` ~111 | Аналогично |
|
||
| `controllers/trigger_controller.go` ~258 | Заменить `curlimages/curl:latest` → `curlimages/curl:8.5.0` |
|
||
| `internal/api/handler/functions.go` ~UpdateFunction | Добавить валидацию required полей |
|
||
| `internal/config/config.go` | Удалить FunctionNamespacePrefix |
|
||
| `internal/api/handler/invocations.go` или `router.go` | Заглушка 501 или убрать endpoint |
|
||
|
||
### Этап 2 (security минимум)
|
||
|
||
| Файл | Изменение |
|
||
|------|-----------|
|
||
| `controllers/function_controller.go` ~ensureDeployment | После создания namespace — создать ResourceQuota и NetworkPolicy |
|
||
|
||
### Этап 3 (watches)
|
||
|
||
| Файл | Изменение |
|
||
|------|-----------|
|
||
| `controllers/trigger_controller.go` ~SetupWithManager | Добавить Watches на Function |
|
||
| `controllers/functionjob_controller.go` ~SetupWithManager | Добавить Watches на Function |
|
||
|
||
---
|
||
|
||
## Резюме одной строкой
|
||
|
||
**Проект хороший, работает, фундамент правильный. Нужны 1 час быстрых фиксов + ResourceQuota/NetworkPolicy — и можно открывать для первых пользователей.**
|