diff --git a/doc/architecture/overview.md b/doc/architecture/overview.md index 147b5df..05bd707 100644 --- a/doc/architecture/overview.md +++ b/doc/architecture/overview.md @@ -1,6 +1,6 @@ # Архитектура системы -Последнее обновление: 2026-03-11 +Последнее обновление: 2026-03-11 (v0.1.22) ## Общее описание @@ -111,7 +111,9 @@ sless/ | | +-- middleware/ | | |-- auth.go JWT validation (struct + sub + exp, подпись не проверяется) | | +-- logging.go slog request logger -| |-- builder/builder.go kaniko Job lifecycle (Build, JobStatus, Cleanup) +| |-- builder/ +| | |-- builder.go kaniko Job lifecycle (Build, JobStatus, Cleanup) +| | +-- context.go PrepareContext: zip+runtime → tar.gz+Dockerfile для kaniko | |-- config/config.go Load() из env vars | +-- storage/ | |-- postgres/store.go SaveInvocation, ListInvocations, RunMigrations @@ -157,7 +159,7 @@ API endpoint: https://sless-api.kube5s.ru | Артефакт | Тег/Версия | |---------|-----------| -| naeel/sless-operator | v0.1.21 | +| naeel/sless-operator | v0.1.22 | | terra.k8c.ru/naeel/sless провайдер | v0.1.13 | | naeel/sless-runtime-python3.11 | v0.1.1 | | naeel/sless-runtime-nodejs20 | v0.1.2 | diff --git a/doc/decisions/log.md b/doc/decisions/log.md index ba27fcf..ac13e7e 100644 --- a/doc/decisions/log.md +++ b/doc/decisions/log.md @@ -520,3 +520,129 @@ 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: +```go +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 парсится один раз в `PrepareContext` +- `PrepareContext` сама сканирует 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`: +```go +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):** +1. `GET {NUBES_JWKS_URL}/.well-known/jwks.json` +2. Найти ключ по `kid` из JWT header +3. Проверить подпись RS256/ES256 через `github.com/lestrrat-go/jwx/v2` +4. Добавить вызов `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 в ObjectMeta +- `r.Client.Create` — нет изменений (namespace из объекта) +- `r.Client.Get` в reconcile — `deployNS` вместо ns +- `handleTriggerDeletion` — удаление 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'ы. + +**Решение:** +```go +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). diff --git a/doc/progress.md b/doc/progress.md index f2f74bc..cae6252 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -155,15 +155,35 @@ --- +## 2026-03-11 — Opus review: fixes + Builder SoC + unit tests (v0.1.22) + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 1 | CronJob перемещён в deployNS (`sless-fn-{userNS}`) | ✅ | NetworkPolicy-ready | +| 2 | curl:8.5.0 (pin вместо :latest) | ✅ | стабильный образ | +| 3 | sort env vars в buildDeployment | ✅ | нет лишних rollout'ов при reconcile | +| 4 | cleanup kaniko Job в handleDeletion | ✅ | при удалении Function во время Building | +| 5 | hop-by-hop headers отфильтрованы в /fn/ прокси | ✅ | RFC 2616 §13.5.1, 8 заголовков | +| 6 | SLESS_API_TOKEN — убран required | ✅ | был dead code (auth через JWT) | +| 7 | Builder SoC: `internal/builder/context.go` | ✅ | PrepareContext публичный API, upload.go 200→60 LOC | +| 8 | JWKS insertion point stub в auth.go | ✅ | `verifySignature()` заготовка для v2 | +| 9 | Unit tests: 9 тестов, все PASS | ✅ | controllers×2, handler×3, builder×4 | +| 10 | operator v0.1.22 задеплоен | ✅ | `kubectl rollout status` — complete | +| 11 | Коммиты e761439 + 18f25e7 | ✅ | ветка feat/namespace-per-user | + +**Последний коммит:** 18f25e7 + +--- + ## Остаток технического долга (не блокирует) | # | Что | Приоритет | |---|-----|-----------| -| 1 | `upload.go`: generateDockerfile/zipToTarGz — builder logic в HTTP handler | Средний | -| 2 | `ensureRegistrySecret` в FunctionReconciler — cross-namespace инфра операция | Низкий | -| 3 | `invocations.go` — 501 stub, PG подключён но endpoint не реализован | Средний | -| 4 | LLM-валидация кода при upload | Средний (решение задизайнено в decisions/log.md) | +| 1 | `ensureRegistrySecret` в FunctionReconciler — cross-namespace инфра операция | Низкий | +| 2 | `invocations.go` — 501 stub, PG подключён но endpoint не реализован | Средний | +| 3 | LLM-валидация кода при upload | Средний (решение задизайнено в decisions/log.md) | +| 4 | JWKS подпись (verifySignature) — stub есть, логика не реализована | Средний (ждёт JWKS endpoint от nubes) | | 5 | Scale-to-zero через KEDA HTTP Add-on | Низкий (v2) | -| 6 | replicas field в FunctionSpec | Низкий (v1.1) | +| 6 | `replicas` field в FunctionSpec | Низкий (v1.1) | | 7 | RabbitMQ event triggers | Низкий (v2) | | 8 | Метрики → Victoria Metrics | Низкий |