diff --git a/console/internal/api/function_clone_test.go b/console/internal/api/function_clone_test.go index 6409abe..cfc05b1 100644 --- a/console/internal/api/function_clone_test.go +++ b/console/internal/api/function_clone_test.go @@ -13,6 +13,7 @@ import ( "strings" "testing" + "fission-console/internal/billing" "fission-console/internal/fission" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,9 +49,10 @@ func newCloneTestServer(t *testing.T, storagesvcHandler http.HandlerFunc, objs . } s := &Server{ - dyn: dynClient, - ns: "fission-test", - http: httpClient, + dyn: dynClient, + ns: "fission-test", + http: httpClient, + billing: billing.NoopStore{}, } return s, storagesvc } diff --git a/doc/structure/analysis-2026-05-21.md b/doc/structure/analysis-2026-05-21.md new file mode 100644 index 0000000..43369d1 --- /dev/null +++ b/doc/structure/analysis-2026-05-21.md @@ -0,0 +1,275 @@ +# Анализ репозитория fission-console + +Составлен: 2026-05-21. Основан только на чтении исходного кода + истории git. + +--- + +## Что это + +**Web UI + API proxy** для управления Fission (Kubernetes-native serverless). +Написан на Go. Работает внутри K8s-кластера. Пользователи аутентифицируются +через Deck API (облачный SSO). + +## Архитектура — поток данных + +``` +Browser (SPA) → Console API → K8s API (CRD — dynamic client) + → Fission Router (invoke proxy — /fission-function) + → storagesvc (архивы → S3 ngcloud msk-1, бакет sless-functions) + → Deck API (auth — prod/dev/test) + → PostgreSQL (billing invocations) + → Grafana (дашборды — по одному Org на namespace) +``` + +### Пакеты + +| Пакет | Роль | +|---|---| +| `cmd/server/` | Точка входа: парсинг env vars, создание зависимостей, запуск HTTP | +| `internal/api/` | **Ядро**: все HTTP-хендлеры (29 файлов) | +| `internal/auth/` | Аутентификация: Deck API + Demo (test fallback) | +| `internal/cloud/` | Namespace lifecycle: квата, NetworkPolicy, одноразовый создаватель | +| `internal/fission/` | Константы GVR + SetupFissionNamespace + EnsureEnvironment | +| `internal/model/` | Общие типы: CreateFunctionRequest, LangEnvMap | +| `internal/runtime/` | Упаковка кода: zip, ESM-обёртка NodeJS, python/phh/ruby/go | +| `internal/billing/` | Статистика: PostgreSQL (NoopStore если BILLING_DSN пуст) | +| `internal/stats/` | Grafana: Organization/dashboard per namespace | +| `ui/` | Embed SPA (index.html), отдельный /cron/ UI | + +--- + +## Фичи — детальный разбор + +### 1. Аутентификация + +**Файлы:** `auth.go`, `middleware.go`, `handlers.go` (строка handleAuth) + +- `POST /console/api/auth` — тело `{token, env}`. Валидация через Deck API. + Возвращает `{ok, env, namespace, email}`. +- **Middleware** — каждый приватный роут → `Authenticator.Authenticate(token, env)`. + Без токена → 401. +- **Namespace** = `"fission-"` + hex(SHA256(sub)[:8]). Для test-пользователей: + `"fission-test-"` + hex(SHA256(sub)[:8]). +- **DeckAuthenticator** — HTTP-запрос к `deck-api-{env}.ngcloud.ru/api/v1/user`. + Кеш в `sync.Map` (ключ = `env:token`, значение = expiry time). Таймаут 5s. +- **DemoAuthenticator** — для тестовых стендов: sub = email = token, без проверки. +- **MultiAuthenticator** — последовательно пробует JWT → Demo. + +### 2. Управление Namespace + +**Файлы:** `cloud/tenant.go`, `cloud/quota.go`, `cloud/network.go` + +- **EnsureUserNS** — единая точка входа. Singleflight (параллельные запросы + для одного NS ждут на chan). Семафор: не более 3 одновременных созданий. + In-memory кэш уже созданных NS. +- **Что создаётся при первом запросе пользователя:** + - Namespace с лейблами `managed-by=fission-console`, `fission.io/managed=true` + - ServiceAccounts: `fission-fetcher`, `fission-builder` + - RoleBindings: для executor, router, buildermgr, kubewatcher, timer, fetcher, builder + - ResourceQuota (из env vars, по умолчанию: CPU req 1/lim 12, mem 2Gi/6Gi, pods 30, functions 20) + - LimitRange (дефолтный CPU 500m, mem 256Mi на контейнер) + - NetworkPolicy `deny-cross-tenant` — ingress только из своего NS, fission NS, kube-system +- **ExpiryReaper** — горутина в main, раз в 5 минут (REAPER_INTERVAL). Удаляет + функции с аннотацией `expires_at` в прошлом. +- **NSReconciler** — сверяет FISSION_RESOURCE_NAMESPACES. + +### 3. CRUD функций + +**Файлы:** `function_code.go`, `function_archive.go`, `function_crud.go`, +`function_clone.go`, `storagesvc.go` + +#### Создание из кода (POST /console/api/functions, JSON) + +- Тело: `{name, language, code, deps?, entrypoint?, route?, methods?, timeout?, ttl?}` +- **Валидация:** имя `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`, ≤57 символов, код ≤1MB. +- **Языки:** python, nodejs, php, ruby, go. +- **Порядок:** Package CRD → Function CRD → HTTPTrigger CRD. При ошибке откат. +- **Типы пакетов:** + - Python: zip с кодом + опционально requirements.txt (если deps не пуст) + - NodeJS: ESM-wrapped zip (package.json + main.js с `__invoke` dispatcher) + - PHP: zip с `main.php` + опционально composer.json + - Ruby: zip с `handler.rb` + опционально Gemfile + - Go: source package (builder компилирует .so плагин) +- **Загрузка:** через storagesvc → S3 (ngcloud msk-1, бакет `sless-functions`). + Fallback на type:literal если storagesvcURL пуст. +- **TTL:** парсит `1h`, `30m`, `7d`, `24h`. Ставит аннотацию `expires_at` на + Function CRD в `metadata.annotations`. +- **Env vars:** сохраняются в аннотации `fission-console/env-vars` (JSON). + +#### Создание из архива (POST /console/api/functions, multipart/form-data) + +- Поле формы: `archive` (zip-файл). +- Проверка magic bytes: `PK` (0x50 0x4B). +- Размер ≤32MB. +- Загружается в storagesvc как есть, без модификации. +- Аннотация `fission-console/source-type: "archive"`. +- Поля формы: `name, language/environment, entrypoint, route, methods, timeout, ttl`. + +#### GET функции (GET /console/api/functions/:name) + +- Возвращает: name, namespace, environment, package, entrypoint, timeout, + created_at, updated_at, code, deps, source_type, archive_filename, route, + methods, env_vars, raw CRD. +- **Извлечение кода:** пробует source.literal → deployment.literal → URL + (через storagesvc download для type:url). Для NodeJS распознаёт и анрапает + ESM-обёртку (`decodeArchiveBytesToSource` в `package.go`). + +#### Обновление кода (PUT /console/api/functions/:name/code) + +- Тело: `{code, deps?, timeout?}` +- Создаёт **новый Package** (новое имя = cache miss в executor). Старый удаляется. +- Та же сборка архива что и при создании (buildDeployArchive). + +#### Обновление архива (PUT /console/api/functions/:name/archive) + +- multipart/form-data, поле `archive`. Загружается в storagesvc, новый Package. + +#### Клонирование (POST /console/api/functions/:name/clone) + +- Тело: `{new_name, route?}` +- Для type:literal — копирует base64 literal в новый Package. +- Для type:url — скачивает архив из storagesvc → заливает новый → новый Package. +- Создаёт новый Function + HTTPTrigger. Для Go копирует source-пакет. +- При ошибке откатывает созданные объекты. + +#### DELETE (DELETE /console/api/functions/:name) + +- Каскадное: удаляет HTTPTrigger → Package CRD → удаляет архив из storagesvc + (DELETE /v1/archive?id=... через storagesvc). +- Проверяет что функция реально удалена из K8s. + +#### Timeout, Logs, EnvVars + +- `PUT /functions/:name/timeout` — патчит только `spec.functionTimeout` + (без замены кода/архива). +- `GET /functions/:name/logs` — читает логи пода функции через K8s API + (rest client, `pod/log`). +- `GET /functions/:name/envvars` / `PUT /functions/:name/envvars` — аннотация + `fission-console/env-vars`. + +### 4. Invoke (вызов функций) + +**Файл:** `function_invoke.go` + +- **`POST /console/api/functions/:name/invoke`** — для UI. Проксирует через + Fission Router. URL: `routerURL/fission-function//`. + Ответ: `{status, body, headers, cold_start, response_raw, pod, duration_ms}`. + Записывает в billing. +- **`/fn/`** — публичный gateway. Строит URL по маршруту, проксирует + тело/заголовки (кроме служебных). Таймаут из `spec.functionTimeout`. +- **`/fission-function//`** — внутренний gateway для cron/timer/webhook. +- JWT-токен router: ServiceAccount token из `/var/run/secrets/...` (или SA_TOKEN_PATH), + кешируется на 12ч в `sync.Mutex`. + +### 5. Триггеры + +**Файлы:** `timetriggers.go`, `mqtriggers.go`, `kwtriggers.go` + +- **HTTP Triggers** — создаются автоматически при создании функции. + Чтение списка: GET `/console/api/httptriggers`. +- **Time Triggers** — CRUD через `/console/api/timetriggers`. + Поля: `name, functionName, cron, method, subpath`. Создаются как + `fission.io/v1 TimeTrigger` CRD. +- **MQ Triggers** — `/console/api/mqtriggers`. Поля: `name, functionName, + queue, sqsEndpoint, accessKey, secretKey`. Создаёт Deployment (sqs-consumer) + + Secret с credentials в NS пользователя. sqs-consumer поллит SQS → + вызывает функцию через JWT-authenticated router. +- **Kubernetes Watch Triggers** — `/console/api/kwtriggers`. Поля: + `name, functionName, resourceType, namespace, labelSelector`. + +### 6. AI/Lint фичи + +**Файлы:** `ai_check.go`, `lint_archive.go`, `explain_archive.go`, `ai_prompts.go` + +- **`POST /ai/check`** — синтаксическая проверка кода. Реальные линтеры + (не LLM): `node --check`, `python3 -m py_compile`, `ruby -c`, `php -l`, + Go через `go/parser` прямо в процессе. +- **`POST /ai/lint-archive`** — принимает zip, линтит каждый файл с + известным расширением. Защита от zip bomb: max 100KB архив, max 100KB + распакованных. Опционально: проверка entrypoint и соответствия языка. +- **`POST /ai/explain-archive`** — извлекает код из архива (max 20KB), + отправляет в LLM через OpenRouter API с вопросом "что делает?". +- **`POST /ai/ask`** — chat/генерация кода через LLM. + 4 режима codegen: генерация с нуля, объяснение, исправление ошибки, + универсальный. Плюс explain и chat режимы. +- **Prompts** (`ai_prompts.go`) — шаблоны для каждого языка с сигнатурами + функций (Python: `def main(event, context)`, NodeJS: `module.exports`, + PHP: `function handler(...)`, Ruby: `def handler(context)`, + Go: `func Handler(...)`). + +### 7. Billing (статистика) + +**Файлы:** `billing/billing.go`, `billing/pg.go`, `billing/noop.go`, +`billing/factory.go` + +- **Интерфейс Store** — `RecordInvocation(Invocation)`. Асинхронный, + fire-and-forget. +- **pgStore** — PostgreSQL через pgxpool. Таблица `invocations`. + Пулы: max 4 conn, min 1. Каждая запись в своей горутине. +- **NoopStore** — если `BILLING_DSN` не задан или не удалось подключиться — + статистика молча отключена. +- **Поля Invocation:** Namespace, FunctionName, TriggerType (http/cron/console/event), + Route, HTTPMethod, StartedAt, DurationMS, StatusCode, ColdStart, + RequestBytes, ResponseBytes, ErrorMsg, RecordedBy ("console"/"router"), + EventType (create/delete/clone/update/invoke). +- **Где записывается:** handleInvokeFunction, handleInvokeRoute, + handleFissionFunctionGateway, handleCreateFunction, handleDeleteFunction, + handleUpdateFunctionCode, handleCloneFunction. + +### 8. Stats / Grafana + +**Файлы:** `stats/grafana.go`, `stats/factory.go`, `stats/noop.go`, +`stats/provider.go` + +- **GrafanaProvider** — для каждого namespace создаёт Grafana Organization + (имя = namespace), PostgreSQL datasource (тот же DSN что billing, + uid="fission-user-pg"), hardcoded dashboard (SQL: WHERE namespace='...'), + public dashboard link (accessToken). +- **Кеш:** `sync.RWMutex` + per-namespace кеш orgID и accessToken. + Идемпотентно — если Org уже есть, повторно не создаётся. +- **NoopProvider** — если `GRAFANA_INTERNAL_URL` не задан. +- **Провизия:** вызывается в `handleAuth` (fire-and-forget горутина), + и в `handleStatsDashboard`. + +### 9. UI + +**Файлы:** `ui/embed.go`, `ui/cron.go` + +- **Embedded SPA** — `//go:embed index.html`. Раздаётся на `/console/`. +- **Cron UI** — отдельный handler для `/cron/` (Cron метрики, статика). +- **CORS:** `Access-Control-Allow-Origin: *` (все операции защищены токеном, + не cookie). +- **Security headers:** nosniff, X-Frame-Options DENY, CSP (default-src 'self'), + Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (нет камере/микрофону). + +### 10. Мониторинг + +- `/health`, `/console/health` — `ok\n` +- `/console/api/ns/status` — статус namespace (существует, кол-во функций) +- `/console/api/ns/debug` — дебаг-инфо (NS, user, labels) +- `/console/api/stats/dashboard-url` — возвращает Grafana public dashboard URL + +--- + +## Эволюция storagesvc (история git) + +- **До 19 мая 2026:** S3 (изначальная архитектура) +- **19 мая:** `3eb4f56 feat: PV storage backend` — перешли на PVC (временное решение) +- **20 мая:** `7539680 chore: switch storagesvc to S3 backend (ngcloud msk-1)` — + вернулись на S3. PV-конфиг сохранён в `helm/fission-values-pv.yaml` для быстрого + отката. +- **Текущее состояние (21 мая):** S3, бакет `sless-functions`, endpoint + `https://s3.msk-1.ngcloud.ru`. Ветка `feat/s3-replicated`. + +--- + +## Чего нет + +- **Нет аутентификации через cookie** — только токен в заголовке. +- **Нет rate limiting** — только ResourceQuota на уровне K8s. +- **Нет WebSocket** — только REST. +- **Нет аудит-лога** — billing таблица не immutable, можно переписать. +- **Нет swagger/OpenAPI** — документация только в doc/. +- **Нет gRPC** — только HTTP/JSON. +- **Нет метрик Prometheus** — только billing в PostgreSQL. +- **Нет CI/CD** — тесты запускаются вручную.