From 7d6f8d6079520a86d39c016f16ecff202209aeb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 10 Mar 2026 08:56:59 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=20GPT-5.4?= =?UTF-8?q?=20=D0=B8=20Opus=204.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 1 + doc/architecture/agent-handoff-2026-03-10.md | 602 +++++++++++++++++ .../opus-pragmatic-review-2026-03-10.md | 613 ++++++++++++++++++ doc/decisions/log.md | 130 ++++ doc/progress.md | 4 +- 5 files changed, 1349 insertions(+), 1 deletion(-) create mode 100644 doc/architecture/agent-handoff-2026-03-10.md create mode 100644 doc/architecture/opus-pragmatic-review-2026-03-10.md diff --git a/.gitignore b/.gitignore index 897fe00..9f9ec05 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ terraform/provider/build/ # Собранные zip-архивы функций (генерируются при terraform apply) examples/*/dist/ **/handler.zip +test.token diff --git a/doc/architecture/agent-handoff-2026-03-10.md b/doc/architecture/agent-handoff-2026-03-10.md new file mode 100644 index 0000000..20fc9b0 --- /dev/null +++ b/doc/architecture/agent-handoff-2026-03-10.md @@ -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 без лишнего расширения функциональности. \ No newline at end of file diff --git a/doc/architecture/opus-pragmatic-review-2026-03-10.md b/doc/architecture/opus-pragmatic-review-2026-03-10.md new file mode 100644 index 0000000..768af59 --- /dev/null +++ b/doc/architecture/opus-pragmatic-review-2026-03-10.md @@ -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 — и можно открывать для первых пользователей.** diff --git a/doc/decisions/log.md b/doc/decisions/log.md index bf89e2a..91ba2db 100644 --- a/doc/decisions/log.md +++ b/doc/decisions/log.md @@ -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: "} + → 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: ` — пользователь видит причину в `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 не ловит всё. Это дополнительный слой, не единственный. diff --git a/doc/progress.md b/doc/progress.md index 04e033f..e55d46b 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -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, план файлов | ---