Files
sless/doc/progress.md
T

1323 lines
90 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Прогресс разработки
Последнее обновление: 2026-03-23
---
## 2026-03-23 — Сессия 11: Баги cache-тест, fix оператора v0.1.62, fix провайдера
### Что сделано
**Bug 1: Go 1.23 go.work + replace конфликт (FIXED → v0.1.61)**
- `internal/builder/context.go`: убран `replace sless/fn/handler => ./handler` из go.work шаблона
- Добавлен `sed` переименования модуля вместо replace
- Ошибка была: `go: workspace module sless/fn/handler is replaced at all versions in the go.work file`
**Bug 2: controller-runtime cache lag → 404 на upload (FIXED → v0.1.62)**
- `internal/api/handler/services.go`: retry loop 5×200ms в `UploadServiceCode`
- `internal/api/handler/jobs.go`: retry loop 5×200ms в `UploadJobCode`
- Причина: сразу после POST /services (201) informer cache ещё не синхронизирован → IsNotFound
**Bug 3: Провайдер — 409 при повторном apply после сбоя upload (FIXED)**
- `terraform/provider/internal/resources/service_resource.go`: в `Create()` при ошибке upload — rollback `DeleteService()`
- `terraform/provider/internal/resources/job_resource.go`: аналогично `DeleteJob()`
- Причина: upload в S3 падал (сетевой сбой), terraform не записывал state, CR оставался в k8s → следующий apply получал 409
**test_cache_matrix.sh v4**
- Убраны все `-target` из скрипта — terraform не должен касаться postgres при частичных операциях
- `destroy_sless_only()`: переименует `chaos_marathon.tf`, `functions.tf`, `stress.tf``.bak`, делает apply (terraform сам удаляет), возвращает файлы
- Phase 3a: закомментирует блоки ресурсов через Python вместо `-target`
**Operator v0.1.62** собран и задеплоен (`naeel/sless-operator:v0.1.62`)
### Статус
✅ v0.1.62 Running
✅ Провайдер пересобран на VM (`/tmp/sless-provider-dev/`)
⏳ test_cache_matrix.sh v4 — Phase 1 в процессе (pg-stats удалили вручную, apply идёт)
---
## 2026-03-23 — Сессия 10: ImageExists + деплой v0.1.58
### Что сделано
- **ImageExists** — новый метод `Builder.ImageExists(ctx, imageRef) bool` в `internal/builder/builder.go`
- Docker Registry v2 API: anonymous bearer token → HEAD `/v2/{repo}/manifests/{tag}`
- timeout 5s, fallback false при любой ошибке (безопасный fallback → kaniko)
- Приватные registry (Harbor и др.) → 401 → false → строим заново
- **service_controller.go** `startServiceBuild()`: кеш-проверка перед Build(); cache hit → `Status.Phase=Ready` + `Status.ImageRef` без kaniko
- **function_controller.go** `startBuild()`: аналогично
- **functionjob_controller.go** `startJobBuild()`: аналогично; cache hit → Requeue для немедленного перехода к run
- **v0.1.58** собран и задеплоен: `naeel/sless-operator:v0.1.58` Running, RESTARTS:0
### Статус
✅ Код написан (no errors)
✅ Docker build + push наDockerHub (sha256:7b0d48a01a29f2a5...)
✅ kubectl rollout: `deployment "sless-operator" successfully rolled out`
✅ Pod `sless-operator-85d4685c4b-6nxqw` Running v0.1.58
### Следующий шаг
Протестировать cache hit: вернуть `.tf.disabled``.tf`, `terraform apply` — образы уже на DockerHub, должны восстановиться без kaniko за секунды.
---
## TODO (backlog — не скоро)
| # | Задача | Заметка |
|---|--------|---------|
| T1 | `nubes_endpoint` в sless провайдере — сделать обязательным или ввести флаг `require_token_validation` | Сейчас если `nubes_endpoint` не задан — проверка токена через nubes API пропускается. Упущение безопасности. |
| T2 | **Terraform провайдер nubes — end-to-end тестирование** | Провайдер написан но ни разу не прогонялся с реальным сервером. Нужно: `init → apply → apply (idempotency) → update → destroy`. Тест-кейсы уже есть в `examples/`. Это отдельный большой блок работы для облачных DevOps-инженеров nubes. |
---
## 2026-03-22 — Сессия 9: G_MULTIUSER
### G_MULTIUSER: 10 параллельных пользователей с Postgres
**Статус:****77/80 PASS** (3 flaky — не баг оператора)
**Результат прогона:**
| User | NS | PASS | FAIL |
|------|----|------|------|
| 1 | sless-mu01 | 8 | 0 |
| 2 | sless-mu02 | 8 | 0 |
| 3 | sless-mu03 | 8 | 0 |
| 4 | sless-mu04 | 7 | 1 |
| 5 | sless-mu05 | 8 | 0 |
| 6 | sless-mu06 | 7 | 1 |
| 7 | sless-mu07 | 8 | 0 |
| 8 | sless-mu08 | 8 | 0 |
| 9 | sless-mu09 | 8 | 0 |
| 10 | sless-mu10 | 7 | 1 |
**3 фейла (U4-5, U6-5, U10-5):** race condition — сервис стал `Ready` (CRD фаза), но pod-сеть ещё не поднялась (`operation not permitted`). Это flakiness теста при 10 параллельных Kaniko-сборках, **не баг оператора**. Фикс в скрипте: sleep 40s + 8 retries.
**Что проверял тест:**
- 10 параллельных пользователей с уникальными JWT (`sub=test-user-01..10`)
- Namespace isolation: каждый NS независим, чужой сервис → 404
- Real Postgres: CREATE TABLE + INSERT + SELECT COUNT(*) через env_vars
- Full lifecycle: ensure → create → upload → build → Ready → invoke×2 → delete
**Known limitation зафиксирован:** `Ready` в CRD фазе опережает готовность pod-сети при высокой параллельной нагрузке.
---
## 2026-03-22 — Сессия 8: тесты G17/G18/G19/G20/G22
### Что сделано
| # | Компонент | Результат |
|---|-----------|-----------|
| 1 | `operator_namespace_test.sh` (G22) — 5 секций изоляции namespace | ✅ **15/15 PASS** |
| 2 | `operator_features_test.sh` (G17+G18+G19) — nodejs20, env_vars, source API | ✅ **38/38 PASS** |
| 3 | `operator_load_test.sh` (G20) — parallel build, parallel invoke, multi-svc | ✅ **19-20/20 PASS** |
| 4 | Задокументировано known limitation: Ready до pod-ready | ✅ |
### Тесты G22 — Namespace Isolation (15/15)
- **22A** CROSS-NS VISIBILITY: объект в NS_A не виден через URL NS_B → 404
- **22B** LIST ISOLATION: list в несуществующем NS → пустой `[]`
- **22C** CRUD ISOLATION: DELETE/PUT через фейковый NS → 404, не затрагивает реальный объект
- **22D** INVOKE ISOLATION: `/fn/FAKE_NS/NAME` → 404 (нет Deployment в том NS)
- **22E** UPLOAD ISOLATION: upload/source через фейковый NS → 404
### Тесты G17 — nodejs20 Runtime (14/14)
- Базовый create+upload+invoke: handler возвращает `{ok:true, runtime:"nodejs20"}`
- Event body передаётся handler'у корректно
- Кастомное имя модуля (`app.process`) работает
### Тесты G18 — env_vars (16/16)
- env_vars создаются и читаются функцией через `process.env`
- PUT обновляет env_vars → Deployment пересоздаётся → новые значения видны
- Edge cases: пустая строка, спецсимволы (=, &) — принимаются корректно
### Тесты G19 — Source API (8/8)
- До upload: `GET /source``200 + []`
- После upload: файлы видны, `Dockerfile` исключён из ответа
- Несуществующий сервис → 404
### Тесты G20 — Load (19-20/20)
- 5 параллельных Kaniko-сборок: все Ready (допустим 1 таймаут при очереди)
- 20 параллельных invoke к одному сервису: 100% успех
- Параллельные invoke к нескольким сервисам: ≥80% (pod startup race — known)
### Баги тестов (не оператора)
1. **G22D 502** — sleep 10s + 3 retry недостаточно после Ready; фикс: sleep 30 + 5 retry x 15s
2. **G22E HTTP 000** — имя zip-файла не совпадало (`${NS_FN}-fake` vs `${NS_FN}`); фикс: единое имя
3. **G17 502** — invoke сразу после Ready; фикс: `invoke_with_retry()` helper во всех тестах
4. **G18 502** — то же; фикс: тот же helper
5. **G17A control flow** — invoke вызывался вне `if then` после timeout; фикс: перенесён внутрь
### Known limitation оператора (не баг)
`phase=Ready` выставляется когда Deployment **создан**, не когда pod принял трафик.
Задержка 5-30с. Решение на уровне тестов: sleep + retry.
Потенциальное улучшение оператора: watch `Deployment.Status.ReadyReplicas`.
---
---
## 2026-03-21 — Сессия 6: исправление багов оператора v0.1.49
### Что сделано
| # | Компонент | Результат |
|---|-----------|-----------|
| 1 | БАГ 1 исправлен: добавлен `Resources` в UPDATE блок `ensureServiceDeployment` | ✅ |
| 2 | БАГ 2 исправлен: добавлен `RequeueAfter: 60s` в конец `ensureServiceDeployment` | ✅ |
| 3 | go build, docker build+push v0.1.49, kubectl rollout | ✅ |
| 4 | `operator_lifecycle_test.sh` после фикса | ✅ 47/47 PASS (ранее 44/44 с 2 known bugs) |
### Изменения в коде
**controllers/service_controller.go**:
```go
// БАГ 1 — UPDATE блок ensureServiceDeployment
existing.Spec.Template.Spec.Containers[0].Resources = desired.Spec.Template.Spec.Containers[0].Resources // ДОБАВЛЕНО
// БАГ 2 — конец ensureServiceDeployment
return ctrl.Result{RequeueAfter: 60 * time.Second}, nil // БЫЛО: Result{}, nil
```
---
## 2026-03-21 — Сессия 5: lifecycle-тест оператора — 44/44 PASS; 2 бага
### Что сделано
| # | Компонент | Результат |
|---|-----------|-----------|
| 1 | `operator_lifecycle_test.sh` — 44 теста жизненного цикла | ✅ 44/44 PASS |
| 2 | Найден БАГ 1: `memory_mb` через PUT не применяется в k8s | ⚠️ зафиксировано |
| 3 | Найден БАГ 2: нет self-healing при ручном удалении Deployment | ⚠️ зафиксировано |
### Структура теста (operator_lifecycle_test.sh)
| Группа | Что тестируется | Тестов |
|--------|----------------|--------|
| 1 (API validation) | `memory_mb=0/5000` → 400; `timeout_sec=-1/901` → 400; без runtime → 400; GET/PUT/DELETE несущ. → 404 | 8 |
| 2 (Full lifecycle solo) | create→upload→build→ready→invoke→env update→k8s verify→memory update→timeout→delete→k8s cleanup | 18 |
| 3 (Edge cases) | DELETE while Building (kaniko убит, Deployment не создан); Reconcile (self-healing) | 10 |
| 4 (Multi parallel) | 3 функции параллельно; delete одной в Building; 2 дошли до Ready; 40 parallel invoke | 8 |
### Найденные баги оператора
| # | Баг | Место | Суть |
|---|-----|-------|------|
| 1 | `memory_mb` через PUT не применяется | `controllers/service_controller.go:ensureServiceDeployment` | При UPDATE обновляются только `Image` и `Env`. `Resources` (memory limit) не трогается — k8s Deployment хранит старое значение |
| 2 | Нет self-healing при удалении Deployment | `ServiceReconciler` | Если Deployment удалили вручную — контроллер не пересоздаёт. Нет cross-namespace watch (`sless-fn-*`) и нет `RequeueAfter`. Reconcile срабатывает только при изменении CRD |
### Что НЕ упало (важно)
- DELETE во время Building → CRD удалён, kaniko Job убит, Deployment не создан ✅
- Duplicate create + Ready → 409 ✅
- env_vars через PUT → k8s Deployment обновился, rollout OK, invoke вернул новое значение ✅
- 3 параллельных build → все 3 независимы, delete одного не ломает остальные ✅
- 40 параллельных invoke → 40/40 OK ✅
- Финальный teardown → все lc-* сервисы удалены ✅
---
## 2026-03-21 — Сессия 4: фича timeout_sec без дефолта + деплой v0.1.48
### Что сделано
| # | Компонент | Версия | Результат |
|---|-----------|--------|-----------|
| 1 | `api/v1alpha1/service_types.go` — убрать `+kubebuilder:default=30` | — | ✅ `TimeoutSec=0` → нет ограничения |
| 2 | `internal/api/handler/invoke.go` — убрать хардкод 35s дефолт | — | ✅ `TimeoutSec=0``&http.Client{}` (нет таймаута) |
| 3 | `internal/api/handler/services.go` — валидация `timeout_sec` | — | ✅ `< 0` или `> 900` → HTTP 400 |
| 4 | `terraform/provider/…/service_resource.go` — schema + svcToModel | — | ✅ `0``null` в Terraform state |
| 5 | Оператор Docker build v0.1.48 + push | operator | ✅ задеплоен, rollout OK |
| 6 | kubectl rollout + rollout status | k8s | ✅ |
### Изменения логики timeout_sec
| Место | Было | Стало |
|-------|------|-------|
| CRD default | `+kubebuilder:default=30` (всегда 30) | нет default (0 = нет лимита) |
| invoke.go TimeoutSec=0 | 35s hardcoded fallback | `&http.Client{}` (Go: Timeout=0 → нет таймаута) |
| services.go validation | нет проверки | `< 0 \|\| > 900` → 400 Bad Request |
| TF schema timeout_sec | `Optional + Computed` | `Optional` (нет Computed) |
| svcToModel | `Int64Value(0)` в state | `Int64Null()` в state (null = не задан) |
### Семантика для пользователя
- Не указывать `timeout_sec` в terraform → функция работает без ограничений времени
- `timeout_sec = 60` → функция убивается через 60 секунд (+ 5s grace)
- `timeout_sec = 0` → явный «нет лимита» (эквивалентно отсутствию поля)
- Диапазон: 1–900 секунд. Именно: `1 ≤ timeout_sec ≤ 900` или не задан (null/0)
---
## 2026-03-21 — Сессия 3: стресс-тестирование, баги рантаймов, паника Go → 500, Python threading
### Что сделано
| # | Компонент | Версия | Результат |
|---|-----------|--------|-----------|
| 1 | Деплой 10 стресс-сервисов (Go/Node/Python) | stress.tf | ✅ все 13 сервисов в Ready |
| 2 | Написан и прогнан full_test.sh | — | 43/48 PASS → 48/48 PASS после фиксов |
| 3 | Фикс: Go panic → 502 | go1.23 v0.1.2 | ✅ defer recover() → HTTP 500 |
| 4 | Фикс: Python exception → 502 | python3.11 v0.1.5 | ✅ try/except в do_GET/_handle_with_body |
| 5 | Фикс: Python single-thread → connection reset | python3.11 v0.1.5 | ✅ ThreadingHTTPServer |
| 6 | Фикс: Python TCP backlog=5 → reset при 40+ параллельных | python3.11 v0.1.6 | ✅ _HighBacklogHTTPServer(request_queue_size=128) |
| 7 | operator v0.1.46 → v0.1.47 | оператор | ✅ задеплоен, rollout OK |
### Найденные и исправленные баги
| Баг | Причина | Фикс |
|-----|---------|------|
| Go panic → HTTP 502 | Go `http.Server` закрывает соединение при panic — proxy получает EOF | `defer func() { recover() → w.WriteHeader(500) }()` в server.go |
| Python exception → HTTP 502 | `BaseHTTPRequestHandler.handle_error()` не отправляет response, закрывает соединение | `try/except Exception` в do_GET/_handle_with_body/do_HEAD → `_respond(500, {"error":...})` |
| Python 40× parallel → 13/40 connection reset | `HTTPServer` однопоточный, очередь accept = 5 | `ThreadingHTTPServer` — каждый запрос в отдельном потоке |
| Python 40× parallel → 7/40 connection reset даже с threading | TCP listen backlog = 5 (дефолт `socketserver.TCPServer`) | `_HighBacklogHTTPServer(request_queue_size=128)``listen(128)` |
### Финальные результаты full_test.sh
- **48/48 PASS** ✅
- Фаза 1: CRUD — 16/16 сервисов + job ✅
- Фаза 2: Функциональные тесты — Go (7), Node (4), Python (13) ✅
- Фаза 3: PG-стресс — 40× parallel writer 40/40 OK, 30× js-async 30/30 OK, pgstorm 14k ops 0 err ✅
- Фаза 4: Краш-шторм — 75× panic/exception → 75× HTTP 500, платформа жива ✅
### Итоговое состояние кластера
| Ресурс | Версия |
|--------|--------|
| operator | `pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.47` |
| go1.23 runtime | `naeel/sless-runtime-go1.23:v0.1.2` |
| python3.11 runtime | `naeel/sless-runtime-python3.11:v0.1.6` |
| nodejs20 runtime | без изменений (ошибки → 500 уже работали) |
| Строк в terraform_demo_table | ~21 000 (накоплено за сессию тестирования) |
---
## 2026-03-21 — Сессия 2: тестирование API, баги, web-консоль, source для services
### Что сделано
| # | Компонент | Версия | Результат |
|---|-----------|--------|-----------|
| 1 | Полный цикл CRUD-тестирования | — | ✅ functions / triggers / services / jobs — все операции проверены |
| 2 | Фикс: DELETE несуществующего ресурса → 204 вместо 404 | operator v0.1.44 | ✅ исправлено в 4 хендлерах |
| 3 | funcs-console: объединить functions + services в единый листинг | funcs-service v0.2.1 | ✅ сервисы отображаются с бейджем `always-on` |
| 4 | Фикс: imagePullSecrets отсутствовал в funcs-service.yaml | manif. fix | ✅ образ v0.2.1 успешно стягивается на всех нодах |
| 5 | Фикс: `/funcs/{ns}/source/{fn}` → 404 для sless_service | operator v0.1.45 + funcs-service v0.2.2 | ✅ новый endpoint `/services/{name}/source` |
### Найденные и исправленные баги
| Баг | Причина | Фикс | Коммит |
|-----|---------|------|--------|
| `DELETE /functions/not-exists` → HTTP 204 | Все 4 Delete-хендлера возвращали `204 No Content` при `IsNotFound` | `writeJSON(w, 404, errResp("... not found"))` в functions.go / services.go / triggers.go / jobs.go | `e8d0d78` |
| funcs-service pod в `ImagePullBackOff` после обновления образа | `deployments/k8s/funcs-service.yaml` не содержал `imagePullSecrets` | Добавить `imagePullSecrets: [{name: sless-registry-auth}]` + полный образ с pearlharbor prefix | `09b3588` |
| `/funcs/{ns}/source/{fn}` → 404 для sless_service (pg-info, pg-table-reader, etc.) | `proxySourceGet` всегда обращался к `/functions/{fn}/source`; у оператора не было эндпоинта `/services/{name}/source` | Добавить `GetServiceSource` в source.go + роут в router.go; в funcs-service передавать `?kind=service` | `50f2456` |
### Новые ресурсы / изменения кода
| Файл | Изменение |
|------|-----------|
| `internal/api/handler/functions.go` + `services.go` + `triggers.go` + `jobs.go` | `Delete*`: `IsNotFound` → HTTP 404 вместо 204 |
| `internal/api/handler/source.go` | Новый хендлер `GetServiceSource` — аналог `GetSource` для Service CRD |
| `internal/api/router.go` | Новый маршрут `GET /v1/namespaces/{ns}/services/{name}/source` |
| `services/funcs/main.go` | `fnResponse` + поля `Kind/URL`; новая структура `svcResponse`; `fetchAndRender` объединяет functions + services; `proxySourceGet` поддерживает `?kind=service` |
| `services/funcs/index.html` | Бейджи `badge-kind-service` / `badge-kind-function`; `loadSource(…, fnKind)` передаёт `?kind=service`; счётчик "Функций N \| Сервисов N \| Всего N" |
| `deployments/k8s/funcs-service.yaml` | Добавлен `imagePullSecrets: sless-registry-auth`; образ → `pearlharbor…/sless-funcs-service:v0.2.2` |
| `deployments/k8s/operator.yaml` | Образ → `pearlharbor…/sless-operator:v0.1.45` |
### Результаты тестирования (ключевые)
- CREATE 3 функций (nodejs20 / python3.11 / go1.23) → 201 ✅
- UPDATE memory / timeout / env_vars → 200 ✅
- DELETE существующей → 204; DELETE несуществующей → 404 ✅ (после фикса)
- Дубликат → 409; bad input → 400; no auth → 401 ✅
- Триггеры: CREATE http+cron / PATCH enable-disable / DELETE ✅
- Services CRUD ✅; Jobs API ✅
- `/funcs/sless-ffd1f598c169b0ae` показывает 5 ресурсов: 3 sless_service + 2 sless_function ✅
- `source` для function → 200, для service → 200 ✅
### Коммиты сессии (feat/function-service-split)
- `e8d0d78` fix(api): DELETE несуществующего ресурса — 404 вместо 204 (function/service/trigger/job)
- `683d728` feat(funcs-console): показывать sless_service вместе с функциями — единый листинг
- `09b3588` fix(deploy): imagePullSecrets + образ v0.2.1 в funcs-service.yaml
- `50f2456` fix(source): add /services/{name}/source endpoint; fix 404 for service code view in funcs-console
### Задеплоено
- **Оператор**: `pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.45` — rollout ✅
- **funcs-service**: `pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-funcs-service:v0.2.2` — rollout ✅
---
## 2026-03-21 — Сессия 1: Deploy + E2E тест: sless_job self-contained (POSTGRES пример)
### Что сделано
| # | Компонент | Версия | Результат |
|---|-----------|--------|-----------|
| 1 | Operator Docker image | v0.1.43 | ✅ собран, запушен в pearlharbor, задеплоен |
| 2 | CRD `functionjobs.sless.kube5s.ru` | перегенерирован | ✅ `runtime/entrypoint/env` поля добавлены |
| 3 | Terraform provider | v0.1.19 | ✅ собран, опубликован в S3 (terra.k8c.ru) |
| 4 | `terraform apply` POSTGRES | — | ✅ `Apply complete! Resources: 4 added` |
| 5 | E2E smoke test | — | ✅ table-writer / table-reader отвечают |
### Найденные и исправленные баги при deploy
| Баг | Файл | Фикс |
|-----|------|------|
| `FunctionRef` в `invoke.go` не убрали при merge | `internal/api/handler/invoke.go` | Заменить FunctionRef на поля из `fn.Spec` (Runtime/Entrypoint/S3Bucket/S3Key/MemoryMB/TimeoutSec/Env) |
| Нет `imagePullSecrets` для pearlharbor registry | `deployments/k8s/operator.yaml` | Добавить `imagePullSecrets: [{name: sless-registry-auth}]` |
| Проект `naeel` не существовал в Harbor | Harbor API | `POST /api/v2.0/projects` → 201 Created |
| CRD не обновлён (старые поля: только `functionRef/eventJson/runId`) | `config/crd/bases/` | `bin/controller-gen crd paths="./api/..." ...``kubectl apply -f config/crd/bases/` |
| Контроллер запускал kaniko до загрузки кода (`S3Key == ""`) | `controllers/functionjob_controller.go` | В `startJobBuild`: `if fj.Spec.S3Key == "" { return RequeueAfter: 5s }` |
| dev override содержал старый бинарник v0.1.18 | `/tmp/sless-provider-dev/` | `rm terraform-provider-sless_v0.1.18` |
### Коммиты этой сессии (feat/function-service-split)
- `0d83c0e` docs: убрать упоминания 192.168.1.220, исправить шаблоны SSH
- `6f76ecb` fix(invoke): FunctionRef → поля из Function.Spec
- `23141e5` chore: operator image v0.1.42 — pearlharbor registry
- `3dfe98e` fix(operator): добавить imagePullSecrets sless-registry-auth
- `b3559c9` chore(crd): регенерация FunctionJobSpec
- `3b3d510` chore(postgres): provider version 0.1.18 → 0.1.19
- `735958b` fix(controller): ждать S3Key перед startJobBuild
- `c910bb8` chore: operator image v0.1.43
### E2E результаты
- `sless_job.postgres_table_init_job` → фаза `Succeeded`
- `sless_service.pg_info` → создан ✅
- `sless_service.postgres_table_reader` → URL: `https://sless.kube5s.ru/fn/.../pg-table-reader`
- `sless_service.postgres_table_writer` → запись `test-v0.1.43` → ответ HTML с таблицей ✅
### Следующий шаг
Смёрджить `feat/function-service-split` в `main`.
---
## 2026-03-20 — Merge: sless_function + sless_job → единый self-contained sless_job
### Цель
Убрать обязательную зависимость `sless_job` от `sless_function`. Раньше для запуска одноразового
джоба нужно было сначала создать `sless_function` (CRD + kaniko build), потом `sless_job`.
Теперь `sless_job` самодостаточен: содержит runtime/entrypoint/env/source_dir и сам запускает сборку.
### Изменённые файлы
| Файл | Что сделано |
|------|-------------|
| `api/v1alpha1/job_types.go` | Убран `FunctionRef`. Добавлены: `Runtime/Entrypoint/S3Bucket/S3Key/MemoryMB/TimeoutSec/Env map[string]string`. Фаза `Building`. `ImageRef` в status. |
| `api/v1alpha1/zz_generated.deepcopy.go` | DeepCopyInto для FunctionJobSpec: proper deep copy map[string]string Env |
| `controllers/functionjob_controller.go` | Полная переработка: убрана зависимость от Function CRD. Новые поля Builder+OperatorNamespace. Новая фаза Building (kaniko). Методы startJobBuild/checkJobBuild. |
| `main.go` | Передача `Builder: bldr` и `OperatorNamespace: "sless"` в FunctionJobReconciler |
| `internal/api/handler/jobs.go` | jobRequest/jobResponse без FunctionRef, с Runtime/Entrypoint/Env/S3Key. Новый handler UploadJobCode. |
| `internal/api/router.go` | Добавлен маршрут `/namespaces/{ns}/jobs/{name}/upload` |
| `terraform/provider/internal/client/client.go` | JobRequest/JobResponse без FunctionRef, с Runtime/Entrypoint/Env/ImageRef. UploadJobCode метод. Рефакторинг UploadCodeReader → uploadCodeToURL. |
| `terraform/provider/internal/resources/job_resource.go` | JobModel без Function, с Runtime/Entrypoint/EnvVars/SourceDir/CodeHash/ImageRef. ModifyPlan. Create с upload. Дефолт wait_timeout_sec=900. |
| `examples/POSTGRES/functions.tf` | Убран `sless_function.postgres_sql_runner_create_table`. `sless_job.postgres_table_init_job` теперь самодостаточен: inline source_dir/runtime/entrypoint/env_vars. |
### Статус
- ✅ Go код скомпилировался (controllers, main, internal/api)
- ✅ Terraform provider: нет ошибок компилятора
- ✅ functions.tf: раскомментирован и обновлён
- ⏳ Требует: кросс-компиляция provider → deploy в кластер → тест apply
## 2026-03-20 — Восстановление PostgreSQL и отладка провайдера nubes
### Что произошло
1. Старый PG-инстанс (`teststand-pg-2`) был удалён. Девопсы позднее вернули PG под именем `pg-sless-demo`.
2. Обнаружен неверный `api_endpoint` в провайдере nubes: `deck-test` вместо `deck-api-test` → 404 на всех ресурсах.
3. Исправлен `nubes_endpoint` в провайдере `sless` — та же проблема.
4. Обнаружен баг платформы: `delete_user` не удаляет CRD → база/пользователь зависают в кластере → повторный `apply` падает с "нарушена консистентность".
5. Workaround: ручное удаление PG через UI + `terraform state rm` + пересоздание.
### Изменения в файлах
| Файл | Что изменено |
|------|-------------|
| `examples/POSTGRES/main.tf` | Исправлен `api_endpoint` и `nubes_endpoint``deck-api-test.ngcloud.ru` в обоих провайдерах |
| `examples/POSTGRES/postgres.tf` | `try()`-обёртка для `vault_secrets["users"]`; `resource_name = "pg-sless-demo"` |
| `examples/POSTGRES/stress_destroy_apply.sh` | Новый скрипт: 5 итераций destroy+apply с логами и остановкой при ошибке |
| `doc/infrastructure/overview.md` | Добавлена таблица Nubes endpoints (API vs UI) |
| `doc/errors/log.md` | Задокументированы 3 новые ошибки |
### Статус
- ✅ Провайдер nubes работает с правильным endpoint
- ✅ PG пересоздан и managed через terraform
- ⏳ Стресс-тест (stress_destroy_apply.sh) — в процессе, выявлен баг платформы
- 📋 TODO: убрать `sless_function`, встроить в `sless_job`
---
## 2026-03-20 — Архитектурный рефакторинг: sless_function + sless_service (ветка feat/function-service-split)
### Цель
Разделить единый тип `sless_function` на два независимых:
- `sless_function`**oneshot** (запускается один раз через Kubernetes Job, нет постоянного URL)
- `sless_service`**long-running** (постоянный Deployment, Ingress, встроенный URL без sless_trigger)
Мотивация: устранить архитектурное несоответствие — функции с постоянным HTTP URL не должны требовать отдельного sless_trigger.
### Изменения в operator (Go)
| Файл | Что сделано |
|------|-------------|
| `api/v1alpha1/service_types.go` | Новый CRD-тип `Service` с фазами Pending/Building/Ready/Failed, поле `URL` в статусе |
| `api/v1alpha1/zz_generated.deepcopy.go` | DeepCopy методы для Service/ServiceList/ServiceSpec/ServiceStatus |
| `controllers/service_controller.go` | Полный `ServiceReconciler`: kaniko build → Deployment → k8s Service → Ingress → Status.URL |
| `controllers/function_controller.go` | Удалены мёртвые методы: `ensureDeployment`, `buildDeployment`, `ensureRegistrySecret`. Function = только oneshot (Job). |
| `internal/api/handler/services.go` | CRUD handlers: ListServices, CreateService, GetService, UpdateService, DeleteService, UploadServiceCode |
| `internal/api/handler/invoke.go` | Dual-mode invoke: сначала проверяет Service CRD (proxy к Deployment), затем Function CRD (FunctionJob poll) |
| `internal/api/router.go` | 6 новых маршрутов `/namespaces/{ns}/services/...` |
| `main.go` | Регистрация ServiceReconciler |
### Изменения в Terraform-провайдере
| Файл | Что сделано |
|------|-------------|
| `terraform/provider/internal/client/client.go` | `ServiceRequest`, `ServiceResponse`, `CreateService/Get/Update/Delete`, `UploadServiceCode`, `WaitServiceReady` |
| `terraform/provider/internal/resources/service_resource.go` | Новый ресурс `sless_service` с полями name/runtime/entrypoint/memory_mb/timeout_sec/env_vars/source_dir/url (computed) |
| `terraform/provider/internal/provider/provider.go` | `NewServiceResource` добавлен в список ресурсов |
### Изменения в примерах (examples/POSTGRES/)
| Файл | Что сделано |
|------|-------------|
| `resources.tf` | Разбит на два файла; содержит только комментарий-указатель |
| `postgres.tf` | Managed PostgreSQL ресурсы: `locals`, `nubes_postgres`, `nubes_postgres_user`, `nubes_postgres_database` |
| `functions.tf` | Sless ресурсы: `sless_function` (только postgres_sql_runner_create_table + stress-*), `sless_service` (pg-info, pg-table-reader, pg-table-writer), `sless_job`; все `sless_trigger` блоки удалены |
### Изменения в deployments/k8s/
| Файл | Что сделано |
|------|-------------|
| `operator.yaml` | Обновлён image `v0.1.33``v0.1.41` |
| `rbac.yaml` | Добавлен `services` в rules группы `sless.kube5s.ru` (resources + status + finalizers) |
### Полная миграция terraform state
```
# Удалено из state (миграция Function → Service):
terraform state rm sless_function.pg_info ✅
terraform state rm sless_trigger.pg_info_http ✅
terraform state rm sless_function.postgres_table_reader ✅
terraform state rm sless_trigger.postgres_table_reader_http ✅
terraform state rm sless_function.postgres_table_writer ✅
terraform state rm sless_trigger.postgres_table_writer_http ✅
# Дополнительно удалено 9 sless_trigger для stress-* функций:
terraform state rm sless_trigger.stress_bigloop_http ✅
terraform state rm sless_trigger.stress_divzero_http ✅
terraform state rm sless_trigger.stress_go_fast_http ✅
terraform state rm sless_trigger.stress_go_nil_http ✅
terraform state rm sless_trigger.stress_go_pgstorm_http ✅
terraform state rm sless_trigger.stress_js_async_http ✅
terraform state rm sless_trigger.stress_js_badenv_http ✅
terraform state rm sless_trigger.stress_slow_http ✅
terraform state rm sless_trigger.stress_writer_http ✅
```
### RBAC fix (2026-03-20)
ClusterRole `sless-operator` не содержала прав на новый CRD `services.sless.kube5s.ru`.
Причина: `rbac.yaml` создавался до введения `Service` CRD; `controller-gen rbac` не запускался перед деплоем v0.1.41.
Исправление:
```bash
kubectl patch clusterrole sless-operator --type=json -p='[
{"op":"add","path":"/rules/0/resources/-","value":"services"},
{"op":"add","path":"/rules/1/resources/-","value":"services/status"},
{"op":"add","path":"/rules/2/resources/-","value":"services/finalizers"}
]'
```
После патча `terraform apply` завершился успешно. Файл `deployments/k8s/rbac.yaml` синхронизирован.
### Статус задач
| # | Задача | Статус |
|---|--------|--------|
| 1 | Создать `service_types.go` + DeepCopy | ✅ |
| 2 | `service_controller.go` с полным lifecycle | ✅ |
| 3 | Очистить `function_controller.go` от мёртвых методов | ✅ |
| 4 | API handler `services.go` | ✅ |
| 5 | Dual-mode `invoke.go` | ✅ |
| 6 | `router.go` + 6 маршрутов | ✅ |
| 7 | `main.go` + ServiceReconciler | ✅ |
| 8 | Terraform client + service_resource.go + provider.go | ✅ |
| 9 | Разбить `resources.tf``postgres.tf` + `functions.tf` | ✅ |
| 10 | terraform state rm (15 ресурсов: 6 pg + 9 stress triggers) | ✅ |
| 11 | Удалить все `sless_trigger` блоки из `functions.tf` | ✅ |
| 12 | Новый провайдер скомпилирован на VM (v0.1.18 dev) | ✅ |
| 13 | Build + push operator image `v0.1.41` | ✅ |
| 14 | Apply CRD Service + deploy оператора v0.1.41 | ✅ |
| 15 | Исправить RBAC ClusterRole: добавить services.sless.kube5s.ru | ✅ |
| 16 | `terraform apply -target sless_service.*` | ✅ |
| 17 | Удалить старые Function объекты pg-info/reader/writer из k8s | ✅ |
| 18 | Smoke test curl (pg-info, pg-table-reader, pg-table-writer) | ✅ |
| 19 | Синхронизировать `deployments/k8s/rbac.yaml` с кластером | ✅ |
| 20 | Commit + push | ✅ |
### Итоговое состояние кластера (sless-ffd1f598c169b0ae)
```
service.sless.kube5s.ru/pg-info nodejs20 Ready https://sless.kube5s.ru/fn/.../pg-info
service.sless.kube5s.ru/pg-table-reader python3.11 Ready https://sless.kube5s.ru/fn/.../pg-table-reader
service.sless.kube5s.ru/pg-table-writer python3.11 Ready https://sless.kube5s.ru/fn/.../pg-table-writer
function.sless.kube5s.ru/pg-create-table-runner python3.11 Ready
function.sless.kube5s.ru/stress-bigloop python3.11 Ready
... (8 stress functions)
```
Старые `function.sless.kube5s.ru/pg-info`, `pg-table-reader`, `pg-table-writer` — удалены.
---
## 2026-03-19 — Go runtime v0.1.1: pgx/v5 + stress-go-pgstorm ✅ ЗАВЕРШЕНО
### Цель
Новая функция `stress-go-pgstorm` — Go код со 100 горутинами, которые 10 минут
долбят PostgreSQL напрямую через `pgxpool`. Проверяем: Go runtime под нагрузкой,
connection pool под конкурентными запросами, устойчивость кластера.
**Попутно выявлены и исправлены два системных бага:**
- Хардкодный таймаут 30s в invoke.go (прокси оператора)
- Отсутствие proxy-read-timeout на nginx ingress sless-operator
### Задачи
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | `runtimes/go1.23/go.mod` — добавить `pgx/v5 v5.7.2` | ✅ | go mod tidy на remote, go.sum сгенерирован (28 строк) |
| 2 | `runtimes/go1.23/Dockerfile``go mod download` кешируем deps | ✅ | Один stage `golang:1.23-alpine`, COPY go.mod+go.sum перед server.go |
| 3 | Собрать `naeel/sless-runtime-go1.23:v0.1.1`, запушить | ✅ | docker build + push, sha256 verified |
| 4 | `internal/builder/context.go` — тег `go1.23` v0.1.0 → v0.1.1 | ✅ | строка `return "naeel/sless-runtime-go1.23:v0.1.1", nil` |
| 5 | `internal/api/handler/invoke.go` — динамический таймаут | ✅ | Таймаут = Function.Spec.TimeoutSec + 5s (был хардкод 30s) |
| 6 | Пересобрать и задеплоить оператор `v0.1.40` | ✅ | rodlout complete |
| 7 | `deployments/k8s/operator.yaml` — nginx proxy-read-timeout=900s | ✅ | kubectl annotate + манифест обновлён |
| 8 | `code/stress-go-pgstorm/handler.go` — горутины + pgxpool | ✅ | 100 горутин, INSERT/COUNT/MAX, параметры workers/duration_sec/max_delay_ms |
| 9 | `resources.tf` — новая функция + trigger | ✅ | timeout_sec=700, memory_mb=256, все PG env vars |
| 10 | terraform apply — 2 ресурса добавлено | ✅ | apply complete: 2 added |
| 11 | Запуск 10-минутного стресс-теста | ✅ | workers=100, duration_sec=600, err_ops=0, ok_ops=45918, ops_per_sec=76.5 — PostgreSQL выдержал |
| 12 | Коммит d7fda15 | ✅ | feat: Go runtime v0.1.1 (pgx/v5)... |
### Архитектура функции stress-go-pgstorm
```
Handle(event) → запускает N горутин (default 100)
каждая горутина в цикле duration_sec (default 600):
- случайная задержка 0-300ms (max_delay_ms)
- чередующиеся операции: INSERT / SELECT COUNT / SELECT MAX
- считает ok/err атомарно через sync/atomic
WaitGroup.Wait() → возвращает итог
возвращает: {runtime, version, workers, duration_sec, elapsed_sec,
total_ops, ok_ops, err_ops, ops_per_sec}
pgxpool.New() — connection pool, MaxConns=20
env: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
```
### Цепочка таймаутов (после фиксов)
| Слой | Таймаут | Где задаётся |
|------|---------|--------------|
| curl --max-time | 720s | клиент |
| nginx ingress proxy-read-timeout | 900s | аннотация ingress sless-operator |
| operator http.Client (invoke.go) | TimeoutSec+5s = 705s | Function.Spec.TimeoutSec=700 |
| function runtime (Go) | duration_sec = 600s | параметр в JSON body |
### Что было сломано до этой сессии
1. invoke.go: `var httpClient = &http.Client{Timeout: 30 * time.Second}` — хардкод в строке 24
2. nginx ingress sless-operator: аннотации proxy-read-timeout не было → nginx дефолт 60s → 504
## 2026-03-19 — Tests 3-7: E2E прогон POSTGRES + 8 стресс-функций
### Tests 3-7
| Тест | Действие | Результат |
|------|----------|-----------|
| Test 3 | `table_rw.py`: добавлен `version: v2-with-hostname`, `host: socket.gethostname()` | ✅ Terraform apply, функция пересобрана |
| Test 4 | `pg_info.js`: добавлен `code_version: v2-agent-test` | ✅ code_hash изменился, вернул `{"code_version":"v2-agent-test"}` |
| Test 5 | Удалить trigger → 404 → пересоздать → работает | ✅ |
| Test 6 | Удалить function + trigger → 404 → пересоздать (rebuild 46s) → работает | ✅ |
| Test 7 | Новая функция `pg-stats` (Python): version, total_rows → протестирована → удалена | ✅ `{"version":"v1-test7","total_rows":3}` |
### 8 стресс-функций (коммит `014b99e`)
Задеплоены и прогнаны дважды через `stress_test.sh` (3 раунда).
После crash-тестов все 8 подов: `Running`, 0 restarts.
| # | Функция | Runtime | Что проверяет | Результат |
|---|---------|---------|---------------|-----------|
| 1 | `stress-slow` | Python | sleep 3-N сек | ✅ `{"slept_sec":3}` |
| 2 | `stress-bigloop` | Python | CPU n=2M | ✅ 0.31s |
| 3 | `stress-divzero` | Python | ZeroDivisionError crash | ✅ pod restart → при d=7: `{"result":6.0}` |
| 4 | `stress-writer` | Python | batch INSERT в PG | ✅ +3/+10 строк |
| 5 | `stress-go-fast` | Go | factorial+fib без deps | ✅ factorial(20), fib(20) |
| 6 | `stress-go-nil` | Go | nil pointer panic | ✅ pod restart → при crash=false: ok |
| 7 | `stress-js-async` | NodeJS | 3 PG запроса Promise.all | ✅ version/count/max_id |
| 8 | `stress-js-badenv` | NodeJS | TypeError (pod жив, HTTP 500) | ✅ нет restart |
**Итого в таблице после двух прогонов: 32 строки.**
### Поведение crash-функций
- `stress-divzero` / `stress-go-nil`: **pod restart** при краше (EOF на первый запрос после падения) — нормальное k8s поведение, runtime процесс умирает целиком
- `stress-js-badenv`: **HTTP 500 без restart** — NodeJS поймал TypeError внутри async handler, pod остался жив
- Это различие задокументировано: Python/Go crash = process exit, NodeJS crash = unhandled rejection в async = 500
### git
- Бинарник `event-dispatcher` (50MB) удалён из истории через `git reset --soft HEAD~2`
- Добавлен в `.gitignore`
- Force push: `d879817``014b99e`
---
## 2026-03-19 — event-trigger refactor (ветка feat/event-trigger-refactor)
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Аудит кластера — удаление мусора | ✅ | Удалены: event-monitor/writer/cleaner, pg-* тестовые функции, failed build jobs, orphan namespaces b1e4df2d/b794a3c4 |
| 2 | Managed RabbitMQ через Terraform | ✅ | `terraform/RABBIT/`, ns `1dbfe9da-...`, host: `rabbitmqk8s.1dbfe9da-...svc.cluster.local` |
| 3 | Архитектура event-trigger задокументирована | ✅ | `doc/decisions/log.md` — выбран Вариант A (отдельный event-dispatcher) |
| 4 | trigger_types.go: +TriggerTypeEvent, +Queue | ✅ | |
| 5 | internal/config: +RabbitMQURL | ✅ | |
| 6 | services/event-dispatcher/ | ✅ | Go-сервис: dispatcher.go, watcher.go, main.go |
| 7 | controllers/trigger_controller.go: +reconcileEvent | ✅ | Создаёт Service, обновляет status |
| 8 | deployments/k8s/event-dispatcher.yaml | ✅ | ServiceAccount + ClusterRole + Deployment |
| 9 | Образ naeel/sless-event-dispatcher:v0.1.0 | ✅ | Собран, запушен в DockerHub |
| 10 | RABBITMQ_URL в sless-operator-secret | ✅ | managed rabbit (1dbfe9da namespace) |
| 11 | event-dispatcher задеплоен в кластер | ✅ | Running в sless namespace |
## 2026-03-19 — pg-table-writer HTML + bugfix invoke.go Content-Length (оператор v0.1.37)
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Python runtime v0.1.4: `text/html` поддержка | ✅ | `server.py`: str начинается с `<``text/html; charset=utf-8`; добавлен `_accept` в event |
| 2 | `internal/builder/context.go`: python runtime → v0.1.4 | ✅ | |
| 3 | Образ `naeel/sless-runtime-python3.11:v0.1.4` | ✅ | Собран и запушен |
| 4 | `code/table-rw/table_rw.py` | ✅ | Комбинированный reader+writer. `list_rows` — JSON. `add_row`: GET → HTML форма, POST form → INSERT + HTML ответ. `_render_page` шаблон с тёмной темой |
| 5 | `examples/POSTGRES/resources.tf` | ✅ | reader: `source_dir=code/table-rw`, `entrypoint=table_rw.list_rows`. Новые: `sless_function.postgres_table_writer`, `sless_trigger.postgres_table_writer_http`, `output.table_writer_url` |
| 6 | Удалена папка `code/table-reader/` | ✅ | |
| 7 | `internal/builder/builder.go`: убран `--cache=true` | ✅ | kaniko по умолчанию не кэширует слои |
| 8 | Оператор v0.1.35 | ✅ (промежуточный) | |
| 9 | Оператор v0.1.36 | ✅ | Убран ошибочный `--no-cache` (kaniko v1.24.0 не поддерживает этот флаг) |
| 10 | Оператор v0.1.37 | ✅ | **Bugfix `invoke.go`**: добавлен `proxyReq.ContentLength = r.ContentLength`. Без этого Python-сервер читал тело как 0 байт (бесконечно chunked) — форма не парсилась |
| 11 | POST через прокси | ✅ | `curl POST title=test-proxy-fix` → HTML с «Добавлено: «test-proxy-fix»», строка в таблице |
### Root cause бага формы
`internal/api/handler/invoke.go`: при проксировании POST-запроса не устанавливался `Content-Length`.
Go `http.Client` отправлял запрос без `Content-Length` → Python `BaseHTTPRequestHandler.headers.get("Content-Length", 0)` = 0 → тело не читалось → `title` пустой → JSON ошибка.
Фикс: одна строка `proxyReq.ContentLength = r.ContentLength`.
### Образы
| Образ | Версия | Что изменилось |
|-------|--------|----------------|
| `naeel/sless-operator` | v0.1.37 | `invoke.go`: `proxyReq.ContentLength = r.ContentLength` |
| `naeel/sless-runtime-python3.11` | v0.1.4 | `text/html` support, `_accept` in event |
---
## 2026-03-18 — web-консоль (оператор v0.1.34 + funcs-service v0.2.0)
## 2026-03-18 — web-консоль (оператор v0.1.34 + funcs-service v0.2.0)
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Оператор: `GET /v1/.../functions/{name}/source` | ✅ | `internal/api/handler/source.go` — скачивает tar.gz контекст из S3; извлекает пользовательские файлы (без Dockerfile); возвращает `[{name, content, binary}]` |
| 2 | Оператор: `PATCH /v1/.../triggers/{name}` | ✅ | Уже существовал в v0.1.33 — Handler обрабатывает поле `enabled` |
| 3 | `internal/api/router.go` — маршрут /source | ✅ | `GET /namespaces/{ns}/functions/{name}/source → h.GetSource` |
| 4 | `services/funcs/index.html` — HTML web-консоль | ✅ | Тёмная тема, аккордеоны, highlight.js, кнопки ▶/■ для enable/disable триггеров |
| 5 | `services/funcs/main.go` — HTML режим + proxy endpoints | ✅ | `Accept: text/html` → HTML; `GET /funcs/{ns}/source/{fn}` и `PATCH /funcs/{ns}/triggers/{name}` — прокси к оператору через SLESS_SERVICE_TOKEN |
| 6 | Backward compat: plain text | ✅ | curl без `Accept: text/html` — старое поведение v0.1.x |
| 7 | Оператор v0.1.34 | ✅ | Build+push+deploy в кластер |
| 8 | funcs-service v0.2.0 | ✅ | Build+push+deploy в кластер |
| 9 | Коммит | ✅ | `bf9f073``feat: web-console — HTML UI + source viewer + trigger toggle` |
### Текущие URL
```
https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae # браузер → HTML консоль
# curl → plain text (backward compat)
https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae/source/pg-info # JSON файлы функции
https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae/pg-info # вызов функции
```
### Образы
| Образ | Версия | Что изменилось |
|-------|--------|----------------|
| `naeel/sless-operator` | v0.1.34 | GET /source endpoint, HTML web-консоль proxy routes |
| `naeel/sless-funcs-service` | v0.2.0 | HTML консоль + source/trigger proxy endpoints |
---
## 2026-03-18 — FunctionJob bugfix (functionjob label + stderr capture)
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | `functionjob_controller.go`: label на PodTemplate | ✅ | Добавлен `PodTemplate.ObjectMeta.Labels: {managed-by, functionjob, function}` — k8s 1.27+ удалил встроенный `job-name=` лейбл |
| 2 | `getJobPodOutput` по `labelSelector` | ✅ | Принимает `"functionjob=<name>"` вместо `jobName`; используется для успешного и упавшего job |
| 3 | Захват stderr при ошибке Job | ✅ | При `job.Status.Failed > 0` — берём `podOutput`; если непустой — пишем в `status.Message` (truncate до 2000 символов) |
| 4 | `truncateForStatus()` helper | ✅ | Ограничивает длину `status.Message` для безопасной записи в k8s |
| 5 | terraform client: `ErrJobAlreadyExists` | ✅ | `client.go`: 409 Conflict → `ErrJobAlreadyExists`; `job_resource.go`: при конфликте создания — читаем существующий job вместо ошибки |
---
## 2026-03-18 — Python runtime str→text/plain
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | `runtimes/python3.11/server.py` | ✅ | Если функция вернула `str``Content-Type: text/plain`, тело без `json.dumps`. Dict/list остаются `application/json` |
| 2 | `internal/builder/context.go` | ✅ | Python runtime базовый образ → `v0.1.3` |
| 3 | Образ `naeel/sless-runtime-python3.11:v0.1.3` | ✅ | Собран и задеплоен |
---
## 2026-03-18 — funcs: глобальный Go сервис + ветка web-console
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Python runtime str→text/plain | ✅ | `runtimes/python3.11/server.py`: если функция возвращает `str``Content-Type: text/plain`. Образ `naeel/sless-runtime-python3.11:v0.1.3` |
| 2 | funcs_list.py: plain text вывод | ✅ | Переписан с JSON на plain text с `─` разделителями, фильтр `SLESS_EXCLUDE` |
| 3 | Оператор v0.1.33 | ✅ | context.go: python runtime → v0.1.3; `internal/builder/context.go` обновлён |
| 4 | `funcs` как глобальный Go сервис | ✅ | `services/funcs/main.go` — standalone Go HTTP сервер. Деплоится ОДИН РАЗ в namespace `sless`, работает для ВСЕХ пользователей. Образ `naeel/sless-funcs-service:v0.1.3` |
| 5 | URL без токена | ✅ | `https://sless.kube5s.ru/funcs/<namespace>` — открывается в браузере без токена. `SLESS_SERVICE_TOKEN` задан через `kubectl set env` |
| 6 | Удалить funcs из terraform | ✅ | `examples/POSTGRES/resources.tf`: удалены `sless_function.funcs_list`, `sless_trigger.funcs_list_http`, `output "funcs_url"`, `local.user_namespace`. `terraform apply` — 2 destroyed |
| 7 | Ветка feat/web-console | ✅ | Создана от `feat/harbor-integration` на remote, запушена |
| 8 | Документация архитектуры хранения кода | ✅ | См. `doc/decisions/log.md` раздел "Хранение кода функций и web-консоль" |
| 9 | Коммиты | ✅ | `e8cd62e` (funcs Go service), `38bb494` (v0.1.3 — /funcs/namespace без токена) |
### Текущие URL функций
```
https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae # список функций (без токена)
https://sless.kube5s.ru/funcs?token=<jwt> # то же, через токен
https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae/pg-info
https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae/pg-table-reader
```
### Образы
| Образ | Версия | Что изменилось |
|-------|--------|----------------|
| `naeel/sless-operator` | v0.1.33 | created_at, last_built_at в API; python runtime v0.1.3 |
| `naeel/sless-runtime-python3.11` | v0.1.3 | str → text/plain в server.py |
| `naeel/sless-funcs-service` | v0.1.3 | /funcs/<namespace> без токена, /health probe |
---
## 2026-03-18 — Следующие шаги: web-консоль (ветка feat/web-console)
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Оператор: `GET /v1/namespaces/{ns}/functions/{name}/source` | ✅ | Реализовано — `bf9f073` |
| 2 | Оператор: `PATCH /v1/namespaces/{ns}/triggers/{name}` | ✅ | Существовал — проверено, поддерживает `enabled` |
| 3 | `sless-funcs-service`: HTML страница | ✅ | `Accept: text/html` → HTML консоль. Аккордеоны + highlight.js + кнопки ▶/■ |
| 4 | Bump оператора до v0.1.34 | ✅ | Задеплоен |
| 5 | Bump funcs-service до v0.2.0 | ✅ | Задеплоен |
---
## 2026-03-18 12:00 — DNS инцидент устранён, POSTGRES E2E PASS
## 2026-03-18 — DNS инцидент: sless-api.kube5s.ru → мёртвый кластер
| # | Задача | Статус | Заметки |
|---|--------|--------|----------|
| 1 | Диагностика TLS handshake timeout при `terraform apply` POSTGRES | ✅ | `sless-api.kube5s.ru``5.172.178.182` (старый кластер). Порт 443 принимал TCP, но TLS не завершался — Go HTTP клиент висел бесконечно |
| 2 | Подтверждение нового кластера | ✅ | `kubectl get nodes` в контексте `tazetdinovn@gmail.com@naeel-test-3` → 3 ноды Ready. Ingress IP `185.247.187.147`. DNS `sless.kube5s.ru` уже указывал на него |
| 3 | Обновить Ingress в кластере | ✅ | `kubectl patch ingress sless-operator -n sless`: host + TLS → `sless.kube5s.ru`, secretName → `sless-operator-tls-sless` |
| 4 | Обновить ConfigMap оператора | ✅ | `EXTERNAL_URL: https://sless.kube5s.ru` (было `https://sless-api.kube5s.ru`) |
| 5 | Let's Encrypt сертификат | ✅ | `certificate/sless-operator-tls-sless` READY=True, order valid — выдан сразу после смены ingress host (ACME HTTP-01 challenge на новом IP прошёл) |
| 6 | Заменить `sless-api.kube5s.ru``sless.kube5s.ru` во всех examples + deployments | ✅ | `POSTGRES/`, `hello-go/`, `hello-node/`, `simple-node/`, `simple-python/`, `notes-python/`, `pg-list-python/`, `demo-managed-functions/`, `README.md`, `deployments/k8s/operator.yaml` |
| 7 | POSTGRES `terraform apply` с функцией и job | ✅ PASS | `sless_function` `pg-create-table-runner` — создан за 55s. `sless_job` `pg-create-table-job-main-v13` — создан за 20s. `Apply complete! Resources: 2 added` |
| 8 | Коммит и push | ✅ | `cca3a8c``fix: migrate sless endpoint from sless-api.kube5s.ru to sless.kube5s.ru` |
---
## 2026-03-17 — E2E тесты всех примеров на тест-стенде
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Исправить `nubes_endpoint` во всех examples (prod→test) | ✅ | `deck-api.ngcloud.ru``deck-api-test.ngcloud.ru/api/v1` в hello-node, hello-go, simple-node, notes-python, pg-list-python, demo-managed-functions |
| 2 | Исправить `sless_endpoint` в demo-managed-functions | ✅ | `185.247.187.147.nip.io``https://sless-api.kube5s.ru` |
| 3 | Postgres для тестов | ✅ | `postgres.sless.svc.cluster.local:5432`, user=`sless`, pass=`sless-pg-password`, db=`sless`. Уже запущен в кластере |
| 4 | `hello-node` E2E | ✅ PASS | init → apply (4 ресурса) → modify (memory_mb 128→256, env_var) → apply → destroy |
| 5 | `simple-python` E2E | ✅ PASS | init → apply → modify (memory_mb 64→96) → apply → destroy. `job_result = {"time": "..."}` |
| 6 | `simple-node` E2E | ✅ PASS | Аналогично simple-python но nodejs20 |
| 7 | `notes-python` E2E | ✅ PASS | 7 ресурсов, PostgreSQL init job: `{"ok": true, "executed": 1}`, destroy complete |
| 8 | Коммит на remote | ✅ | `a768454` в `/home/naeel/terra/sless/examples` |
| 9 | POSTGRES example | 🔄 | Следующий шаг — вернуться к POSTGRES после прохождения всех тестов |
---
## 2026-03-17 — v0.1.31: compile fix + POSTGRES example end-to-end
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Баг 3: `PodLogOptions{Stdout/Stderr}` compile error | ✅ | Убраны несуществующие поля. `getJobPodOutput` в `functionjob_controller.go` |
| 2 | Сборка `naeel/sless-operator:v0.1.31` | ✅ | Собрано на `naeel@5.172.178.213`, запушено в Docker Hub |
| 3 | Деплой v0.1.31 в кластер | ✅ | `kubectl rollout status` → success. Pod `sless-operator-545556c59d-n8qv9` |
| 4 | `simple-python` — сквозной тест на тест-стенде | ✅ | `terraform apply` на remote: `Apply complete! Resources: 3 added`. `job_result = {"time": "..."}` |
| 5 | Найдена главная причина падения POSTGRES | ✅ | `nubes_endpoint` в `provider "sless"` указывал на **прод** `deck-api.ngcloud.ru` |
| 6 | Исправлен `nubes_endpoint` в POSTGRES/main.tf | ✅ | Теперь `deck-api-test.ngcloud.ru/api/v1` |
| 7 | Исправлен `nubes_endpoint` в simple-python/main.tf | ✅ | Аналогично |
| 8 | End-to-end `terraform apply` для POSTGRES | 🔄 | Следующий шаг: синхронизировать на remote и запустить `terraform apply` |
---
## 2026-03-17 — POSTGRES example: внешний managed PG, FunctionJob и реальные блокеры
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Перевод `examples/POSTGRES` на `PGSSLMODE=require` | ✅ | Для managed PostgreSQL `allow_no_s_s_l = false`; `disable` был некорректен |
| 2 | Подтверждение root-cause `password authentication failed` | ✅ | Из runtime namespace поднят диагностический pod того же image: `psycopg2.OperationalError: FATAL: password authentication failed for user "u-user0"` |
| 3 | Проверка источника пароля | ✅ | `nubes_postgres.npg.vault_secrets["users"]` не совпадает с фактическим паролем пользователя в кластере managed PostgreSQL; authoritative source сейчас — k8s secret `u-user0.postgresqlk8s.credentials.postgresql.acid.zalan.do` |
| 4 | Попытка читать пароль через `hashicorp/external` | ❌ | В текущем окружении доступен только кастомный registry; `registry.terraform.io/hashicorp/external` не устанавливается |
| 5 | One-command `terraform apply` для `examples/POSTGRES` | 🔄 | Остаточный баг локализован в связке credential-source + FunctionJob observability. После переключения на актуальный пароль `sless_job` всё ещё падает, но актуальные pod logs реального job теряются из-за TTL/слабой наблюдаемости |
| 6 | Необходимый следующий технический шаг | 🔄 | Либо добавить в проект доступный data-source/провайдер для чтения k8s secret внутри apply, либо починить источник пароля в nubes-потоке, чтобы `vault_secrets` и реальный Postgres user password совпадали |
## 2026-03-17 — Fix: `examples/POSTGRES` / `sless_job` 409 + диагностика FunctionJob
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | Идемпотентность `sless_job` при `409 job already exists` | ✅ | `terraform/provider/internal/client/client.go`, `terraform/provider/internal/resources/job_resource.go`: добавлен typed-error `ErrJobAlreadyExists`, в Create при 409 читается существующий Job и продолжается ожидание/синхронизация вместо немедленного падения |
| 2 | Диагностика падений FunctionJob | ✅ | `controllers/functionjob_controller.go`: labels добавлены в `PodTemplate`, fail-message теперь включает output pod-а (обрезка до 2000), fallback-команда логов по `job-name` |
| 3 | Root-cause для `examples/POSTGRES` | ✅ | Рабочие `examples/notes-python` используют внутренний DSN (`postgres.sless.svc`), а `examples/POSTGRES` работает через managed Nubes Postgres + динамические креды; критичный UX-дефект был в повторном Create после failed apply |
## Статусы: ✅ готово | 🔄 в процессе | ⏳ не начато
---
## 2026-03-10 — Архитектурный разбор и handoff для следующего агента
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | GPT-5.4: Подробный разбор кода и lifecycle issues | ✅ | `doc/architecture/agent-handoff-2026-03-10.md` — детальный code review, event model problems, invocation history gap, приоритеты |
| 2 | Claude Sonnet 4.6: Review + security roadmap | ✅ | `doc/architecture/sonnet-review-of-gpt-analysis.md` — где GPT-5.4 прав, что пропустил (gVisor, LLM validation, NetworkPolicy), production security roadmap |
| 3 | Claude Opus 4.6: Прагматичный обзор для небольшого провайдера | ✅ | `doc/architecture/opus-pragmatic-review-2026-03-10.md` — анализ с учётом масштаба, конкретный план по этапам, разбор где коллеги перемудрили |
| 4 | Claude Opus 4.6: Дизайн LLM-валидации кода | ✅ | `doc/decisions/log.md` → "2026-03-10 — LLM-валидация кода при upload" — архитектура, интерфейс, prompt, план файлов |
---
## v1 — Базовый сервис
### Код
| # | Компонент | Статус | Заметки |
|---|-----------|--------|---------|
| 1 | Структура проекта, operator-sdk init | ✅ | module: gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless |
| 2 | CRD types: Function, Trigger | ✅ | api/v1alpha1/, zz_generated.deepcopy.go |
| 3 | internal/config | ✅ | config.go, Load() из env vars, добавлены IngressHost + APIToken |
| 4 | Миграции PostgreSQL | ✅ | migrations/001_initial.sql — таблица invocations |
| 5 | internal/storage/postgres | ✅ | store.go: SaveInvocation, ListInvocations, RunMigrations |
| 6 | internal/storage/s3 | ✅ | client.go: Upload, Download, Delete, EnsureBucket (Ceph S3) |
| 7 | internal/builder | ✅ | builder.go: kaniko Jobs для сборки образов функций |
| 8 | controllers/function_controller.go | ✅ | Pending→Building(kaniko)→Ready/Failed, Deployment, finalizer; handleDeletion удаляет Service+Ingress |
| 9 | controllers/trigger_controller.go | ✅ | HTTP→Service+Ingress (fallback) или прокси через ExternalURL, Cron→CronJob, finalizer; handleTriggerDeletion удаляет Service+Ingress |
| 10 | internal/api/router.go | ✅ | gorilla/mux, /v1/namespaces/{ns}/... (auth), /fn/{ns}/{name} (публичный прокси) |
| 11 | internal/api/handler/ | ✅ | functions.go, triggers.go, invocations.go, invoke.go (proxy — no such host → 404), handler.go (slog) |
| 12 | internal/api/middleware/ | ✅ | auth.go (Bearer token), logging.go (slog) |
| 13 | main.go | ✅ | operator manager + HTTP сервер параллельно, wire всех зависимостей |
### Кластер (namespace: sless)
| # | Ресурс | Статус | Заметки |
|---|--------|--------|---------|
| 1 | namespace sless | ✅ | создан |
| 2 | CRD functions.sless.kube5s.ru | ✅ | установлен из config/crd/bases/ |
| 3 | CRD triggers.sless.kube5s.ru | ✅ | установлен из config/crd/bases/ |
| 4 | ServiceAccount sless-operator | ✅ | deployments/k8s/rbac.yaml |
| 5 | ClusterRole + ClusterRoleBinding | ✅ | deployments/k8s/rbac.yaml |
| 6 | PostgreSQL Deployment + Service | ✅ | deployments/k8s/postgres.yaml, Running |
| 7 | Secret postgres-secret | ✅ | в namespace sless |
### Runtime image + Upload endpoint (2026-03-07)
| # | Компонент | Статус | Заметки |
|---|-----------|--------|---------|
| 1 | runtimes/python3.11/server.py | ✅ | HTTP wrapper :8080, loads /app/function/handler.py |
| 2 | runtimes/python3.11/Dockerfile | ✅ | FROM python:3.11-slim, naeel/sless-runtime-python3.11:v0.1.0 |
| 3 | Secret sless-registry-auth | ✅ | DockerHub creds для kaniko в namespace sless |
| 4 | internal/api/handler/upload.go | ✅ | POST /v1/namespaces/{ns}/functions/{name}/upload |
| 5 | Fix: kaniko S3 endpoint | ✅ | https:// + S3_FORCE_PATH_STYLE=true (Ceph) |
| 6 | Fix: бесконечный цикл сборки | ✅ | idempotency guard через аннотацию last-built-s3key |
### E2E тест (2026-03-07)
| # | Шаг | Результат |
|---|-----|-----------|
| 1 | Запуск оператора локально | ✅ `source hack/local.env && go run main.go` |
| 2 | Создание функции hello + upload handler.zip | ✅ kaniko собрал naeel/sless-default-hello:latest |
| 3 | Function phase=Ready, Deployment Running | ✅ namespace sless-fn-default |
| 4 | `GET /health` | ✅ `{"status": "ok"}` |
| 5 | `POST / {"name":"Naeel"}` | ✅ `{"message": "Hello, Naeel!"}` |
| 6 | terraform apply (pg-query python3.11) | ✅ `{"invocations": [], "count": 0}` через https://sless-api.kube5s.ru |
| 7 | hello-node (nodejs20) E2E | ✅ | `{"message": "Hello, Naeel! (nodejs20)"}` via https://sless-api.kube5s.ru/fn/default/hello-node |
### Terraform провайдер (2026-03-07)
| # | Компонент | Статус | Заметки |
|---|-----------|--------|---------|
| 1 | terraform/provider/ — независимый Go-модуль | ✅ | module: terraform-provider-sless |
| 2 | internal/client/client.go | ✅ | HTTP-клиент sless API, готов к переносу в nubes |
| 3 | internal/provider/provider.go | ✅ | паттерн идентичен nubes NubesProvider |
| 4 | sless_function resource | ✅ | CRUD + upload zip + WaitReady (kaniko) |
| 5 | sless_trigger resource | ✅ | CRUD (http/cron); Delete ждёт исчезновения CR (WaitGone, 90с) |
| 6 | hack/build-and-publish.sh | ✅ | GPG-подпись + mc upload в S3 |
| 7 | Публикация в terra.k8c.ru | ✅ | v0.1.3, key CB3A0DF161ECC416 |
| 8 | terraform init | ✅ | Installed terra.k8c.ru/naeel/sless v0.1.3 |
| 9 | sless_job resource | ✅ | одноразовые запуски функции через FunctionJob CRD |
| 10 | build_timeout_sec / wait_timeout_sec | ✅ | настраиваемые таймауты в sless_function и sless_job |
**Примечание:** Код провайдера писался с прицелом на перенос в nubes провайдер.
`internal/client/``internal/core/` nubes, ресурсы → `internal/resources_gen/`.
### source_dir + fix destroy cleanup (2026-03-09)
| # | Компонент | Статус | Заметки |
|---|-----------|--------|----------|
| 1 | provider: source_dir атрибут | ✅ | zip в памяти, SHA256 автоматически; hashicorp/archive не нужен |
| 2 | fix: trigger_controller handleTriggerDeletion | ✅ | удаляет Service+Ingress из sless-fn-{ns} |
| 3 | fix: function_controller handleDeletion | ✅ | удаляет Service+Ingress из sless-fn-{ns} |
| 4 | fix: trigger_resource.go Delete WaitGone | ✅ | polling GetTrigger до 404, таймаут 90с |
| 5 | fix: invoke.go no such host → 404 | ✅ | после destroy DNS NXDOMAIN → 404 вместо 502 |
| 6 | E2E тест run_terraform_examples.sh | ✅ | все 4 примера: hello-node, simple-node, simple-python, notes-python |
### Что ещё не сделано
| # | Компонент | Статус | Заметки |
|---|-----------|--------|---------|
| 1 | terraform apply e2e — функция с postgres | ✅ | `{"invocations": [], "count": 0}` — функция подключилась к postgres.sless.svc, таблица пустая |
| 2 | HTTP trigger e2e тест | ✅ | прокси /fn/ через sless-api, без wildcard DNS |
| 3 | deployments/k8s/operator.yaml | ✅ | Deployment+Service+Ingress, naeel/sless-operator:v0.1.5, Running |
| 4 | Внешний доступ к API оператора | ✅ | https://sless-api.kube5s.ru → HTTP/2 200, TLS cert OK |
| 5 | DNS запись sless-api.kube5s.ru | ✅ | A → 5.172.178.182, TTL 3600, zone kube5s.ru |
| 6 | nodejs20 runtime | ✅ | runtimes/nodejs20/server.js + Dockerfile, naeel/sless-runtime-nodejs20:v0.1.0 |
| 7 | upload.go: nodejs20 + package.json | ✅ | runtimeBaseImage switch, hasPackageJSON → npm install |
| 8 | go.mod: direct/indirect fix | ✅ | go mod tidy — gorilla/mux, lib/pq, minio-go, k8s.io/api помечены direct |
| 9 | Runtime образы: версионированные теги | ✅ | python3.11:v0.1.0, nodejs20:v0.1.0 (убираем :latest) |
---
## v2 — Расширения
| # | Компонент | Статус | Заметки |
|---|-----------|--------|---------|
| 1 | RabbitMQ event triggers | ⏳ | |
| 0 | Изоляция пользователей по namespace | ⏳ | Каждый токен → свой k8s namespace. API: GET /v1/whoami → {namespace}. Провайдер получает ns при Configure(), юзер не видит. Сейчас: хардкод "default" для демо. |
| 2 | Облачный auth (nubes.ru token validation) | ⏳ | v1 — статический токен |
| 3 | Метрики → Victoria Metrics | ⏳ | |
| 4 | Managed PostgreSQL от провайдера | ⏳ | сейчас postgres в k8s |
| 5 | Pre-warm для cron триггеров | ⏳ | TriggerSpec.PreWarmSeconds — поле есть, логика не реализована |
| 11 | trigger.enabled | ✅ | enabled=false → Deployment replicas=0, in-place update через PATCH |
| 12 | job.run_id | ✅ | run_id=0 → skip, run_id>0 → execute. Повторный запуск = увеличить run_id |
---
## 2026-03-11 — Namespace-per-user + SoC рефакторинг
| # | Задача | Статус | Заметки |
|---|--------|--------|---------|
| 1 | JWT decode в провайдере (SubFromJWT, NamespaceFromSub) | ✅ | `terraform/provider/internal/client/client.go` |
| 2 | PingNubesAPI в provider.Configure() | ✅ | атрибут nubes_endpoint, env NUBES_ENDPOINT |
| 3 | JWT auth в операторе (validateJWT vs статический токен) | ✅ | `internal/api/middleware/auth.go` — sub + exp, без подписи |
| 4 | EnsureNamespace как отдельный endpoint | ✅ | `POST /v1/namespaces/{ns}/ensure`, `handler/namespace.go` |
| 5 | router.go: маршрут `/ensure` | ✅ | добавлен перед Functions CRUD |
| 6 | client.go провайдера: метод EnsureNamespace | ✅ | `terraform/provider/internal/client/client.go` |
| 7 | provider.Configure(): вызов EnsureNamespace | ✅ | namespace создаётся один раз при init |
| 8 | handler.go: убраны k8s-типы (SoC) | ✅ | только Handler struct + helpers |
| 9 | secrets/ исключены из git (.gitignore) | ✅ | токены не попадают в репу |
| 10 | E2E тест: apply + destroy | ✅ | namespace sless-cdd874dfa31ba6ca, все 4 ресурса |
| 11 | operator v0.1.21 задеплоен | ✅ | naeel/sless-operator:v0.1.21 |
| 12 | provider v0.1.13 опубликован | ✅ | terra.k8c.ru/naeel/sless v0.1.13 |
| 13 | commit + push feat/namespace-per-user | ✅ | a1774e1 |
**Текущая ветка:** feat/namespace-per-user
**Последний коммит:** a1774e1
---
## 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
---
## 2026-03-11 — operator v0.1.23: bug fixes + полный stress test PASS
### Исправленные баги
| # | Компонент | Баг | Исправление |
|---|-----------|-----|-------------|
| 1 | `run_stress_test.sh` | mod2 для simple-node/python: `patch_memory time-getter.tf 64→96``time-getter.tf` начинался с 96, не с 64 → "No changes" → assertion FAIL | Изменено на `96→128` |
| 2 | `internal/builder/builder.go` | `BackoffLimit=0` — при транзиентном сбое kaniko (OOM, network blip) сборка сразу считалась провалившейся без retry | `BackoffLimit=2` |
| 3 | `internal/api/handler/functions.go` | При BUILD FAIL: Function оставалась в API в статусе Failed, terraform её не добавлял в state → следующий apply → 409 "function already exists" → orphaned resource | На 409+phase=Failed: удаляем + пересоздаём |
### Результат стресс-теста (operator v0.1.23)
| Пример | Результат | Время |
|--------|-----------|-------|
| hello-node | **PASS** | 165s |
| simple-node | **PASS** | 250s |
| simple-python | **PASS** | 222s |
| notes-python | **PASS** | 112s |
| **ИТОГО** | **4/4 = 100%** | **749s** |
**Коммит:** `59563eb`
**Схема запуска:** агент запускает `run_stress_test.sh` на удалённом сервере `5.172.178.213` через SSH, получает логи из `/tmp/stress_test_run2.log`
---
## 2026-03-11 — E2E тесты через скрипт run_e2e_tests.sh
| Пример | init | apply | modify + apply | destroy | Итог |
|--------|------|-------|----------------|---------|------|
| hello-node | ✅ | ✅ | ✅ memory 128→256 MB | ✅ 4 destroyed | **PASS** |
| simple-node | ✅ | ✅ | ✅ memory 64→96 MB | ✅ 4 destroyed | **PASS** |
**Скрипт:** `run_e2e_tests.sh` в корне репы
**Возможности:** retry 3× при TLS timeout, emergency destroy trap, логи в `.e2e-logs/`
**Провайдер в примерах:** токен через `var.token` + `terraform.tfvars` (gitignored), version `~> 0.1.13`
**Наблюдения:**
- Периодические TLS handshake timeout на `sless-api.kube5s.ru` — сеть нестабильна, retry помогает
- `terra.k8c.ru` иногда даёт `unexpected EOF` при скачивании провайдера — то же, retry
- Namespace `sless-cdd874dfa31ba6ca` жив после всех destroy — поведение корректное
---
## Остаток технического долга (не блокирует)
| # | Что | Приоритет |
|---|-----|-----------|
| 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) |
| 7 | RabbitMQ event triggers | Низкий (v2) |
| 8 | Метрики → Victoria Metrics | Низкий |
---
## 2026-03-11 — Harbor integration + Go 1.23 runtime (v0.1.24v0.1.26)
### Что сделано
| # | Компонент | Изменение |
|---|-----------|-----------|
| 1 | `internal/harbor/` | Harbor-клиент: `EnsureProject` создаёт проект перед push образа |
| 2 | `internal/builder/builder.go` | Поддержка `HarborClient`, push в `pearlharbor.registryk8s.services.ngcloud.ru` |
| 3 | `runtimes/go1.23/` | Новый Go 1.23 runtime: `server.go` + `runner.go` + Dockerfile |
| 4 | `internal/builder/context.go` | Go 1.23 в switch; `COPY . /app/handler/` вместо `COPY handler/` |
| 5 | `api/v1alpha1/function_types.go` | `go1.23` в enum поддерживаемых рантаймов |
| 6 | `terraform/provider/` | v0.1.14: `go1.23` в `stringvalidator.OneOf` |
| 7 | `deployments/k8s/operator.yaml` | v0.1.26 |
| 8 | `examples/hello-go/` | Новый пример: HTTP + job на Go 1.23 |
| 9 | `examples/hello-go/code/greeting.go` | Переименован из handler.go, функция `buildGreeting(guestName)` |
| 10 | `examples/pg-list-python/` | Новый пример: Python + PostgreSQL, только HTTP, без job |
### Docker Hub auth на удалённой машине
- Токен: `dckr_pat_aElN35tdXMBrtOAPraV5Fi0o58s` (сохранён в `secrets/dockerhub.env`, chmod 600)
- `docker login -u naeel --password-stdin` выполнен на remote machine
### Исправленные баги
- kaniko `COPY handler/ /app/handler/` — файлы в tar-контексте без prefix → исправлено на `COPY .`
- `Decimal` из psycopg2 не сериализуется → `float(row[2])` в `catalog.py`
- Formatter добавил дублирующий `package code` в `greeting.go` — удалён вручную
- CRD в кластере имел только `go1.21` в enum → `kubectl apply` обновлён CRD
- Provider v0.1.13 имел только `go1.21` → пересобран v0.1.14
### Git HEAD
```
d6a2e22 fix: восстановлен requirements.txt после теста
5c6a373 docs: переработан examples/README.md + комментарии function.tf
2ca3137 feat: пример pg-list-python
c4559dd fix: go1.23 build context
78d11ae refactor: rename handler.go→greeting.go
a709b38 feat: go1.23 runtime support
babd8e6 feat: harbor integration
```
---
## 2026-03-11 — UX улучшения: build logs + JSON для всех HTTP методов (v0.1.27v0.1.28)
### Проблемы до исправления
| Проблема | Симптом |
|---|---|
| Ошибка сборки без деталей | `terraform apply` показывал только `function build failed: build job failed` без причины |
| HTML 501 при DELETE/HEAD/PUT | Python runtime `http.server` не обрабатывал эти методы → HTML-ответ вместо JSON |
### Что изменили
#### `controllers/function_controller.go`
- `FunctionReconciler` получил поле `KubeClient kubernetes.Interface`
- Добавлен `getBuildPodLogs(ctx, kube, namespace, jobName)` — читает логи kaniko-пода (последние 50 строк)
- При сбое сборки (`case "failed"`) вызывает `getBuildPodLogs` и включает вывод в `Function.Status.Message`
- Провайдер пробрасывает `Message` в ошибку `terraform apply` — разработчик видит точную причину
#### `runtimes/python3.11/server.py`
- Выделен `_handle_with_body()` — общий обработчик для методов с телом
- `do_PUT`, `do_DELETE`, `do_PATCH` = `_handle_with_body` — вызывают функцию пользователя
- `do_HEAD` — отдаёт заголовки без тела (HTTP-стандарт; тело вычисляется но не отправляется)
- `send_error()` переопределён → JSON `{"error": "...", "code": N}` вместо HTML 501
- Runtime пересобран: `naeel/sless-runtime-python3.11:v0.1.2`
#### `main.go`
- `KubeClient: kubernetes.NewForConfigOrDie(mgr.GetConfig())` передаётся в `FunctionReconciler`
#### `internal/builder/context.go`
- python3.11 runtime: `v0.1.1``v0.1.2`
### Версии после исправления
- Оператор: `naeel/sless-operator:v0.1.28`
- Python runtime: `naeel/sless-runtime-python3.11:v0.1.2`
- Provider: `terra.k8c.ru/naeel/sless v0.1.14` (без изменений)
### Результат тестирования
| Тест | До | После |
|---|---|---|
| `DELETE /fn/...` | HTML 501 | ✅ JSON 200, функция вызывается |
| `HEAD /fn/...` | HTML 501 | ✅ HTTP 200, Content-Type: application/json |
| `PUT /fn/...` | HTML 501 | ✅ JSON 200, функция вызывается |
| Невалидный `requirements.txt` | `build job failed` | ✅ Полный вывод pip включая строку с ошибкой |
### Git коммиты
```
6de90ac chore: python runtime v0.1.2 + operator v0.1.28
3372cb1 fix: build logs in status + JSON for all HTTP methods in python runtime
```
---
## 2026-03-22 — Тестовая сессия: G13 / G14 / G15 + fix v0.1.51
### Цель
Написать и прогнать три группы тестов, закрывающих:
- G13: пользовательские ошибки и граничные случаи (40 тестов)
- G14: кластерный хаос — удаление ресурсов, kill pods, OOM, kaniko interrupt (20 тестов)
- G15: комбинированный — хаос + пользовательские ошибки одновременно (21 тест)
### Исходное состояние
- Оператор: **v0.1.50**, 45/45 failure test (G12)
- Известные баги: исправлены `CreateService IsInvalid→400` и `SLESS_ENTRYPOINT` в Deployment
### Проведённые работы
#### 1. Написан `operator_edge_cases_test.sh` (G13)
6 секций:
- **13A** Name validation (uppercase, пробелы, underscore, slash, длина)
- **13B** Boundary values (timeout_sec 0/-1/900/901, memory_mb limits)
- **13C** Lifecycle edge cases (GET/DELETE/PUT 404, 409 duplicate, upload 404)
- **13D** State transitions (invoke during build, re-upload Ready, upload Failed → restart)
- **13E** Update validation (PUT invalid runtime/entrypoint/memory, PUT ruby3.0)
- **13F** Upload edge cases (wrong field, empty body, nested handler.py, 40MB)
Первый прогон: **36P / 4F**
Найденные баги и исправления:
| # | Проблема | Тип | Решение |
|---|----------|-----|---------|
| G13E-5 | PUT ruby3.0 → 500 | БАГ кода | `UpdateService` + `IsInvalid→400` |
| G13F-2 | upload empty body → 404 | Баг теста | добавить `-X POST` в curl |
| G13D-3 | GET /fn/ → FAIL | By design | тест обновлён (all methods allowed) |
| G13F-4 | 40MB → 413 | Баг теста | принять 413 (nginx limit) |
#### 2. Исправлен `internal/api/handler/services.go` (UpdateService)
Добавлена обработка `errors.IsInvalid``400 Bad Request` в `UpdateService`.
#### 3. Собран и задеплоен **v0.1.51**
```
docker build + push → pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.51
kubectl -n sless set image deployment/sless-operator operator=...v0.1.51
```
#### 4. G13 перезапущен → **40/40 PASS ✅**
#### 5. Написан `operator_chaos_test.sh` (G14)
5 секций:
- **14A** Self-healing: удалить Deployment/Service → оператор пересоздаёт за 60s
- **14B** Pod kill resilience: `kubectl delete pod` → ReplicaSet поднимает новый
- **14C** OOM Kill: `memory_mb=32` + функция выделяет 300MB → OOMKill (фаза остаётся Ready — Gap)
- **14D** Kaniko interrupt: убить kaniko Job → `builder.IsNotFound → "failed"` → phase=Failed
- **14E** Operator restart: kill operator pod → reconcile восстанавливает все сервисы
Прогон: **20/20 PASS ✅**
#### 6. Написан `operator_combined_test.sh` (G15)
5 секций:
- **15A** CRUD under chaos (DELETE Deployment → API отвечает, оператор восстанавливает)
- **15B** Upload during self-heal (загрузка кода пока оператор восстанавливает ресурсы)
- **15C** Concurrent creates (3 параллельных POST → 1×201, 2×409)
- **15D** API errors after restart (ошибочные запросы сразу после kill operator pod)
- **15E** Rapid lifecycle (create→upload→delete→create→upload за ~60s)
Первый прогон: **15P / 6F**
Баги тестов (не кода):
| # | Проблема | Решение |
|---|----------|---------|
| G15C-2 | GET /services возвращает `[]` не `{"items":[]}` | тест исправлен |
| G15D | kill operator → 503 (API = operator pod) | тест принимает 503 как transient |
| G15E-3 | DELETE → 204, тест ждал 200 | тест принимает 204 |
G15 перезапущен → **21/21 PASS ✅**
### Итоговые результаты
| Группа | Тесты | Результат | Файл |
|--------|-------|-----------|------|
| G12 Failure | 45 | ✅ 45/45 | `run_e2e_tests.sh` |
| G13 Edge Cases | 40 | ✅ 40/40 | `operator_edge_cases_test.sh` |
| G14 Cluster Chaos | 20 | ✅ 20/20 | `operator_chaos_test.sh` |
| G15 Combined | 21 | ✅ 21/21 | `operator_combined_test.sh` |
| **ИТОГО** | **126** | **✅ 126/126** | |
### Gap'ы (не баги, но отмечены в тестах)
| # | Описание | Где задокументировано |
|---|----------|----------------------|
| 1 | OOM Kill не меняет phase → наблюдаемость ухудшена | G14C NOTE |
| 2 | handler.py в подпапке zip принимается при upload, ошибка только при запуске контейнера | G13F-3 NOTE |
| 3 | operator pod = API server → 503 при restart (нет HA) | G15D NOTE |
| 4 | nginx `client_max_body_size` ограничивает upload → 413 (не настроено явно) | G13F-4 NOTE |
### Версия оператора
`v0.1.51` — задеплоен, работает