Files
fission-src/doc/thinking/2026-05-15-multitenant-architecture-summary.md
T
“Naeel” 5f0ab79f00 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)
- Порядок деплоя нового форка
- Направления дальнейшей работы
2026-05-15 07:08:32 +04:00

232 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Мультитенантный 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 тега