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:
@@ -39,3 +39,4 @@ terraform/provider/build/
|
||||
# Собранные zip-архивы функций (генерируются при terraform apply)
|
||||
examples/*/dist/
|
||||
**/handler.zip
|
||||
test.token
|
||||
|
||||
@@ -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 — и можно открывать для первых пользователей.**
|
||||
@@ -262,3 +262,133 @@ existing.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = fn.Sta
|
||||
**Правило проекта:** При использовании `:latest` tag всегда явно проставлять `restartedAt` annotation при обновлении кода.
|
||||
|
||||
**Версия:** operator `naeel/sless-operator:v0.1.11`
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-10 — LLM-валидация кода при upload (pre-build security gate)
|
||||
|
||||
**Решение:** Интегрировать вызов облачного LLM в pipeline upload кода. LLM анализирует исходники пользователя **до** отправки в S3 и запуска kaniko. Если код подозрительный — upload отклоняется с HTTP 400 и причиной.
|
||||
|
||||
**Причина:**
|
||||
1. LLM уже развёрнут (или скоро будет) в облаке nubes.ru — его нужно загрузить реальной работой.
|
||||
2. Dogfooding: облачный провайдер использует собственный сервис ИИ в своём же продукте serverless.
|
||||
3. Маркетинг: "ваш код проверяется ИИ перед деплоем" — реальная продающая фича.
|
||||
4. Security: защита от криптомайнеров, ботнетов, DDoS-агентов, port scanners в пользовательских функциях.
|
||||
|
||||
**Точка интеграции:** `internal/api/handler/upload.go` — между распаковкой zip и упаковкой tar.gz.
|
||||
|
||||
**Pipeline с LLM:**
|
||||
```
|
||||
POST /upload (zip)
|
||||
→ распаковка zip
|
||||
→ извлечение текстовых файлов (.py, .js, .ts, .json, .sh, .sql ...)
|
||||
→ POST к облачному LLM API с исходниками + системным промптом
|
||||
→ safe=true → Dockerfile + tar.gz → S3 → CRD patch (обычный путь)
|
||||
→ safe=false → HTTP 400 {"error": "code validation failed: <reason>"}
|
||||
→ LLM error → warning в лог, upload продолжается (soft-fail)
|
||||
```
|
||||
|
||||
**Режим работы: blocking + soft-fail**
|
||||
- `safe=false` → upload отклоняется (HTTP 400), код не попадает в S3, сборка не начинается.
|
||||
- LLM недоступен (timeout, 5xx) → upload **пропускается** (soft-fail), логируется warning.
|
||||
Причина: недоступность LLM не должна ломать весь pipeline деплоя.
|
||||
|
||||
**Новый пакет:** `internal/validator/`
|
||||
|
||||
**Интерфейс:**
|
||||
```go
|
||||
// internal/validator/validator.go
|
||||
type CodeValidator interface {
|
||||
// Validate проверяет код функции перед сборкой.
|
||||
// files — map[filename]content (текстовые файлы из zip).
|
||||
// Возвращает (true, "") если код safe, (false, reason) если нет.
|
||||
// При ошибке связи с LLM — возвращает (true, "") + логирует warning (soft-fail).
|
||||
Validate(ctx context.Context, files map[string]string, runtime string) (safe bool, reason string, err error)
|
||||
}
|
||||
```
|
||||
|
||||
**LLM-реализация:**
|
||||
```go
|
||||
// internal/validator/llm.go
|
||||
type LLMValidator struct {
|
||||
endpoint string // URL облачного LLM API (OpenAI-compatible)
|
||||
apiKey string // токен доступа
|
||||
timeout time.Duration // default: 15s
|
||||
log *slog.Logger
|
||||
}
|
||||
```
|
||||
|
||||
**Конфигурация (env vars):**
|
||||
| Переменная | Default | Описание |
|
||||
|------------|---------|----------|
|
||||
| `LLM_ENABLED` | `false` | Включатель. false → NoopValidator (всегда safe) |
|
||||
| `LLM_ENDPOINT` | — | URL LLM API, например `https://llm.nubes.ru/v1/chat/completions` |
|
||||
| `LLM_API_KEY` | — | Bearer-токен для LLM API |
|
||||
| `LLM_TIMEOUT` | `15s` | Максимальное время ожидания ответа |
|
||||
|
||||
`LLM_ENABLED=false` → оператор работает без LLM зависимости. По умолчанию выключено.
|
||||
|
||||
**Prompt-стратегия:**
|
||||
|
||||
Промпт НЕ хардкодится в Go — выносится в константу с возможностью override через ConfigMap.
|
||||
|
||||
```
|
||||
You are a security reviewer for a serverless cloud platform.
|
||||
Analyze the following {runtime} code deployed as a cloud function.
|
||||
|
||||
Check for:
|
||||
1. Cryptocurrency mining (crypto hash algorithms, pool connections, stratum protocol)
|
||||
2. DDoS/botnet behavior (mass outbound HTTP/UDP, connection floods)
|
||||
3. Port scanning / network reconnaissance
|
||||
4. Reverse shells, backdoors, C2 communication
|
||||
5. Attempts to escape container (access host filesystem, /proc, /sys)
|
||||
6. Obfuscated code designed to hide malicious intent
|
||||
|
||||
Files:
|
||||
{files_content}
|
||||
|
||||
Respond ONLY with valid JSON, no other text:
|
||||
{"safe": true} or {"safe": false, "reason": "brief explanation"}
|
||||
```
|
||||
|
||||
**Что НЕ проверяем через LLM (не его задача):**
|
||||
- Качество кода, стиль, best practices
|
||||
- Уязвимости в зависимостях (это Trivy/npm audit, потом)
|
||||
- Бизнес-логику пользователя
|
||||
|
||||
**Ограничения по размеру:**
|
||||
- Суммарный размер текстовых файлов > 100KB → skip LLM (дорого, context window). Деплой проходит.
|
||||
- Бинарные файлы (.pyc, .so, node_modules/) → не отправляются в LLM.
|
||||
- Только расширения: `.py`, `.js`, `.ts`, `.json`, `.yaml`, `.yml`, `.txt`, `.sh`, `.sql`, `.go`.
|
||||
|
||||
**Встраивание в upload.go:**
|
||||
```go
|
||||
// После распаковки zip, до generateDockerfile
|
||||
if h.Validator != nil {
|
||||
files := extractTextFiles(zipData)
|
||||
safe, reason, err := h.Validator.Validate(r.Context(), files, fn.Spec.Runtime)
|
||||
if err != nil {
|
||||
h.Log.Warn("llm validation error (soft-fail)", "err", err)
|
||||
} else if !safe {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("code validation failed: "+reason))
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Terraform provider:** Получит `status 400: code validation failed: <reason>` — пользователь видит причину в `terraform apply` output.
|
||||
|
||||
**Файлы для реализации:**
|
||||
1. `internal/validator/validator.go` — интерфейс CodeValidator + NoopValidator
|
||||
2. `internal/validator/llm.go` — LLMValidator с HTTP client к OpenAI-compatible API
|
||||
3. `internal/validator/extract.go` — extractTextFiles: zip → map[string]string
|
||||
4. `internal/config/config.go` — добавить LLM_ENABLED, LLM_ENDPOINT, LLM_API_KEY, LLM_TIMEOUT
|
||||
5. `internal/api/handler/handler.go` — добавить Validator поле
|
||||
6. `internal/api/handler/upload.go` — вызов Validator между zip и tar.gz
|
||||
7. `main.go` — wire: if LLM_ENABLED → LLMValidator, else → NoopValidator
|
||||
|
||||
**Компромиссы:**
|
||||
- +5-15 секунд к каждому деплою (зависит от скорости LLM).
|
||||
- False positives: пользователь получит 400 с причиной, может обратиться в support.
|
||||
- Soft-fail при недоступности LLM: security degraded, но деплой работает.
|
||||
- Prompt не идеален: LLM не ловит всё. Это дополнительный слой, не единственный.
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# Прогресс разработки
|
||||
|
||||
Последнее обновление: 2026-03-10 (добавлен подробный архитектурный handoff-документ для следующего агента)
|
||||
Последнее обновление: 2026-03-10 (добавлен прагматичный обзор Claude Opus 4.6)
|
||||
|
||||
## Статусы: ✅ готово | 🔄 в процессе | ⏳ не начато
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|---|--------|--------|---------|
|
||||
| 1 | GPT-5.4: Подробный разбор кода и lifecycle issues | ✅ | `doc/architecture/agent-handoff-2026-03-10.md` — детальный code review, event model problems, invocation history gap, приоритеты |
|
||||
| 2 | Claude Sonnet 4.6: Review + security roadmap | ✅ | `doc/architecture/sonnet-review-of-gpt-analysis.md` — где GPT-5.4 прав, что пропустил (gVisor, LLM validation, NetworkPolicy), production security roadmap |
|
||||
| 3 | Claude Opus 4.6: Прагматичный обзор для небольшого провайдера | ✅ | `doc/architecture/opus-pragmatic-review-2026-03-10.md` — анализ с учётом масштаба, конкретный план по этапам, разбор где коллеги перемудрили |
|
||||
| 4 | Claude Opus 4.6: Дизайн LLM-валидации кода | ✅ | `doc/decisions/log.md` → "2026-03-10 — LLM-валидация кода при upload" — архитектура, интерфейс, prompt, план файлов |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user