369 lines
18 KiB
Markdown
369 lines
18 KiB
Markdown
# 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> # листинг через JWT
|
||
GET /funcs/<namespace> # листинг по namespace (браузер→HTML, curl→plain text)
|
||
GET /funcs/<namespace>/source/<fn> # прокси → оператор GET /source (serviceToken)
|
||
PATCH /funcs/<namespace>/triggers/<name> # прокси → оператор 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 = <JWT> (задаётся через 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=<jwt>
|
||
```
|
||
|
||
---
|
||
|
||
## 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=<name>` на 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 |
|