# Agent Handoff — 2026-03-18 (финальное состояние сессии) Этот файл — полный срез для нового агента: что сделано, как устроено, как работать. --- ## 1. Идентификация проекта - **Репозиторий:** `gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless` - **Локальная копия:** `/home/naeel/remote_dev/sless/` - **Remote server:** `naeel@5.172.178.213` (workspace: `~/terra/sless/`) - **SSH ключ:** `/home/naeel/.ssh/naeel_vm_id_ed25519` - **SSH команда:** `ssh -i <ключ> -o StrictHostKeyChecking=no naeel@5.172.178.213` - **Активная ветка:** `feat/web-console` (последний коммит `a04dfb2`) - **Git origin:** `https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless.git` ### Важно про git Git **не работает локально** (зависает при записи объектов из-за NFS-подобного поведения volume). Все `git add / commit / push` — **только через SSH на remote machine**. `scp` для копирования файлов → `ssh` для git-операций. --- ## 2. Что такое проект Managed Serverless Functions Service для облачного провайдера **nubes.ru**. Пользователь пишет `main.tf` с ресурсами `sless_function`, `sless_trigger`, `sless_job`. Terraform провайдер собирает zip → загружает в оператор → оператор запускает kaniko → Docker образ → Deployment в k8s. **Внешнее API оператора:** `https://sless.kube5s.ru` **Ingress IP:** `185.247.187.147` **Namespace оператора в k8s:** `sless` --- ## 3. Стек и компоненты | Компонент | Технология | Namespace / где | |-----------|-----------|-----------------| | Operator (API + Controllers) | Go (controller-runtime) | k8s namespace `sless` | | funcs-service (web-консоль) | Go (net/http) | k8s namespace `sless` | | PostgreSQL | PostgreSQL 16 | k8s namespace `sless` | | S3 | Ceph (облачный) | `s3.msk-1.ngcloud.ru` | | Container Registry | DockerHub (`naeel/`) | внешний | | Builder | kaniko Job | namespace пользователя | | Function (HTTP trigger) | k8s Deployment + Service | namespace пользователя | | Function (one-shot) | k8s Job | namespace пользователя | | Function (cron) | k8s CronJob | namespace пользователя | | Terraform Provider | Go (plugin-framework v6) | localhost/CI | --- ## 4. Текущие версии образов | Образ | Версия | Что внутри | |-------|--------|-----------| | `naeel/sless-operator` | **v0.1.34** | REST API + k8s controllers; GET /source; proxy-готовый PATCH /triggers | | `naeel/sless-funcs-service` | **v0.2.0** | HTML web-консоль + plain text (backward compat) | | `naeel/sless-runtime-python3.11` | **v0.1.3** | str return → text/plain | | `naeel/sless-runtime-nodejs20` | **v0.1.2** | без изменений | | `naeel/sless-runtime-go1.23` | **v0.1.0** | без изменений | --- ## 5. Структура директорий (актуальная) ``` sless/ ├── main.go # точка входа оператора (controller-runtime + HTTP сервер) ├── Dockerfile # сборка оператора ├── go.mod # module: gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless ├── api/v1alpha1/ # CRD типы: Function, FunctionJob, Trigger │ ├── function_types.go │ ├── job_types.go │ └── trigger_types.go ├── controllers/ # k8s reconcilers │ ├── function_controller.go # Function CRD → kaniko → Deployment/Service │ ├── functionjob_controller.go # FunctionJob CRD → k8s Job → собирает stdout/stderr │ └── trigger_controller.go # Trigger CRD → Deployment scale / CronJob ├── internal/ │ ├── api/ │ │ ├── router.go # gorilla/mux: все REST маршруты (актуальный) │ │ ├── middleware/ # Auth (JWT→namespace), Logging │ │ └── handler/ │ │ ├── handler.go # Handler struct (K8s, S3, PG, Log) │ │ ├── functions.go # CRUD Functions │ │ ├── triggers.go # CRUD Triggers + UpdateTrigger (PATCH enabled) │ │ ├── upload.go # POST /upload — zip → tar.gz → S3 → Function CRD patch │ │ ├── source.go # GET /source — tar.gz из S3 → JSON файлы (НОВЫЙ) │ │ ├── jobs.go # CRUD FunctionJobs │ │ ├── invocations.go # logs из Postgres │ │ ├── invoke.go # прокси вызова HTTP функций │ │ └── namespace.go # EnsureNamespace │ ├── builder/ │ │ └── context.go # zip + runtime → tar.gz + Dockerfile для kaniko │ ├── storage/ │ │ ├── s3/client.go # minio-go: Upload, UploadContext, Download, Delete │ │ └── postgres/ # хранение invocation logs │ └── config/ # env vars конфиг ├── services/ │ └── funcs/ │ ├── main.go # web-консоль сервис (v0.2.0) │ ├── index.html # HTML шаблон (embed) │ ├── Dockerfile # multi-stage Go → alpine │ └── funcs-service.yaml # (дубль, не деплоится отсюда) ├── deployments/k8s/ │ ├── operator.yaml # ConfigMap + Secret + Deployment + Service + Ingress оператора │ ├── funcs-service.yaml # Deployment + Service + Ingress funcs-service │ ├── postgres.yaml # PostgreSQL │ └── rbac.yaml # ClusterRole для оператора ├── terraform/provider/ # terraform-provider-sless │ ├── main.go │ └── internal/ │ ├── client/client.go # HTTP клиент к оператору │ └── resources/ │ ├── function_resource.go # sless_function: source_dir→zip, code_hash, ModifyPlan │ ├── trigger_resource.go # sless_trigger │ └── job_resource.go # sless_job + ErrJobAlreadyExists handling ├── runtimes/ │ ├── python3.11/server.py # HTTP wrapper (str → text/plain) │ ├── nodejs20/ # HTTP wrapper │ └── go1.23/ # multi-stage builder образ ├── examples/ │ ├── POSTGRES/ # pg функции: create-table, pg-info, pg-table-reader │ ├── hello-go/ │ ├── hello-node/ │ └── ... ├── migrations/001_initial.sql # PostgreSQL схема └── doc/ # ← ты здесь ├── architecture/ │ └── agent-handoff-2026-03-18.md ← ЭТОТ ФАЙЛ ├── api/design.md ├── decisions/log.md ├── errors/log.md └── progress.md ``` --- ## 6. REST API оператора — полный список маршрутов Все `/v1/` защищены JWT (middleware.Auth проверяет Bearer токен + namespace). `/fn/` — публичный прокси для HTTP-триггеров (без auth). ``` POST /v1/namespaces/{ns}/ensure # создать namespace (идемпотентно) GET /v1/namespaces/{ns}/functions # список функций POST /v1/namespaces/{ns}/functions # создать функцию GET /v1/namespaces/{ns}/functions/{name} # получить функцию PUT /v1/namespaces/{ns}/functions/{name} # обновить функцию DELETE /v1/namespaces/{ns}/functions/{name} # удалить функцию POST /v1/namespaces/{ns}/functions/{name}/upload # загрузить zip → S3 → kaniko GET /v1/namespaces/{ns}/functions/{name}/source # НОВЫЙ: файлы кода из S3 (JSON) GET /v1/namespaces/{ns}/functions/{name}/invocations # логи вызовов GET /v1/namespaces/{ns}/triggers # список триггеров POST /v1/namespaces/{ns}/triggers # создать триггер GET /v1/namespaces/{ns}/triggers/{name} # получить триггер PATCH /v1/namespaces/{ns}/triggers/{name} # enable/disable: {"enabled": bool} DELETE /v1/namespaces/{ns}/triggers/{name} # удалить триггер POST /v1/namespaces/{ns}/jobs # создать FunctionJob GET /v1/namespaces/{ns}/jobs/{name} # получить FunctionJob DELETE /v1/namespaces/{ns}/jobs/{name} # удалить FunctionJob GET/POST/... /fn/{namespace}/{name}[/...] # вызов HTTP функции (без auth) ``` --- ## 7. funcs-service — маршруты Сервис `sless-funcs-service`, namespace `sless`, порт 8090. Ingress: `sless.kube5s.ru/funcs` → `sless-funcs-service:8090`. ``` GET /health # liveness/readiness probe (без auth) GET /funcs # usage hint (нет токена → 401 с подсказкой) GET /funcs?token= # листинг через JWT GET /funcs/ # листинг по namespace (браузер→HTML, curl→plain text) GET /funcs//source/ # прокси → оператор GET /source (serviceToken) PATCH /funcs//triggers/ # прокси → оператор PATCH /triggers (serviceToken) ``` Логика переключения HTML/plain text: `strings.Contains(Accept header, "text/html")`. Браузер всегда шлёт `text/html` в Accept → HTML консоль. `curl` без `-H "Accept: text/html"` → plain text (совместимость с v0.1.x). **Env vars funcs-service:** ``` SLESS_OPERATOR_URL = http://sless-operator.sless.svc.cluster.local:9090 SLESS_EXTERNAL_URL = https://sless.kube5s.ru SLESS_EXCLUDE = (список функций скрытых из листинга, через запятую) SLESS_SERVICE_TOKEN = (задаётся через kubectl set env, НЕ в git) PORT = 8090 ``` --- ## 8. Namespace пользователя ``` JWT.sub (UUID) → SHA256(sub)[:8] → hex → "sless-" + 16 hex символов ``` Пример: sub `019cc268-6c6a-781e-8613-4bed4ec7cd20` → namespace `sless-ffd1f598c169b0ae` Эта логика **одинакова** в трёх местах: - `internal/api/middleware/auth.go` (оператор) - `services/funcs/main.go` (funcs-service) - `terraform/provider/internal/client/client.go` (провайдер) --- ## 9. S3 хранение кода — ключи При `POST /upload` создаются **два объекта:** ``` functions/{ns}/{name}/{timestamp}.zip ← исходный код (zip от пользователя) contexts/{ns}/{name}/{timestamp}.tar.gz ← build context для kaniko (zip + Dockerfile) ``` `Function.Spec.S3Key` хранит путь к `contexts/...`. `GET /source` читает `Function.Spec.S3Key`, скачивает tar.gz, извлекает файлы без Dockerfile. **Важно:** zip исходника (`functions/...`) отдельно не хранится в CRD. Код источника берётся из tar.gz контекста — там те же файлы пользователя. --- ## 10. Terraform провайдер — sless_function ```hcl resource "sless_function" "my_func" { name = "my-func" runtime = "python3.11" # python3.11 | nodejs20 | go1.23 entrypoint = "handler.handle" source_dir = "${path.module}/code/my-func" # директория → провайдер делает zip сам # ИЛИ: # code_path = "./handler.zip" # готовый zip # code_hash = filesha256("./handler.zip") # для детекции изменений memory_mb = 128 timeout_sec = 30 env_vars = { KEY = "value" } build_timeout_sec = 300 # ожидание kaniko (дефолт 300 сек) } ``` При изменении файлов в `source_dir`: - `terraform plan` → пересчитывает `code_hash` (ModifyPlan), показывает diff - `terraform apply` → загружает новый zip, ждёт сборки → `phase = Ready` - Новый код виден в браузере сразу после apply --- ## 11. Где что запущено (kubectl) ```bash # Проверить поды оператора kubectl get pods -n sless # Проверить версию образа оператора kubectl get deployment sless-operator -n sless -o jsonpath='{.spec.template.spec.containers[0].image}' # Посмотреть логи оператора kubectl logs -n sless -l app=sless-operator --tail=50 # Посмотреть логи funcs-service kubectl logs -n sless -l app=sless-funcs-service --tail=50 # Обновить образ оператора kubectl set image deployment/sless-operator operator=naeel/sless-operator:vX.X.X -n sless kubectl rollout status deployment/sless-operator -n sless --timeout=90s # Обновить образ funcs-service kubectl set image deployment/sless-funcs-service funcs=naeel/sless-funcs-service:vX.X.X -n sless # Посмотреть функции пользователя kubectl get functions -n sless-ffd1f598c169b0ae # Посмотреть триггеры kubectl get triggers -n sless-ffd1f598c169b0ae # Обновить SLESS_SERVICE_TOKEN (не хранится в git!) kubectl set env deployment/sless-funcs-service -n sless SLESS_SERVICE_TOKEN= ``` --- ## 12. Тестовые данные - **Тестовый токен:** `/home/naeel/remote_dev/sless/secrets/test.token` - **Namespace тест-пользователя:** `sless-ffd1f598c169b0ae` - **Рабочие функции:** - `pg-info` (nodejs20) — читает версию PostgreSQL и счётчик строк - `pg-table-reader` (python3.11) — читает строки из таблицы - `pg-create-table-runner` (python3.11) — job: создаёт таблицу - **Быстрый тест:** ```bash # plain text список (curl) curl https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae # HTML консоль (браузер или curl с Accept) curl -H "Accept: text/html" https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae # исходный код функции curl https://sless.kube5s.ru/funcs/sless-ffd1f598c169b0ae/source/pg-info # вызов функции curl https://sless.kube5s.ru/fn/sless-ffd1f598c169b0ae/pg-info ``` --- ## 13. Деплой нового образа — стандартный workflow ```bash SSH="ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213" SCP="scp -i /home/naeel/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no" # 1. Скопировать изменённые файлы оператора на remote $SCP /home/naeel/remote_dev/sless/path/to/file.go naeel@5.172.178.213:~/terra/sless/path/to/file.go # 2. Собрать образ на remote $SSH "cd ~/terra/sless && docker build -t naeel/sless-operator:vX.X.X . 2>&1 | tail -5" # 3. Запушить $SSH "docker push naeel/sless-operator:vX.X.X 2>&1 | tail -3" # 4. Задеплоить $SSH "kubectl set image deployment/sless-operator operator=naeel/sless-operator:vX.X.X -n sless && kubectl rollout status deployment/sless-operator -n sless --timeout=90s" # 5. Коммит (через SSH! не локально) $SSH "cd ~/terra/sless && git add <файлы> && git commit -m 'msg' && git push origin feat/web-console" ``` Для funcs-service: путь `~/terra/sless/services/funcs/`, контейнер называется `funcs`. --- ## 14. Известные проблемы / особенности | Проблема | Решение | |---------|---------| | `git` зависает локально | Только через SSH на remote machine | | `SLESS_SERVICE_TOKEN` не в git | Задан через `kubectl set env`, при пересоздании пода — пропадёт! Нужно переставить вручную | | `job-name=` label удалён в k8s 1.27+ | Используем свой label `functionjob=` на PodTemplate (исправлено в v0.1.34) | | S3 endpoint с SSL | `useSSL=false` для внутреннего s3, `useSSL=true` для облачного `s3.msk-1.ngcloud.ru` | | `python3.11`: str return → text/plain | Начиная с `naeel/sless-runtime-python3.11:v0.1.3` | --- ## 15. Следующие возможные задачи (не начаты) | Задача | Сложность | Заметки | |--------|-----------|---------| | Слияние `feat/web-console` в `main` (или базовую ветку) | низкая | Ветка стабильная, все тесты проходят | | Кнопка "Обновить код" в HTML консоли (upload из браузера) | средняя | Drag&drop zip или указать source_dir | | Обратная синхронизация (скачать код из S3 в source_dir) | средняя | terraform data source или отдельная команда | | History/versioning (несколько версий кода) | высокая | S3 уже хранит по timestamp — нужен UI | | SLESS_SERVICE_TOKEN из k8s Secret | низкая | Сейчас задаётся через kubectl set env — надо в YAML (sealed secret) | | Логи функции в HTML консоли | средняя | GET /invocations уже есть в операторе | | Публикация terraform провайдера | средняя | Terraform Registry или Gitea Releases |