docs: добавлен анализ GPT-5.4 и Opus 4.6

- 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
This commit is contained in:
“Naeel”
2026-03-10 08:56:59 +04:00
parent 9d6db0d223
commit 7d6f8d6079
5 changed files with 1349 additions and 1 deletions
@@ -0,0 +1,602 @@
# Подробный handoff для следующего агента
Последнее обновление: 2026-03-10
## Назначение документа
Этот документ нужен не для презентации проекта, а для практической передачи контекста следующему агенту.
Цель: после чтения файла должно быть понятно:
1. Что в проекте уже сделано и действительно работает.
2. Где архитектура сильная.
3. Где есть реальные дефекты, а где просто незавершённые v2-задачи.
4. Что нужно делать следующим шагом и в каком порядке.
5. Как проверять изменения с учётом ограничений среды (VPN, возможные TLS timeout).
Документ основан на анализе текущего кода, существующей документации и структуры проекта.
Сетевые E2E-проверки намеренно не делались в рамках этого разбора, потому что среда может шуметь из-за VPN и TLS timeout.
---
## Краткий вывод
Проект находится в хорошем состоянии для рабочего MVP managed serverless platform, но ещё не находится в состоянии production-grade managed cloud service.
Что важно понимать сразу:
1. Основа выбрана правильно: Kubernetes operator + CRD + build pipeline через S3 + kaniko.
2. Пользовательский путь уже частично доказан существующими Terraform examples и E2E заметками.
3. Главные проблемы сейчас не в том, что код "вообще не работает", а в том, что системные свойства платформы пока неполные: event lifecycle, observability, auth/tenancy, строгая валидация контрактов, тестовое покрытие.
Итоговая инженерная оценка:
1. Как технический фундамент для v1: хорошо.
2. Как demo/MVP для внутренней обкатки: хорошо.
3. Как настоящий managed cloud service: пока рано, есть несколько архитектурно значимых дыр.
---
## Что уже реализовано и выглядит здраво
### 1. Правильный control plane фундамент
Текущая модель сервиса строится через CRD и operator-подход:
1. Function описывает функцию и её runtime/config.
2. Trigger описывает способ вызова.
3. FunctionJob описывает одноразовый запуск.
Ключевые файлы:
1. api/v1alpha1/function_types.go
2. api/v1alpha1/trigger_types.go
3. api/v1alpha1/job_types.go
4. controllers/function_controller.go
5. controllers/trigger_controller.go
6. controllers/functionjob_controller.go
Это хорошее решение для системы, которая управляет lifecycle k8s-ресурсов, потому что:
1. Desired state вынесен в CRD.
2. Reconcile loop естественно ложится на build/deploy/cleanup.
3. Kubernetes остаётся источником правды по текущему фактическому состоянию ресурсов.
### 2. Build pipeline собран прагматично и без лишней магии
Фактически реализована понятная цепочка:
1. API принимает zip.
2. Upload handler генерирует Dockerfile и tar.gz контекст.
3. Контекст кладётся в S3.
4. Function controller запускает kaniko Job.
5. После успешной сборки создаётся/обновляется Deployment функции.
Ключевые файлы:
1. internal/api/handler/upload.go
2. internal/builder/builder.go
3. controllers/function_controller.go
Сильные стороны текущего решения:
1. Пользователь не думает про Dockerfile.
2. Сборка отделена от API и не делается внутри процесса оператора.
3. Используется idempotency guard через аннотацию last-built-s3key.
4. Используются version-like image refs через hash от S3 key, а не один общий latest для результата сборки.
### 3. API и provider уже образуют реальный пользовательский путь
Есть не только внутренние CRD, но и внешний контракт:
1. REST API на gorilla/mux.
2. Terraform provider в отдельном модуле.
3. Набор examples, которые уже реально гонялись.
Ключевые файлы:
1. internal/api/router.go
2. internal/api/handler/functions.go
3. internal/api/handler/triggers.go
4. internal/api/handler/jobs.go
5. terraform/provider/...
6. examples/...
Это важно: проект уже живёт не только как "внутренний оператор", а как зачаток полноценного managed продукта.
### 4. Видно, что проект уже проходил через реальные эксплуатационные проблемы
Это видно по документам:
1. doc/errors/log.md
2. doc/decisions/log.md
3. doc/progress.md
Плюс в том, что проект не застрял на happy path. Уже обнаружены и исправлялись:
1. бесконечные build loops,
2. cleanup после destroy,
3. cross-namespace проблемы с owner references,
4. image pull / registry проблемы,
5. несогласованность Terraform state.
Это хороший признак инженерной зрелости даже при сырой архитектуре.
---
## Текущая фактическая архитектура
### 1. Один бинарник совмещает operator manager и REST API
Файл: main.go
Что происходит:
1. Загружается env-конфиг.
2. Поднимается PostgreSQL store.
3. Выполняются миграции.
4. Поднимается S3 client.
5. Создаётся controller-runtime manager.
6. Регистрируются Function, Trigger и FunctionJob reconcilers.
7. Параллельно запускается HTTP API.
Это нормальное решение для раннего v1. Оно упрощает деплой и снижает количество moving parts.
Ограничение: по мере роста системы API и control plane логика будут мешать друг другу по масштабированию, отказам и ответственности. Но для текущей стадии это приемлемо.
### 2. Данные хранятся в двух разных источниках правды
Реально сейчас:
1. Состояние lifecycle функции и триггеров живёт в Kubernetes CRD/status.
2. Invocation history задумана в PostgreSQL.
3. Исходный код и build context живут в S3.
4. Docker image живёт во внешнем registry.
Это нормальная модель, но только если границы между источниками правды строго определены.
Сейчас эта модель задумана верно, но реализована не до конца, особенно вокруг invocation history.
### 3. HTTP вызов функции сейчас устроен через публичный прокси /fn/
Ключевые файлы:
1. internal/api/router.go
2. internal/api/handler/invoke.go
3. controllers/trigger_controller.go
Схема:
1. Trigger типа http формирует URL вида /fn/{namespace}/{name} через ExternalURL.
2. API-сервер принимает запрос.
3. InvokeFunction проксирует его во внутренний Service функции.
Для текущего окружения это разумный обход ограничения wildcard DNS.
Минус: это превращает API-процесс в data plane proxy для пользовательского трафика. Для MVP годится, для production это создаст узкое место и дополнительные требования к auth, rate limiting, tracing и HA.
---
## Подтверждённые проблемы и архитектурные долги
Ниже перечислены именно подтверждённые проблемы по текущему коду, а не абстрактные придирки.
### Критичность A — нужно чинить в ближайших итерациях
#### A1. TriggerReconciler рассчитывает на события Function, но реально не подписан на них
Файл: controllers/trigger_controller.go
Проблема:
1. В Reconcile есть ветка: если Function ещё не Ready, Trigger пишет статус waiting и выходит.
2. Комментарий говорит, что повторный reconcile придёт, когда Function изменится.
3. Но SetupWithManager регистрирует только For(&Trigger{}), без watch на Function.
Следствие:
1. Trigger, созданный раньше готовности Function, может застрять в промежуточном состоянии.
2. Пересчёт будет зависеть не от правильного события, а от случайного следующего изменения Trigger.
Почему это важно:
Для operator-системы это уже логический дефект event model, а не просто TODO.
Что делать:
1. Добавить watch на Function.
2. Обеспечить маппинг Function -> Trigger по namespace + FunctionRef.
3. Покрыть тестом сценарий "Trigger создан до готовности Function".
#### A2. FunctionJobReconciler имеет ту же проблему ожидания готовности Function
Файл: controllers/functionjob_controller.go
Проблема:
1. Если Function не Ready, FunctionJob уходит в Pending и выходит.
2. В комментарии подразумевается, что reconcile придёт позже.
3. Но SetupWithManager также подписан только на FunctionJob.
Следствие:
1. Job может зависнуть в Pending без события-пробуждения.
2. Фактическое выполнение зависит от внешнего изменения или ручного повторного reconcile.
Что делать:
1. Добавить watch на Function.
2. Либо ввести RequeueAfter polling для ожидания Function Ready.
3. Предпочтительнее watch, если можно аккуратно сматчить зависимости.
#### A3. Invocation history заявлена, но фактически не пишется
Файлы:
1. migrations/001_initial.sql
2. internal/storage/postgres/store.go
3. internal/api/handler/invocations.go
4. internal/api/handler/invoke.go
Подтверждённый факт:
1. Есть таблица invocations.
2. Есть SaveInvocation и ListInvocations.
3. Есть API-эндпоинт чтения истории.
4. В текущем коде нет места, где SaveInvocation реально вызывается.
Следствие:
1. API истории вызовов обещает поведение, которого фактически нет.
2. Документация и UX вводят в заблуждение: кажется, что история есть, но она пустая не потому, что вызовов не было, а потому что они не сохраняются.
Что делать:
1. Встроить запись инвокации в HTTP proxy path и, отдельно, в FunctionJob execution path.
2. Определить, что именно считается logs/result/status/duration.
3. Синхронизировать это решение с API design и examples.
#### A4. Multi-tenant модель ещё не реализована, а API уже строится вокруг namespace из URL
Файлы:
1. internal/api/router.go
2. internal/api/middleware/auth.go
3. doc/progress.md
4. doc/api/design.md
Проблема:
1. Namespace передаётся пользователем в URL.
2. Auth пока основан на одном статическом токене.
3. В документации прямо написано, что настоящая namespace isolation по токену ещё не сделана.
Следствие:
1. Контракт API пока demo-only.
2. При переходе к реальному tenant-aware auth почти наверняка придётся менять или сильно прятать namespace от клиента.
Что делать:
1. Зафиксировать как архитектурное решение: namespace выводится из identity, а не приходит от клиента.
2. Добавить whoami/identity resolution слой.
3. Планировать миграцию API аккуратно, пока пользователей мало.
### Критичность B — не ломает demo, но делает платформу хрупкой
#### B1. Важная часть конфигурации декларативна только на бумаге, но не используется последовательно
Файлы:
1. internal/config/config.go
2. main.go
3. controllers/function_controller.go
4. controllers/trigger_controller.go
5. controllers/functionjob_controller.go
Проблема:
1. Есть FunctionNamespacePrefix в конфиге.
2. Но контроллеры и wiring жёстко используют sless и sless-fn-.
Следствие:
1. Конфиг частично ложный: кажется, что поведение настраивается, но реально нет.
2. Перенос и переиспользование сервиса усложняются.
Что делать:
1. Либо реально протащить конфиг до всех мест использования.
2. Либо убрать мнимо-настраиваемое поле, если в v1 это сознательный hardcode.
#### B2. TimeoutSec описан в модели, но по сути не обеспечивается runtime-слоем
Файлы:
1. api/v1alpha1/function_types.go
2. internal/api/handler/functions.go
3. runtimes/python3.11/server.py
4. runtimes/nodejs20/server.js
Проблема:
1. Поле timeout есть в API/CRD.
2. Но runtime-обёртка не ограничивает время выполнения функции.
3. HTTP proxy timeout клиента не равен execution timeout функции как платформенной гарантии.
Следствие:
1. Контракт платформы частично ложный.
2. Длительный обработчик может вести себя непредсказуемо с точки зрения пользователя.
Что делать:
1. Либо честно задокументировать, что timeout пока informational.
2. Либо реализовать enforcement через runtime/process/k8s job semantics.
#### B3. Публичный /fn/ endpoint не отделён от control plane auth модели
Файлы:
1. internal/api/router.go
2. internal/api/middleware/auth.go
3. internal/api/handler/invoke.go
Проблема:
1. /v1 защищён статическим token.
2. /fn/ публичен.
3. Никакой tenant-aware authz на invoke path сейчас нет.
Для MVP это допустимо, но следующий агент должен понимать:
1. Это не production-ready security model.
2. Любая работа по auth должна учитывать отдельно control plane и data plane.
#### B4. Тестов ядра почти нет
Файлы:
1. controllers/suite_test.go
2. остальная кодовая база tests почти отсутствуют
Фактическое состояние:
1. Есть bootstrap envtest.
2. Нет содержательных тестов на reconcile lifecycle, cleanup, event propagation, API handler behaviour.
Следствие:
1. Реальная надёжность сейчас в основном держится на ручных E2E и Terraform examples.
2. Регрессии в controller logic будут ловиться поздно.
### Критичность C — скорее незавершённость и техдолг, чем немедленная поломка
#### C1. Статусная модель богаче реальной логики
Файлы:
1. api/v1alpha1/function_types.go
2. api/v1alpha1/trigger_types.go
Замечание:
1. Conditions объявлены, но не ведутся как системная модель статусов.
2. LastScheduleTime есть, но не видно логики обновления.
3. PreWarmSeconds есть, но отмечен как нереализованный.
Это не срочный баг, но следующий агент не должен тратить время, думая что вся эта модель уже рабочая.
#### C2. Cleanup build contexts и registry artefacts не выглядит завершённым
Замечание:
1. При удалении Function чистятся Deployment/Service/Ingress.
2. Не видно системного cleanup для S3 contexts и образов registry.
Для разработки допустимо.
Для managed среды это приведёт к накоплению мусора.
---
## Что не является проблемой само по себе
Следующий агент не должен тратить время на преждевременное "улучшательство".
### 1. Один бинарник для API и operator — нормально для текущей стадии
Не надо сейчас автоматически распиливать на микросервисы.
Это не даст пропорциональной пользы на текущем этапе.
### 2. Выбор gorilla/mux не является узким местом проекта
Главные риски проекта не в роутере и не в HTTP фреймворке.
### 3. Простые runtime wrappers — нормальны для v1
Python/Node runtime обёртки в текущем виде упрощают систему.
Их ограничения понятны, но они не являются главной ближайшей проблемой по сравнению с event model и invocation persistence.
---
## Приоритетный план работ для следующего агента
Ниже порядок работ, который имеет смысл соблюдать.
### Этап 1. Починить lifecycle зависимости Trigger и FunctionJob от Function
Цель:
1. Trigger и Job не должны зависать, если были созданы до готовности Function.
Задачи:
1. Добавить watch/mapping из Function в связанные Trigger.
2. Добавить watch/mapping из Function в связанные FunctionJob или явный polling для ожидания Ready.
3. Убедиться, что reconcile повторяется по правильным событиям, а не случайно.
Файлы-кандидаты:
1. controllers/trigger_controller.go
2. controllers/functionjob_controller.go
Критерии готовности:
1. Trigger, созданный раньше Function Ready, автоматически становится Active после готовности функции.
2. FunctionJob, созданный раньше Function Ready, сам продолжает lifecycle без ручного тычка.
3. Поведение покрыто тестами или хотя бы воспроизводимым локальным сценарием.
### Этап 2. Реально включить запись invocation history
Цель:
1. Сделать так, чтобы API истории вызовов соответствовал реальному поведению платформы.
Задачи:
1. На HTTP invoke path записывать status, duration, http_status и response/error.
2. На FunctionJob path записывать результат и статус отдельно как job-triggered invocation либо честно развести две сущности.
3. Обновить doc/api/design.md и doc/progress.md по факту.
Файлы-кандидаты:
1. internal/api/handler/invoke.go
2. internal/storage/postgres/store.go
3. controllers/functionjob_controller.go
4. internal/api/handler/invocations.go
Критерии готовности:
1. После успешного и неуспешного вызова в PostgreSQL появляется запись.
2. GET invocations возвращает не пустой декоративный список, а реальные записи.
### Этап 3. Синхронизировать контракт timeout/status/validation
Цель:
1. Убрать ложные обещания платформы и несогласованность API.
Задачи:
1. Решить, что означает TimeoutSec прямо сейчас.
2. Если не реализуется быстро, явно задокументировать ограничение.
3. Выправить Create/Update в functions.go и triggers.go так, чтобы значения по умолчанию и validation были последовательными.
Файлы-кандидаты:
1. internal/api/handler/functions.go
2. internal/api/handler/triggers.go
3. api/v1alpha1/function_types.go
4. doc/api/design.md
Критерии готовности:
1. API не заставляет клиента задавать поля, которые CRD должен дефолтить.
2. Update не может случайно испортить spec нулевыми значениями.
3. Документация не обещает больше, чем реально реализовано.
### Этап 4. Уменьшить ложную конфигурируемость
Цель:
1. Либо сделать конфиг реальным, либо убрать фиктивную настраиваемость.
Задачи:
1. Разобраться с FunctionNamespacePrefix и хардкодами sless/sless-fn-.
2. Принять одно из двух решений:
а) протащить конфиг через контроллеры и main;
б) удалить поле до тех пор, пока оно реально не нужно.
Файлы-кандидаты:
1. internal/config/config.go
2. main.go
3. controllers/function_controller.go
4. controllers/trigger_controller.go
5. controllers/functionjob_controller.go
### Этап 5. Добавить минимально полезные автоматические тесты
Цель:
1. Снизить зависимость от ручных прогонов.
Минимальный полезный набор:
1. Trigger ждёт Function Ready и потом активируется.
2. Function deletion чистит дочерние ресурсы.
3. FunctionJob не зависает в Pending/Running навсегда.
4. Invoke path корректно пишет invocation history.
Важно:
Не надо начинать с больших e2e по сети.
Нужны локальные, детерминированные, дешёвые проверки.
---
## Как проверять изменения в этой среде
С учётом замечания пользователя про VPN и возможные TLS timeout, стратегия проверки должна быть осторожной.
### Что предпочитать
1. Локальные Go-тесты.
2. envtest/контроллерные тесты без внешней сети.
3. Анализ кода и существующих E2E результатов в examples/ и doc/progress.md.
### Что не делать первым шагом
1. Не начинать с внешних curl/HTTPS smoke test.
2. Не считать TLS timeout автоматическим подтверждением бага в сервисе.
3. Не делать выводы по сетевому шуму без локального подтверждения.
### Практический порядок проверки
1. Сначала локальные unit/envtest проверки.
2. Потом, если нужно, ограниченные сценарии через provider/examples.
3. Только в самом конце сетевые end-to-end через ingress.
---
## Быстрые ориентиры по файлам
Если следующему агенту нужно быстро войти в проект, читать в таком порядке:
1. main.go
2. api/v1alpha1/function_types.go
3. api/v1alpha1/trigger_types.go
4. api/v1alpha1/job_types.go
5. controllers/function_controller.go
6. controllers/trigger_controller.go
7. controllers/functionjob_controller.go
8. internal/api/router.go
9. internal/api/handler/upload.go
10. internal/api/handler/invoke.go
11. internal/storage/postgres/store.go
12. doc/progress.md
13. doc/errors/log.md
14. doc/decisions/log.md
---
## Короткий practical summary для следующего агента
Если нужно запомнить только главное:
1. Не рефакторить всё подряд. Основа проекта нормальная.
2. Первые реальные проблемы: event model Trigger/FunctionJob и отсутствие записи invocation history.
3. Не путать v2-идеи с текущими дефектами: не всё незавершённое надо чинить прямо сейчас.
4. Не полагаться в первую очередь на внешнюю сеть и TLS smoke test: среда может шуметь.
5. Любые значимые изменения обязательно фиксировать в doc/progress.md и, при необходимости, в doc/errors/log.md или doc/decisions/log.md.
---
## Рекомендуемое следующее действие
Самый рациональный следующий шаг для нового агента:
1. Сфокусироваться на TriggerReconciler и FunctionJobReconciler.
2. Починить повторное пробуждение от изменений Function.
3. Добавить под это минимальные локальные тесты.
Это даст максимальный выигрыш в надёжности control plane без лишнего расширения функциональности.
@@ -0,0 +1,613 @@
# 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 — и можно открывать для первых пользователей.**