doc: add multitenant architecture summary (2026-05-15)

Единый сводный документ, описывающий полную архитектуру мультитенантного Fission.
Заменяет необходимость читать 50+ пошаговых thinking-файлов.

Содержит:
- Причина и концепция решения
- Архитектурная карта изменений (ASCII diagram)
- Таблица ключевых файлов с ролями
- Инженерные решения: Snapshot API, NamespaceManager event bus,
  EnsureNamespaceSA, buildermgr dedup bug, router nil guard
- RBAC: что и почему (включая нетривиальные events:create и LSAR)
- Backward compatibility guarantees
- Описание test scenario (Layer 1, PASS=5)
- Порядок деплоя нового форка
- Направления дальнейшей работы
This commit is contained in:
“Naeel”
2026-05-15 07:08:32 +04:00
parent 4addf254cb
commit 5f0ab79f00
@@ -0,0 +1,231 @@
# Мультитенантный Fission: сводная архитектура и инженерная логика
> Дата: 2026-05-15
> Контекст: форк Fission v1.22.0, ветка `feature/multitenant`
> Статус: реализовано, тесты зелёные
---
## Зачем это было нужно
Стандартный Fission требует, чтобы все namespace-ы, в которых живут функции,
были перечислены в переменной окружения `FISSION_RESOURCE_NAMESPACES` **до старта**
процессов. Добавление нового namespace = rolling restart всех компонентов (executor,
router, buildermgr). На сотнях тенантов — постоянный restart loop, каскадные сбои.
Наша задача: добавить новый tenant (namespace) без какого-либо рестарта.
---
## Концепция решения
Единственный public contract для внешних систем — label на Namespace:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: tenant-abc123
labels:
fission.io/managed: "true"
```
Никакого другого coupling с Fission internals не требуется.
После появления namespace с этим label Fission автоматически:
1. Регистрирует namespace во всех компонентах (executor, router, buildermgr)
2. Создаёт SA `fission-fetcher` и необходимый RBAC в namespace
3. Подключает informer factory для CRD (Functions, Environments, HTTPTriggers и т.д.)
4. Тенант может деплоить функции без задержки
---
## Архитектурная карта изменений
```
Kubernetes Namespace API
│ watch: label fission.io/managed=true
utils.RunManagedNamespaceWatcher(...)
│ (shared utility, один и тот же вызов из трёх компонентов)
utils.NamespaceManager (interface)
├─ Bootstrap(envNamespaces) ← уже существующие NS при старте
├─ DispatchAdd(ns) ← новый NS от watcher
└─ DispatchRemove(ns) ← NS удалён (track-only)
NamespaceSubscriber.OnNamespaceAdd(...)
┌───────────┼───────────┐
▼ ▼ ▼
executor router buildermgr
│ │ │
registerNS AddNS(ts) envw+pkgw
│ .AddNamespace
├─ DefaultNSResolver().AddNamespace(ns) ← thread-safe, dedup
├─ EnsureNamespaceSA(ctx, client, log, ns) ← SA + RBAC provisioning
└─ et.AddNamespace(ns, mgr) ← для каждого executor type
```
---
## Ключевые файлы
| Файл | Роль |
|------|------|
| `pkg/utils/namespace.go` | `NamespaceResolver` — хранит список NS, thread-safe Snapshot/AddNamespace |
| `pkg/utils/namespace_manager.go` | `NamespaceManager` — lifecycle, subscribers, event dispatch |
| `pkg/utils/namespace_manager_model.go` | Типы: Record, Phase, Event, Source, Summary |
| `pkg/utils/serviceaccount.go` | `EnsureNamespaceSA` — создаёт fission-fetcher SA/Role/RoleBinding |
| `pkg/executor/multitenant/ns_watcher.go` | Executor NSWatcher + `registerNamespace` |
| `pkg/executor/multitenant/namespace_subscriber.go` | Executor subscriber adapter |
| `pkg/router/ns_watcher.go` | Router NSWatcher (1 строка, через shared utility) |
| `pkg/router/namespace_subscriber.go` | Router subscriber adapter |
| `pkg/buildermgr/ns_watcher.go` | BuilderMgr NSWatcher (1 строка, через shared utility) |
| `pkg/buildermgr/namespace_subscriber.go` | BuilderMgr subscriber adapter |
| `deploy/multitenant/rbac.yaml` | ClusterRole/ClusterRoleBinding для всех трёх компонентов |
---
## Инженерные решения и почему именно так
### 1. Snapshot API вместо прямого чтения map
**Проблема:** `NamespaceResolver.FissionResourceNS` — mutable map, защищённая mutex
только на запись. Читатели в разных горутинах обращались к ней напрямую — data race.
**Решение:** `Snapshot() []string` — под read lock копирует map в sorted slice.
Потребители итерируют по стабильной копии, безопасно даже при конкурентных `AddNamespace`.
**Почему slice а не map:** потребителям нужен обход, а не lookup. Sorted slice даёт
детерминированный порядок — важно для тестов и для startup factory generation.
### 2. NamespaceManager как event bus
**Проблема:** каждый компонент реализовывал свой namespace watcher с нуля —
дублирование кода watcher setup, event handlers, deduplication, logging.
**Решение:** единый `utils.NamespaceManager` + `NamespaceSubscriber` interface.
Компонент реализует только `OnNamespaceAdd/Remove/Resync`, всё остальное — shared utility.
Это сократило `router/ns_watcher.go` до **5 строк**, `buildermgr/ns_watcher.go` до **5 строк**.
### 3. EnsureNamespaceSA — одно место, один вызов
**Проблема:** при динамической регистрации нового NS executor пытался создать pool pod,
но SA `fission-fetcher` ещё не существовал → `FailedCreate`, pod не стартует.
**Решение:** в `registerNamespace` (executor) вызывается `utils.EnsureNamespaceSA`
**до** вызова `et.AddNamespace`. SA всегда существует к моменту создания первого pod.
**Важно:** `EnsureNamespaceSA` — идемпотентная. Повторный вызов = safe no-op.
### 4. Buildermgr dedup bug
**Проблема:** `buildermgr.StartNSWatcher` при добавлении NS вызывал `envw.AddNamespace`
и `pkgw.AddNamespace`. Но глобальный `DefaultNSResolver().AddNamespace()` вызывался
внутри каждого watcher — dedup срабатывал после первого и блокировал второй.
**Решение:** `buildermgr/namespace_subscriber.go` вызывает `DefaultNSResolver().AddNamespace()`
один раз в `registerBuilderNamespace`, а затем оба watcher добавляют NS независимо.
### 5. Router informer maps — guard против nil panic
**Проблема:** в `router/httpTriggers.go` `AddNamespace` мог вызываться до инициализации
внутренних informer maps → nil pointer dereference.
**Решение:** добавлена explicit проверка nil перед операцией, с логом предупреждения.
---
## RBAC — что и почему
`deploy/multitenant/rbac.yaml` содержит три ClusterRole:
### fission-executor-ns-watcher
```
namespaces: list, watch
```
Нужен executor для регистрации Namespace informer. Без этого NSWatcher не стартует.
### fission-router-ns-watcher
```
namespaces: list, watch
```
То же для router.
### fission-executor-sa-provisioner
```
serviceaccounts: get, list, watch, create, update, patch
roles: get, list, watch, create, update, patch
rolebindings: get, list, watch, create, update, patch
events: create
authorization.k8s.io/localsubjectaccessreviews: create
```
Нетривиальные пункты:
- **events:create** — Kubernetes запрещает создавать `Role`, выдающую право,
которого нет у создающего субъекта. `fission-fetcher` получает `events:create`,
значит executor тоже должен его иметь.
- **localsubjectaccessreviews:create** — `setupSAAndRoleBindings` проверяет
существующие права через LSAR перед созданием Role. Без этого — 403.
---
## Что НЕ изменилось (backward compatibility)
- `FISSION_RESOURCE_NAMESPACES` env var работает как раньше — namespace-ы из него
регистрируются при старте через `Bootstrap()`.
- Существующие tenant namespace-ы, добавленные через env var, не нуждаются в label.
- Поведение функций, HTTP-триггеров, builder — неизменно.
- Helm chart стандартный; RBAC применяется отдельно: `kubectl apply -f deploy/multitenant/rbac.yaml`.
---
## Тест-сценарий (Layer 1)
Проверяет сквозной сценарий без рестарта:
1. Создать namespace `l1-test-XXXXX`
2. Добавить label `fission.io/managed=true`
3. Подождать, пока executor зарегистрирует NS (лог `registered namespace`)
4. Создать Environment + Function + HTTPTrigger в namespace
5. Вызвать функцию через router — ожидаемый ответ `200 OK`
Все шаги (PASS=5 FAIL=0) проходят стабильно после полного RBAC fix.
---
## Порядок деплоя нового форка
```bash
# 1. Применить RBAC (один раз на кластер)
kubectl apply -f deploy/multitenant/rbac.yaml
# 2. Деплоить fission-bundle с нашим образом
# (helm upgrade или kubectl apply с новым image tag)
# 3. Создать tenant
kubectl create namespace tenant-abc123
kubectl label namespace tenant-abc123 fission.io/managed=true
# 4. Готово. Можно деплоить функции в tenant-abc123.
```
---
## Направления дальнейшей работы
1. **e2e тесты** — автоматизированный `test_layer1.sh`-подобный тест в Go
2. **Helm chart** — включить `deploy/multitenant/rbac.yaml` как условный template
3. **Мониторинг** — expose namespace lifecycle events в metrics (Prometheus)
4. **Remove lifecycle** — сейчас при удалении NS стратегия `track-only`;
нужен `dispatch-remove` + cleanup informers
5. **Feature branch portability** — при выходе Fission 1.23 сделать rebase
этой ветки поверх нового upstream тега