Files
sless/doc/architecture/opus-pragmatic-review-2026-03-10.md
“Naeel” 7d6f8d6079 docs: добавлен анализ GPT-5.4 и Opus 4.6
- agent-handoff-2026-03-10.md — GPT-5.4 code review (lifecycle issues, invocation history gap)
- opus-pragmatic-review-2026-03-10.md — Opus прагматичный review для небольшого провайдера
- Opus: gVisor/LLM validation — overkill для MVP, фокус на быстрые фиксы + ResourceQuota/NetworkPolicy
- Обновлён progress.md с новыми документами
- .gitignore — добавлен test.token
2026-03-10 08:56:59 +04:00

36 KiB
Raw Permalink Blame History

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 ломается.

Как чинить:

Два варианта:

  1. PATCH-семантика: обновлять только ненулевые поля (JSON merge patch)
  2. Полная замена: требовать все поля как в 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",

Проблемы:

  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.gohttpClient.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 честно говорит "не реализовано".

Если нужна:

  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 начнёт создавать лишнюю нагрузку.

Реализация:

// 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:

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

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