From 5f0ab79f001a10da20d059d5e7649d3f34736370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 15 May 2026 07:08:32 +0400 Subject: [PATCH] doc: add multitenant architecture summary (2026-05-15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Единый сводный документ, описывающий полную архитектуру мультитенантного 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) - Порядок деплоя нового форка - Направления дальнейшей работы --- ...-05-15-multitenant-architecture-summary.md | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 doc/thinking/2026-05-15-multitenant-architecture-summary.md diff --git a/doc/thinking/2026-05-15-multitenant-architecture-summary.md b/doc/thinking/2026-05-15-multitenant-architecture-summary.md new file mode 100644 index 00000000..b2927feb --- /dev/null +++ b/doc/thinking/2026-05-15-multitenant-architecture-summary.md @@ -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 тега