# Мультитенантный 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 тега