38 KiB
Решения и обоснования
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.
Причина: Удобно держать рядом с кодом оператора во время разработки.
Важно: provider "sless" и provider "nubes" — это два отдельных независимых провайдера. Объединять их нельзя:
- разные зоны ответственности (
nubes— облачная инфраструктура,sless— serverless функции) - разные релизные циклы
- разные команды в будущем
Пользователь использует оба провайдера вместе в одном .tf файле, но это не означает что они должны быть одним бинарником.
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.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/Failedcontrollers/functionjob_controller.go— создаёт k8s Job, ждёт завершения, синхронизирует статусinternal/api/handler/jobs.go— REST: CreateJob/GetJob/DeleteJobterraform/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:8080internal/api/router.go—/fn/регистрируется до auth middleware, публично доступен;/v1/— по-прежнему с Bearer токеном (gorillaUse())internal/config/config.go— новое полеExternalURL(envEXTERNAL_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=truecontrollers/trigger_controller.go— патчит Deployment replicas=0/1 в зависимости от Enabledinternal/api/handler/triggers.go—UpdateTriggerhandler (PATCH), полеenabledв request/responseinternal/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=0controllers/functionjob_controller.go— если RunID==0 → устанавливает phase=Skipped, returninternal/api/handler/jobs.go— полеrun_idв jobRequest/jobResponseinternal/client/client.go—RunID int64в JobRequest/JobResponseterraform/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— configmapREGISTRY_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 и причиной.
Причина:
- LLM уже развёрнут (или скоро будет) в облаке nubes.ru — его нужно загрузить реальной работой.
- Dogfooding: облачный провайдер использует собственный сервис ИИ в своём же продукте serverless.
- Маркетинг: "ваш код проверяется ИИ перед деплоем" — реальная продающая фича.
- 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.
Файлы для реализации:
internal/validator/validator.go— интерфейс CodeValidator + NoopValidatorinternal/validator/llm.go— LLMValidator с HTTP client к OpenAI-compatible APIinternal/validator/extract.go— extractTextFiles: zip → map[string]stringinternal/config/config.go— добавить LLM_ENABLED, LLM_ENDPOINT, LLM_API_KEY, LLM_TIMEOUTinternal/api/handler/handler.go— добавить Validator полеinternal/api/handler/upload.go— вызов Validator между zip и tar.gzmain.go— wire: if LLM_ENABLED → LLMValidator, else → NoopValidator
Компромиссы:
- +5-15 секунд к каждому деплою (зависит от скорости LLM).
- False positives: пользователь получит 400 с причиной, может обратиться в support.
- Soft-fail при недоступности LLM: security degraded, но деплой работает.
- Prompt не идеален: LLM не ловит всё. Это дополнительный слой, не единственный.
2026-03-11 — Два провайдера: sless и nubes — нельзя объединять
Решение: Провайдеры sless и nubes — два отдельных независимых провайдера.
Объединять их в один бинарник нельзя.
Причина:
- Разные зоны ответственности:
nubes— облачная инфраструктура (ВМ, сети, объектное хранилище),sless— serverless функции. - Разные релизные циклы.
- В будущем — разные команды.
Пользователь использует оба в одном .tf файле — это нормально, это не значит что они один бинарник.
2026-03-11 — Namespace-per-user через JWT sub → SHA256
Решение: Каждый пользователь облака получает отдельный k8s namespace. Namespace вычисляется детерминированно из JWT sub.
Алгоритм:
namespace = "sless-" + hex(SHA256(JWT.sub)[:8])
Итоговая длина: 22 символа. Пример: sless-cdd874dfa31ba6ca.
Почему SHA256, а не UUID напрямую:
- UUID (sub) напрямую в имени namespace — раскрывает внутренний ID пользователя.
- SHA256 — необратим, namespace не позволяет восстановить sub.
Реализация:
client.SubFromJWT(token)— декодирует JWT payload → возвращает subclient.NamespaceFromSub(sub)— SHA256(sub)[:8] → hex → "sless-{hex16}"- Вычисляется в
provider.Configure()до создания Client
2026-03-11 — EnsureNamespace как отдельный endpoint (SoC)
Проблема: Создание namespace было в resource-хендлерах (CreateFunction, CreateTrigger, CreateJob). Это нарушение разделения ответственностей: ресурс должен заниматься только тем, для чего предназначен.
Решение:
- Создан отдельный endpoint
POST /v1/namespaces/{namespace}/ensure - Хендлер вынесен в отдельный файл
internal/api/handler/namespace.go - Провайдер вызывает его один раз в
Configure()до создания любых ресурсов handler.goочищен от k8s-типов (corev1, k8serrors, metav1) — только инфраструктура
Поведение endpoint:
- 200 OK
{"namespace": "...", "status": "exists"}— namespace уже был - 201 Created
{"namespace": "...", "status": "created"}— namespace создан - Идемпотентен: параллельные запросы не падают (IsAlreadyExists обработан)
Кто отвечает за namespace:
Только EnsureNamespace. Ни один другой хендлер namespace не трогает.
2026-03-11 — JWT validation в операторе вместо статического токена
Проблема: Оператор сравнивал Bearer токен со статическим apiToken из конфига.
JWT-токены облака не совпадали → все запросы от провайдера отклонялись с 401.
Решение: internal/api/middleware/auth.go — заменена проверка:
- Было:
token == cfg.APIToken(строковое сравнение) - Стало:
validateJWT(token)— проверяет структуру JWT (3 части), наличиеsub, срок действияexp
Почему подпись не проверяется:
Оператор находится за Ingress в закрытом кластере (trusted perimeter).
Проверка подписи требует публичный ключ issuer — усложнение без реальной пользы в данной топологии.
Подпись проверяется косвенно через PingNubesAPI в провайдере при terraform init.
Версия: operator v0.1.20
2026-03-11 — Валидация токена через nubes API при Configure
Решение: При terraform init / terraform apply провайдер пингует nubes API
для подтверждения что токен действителен.
Реализация: client.PingNubesAPI(ctx, endpoint, token):
GET <nubes_endpoint>с Bearer токеном- 401/403 → токен отклонён → ошибка инициализации провайдера
- Ошибка соединения → ошибка инициализации
- Любой другой статус (200, 404, 500...) → токен не декларирован невалидным → OK
Конфигурация:
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = file("./secrets/prod.token")
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
}
Env-альтернативы: SLESS_ENDPOINT, SLESS_API_TOKEN, NUBES_ENDPOINT.
2026-03-11 — SoC рефакторинг handler.go
Решение: Файл handler.go — чистая инфраструктура.
Бизнес-логика по доменам — в отдельных файлах одного package.
Структура handler/ package:
handler.go — Handler struct + helpers (writeJSON, errResp, pathVar, namespace)
namespace.go — EnsureNamespace (k8s namespace lifecycle)
functions.go — CRUD Function
triggers.go — CRUD Trigger
jobs.go — CRUD FunctionJob
upload.go — zip -> tar.gz -> S3 -> CRD patch
invoke.go — прокси /fn/ -> in-cluster
invocations.go — 501 stub
Принцип: каждый файл отвечает за один домен.
handler.go не импортирует corev1/k8serrors/metav1 — эти зависимости только в namespace.go.
2026-03-11 — Namespace пользователя никогда не удаляется
Решение: User namespace (sless-{hex16}) не удаляется ни при каких обстоятельствах.
Причина:
- Namespace вычисляется из
JWT.sub— неизменяемого идентификатора пользователя. - Namespace = "home directory" пользователя в кластере:
terraform destroyудаляет функции/триггеры/джобы, но не сам контейнер для ресурсов. - Удаление namespace уничтожило бы все CRD объекты пользователя.
- Повторный
terraform apply(после destroy) нашёл бы свой ns живым — правильное поведение.
Верификация (проверено):
- В API нет маршрута
DELETE /v1/namespaces/{namespace}. handleDeletionвfunction_controller.goудаляет: Deployment, Service, Ingress, kaniko Job.handleTriggerDeletionвtrigger_controller.goудаляет: CronJob (в deployNS), Service, Ingress.- Оба контроллера содержат явный комментарий: "Namespace sless-fn-{userNS} НЕ удаляется — он принадлежит пользователю".
- Тест:
kubectl get ns sless-cdd874dfa31ba6ca— namespace жив через 93 минуты послеterraform destroy.
Оба namespace предохраняются:
sless-{hex16}— user namespace (хранит CRD объекты Function/Trigger/FunctionJob)sless-fn-{hex16}— deploy namespace (хранит Deployment/Service/Ingress/CronJob)
2026-03-11 — Builder SoC: context.go отделён от upload.go
Проблема: generateDockerfile, runtimeBaseImage, zipToTarGz жили в handler/upload.go.
Знание о runtime образах и структуре build context — детали сборки, не HTTP-хендлера.
Нарушение SoC: HTTP-файл знал о Docker, kaniko, tar.gz, zip-разборе.
Решение: Перенести в internal/builder/context.go, единственный публичный API:
func PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error)
Результат:
upload.go: ~200 LOC → ~60 LOC (только HTTP: принять zip, вызвать PrepareContext, сохранить в S3)context.go: всё знание о runtime образах, zip→tar, Dockerfile генерации
Детали реализации:
zipToTarGzпринимает*zip.Readerвместо[]byte— zip парсится один раз вPrepareContextPrepareContextсама сканирует zip-архив (requirements.txt, package.json) — хендлер не знает об этомruntimeBaseImageвозвращает ошибку для неизвестного runtime — ранний fail до kaniko
Тесты: 4 теста в internal/builder/context_test.go (python+requirements, node без package.json, unsupported runtime, Dockerfile-first в tar).
2026-03-11 — Фильтрация hop-by-hop headers в /fn/ прокси
Проблема: invoke.go пробрасывал все заголовки ответа функции клиенту, включая hop-by-hop.
Transfer-Encoding: chunked особенно опасен: Go http.ResponseWriter не умеет его воспроизводить,
клиент получал некорректное тело ответа (или ошибку framing).
Решение: Фильтровать по RFC 2616 §13.5.1 перед записью в w:
var hopByHopHeaders = map[string]bool{
"Connection": true, "Keep-Alive": true, "Proxy-Authenticate": true,
"Proxy-Authorization": true, "Te": true, "Trailers": true,
"Transfer-Encoding": true, "Upgrade": true,
}
// В цикле:
if hopByHopHeaders[k] { continue }
Почему map[string]bool: O(1) lookup, ключи в canonical form (http.CanonicalHeaderKey),
совпадает с форматом ключей в http.Header — нет нужды нормализовывать.
Тесты: 3 теста в internal/api/handler/invoke_test.go
(filtered from response, map contains all RFC2616, canonical key form).
2026-03-11 — JWKS insertion point stub в auth.go
Контекст: v1 auth — validateJWT проверяет структуру токена (sub, exp) без проверки подписи.
Это допустимо в trusted perimeter (оператор в k8s, доступен только изнутри).
Решение: Добавлена verifySignature() как закомментированная заготовка в auth.go.
v2 план (когда nubes даст JWKS endpoint):
GET {NUBES_JWKS_URL}/.well-known/jwks.json- Найти ключ по
kidиз JWT header - Проверить подпись RS256/ES256 через
github.com/lestrrat-go/jwx/v2 - Добавить вызов
verifySignature(token)вvalidateJWTпосле проверки структуры.
Зачем stub: любой агент или разработчик видит точную строку для вставки. Нет риска забыть.
2026-03-11 — CronJob перенесён в deployNS
Проблема: CronJob для HTTP-триггеров создавался в tr.Namespace (user namespace: sless-{hex16}).
При применении NetworkPolicy (каждый namespace изолирован) — CronJob не мог бы дотянуться до API.
Решение: CronJob создаётся в deployNS = "sless-fn-" + tr.Namespace,
где живут Deployment/Service — NetworkPolicy там уже правильная.
Затронутые места в trigger_controller.go:
buildCronJob— namespace в ObjectMetar.Client.Create— нет изменений (namespace из объекта)r.Client.Getв reconcile —deployNSвместо nshandleTriggerDeletion— удаление CronJob изdeployNS
Дополнительно: curlimages/curl:latest → curlimages/curl:8.5.0 (pin версии).
2026-03-11 — Sort env vars в buildDeployment
Проблема: fn.Spec.Env — это map[string]string. Итерация по map в Go недетерминирована.
Каждый reconcile мог генерировать Pod spec с другим порядком env vars → лишние rollout'ы.
Решение:
keys := make([]string, 0, len(fn.Spec.Env))
for k := range fn.Spec.Env { keys = append(keys, k) }
sort.Strings(keys)
for _, k := range keys { envVars = append(envVars, corev1.EnvVar{Name: k, Value: fn.Spec.Env[k]}) }
Тесты: 2 теста в controllers/function_controller_unit_test.go
(4 env vars → алфавитный порядок после SLESS_ENTRYPOINT; пустой Env → только SLESS_ENTRYPOINT).