fix: billing.NoopStore in clone test + repo analysis doc

This commit is contained in:
“Naeel”
2026-05-21 09:14:31 +04:00
parent ab128515ab
commit 53ef35968e
2 changed files with 280 additions and 3 deletions
@@ -13,6 +13,7 @@ import (
"strings"
"testing"
"fission-console/internal/billing"
"fission-console/internal/fission"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -51,6 +52,7 @@ func newCloneTestServer(t *testing.T, storagesvcHandler http.HandlerFunc, objs .
dyn: dynClient,
ns: "fission-test",
http: httpClient,
billing: billing.NoopStore{},
}
return s, storagesvc
}
+275
View File
@@ -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/<ns>/<name>`.
Ответ: `{status, body, headers, cold_start, response_raw, pod, duration_ms}`.
Записывает в billing.
- **`/fn/<route>`** — публичный gateway. Строит URL по маршруту, проксирует
тело/заголовки (кроме служебных). Таймаут из `spec.functionTimeout`.
- **`/fission-function/<ns>/<name>`** — внутренний 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** — тесты запускаются вручную.