# Решения и обоснования ## 2026-03-06 — Отдельная репа для сервиса **Решение:** Serverless service в отдельной репе, не вместе с Terraform provider. **Причина:** Разные зоны ответственности, разные релизы, потенциально разные команды. --- ## 2026-03-06 — Один бинарник для v1 **Решение:** Один Go бинарник вместо микросервисов. **Причина:** Нагрузки изначально нет. Проще деплоить, проще отлаживать. Разделим при необходимости. --- ## 2026-03-06 — Аутентификация через облачный токен **Решение:** Использовать Bearer token облака, без Keycloak. **Причина:** Terraform provider уже работает с токенами облака. Keycloak — лишняя зависимость для v1. --- ## 2026-03-06 — S3 облачный, остальное в кубере **Решение:** S3 (Ceph) использовать облачный (`ceph.tst.nubes.ru`), PostgreSQL/Redis — в кластере. **Причина:** S3 имеет внешний доступ и уже готов. Для PostgreSQL/Redis сетевого связывания с облаком пока нет — настраивается через devops облака. --- ## 2026-03-06 — Текущий кластер для разработки **Решение:** Использовать существующий k8s кластер (namespace `sless`), потом перенести на новый. **Причина:** Новый кластер ещё не готов. Изоляция через namespace — безопасно для существующих сервисов. --- ## 2026-03-06 — RabbitMQ откладываем **Решение:** В v1 только HTTP и Cron триггеры. RabbitMQ/event triggers — в v2. **Причина:** Упрощение первой итерации. --- ## 2026-03-07 — DockerHub вместо внутреннего registry **Решение:** Образы функций и runtime базовые образы публикуются на DockerHub (user `naeel`). **Причина:** Namespace `registry` в кластере — это Apache NiFi Registry (NOT Docker). Отдельный Docker registry не поднят. DockerHub доступен и достаточен для разработки. --- ## 2026-03-07 — Terraform провайдер sless — отдельный модуль в той же репе **Решение:** `terraform/provider/` — независимый Go-модуль внутри репы `sless`. **Причина:** Удобно держать рядом. Код провайдера писался с прицелом на перенос в nubes провайдер (`/home/naeel/remote_dev/terraform`). Клиент (`internal/client/`) → `internal/core/` nubes, ресурсы → `internal/resources_gen/`. --- ## 2026-03-07 — WaitReady в Terraform провайдере при создании функции **Решение:** После `UploadCode` провайдер ждёт `phase=Ready` (polling каждые 5 сек, таймаут 5 мин). **Причина:** Kaniko-сборка занимает ~1 минуту. Без ожидания `terraform apply` завершился бы с `phase=Building` в state, что неверно отображало бы реальное состояние ресурса. --- ## 2026-03-07 — code_hash для детектирования изменений кода функции **Решение:** Атрибут `code_hash` в `sless_function` — пользователь задаёт через `filemd5("./handler.zip")`. Изменение hash → провайдер перезагружает zip и запускает пересборку. **Причина:** Terraform не отслеживает содержимое файлов автоматически. Это стандартный паттерн (аналогично `aws_lambda_function.source_code_hash`). --- ## 2026-03-07 — Scale-to-zero откладываем до v2 **Решение:** В v1 функции работают как Deployment с постоянно живым подом (always-on). Scale-to-zero — в v2 через KEDA HTTP Add-on. **Причина:** Scale-to-zero меняет архитектуру контроллера и routing. Для MVP это несоразмерная сложность. Пользователь может управлять ресурсами вручную через `replicas = 0/1/N` (планируется в v1.1). **v2 план:** Заменить Deployment на `HTTPScaledObject` (KEDA), минимальные реплики = 0. KEDA буферизует запросы во время cold start (~1-3 сек). --- ## 2026-03-07 — replicas как ручное управление масштабом (TODO v1.1) **Решение:** Добавить поле `replicas *int32` в `FunctionSpec`. Пользователь задаёт через Terraform: `replicas = 0` (выключить), `replicas = 1` (включить), `replicas = N` (масштабировать). **Причина:** Без этого функция жрёт ресурсы 24/7 даже если не нужна. Это минимальный механизм контроля потребления до реализации scale-to-zero. --- ## 2026-03-07 — PostgreSQL опционален для базового Function Hosting **Решение:** Postgres нужен только для логов вызовов (`invocations`). Для базового деплоя функций — не нужен. Оператор работает без него (просто не пишет логи). **Минимальные зависимости для production:** k8s кластер + S3 + Docker registry + Ingress. ## 2026-03-07 — Версионированные теги для runtime образов (не :latest) **Решение:** Runtime базовые образы (`sless-runtime-python3.11`, `sless-runtime-nodejs20`) и образ оператора (`sless-operator`) тегируются по схеме `v..`. `:latest` не используется. **Причина:** - `:latest` приводит к непредсказуемому поведению: kaniko может взять старый кешированный образ, pod не перезапускается если `imagePullPolicy: IfNotPresent`. - Версионированные теги дают явный контроль: при изменении runtime нужно обновить тег в `upload.go` → это принудительно пересобирает все функции с новым базовым образом. - Аудит и откат: можно пинить конкретную версию runtime. **Соглашение:** - Runtime образы: `naeel/sless-runtime-{lang}:v{версия}` (например `v0.1.0`) - Оператор: `naeel/sless-operator:v{версия}` - При изменении runtime — инкрементировать минорную версию образа и обновить константу в `upload.go` --- ## 2026-03-07 — nodejs20 как второй поддерживаемый runtime **Решение:** Добавлен nodejs20 runtime (`node:20-alpine` base, `server.js` HTTP wrapper, `exports.handle(event)`). **Причина:** Node.js — стандарт для serverless (AWS Lambda, Vercel). Покрывает JS/TypeScript аудиторию. Паттерн идентичен python3.11: runtime image → kaniko → Deployment. **Детали реализации:** - `runtimes/nodejs20/server.js` — `http.createServer`, динамический `require(HANDLER_PATH)` - Зависимости через `package.json` → `npm install --omit=dev` (аналог `requirements.txt` → `pip install`) - `entrypoint` в HCL игнорируется для Node.js (всегда `handler.js` + `exports.handle`) — TODO: поддержать произвольный entrypoint в v1.1 --- ## 2026-03-07 — FunctionJob CRD: одноразовые запуски функций **Решение:** Добавлен `FunctionJob` CRD для одноразового запуска функции с произвольным JSON-событием. **Причина:** Нужны sync-вызовы без HTTP — для батч-обработки, миграций, крон-задач через Terraform. **Реализация:** - `api/v1alpha1/job_types.go` — CRD: `FunctionRef`, `EventJSON`, phases: Pending/Running/Succeeded/Failed - `controllers/functionjob_controller.go` — создаёт k8s Job, ждёт завершения, синхронизирует статус - `internal/api/handler/jobs.go` — REST: CreateJob/GetJob/DeleteJob - `terraform/provider/internal/resources/job_resource.go` — ресурс `sless_job` - Настраиваемые таймауты: `build_timeout_sec` (sless_function), `wait_timeout_sec` (sless_job) --- ## 2026-03-07 — Прокси /fn/ вместо wildcard Ingress **Проблема:** wildcard DNS `*.fn.kube5s.ru` недоступен (провайдер не позволяет). **Решение:** HTTP-прокси внутри оператора — маршрут `GET|POST|... /fn/{namespace}/{name}` на `sless-api.kube5s.ru`. **Реализация:** - `internal/api/handler/invoke.go` — форвардит запрос к `http://{fn}.sless-fn-{ns}.svc.cluster.local:8080` - `internal/api/router.go` — `/fn/` регистрируется **до** auth middleware, публично доступен; `/v1/` — по-прежнему с Bearer токеном (gorilla `Use()`) - `internal/config/config.go` — новое поле `ExternalURL` (env `EXTERNAL_URL`) - `controllers/trigger_controller.go` — если `ExternalURL` задан, `Trigger.Status.URL = ExternalURL/fn/{ns}/{name}`; иначе fallback: создаёт Ingress с поддоменом (прежнее поведение) - `deployments/k8s/operator.yaml` — `EXTERNAL_URL=https://sless-api.kube5s.ru` **URL функции:** `https://sless-api.kube5s.ru/fn/{namespace}/{name}` **E2E:** `curl https://sless-api.kube5s.ru/fn/default/hello-node` → `{"message":"Hello, Naeel! (nodejs20)"}` --- ## 2026-03-08 — Lifecycle control: trigger.enabled + job.run_id **Задача:** управление жизненным циклом ресурсов без удаления. ### trigger.enabled **Проблема:** нет способа "заморозить" функцию без удаления Trigger/Function ( освобождение ресурсов под праздники, дебаггинг и т.д.). **Решение:** `enabled bool` (по умолчанию `true`) в `TriggerSpec`. - `enabled=false` → trigger_controller масштабирует Deployment функции до 0 реплик. - Функция не принимает запросы, не потребляет CPU (pod не запущен). - Изменение **не** пересоздаёт ресурс (нет RequiresReplace) — in-place через PATCH. **Реализация:** - `api/v1alpha1/trigger_types.go` — `Enabled bool` в TriggerSpec, `//+kubebuilder:default=true` - `controllers/trigger_controller.go` — патчит Deployment replicas=0/1 в зависимости от Enabled - `internal/api/handler/triggers.go` — `UpdateTrigger` handler (PATCH), поле `enabled` в request/response - `internal/api/router.go` — `PATCH /v1/namespaces/{namespace}/triggers/{name}` - `internal/client/client.go` — `TriggerUpdateRequest`, `UpdateTrigger()` метод - `terraform/provider/internal/resources/trigger_resource.go` — атрибут `enabled` (Optional+Computed, default=true), реализован `Update` метод ### job.run_id **Проблема:** нет способа создать FunctionJob "отложенным" — с явным контролем когда запускать. Также нет механизма повторного запуска с сохранением структуры ресурса. **Решение:** `run_id int64` (по умолчанию `0`) в `FunctionJobSpec`. - `run_id=0` → FunctionJob создаётся в k8s, но k8s Job не запускается (phase=Skipped). - `run_id>0` → запускает Job. Увеличение значения (1→2→3) = повторный запуск через пересоздание. **Реализация:** - `api/v1alpha1/job_types.go` — `RunID int64` в FunctionJobSpec, `//+kubebuilder:default=0` - `controllers/functionjob_controller.go` — если RunID==0 → устанавливает phase=Skipped, return - `internal/api/handler/jobs.go` — поле `run_id` в jobRequest/jobResponse - `internal/client/client.go` — `RunID int64` в JobRequest/JobResponse - `terraform/provider/internal/resources/job_resource.go` — атрибут `run_id` (RequiresReplace, default=0). Если run_id=0 → не ждёт завершения, phase=Skipped сразу в state. **Версии:** - operator: `naeel/sless-operator:v0.1.6` - provider: `terra.k8c.ru/naeel/sless v0.1.4` --- ## 2026-03-08 — Переключение registry с Harbor на DockerHub **Проблема:** Harbor (`pearlharbor.registryk8s.services.ngcloud.ru`) — внешний сервис облачного провайдера. Нестабилен: `/v2/` периодически зависает на 10+ секунд или возвращает 504. Kaniko не мог завершить push образа. **Решение:** `REGISTRY_HOST=naeel` (DockerHub namespace). Образы функций пушатся как `naeel/sless-default-{namespace}-{name}:latest`. **Реализация:** - `deployments/k8s/operator.yaml` — configmap `REGISTRY_HOST: "naeel"` - Secret `sless-registry-auth` уже содержал DockerHub credentials → дополнительных изменений не потребовалось **Компромисс:** DockerHub — публичный registry. Образы функций пользователей публично видимы. Для production нужен приватный registry (Harbor, ECR, GCR и т.д.). **Версии:** - operator: оператор не пересобирался, только configmap - commit: `b69f795` --- ## 2026-03-08 — FunctionJob polling вместо Owns watch **Проблема:** `Owns(&batchv1.Job{})` в `SetupWithManager` не работает cross-namespace. Job создаётся в `sless-fn-{ns}`, FunctionJob — в user namespace. Watch никогда не срабатывал. **Решение:** Убрать `Owns`. В `syncJobStatus` при Running статусе возвращать `ctrl.Result{RequeueAfter: 5 * time.Second}` — контроллер сам поллит k8s Job каждые 5 сек. **Версии:** - operator: `naeel/sless-operator:v0.1.10` - commit: `461ac09` --- ## 2026-03-08 — code_hash: filesha256 вместо output_md5 **Проблема:** `hashicorp/archive v2.7.x` имеет баг: `output_md5` возвращает MD5 предыдущей версии zip. `output_sha` и `output_sha256` обновляются корректно. **Решение:** `code_hash = filesha256("${path.module}/code/handler.js")` — хэшируется исходный файл напрямую. **Правило проекта:** В `sless_function.code_hash` всегда использовать `filesha256(source_file)`, не `archive_file.output_md5`. --- ## 2026-03-08 — Rollout restart после kaniko build (imagePullPolicy + :latest) **Проблема:** После успешной kaniko сборки pod не перезапускался — kubelet брал кешированный образ `:latest` (imagePullPolicy: IfNotPresent). Функция возвращала старый код. **Решение:** В `ensureDeployment` при обновлении существующего Deployment проставляем аннотацию: ```go existing.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = fn.Status.LastBuiltAt.Time.Format(time.RFC3339) ``` Значение привязано к `fn.Status.LastBuiltAt` → меняется при каждой сборке → Kubernetes делает rolling restart → свежий образ гарантированно пул-ится. **Правило проекта:** При использовании `:latest` tag всегда явно проставлять `restartedAt` annotation при обновлении кода. **Версия:** operator `naeel/sless-operator:v0.1.11`