Files
sless/doc/progress.md
T
Naeel a04dfb2d0c fix+docs: FunctionJob label bugfix, job ErrAlreadyExists, python str→text/plain, operator.yaml v0.1.33, progress.md
- controllers/functionjob_controller.go:
  - PodTemplate labels: functionjob=, function= (k8s 1.27+ удалил job-name=)
  - getJobPodOutput принимает labelSelector вместо jobName
  - захват stderr при Failed job; truncateForStatus() helper
- terraform/provider/internal/client/client.go: ErrJobAlreadyExists (409 Conflict)
- terraform/provider/internal/resources/job_resource.go: при конфликте создания — читаем существующий job
- runtimes/python3.11/server.py: str return → text/plain
- internal/builder/context.go: python runtime base image → v0.1.3
- deployments/k8s/operator.yaml: image → v0.1.33
- doc/progress.md: добавлены секции FunctionJob bugfix, str→text/plain, web-console v0.2.0
2026-03-18 17:41:44 +03:00

496 lines
37 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-18 22:00 (web-консоль v0.2.0 задеплоена, ветка feat/web-console)
## 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` на удалённом сервере `192.168.1.220` через 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 на 192.168.1.220
- Токен: `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
```