fix: billing.NoopStore in clone test + repo analysis doc
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"fission-console/internal/billing"
|
||||||
"fission-console/internal/fission"
|
"fission-console/internal/fission"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -48,9 +49,10 @@ func newCloneTestServer(t *testing.T, storagesvcHandler http.HandlerFunc, objs .
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
dyn: dynClient,
|
dyn: dynClient,
|
||||||
ns: "fission-test",
|
ns: "fission-test",
|
||||||
http: httpClient,
|
http: httpClient,
|
||||||
|
billing: billing.NoopStore{},
|
||||||
}
|
}
|
||||||
return s, storagesvc
|
return s, storagesvc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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** — тесты запускаются вручную.
|
||||||
Reference in New Issue
Block a user