Files
sless/doc/decisions/log.md
T
“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

24 KiB
Raw Blame History

Решения и обоснования

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<major>.<minor>.<patch>. :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.jshttp.createServer, динамический require(HANDLER_PATH)
  • Зависимости через package.jsonnpm install --omit=dev (аналог requirements.txtpip 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.yamlEXTERNAL_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.goEnabled bool в TriggerSpec, //+kubebuilder:default=true
  • controllers/trigger_controller.go — патчит Deployment replicas=0/1 в зависимости от Enabled
  • internal/api/handler/triggers.goUpdateTrigger handler (PATCH), поле enabled в request/response
  • internal/api/router.goPATCH /v1/namespaces/{namespace}/triggers/{name}
  • internal/client/client.goTriggerUpdateRequest, 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.goRunID 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.goRunID 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 проставляем аннотацию:

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


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: <reason>"}
  → 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/

Интерфейс:

// 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-реализация:

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

// После распаковки 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: <reason> — пользователь видит причину в 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 не ловит всё. Это дополнительный слой, не единственный.