docs: update for v0.1.22 — Builder SoC, immediate fixes, unit tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Архитектура системы
|
# Архитектура системы
|
||||||
|
|
||||||
Последнее обновление: 2026-03-11
|
Последнее обновление: 2026-03-11 (v0.1.22)
|
||||||
|
|
||||||
## Общее описание
|
## Общее описание
|
||||||
|
|
||||||
@@ -111,7 +111,9 @@ sless/
|
|||||||
| | +-- middleware/
|
| | +-- middleware/
|
||||||
| | |-- auth.go JWT validation (struct + sub + exp, подпись не проверяется)
|
| | |-- auth.go JWT validation (struct + sub + exp, подпись не проверяется)
|
||||||
| | +-- logging.go slog request logger
|
| | +-- 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
|
| |-- config/config.go Load() из env vars
|
||||||
| +-- storage/
|
| +-- storage/
|
||||||
| |-- postgres/store.go SaveInvocation, ListInvocations, RunMigrations
|
| |-- 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 |
|
| terra.k8c.ru/naeel/sless провайдер | v0.1.13 |
|
||||||
| naeel/sless-runtime-python3.11 | v0.1.1 |
|
| naeel/sless-runtime-python3.11 | v0.1.1 |
|
||||||
| naeel/sless-runtime-nodejs20 | v0.1.2 |
|
| naeel/sless-runtime-nodejs20 | v0.1.2 |
|
||||||
|
|||||||
@@ -520,3 +520,129 @@ invocations.go — 501 stub
|
|||||||
|
|
||||||
**Принцип:** каждый файл отвечает за один домен.
|
**Принцип:** каждый файл отвечает за один домен.
|
||||||
`handler.go` не импортирует `corev1/k8serrors/metav1` — эти зависимости только в `namespace.go`.
|
`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).
|
||||||
|
|||||||
+25
-5
@@ -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 | Средний |
|
| 1 | `ensureRegistrySecret` в FunctionReconciler — cross-namespace инфра операция | Низкий |
|
||||||
| 2 | `ensureRegistrySecret` в FunctionReconciler — cross-namespace инфра операция | Низкий |
|
| 2 | `invocations.go` — 501 stub, PG подключён но endpoint не реализован | Средний |
|
||||||
| 3 | `invocations.go` — 501 stub, PG подключён но endpoint не реализован | Средний |
|
| 3 | LLM-валидация кода при upload | Средний (решение задизайнено в decisions/log.md) |
|
||||||
| 4 | LLM-валидация кода при upload | Средний (решение задизайнено в decisions/log.md) |
|
| 4 | JWKS подпись (verifySignature) — stub есть, логика не реализована | Средний (ждёт JWKS endpoint от nubes) |
|
||||||
| 5 | Scale-to-zero через KEDA HTTP Add-on | Низкий (v2) |
|
| 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) |
|
| 7 | RabbitMQ event triggers | Низкий (v2) |
|
||||||
| 8 | Метрики → Victoria Metrics | Низкий |
|
| 8 | Метрики → Victoria Metrics | Низкий |
|
||||||
|
|||||||
Reference in New Issue
Block a user