# 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 — и можно открывать для первых пользователей.**