- 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
36 KiB
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
Что происходит:
// 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:
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:
// После 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 нулевыми значениями
Что происходит:
// 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 ломается.
Как чинить:
Два варианта:
- PATCH-семантика: обновлять только ненулевые поля (JSON merge patch)
- Полная замена: требовать все поля как в Create (PUT-семантика с валидацией)
Рекомендация: PUT с валидацией — проще и надёжнее. Добавить ту же проверку что в Create:
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
// trigger_controller.go, reconcileCron
Image: "curlimages/curl:latest",
Проблемы:
:latest— непредсказуемый тег (вредности этого уже была уроком на этом же проекте)- Внешняя зависимость — если DockerHub rate-limit или сеть мигнула, cron-триггер не работает
- Минорная: не pinned к дайджесту
Как чинить: Запинить версию: curlimages/curl:8.5.0 (или какая стабильная). Или использовать busybox:1.36 с wget.
Оценка трудозатрат: 2 минуты.
Часть 2: Несогласованность контрактов — не ломает, но путает
2.1 FunctionNamespacePrefix объявлен, но не используется
Файлы:
internal/config/config.go— полеFunctionNamespacePrefixиз envFUNCTION_NAMESPACE_PREFIXcontrollers/function_controller.go— хардкод"sless-fn-" + fn.Namespacecontrollers/trigger_controller.go— хардкод"sless-fn-" + tr.Namespacecontrollers/functionjob_controller.go— хардкод"sless-fn-" + fj.Namespaceinternal/api/handler/invoke.go— хардкодsless-fn-%s
Конфиг-поле создаёт иллюзию настраиваемости. Если кто-то изменит env — ничего не произойдёт.
Рекомендация: Удалить поле из конфига. Хардкод sless-fn- это нормально для v1. Если когда-нибудь понадобится менять — тогда и протащить. Сейчас мёртвый конфиг хуже хардкода.
Оценка трудозатрат: 10 минут (удалить поле + обновить комментарий).
2.2 TimeoutSec: есть в CRD и API, не enforcement нигде
Файлы:
api/v1alpha1/function_types.go— полеTimeoutSec int32internal/api/handler/functions.go— принимается в APIruntimes/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 объявлены, но не ведутся
// function_types.go
Conditions []metav1.Condition `json:"conditions,omitempty"`
Код обновляет Phase и Message, но Conditions не заполняет — это мёртвый код в status.
Рекомендация: Не трогать. Conditions — стандартная практика k8s, они пригодятся позже. Пока Phase+Message достаточно. Главное — не строить на них зависимости в контроллере пока не заполняете.
2.4 PreWarmSeconds — объявлен, не реализован
// trigger_types.go
PreWarmSeconds int32 `json:"preWarmSeconds,omitempty"`
Рекомендация: Оставить как есть. Это заготовка для scale-to-zero. Пока нет scale-to-zero — поле бесполезно, но и не мешает.
Часть 3: Что реально работает хорошо
Прежде чем обсуждать проблемы, важно зафиксировать что сделано правильно. Это не "похвала", а правки в оценку рисков: если фундамент плохой — надо переделывать. Фундамент хороший.
3.1 Идемпотентность сборки
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
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 — вот это полезно и просто:
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 честно говорит "не реализовано".
Если нужна:
- Добавить SaveInvocation в invoke.go (асинхронно, в горутине)
- Добавить запись результата в FunctionJobReconciler при завершении Job
- Протестировать через curl + GET /invocations
- Обновить doc/api/design.md
Файлы:
internal/api/handler/invoke.gocontrollers/functionjob_controller.gointernal/storage/postgres/store.gointernal/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).
Файлы:
internal/validator/validator.go— интерфейс + NoopValidatorinternal/validator/llm.go— LLM HTTP clientinternal/validator/extract.go— извлечение текстовых файлов из zipinternal/config/config.go— LLM_* env varsinternal/api/handler/upload.go— точка вызоваmain.go— wire
ResourceQuota:
controllers/function_controller.go→ при создании namespacesless-fn-*создавать ResourceQuota- Лимиты фиксированные для v1: pods=20, memory=4Gi, cpu=4
NetworkPolicy:
controllers/function_controller.go→ при создании namespacesless-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 начнёт создавать лишнюю нагрузку.
Реализация:
// 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):
- Trigger ожидает Function Ready: Create Function (Pending) → Create Trigger → проверить что Trigger в "waiting" → перевести Function в Ready → проверить что Trigger стал Active
- Cleanup при удалении: Create Function → Create Deployment в sless-fn- → Delete Function → проверить что Deployment/Service/Ingress удалены
- 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-конфиги должны работать
Не делать пока не будет:
- Auth-сервиса/endpoint nubes.ru для валидации токенов
- Хотя бы 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:
OperatorNamespace: "sless",
Это нормально. Namespace оператора не должен быть динамическим — это deployment-level решение. Если переносим в другой namespace, меняем одну строку. Не проблема.
6.2 Ошибки проглатываются через _ = r.Status().Update(...)
Встречается в нескольких местах:
_ = 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 читается из файла на диске
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
- Реально работает end-to-end. terraform apply → функция доступна по HTTPS.
- Прошёл через реальные проблемы и решил их все (не застрял на happy path).
- Документация ошибок лучше чем в большинстве проектов.
- Terraform provider полностью функционален с source_dir, WaitReady, lifecycle control.
- Архитектура не over-engineered — один бинарник, простые контроллеры, minimal dependencies.
Что нужно для промышленной эксплуатации (небольшой провайдер)
- Этап 0 — быстрые фиксы (RequeueAfter, validation, pin versions) — обязательно перед любыми пользователями
- ResourceQuota + NetworkPolicy — минимальная изоляция
- 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 — и можно открывать для первых пользователей.