Compare commits

..
40 changed files with 315 additions and 3517 deletions
-61
View File
@@ -1,61 +0,0 @@
# Правила
## ⛔ ОТВЕЧАТЬ КРАТКО — АБСОЛЮТНОЕ ПРАВИЛО
- Вопрос → короткий ответ → СТОП.
- Ничего лишнего.
- Код — только по запросу.
## ⛔⛔⛔ ВОПРОС = СТОП
**Если в сообщении есть вопрос в ЛЮБОЙ форме** ("так ?", "верно ?", "почему ?", "как ?", "так же ?" и т.д.):
1. ТОЛЬКО ответить на вопрос
2. ОСТАНОВИТЬСЯ
3. ЖДАТЬ следующей команды
**ЗАПРЕЩЕНО** начинать работу, писать код, запускать команды — без явного "делай".
1. **⛔⛔⛔ АБСОЛЮТНЫЙ ЗАПРЕТ: не трогать и не читать рабочий код с целью подготовки к правке — без ПРЯМОГО указания "делай". Даже чтение файлов перед правкой — СТОП, сначала разрешение.**
2. Файлы редактируются локально:
~/fission-src (текущая рабочая папка)
После ЛЮБЫХ изменений ОБЯЗАТЕЛЬНО синхронизировать на ВМ командой:
rsync -az \
-e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
~/fission-src/ \
naeel@5.172.178.213:~/terra/fission-src/
3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/fission-src
4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ:
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
- не выполнять инфраструктурные команды локально
- не открывать интерактивные сессии
- не делать цепочки без необходимости
4. Перед запуском команд ОБЯЗАТЕЛЬНО убедиться, что синхронизация выполнена.
5. ЗАПРЕЩЕНО:
- откатывать код
- менять версии
- ломать рабочее состояние
6. После каждого исправления:
- git add/commit ЛОКАЛЬНО (в ~/fission-src)
- затем синхронизация (rsync) на ВМ
## ⛔⛔⛔ ДЕЛАТЬ ТОЛЬКО ЧТО ПРЯМО ПРИКАЗАНО
**АБСОЛЮТНЫЙ ЗАПРЕТ на додумывание:**
- Не расширять масштаб работы
- Не выполнять "логичные следующие шаги"
- Не инициировать дополнительные операции
- Не делать ничего кроме того что сказано
**Пример (2026-05-01):**
- Приказано: "собери"
- Сделано: ✓ собрал образы v1.3.17 и v0.1.2
- СТОП — жду команды дальше
- **ЗАПРЕЩЕНО:** обновлять манифесты, заливать образы, применять на кластер, запускать тесты
**Исключение:** только если приказ явно включает цепочку ("собери И залей И тесты")
-100
View File
@@ -1,100 +0,0 @@
# Правила работы агента
## ⛔⛔⛔ DOCKER — ОБЯЗАТЕЛЬНЫЙ ПОРЯДОК ПЕРЕД КАЖДЫМ BUILD
1. УВЕЛИЧИТЬ ТЕГ в `console/deploy/console.yaml` (vX.Y.Z → vX.Y.Z+1)
2. rsync на ВМ
3. ПРОВЕРИТЬ что файлы на ВМ новые (grep ключевой строки)
4. docker build с НОВЫМ тегом
5. docker push с НОВЫМ тегом
6. kubectl apply (не rollout restart — apply подтягивает новый тег)
**НИКОГДА не делать `docker build` со старым тегом — под не перетянет образ (imagePullPolicy: IfNotPresent)**
## Файловая система (актуально)
1. Все файлы редактируются локально: `~/fission-src` (текущая рабочая папка)
2. После любых изменений — обязательно rsync на ВМ:
rsync -az \
-e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
~/fission-src/ \
naeel@5.172.178.213:~/terra/fission-src/
3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/fission-src
4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ
5. Перед запуском любой команды на ВМ обязательно убедиться, что синхронизация (rsync) выполнена
6. SCP, sshfs, remote_dev и маунты больше НЕ используются
7. Только rsync для синхронизации
Пример:
1. Редактируешь локально (~/fission-src)
2. rsync на ВМ
3. Выполняешь команды через SSH на ВМ
## SSH
Все команды — только через SSH на ВМ. Локально — только читать и редактировать файлы.
```bash
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
```
Запрещено локально: `go`, `docker`, `kubectl`, `helm`, `terraform`, `curl/wget`, `git push/pull`, любые скрипты проекта.
## Документация
- `doc/thinking/` — лог рассуждений агента (обязательно)
- `doc/progress.md` — трекер задач
- Старые файлы `doc/` не перезаписывать — новое в новых файлах с датой
## Git
- Git — ТОЛЬКО ЛОКАЛЬНО в `~/fission-src`. НИКОГДА через SSH на VM.
- Разрешены ТОЛЬКО две операции: `git commit` и `git push`.
- ЗАПРЕЩЕНО: git pull, git fetch, git rebase, git merge, git reset, git stash, git checkout — что угодно кроме commit и push.
- Если push отклонён — СТОП, доложить пользователю. Не лезть в pull/merge/rebase самостоятельно.
Версионирование тегами: `vMAJOR.MINOR.PATCH`
- Patch — любое изменение кода
- Minor — новая фича / компонент
- Major — breaking change
```bash
git tag vX.Y.Z && git push origin vX.Y.Z
```
## ⛔ ТЕРМИНАЛЬНЫЙ БУФЕР — НИКОГДА НЕ ЧИТАТЬ СТАРЫЙ
**АБСОЛЮТНОЕ ПРАВИЛО:**
- get_terminal_output из старых сессий — МУСОР. Там старые прогоны.
- Всегда запускать новую команду через SSH и читать её вывод напрямую.
- НИКОГДА не читать буфер терминала из предыдущей сессии как актуальные данные.
- Актуальный результат — только из команды, которая была запущена СЕЙЧАС.
## ⛔ ДОКУМЕНТАЦИЯ ТЕСТ-ПРОГОНОВ — В РЕАЛЬНОМ ВРЕМЕНИ
**Правила:**
1. Перед запуском `run_all.sh` — создать файл `test-results/YYYY-MM-DD_HH-MM.log` и записать в него метку времени и что запускается.
2. Запускать `run_all.sh 2>&1 | tee ~/terra/fission-src/test-results/YYYY-MM-DD_HH-MM.log` — вывод пишется сразу в файл и отображается в терминале.
3. После завершения — rsync лога локально. Лог остаётся как документация.
4. Папка `test-results/` в репозитории — `.gitignore` не добавлять, логи коммитить.
**Формат запуска:**
```bash
LOG="test-results/$(date +%Y-%m-%d_%H-%M).log"
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 \
"bash ~/terra/fission-src/scripts/run_all.sh 2>&1 | tee ~/terra/fission-src/${LOG}"
rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no" \
naeel@5.172.178.213:~/terra/fission-src/test-results/ ~/fission-src/test-results/
```
**Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.**
## Поведение агента
- **⛔⛔⛔ АБСОЛЮТНЫЙ ЗАПРЕТ: не читать и не трогать код с целью подготовки к правке — без прямого "делай". Даже чтение файлов перед правкой — СТОП, сначала разрешение.**
- Не трогать рабочий код без явного указания
- Не делать НИЧЕГО сверх того, о чём явно приказали — ни git-команд, ни rebase, ни дополнительных шагов
- Если для продолжения нужен выбор — СПРОСИТЬ разрешения, не делать самостоятельно
- Деструктивные операции (`kubectl delete`, `rm -rf`, `terraform destroy` и др.) — только после явного подтверждения с указанием конкретных объектов
- Отвечать кратко, без вступлений, извинений, благодарностей и прочей воды
-2
View File
@@ -20,8 +20,6 @@ environments/php7/vendor/
*.tfstate
*.backup
*.token
# Common backup files
*.swp
*.bak
+79
View File
@@ -0,0 +1,79 @@
# NEXT CHAT: LAYER2 START HERE
Если ты новый агент в новом чате, сначала прочитай этот файл целиком.
## Где работать
Репозиторий: `fission-src`
Ветка:
`rewrite/layer2-namespace-manager-api-step1`
## Что уже готово
Layer1 завершён.
Это значит:
1. Внутренний `NamespaceManager` layer уже реализован.
2. Buildermgr, router и executor/multitenant уже переведены на общий watcher/helper layer.
3. Summary/debug contract стабилизирован.
4. Logging path усилен.
5. Layer1 закрыт commit-ом:
`63ce6ea`
`layer1: close namespace manager step1`
## Что уже было проверено
Целевой прогон для layer1 уже был зелёным:
`go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant`
## Что нужно делать теперь
Нужен layer2.
Layer2 = не переписывать watcher-ы заново, а дать внешний read-only status/debug/API surface поверх уже готового `NamespaceManager` слоя.
Цель:
1. Найти лучший существующий read-only endpoint/status/debug surface.
2. Начать аккуратно выносить наружу `NamespaceManagerSummary`.
3. Не менять runtime semantics watcher-ов.
4. Не плодить второй источник правды о namespace state.
## Как работать
1. Работай маленькими шагами.
2. Перед кодом сначала найди правильную точку интеграции.
3. Все новые заметки пиши только в новые файлы в `doc/thinking/`.
4. Не трогай старые doc-файлы.
5. Не запускай background-команды.
6. Все команды запускай только через SSH на VM и всегда с timeout.
## Важные файлы
- `pkg/utils/namespace_manager.go`
- `pkg/utils/namespace_manager_model.go`
- `pkg/utils/namespace_manager_test.go`
- `pkg/buildermgr/ns_watcher.go`
- `pkg/router/ns_watcher.go`
- `pkg/executor/multitenant/ns_watcher.go`
## Первый шаг в новом чате
Сначала не писать код.
Сначала:
1. проверить текущую ветку и чистоту дерева;
2. найти существующий service-level status/debug/API contour;
3. выбрать один безопасный read-only entrypoint для первого шага layer2.
## Текст первого сообщения нового чата
Можно просто вставить это:
"Прочитай файл NEXT_CHAT_LAYER2.md и продолжай работу строго по нему. Нужен layer2: safe read-only API/status/debug surface поверх NamespaceManager без изменения runtime semantics watcher-ов. Сначала найди правильную точку интеграции, потом делай маленькие шаги с документированием в новых файлах doc/thinking/."
+15
View File
@@ -1,3 +1,18 @@
> [!IMPORTANT]
> ## Это форк Fission с поддержкой мультитенантности (multi-tenant)
>
> **Автор доработок:** Naeel / ngcloud
> **Базовая версия:** Fission v1.22.0 (официальный)
> **Репозиторий:** https://gitea.services.ngcloud.ru/Nail/fission-src
>
> ### Что добавлено по сравнению с официальным Fission:
> - **Динамический multi-tenant:** namespace с меткой `fission.io/managed=true` подхватываются без рестарта Fission
> - **Автоматический SA provisioning:** при появлении нового namespace автоматически создаются ServiceAccount, Role, RoleBinding для fetcher/builder
> - **Namespace Manager:** новый компонент в `pkg/utils/` для отслеживания namespace в реальном времени
> - **Обратная совместимость:** полная, поведение идентично официальному если меток нет
---
<p align="center">
<img src="https://fission.io/images/logo-gh.svg" width="300" />
<br>
+1 -1
View File
@@ -1,3 +1,3 @@
FROM gcr.io/distroless/static-debian12:nonroot
FROM cgr.dev/chainguard/static:latest@sha256:a301031ffd4ed67f35ca7fa6cf3dad9937b5fa47d7493955a18d9b4ca5412d1a
COPY fission-bundle /
ENTRYPOINT ["/fission-bundle"]
+1 -1
View File
@@ -25,7 +25,7 @@ image: fission/fission-bundle
## It is also used by the chart to identify version of the few more images apart from fission-bundle.
## Keep it empty for using latest tag.
##
imageTag: v1.22.1
imageTag: v1.22.0
## pullPolicy represents the pull policy to use for images in the chart.
##
+26
View File
@@ -0,0 +1,26 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fission-executor-ns-watcher
labels:
app: fission-executor
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fission-executor-ns-watcher
labels:
app: fission-executor
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fission-executor-ns-watcher
subjects:
- kind: ServiceAccount
name: fission-executor
namespace: fission
@@ -1,567 +0,0 @@
# Namespace Lifecycle Hardening — Implementation Notes
## Дата: 2026-05-18
## Ветка: `fix/namespace-lifecycle-hardening`
## Коммиты: `3b93c5dc` (предыдущая сессия) → `4eedf95f` (эта сессия)
---
## 1. Контекст: что было сделано до этой сессии
### Предыдущие правки (коммит `3b93c5dc`)
1. **`RemoveNamespace(ns string) bool`** — добавлен в `NamespaceResolver` (`pkg/utils/namespace.go`).
`HandleWatcherNamespaceRemoval` теперь вызывает его при любой стратегии, очищая глобальный resolver. Это исправляет дедупликацию router/buildermgr при re-add NS.
2. **Параллельный `dispatch()`**`pkg/utils/namespace_manager.go`: заменён последовательный обход подписчиков на параллельный с `sync.WaitGroup`. Исправляет 30-минутное окно, когда HTTPTrigger не видел namespace из-за того что обход был последовательным.
3. **`RunReconciler()`** — добавлен в `NamespaceManager` interface и реализован в `inMemoryNamespaceManager`. Каждые 30 секунд сканирует namespace-ы в фазе `NamespacePhaseFailed` и вызывает `DispatchResync`. Исправляет постоянно stuck-failed namespace при транзиентных k8s API ошибках.
### Что оставалось нерешённым (из аудита)
Три проблемы, зафиксированные в `FORENSIC_ARCHITECTURE_AUDIT.md`:
**Проблема 1 — Executor dedup gap (Critical)**
При удалении NS и повторном добавлении executor молча пропускал его.
Причина: `gpm.poolPodC.envLister[ns]` и `deploy.deplLister[ns]` проверялись как дедупликация в `AddNamespace`, но никогда не очищались при удалении NS.
Результат: повторно добавленный namespace не получал informers в executor → функции не запускались.
**Проблема 2 — Goroutine/FD leak (High)**
При удалении NS старые informer factories продолжали работать (goroutines, file descriptors, LIST-запросы к k8s API каждые 30 минут).
Причина: informers запускались с `ctx.Done()` родительского контекста всего процесса, без механизма per-NS остановки.
**Проблема 3 — Router stale routes (Medium)**
После удаления NS router продолжал держать HTTPTrigger routes для этого namespace.
Причина: `triggerInformer[ns]` и `funcInformer[ns]` не чистились, `syncTriggers()` не вызывался.
---
## 2. Анализ перед реализацией
### 2.1 Чтение интерфейса ExecutorType
Файл: `pkg/executor/executortype/executortype.go`
```go
// До правки — нет RemoveNamespace
AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error
}
```
Подтверждено: ни `grep`, ни LSP не нашли `RemoveNamespace` в executor types.
### 2.2 Анализ механизма дедупликации по каждому executor type
**poolmgr** (`gpm.go` строка 807):
```go
if _, ok := gpm.poolPodC.envLister[ns]; ok {
return nil // already registered
}
```
Деdup через `PoolPodController.envLister[ns]` — локальная карта, не связана с глобальным resolver.
**newdeploy** (`newdeploymgr.go` строка 917):
```go
if _, ok := deploy.deplLister[ns]; ok {
return nil // already registered
}
```
Деdup через `deploy.deplLister[ns]`.
**container** (`containermgr.go` строка 805):
```go
if _, ok := caaf.deplLister[ns]; ok {
return nil // already registered
}
```
Деdup через `caaf.deplLister[ns]`.
**router** (`httpTriggers.go` строка 461):
```go
if !utils.DefaultNSResolver().AddNamespace(ns) {
return nil // already registered
}
```
Деdup через глобальный resolver — **уже починен** предыдущим коммитом (`RemoveNamespace` в resolver).
**buildermgr envwatcher** (`envwatcher.go` строка 506):
```go
if _, exists := envw.envWatchInformer[ns]; exists {
return
}
```
Деdup через `envw.envWatchInformer[ns]`.
**buildermgr pkgwatcher** (`pkgwatcher.go` строка 337):
```go
if _, exists := pkgw.pkgInformer[ns]; exists {
return
}
```
Деdup через `pkgw.pkgInformer[ns]`.
### 2.3 Анализ стратегии удаления
`NewDefaultManagedNamespaceWatcherConfig` создаёт конфиг с `RemovalStrategy: NamespaceRemovalStrategyTrackOnly`.
При `TrackOnly``HandleWatcherNamespaceRemoval` вызывает `DefaultNSResolver().RemoveNamespace()` (наш предыдущий фикс), но **не** вызывает `manager.DispatchRemove()``subscriber.OnNamespaceRemove()``RemoveFunc` не срабатывает.
Для вызова `RemoveFunc` нужна стратегия `DispatchRemove`.
### 2.4 Решение: per-namespace context cancellation
Informers запускаются через `factory.Start(ctx.Done())`. Стандартный способ остановить отдельный informer — отменить контекст, с которым он запущен.
**Решение:**
```go
nsCtx, nsCancel := context.WithCancel(ctx)
gpm.nsCancels[ns] = nsCancel
finformer.Start(nsCtx.Done()) // вместо ctx.Done()
```
При `RemoveNamespace`:
```go
if cancel, ok := gpm.nsCancels[ns]; ok {
cancel() // останавливает goroutines informer factories
delete(gpm.nsCancels, ns)
}
```
Это чисто и не требует изменения k8s client-go.
### 2.5 Mutex и thread-safety
Существующий код в executor types не защищает lister maps мьютексами. Записи в них происходят только при `AddNamespace` (из subscriber goroutine). Добавление `RemoveNamespace` добавляет ещё одну запись из той же goroutine. Race condition с event handlers (которые читают эти maps) — известное ограничение существующего дизайна, не добавляем мьютексы чтобы не выходить за рамки задачи.
`HTTPTriggerSet` уже имеет `informerMu sync.RWMutex` — его и используем в `RemoveNamespace` при удалении из `triggerInformer`/`funcInformer`.
---
## 3. Реализация — пошаговое описание
### Шаг 1: `pkg/executor/executortype/executortype.go`
Добавлен метод в `ExecutorType` interface:
```go
// RemoveNamespace deregisters a namespace from the executor, cancelling its
// informer goroutines and clearing dedup state so that a re-add works correctly.
// Called when a Namespace with label fission.io/managed=true is removed.
RemoveNamespace(ctx context.Context, ns string) error
```
**Почему:** Все три executor type реализуют этот интерфейс. Добавление в интерфейс гарантирует, что новый тип executor не забудет реализовать метод (компилятор поймает).
---
### Шаг 2: `pkg/executor/executortype/poolmgr/gpm.go`
**2a. Добавлено поле в struct:**
```go
// nsCancels holds per-namespace context cancel functions so informer
// factories started in AddNamespace can be stopped on RemoveNamespace.
nsCancels map[string]context.CancelFunc
```
**2b. Инициализация в `MakeGenericPoolManager`:**
```go
nsCancels: make(map[string]context.CancelFunc),
```
**2c. Изменение в `AddNamespace`:** вместо `ctx.Done()` передаём `nsCtx.Done()`:
```go
nsCtx, nsCancel := context.WithCancel(ctx)
gpm.nsCancels[ns] = nsCancel
finformer.Start(nsCtx.Done())
gpmInformer.Start(nsCtx.Done())
```
**2d. Новый метод `RemoveNamespace`:**
```go
func (gpm *GenericPoolManager) RemoveNamespace(ctx context.Context, ns string) error {
if ns == "" { return nil }
gpm.logger.Info("RemoveNamespace: cleaning up namespace (poolmgr)", ...)
if cancel, ok := gpm.nsCancels[ns]; ok {
cancel()
delete(gpm.nsCancels, ns)
}
delete(gpm.podLister, ns)
delete(gpm.podListerSynced, ns)
gpm.poolPodC.RemoveNamespace(ns) // очищает envLister/podLister в PoolPodController
return nil
}
```
---
### Шаг 3: `pkg/executor/executortype/poolmgr/poolpodcontroller.go`
Добавлен метод, очищающий lister maps в `PoolPodController`:
```go
func (p *PoolPodController) RemoveNamespace(ns string) {
delete(p.envLister, ns)
delete(p.envListerSynced, ns)
delete(p.podLister, ns)
delete(p.podListerSynced, ns)
p.logger.Info("PoolPodController.RemoveNamespace: cleared lister state", ...)
}
```
**Почему отдельный метод:** `PoolPodController` — отдельная структура внутри poolmgr. Доступ к её полям из `GenericPoolManager.RemoveNamespace` требовал бы либо экспорта полей, либо метода. Метод — чище.
---
### Шаг 4: `pkg/executor/executortype/newdeploy/newdeploymgr.go`
Аналогично gpm:
- Добавлен `nsCancels map[string]context.CancelFunc` в struct `NewDeploy`
- Инициализирован в `MakeNewDeploy`
- `AddNamespace` переключён на `nsCtx.Done()`
- Добавлен `RemoveNamespace` очищающий `deplLister`, `deplListerSynced`, `svcLister`, `svcListerSynced`
---
### Шаг 5: `pkg/executor/executortype/container/containermgr.go`
Аналогично. Struct `Container` получил `nsCancels`. `AddNamespace` использует `nsCtx.Done()`. `RemoveNamespace` очищает `deplLister`, `deplListerSynced`, `svcLister`, `svcListerSynced`.
---
### Шаг 6: `pkg/executor/multitenant/namespace_subscriber.go`
Добавлен `RemoveFunc` в `NamespaceSubscriberFuncs`:
```go
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
return deregisterNamespace(ctx, logger, record.Name, executorTypes)
},
```
`deregisterNamespace` итерирует все executor types и вызывает `et.RemoveNamespace(ctx, ns)`.
---
### Шаг 7: `pkg/executor/multitenant/ns_watcher.go`
**7a. Добавлен `deregisterNamespace`:**
```go
func deregisterNamespace(ctx, logger, ns, executorTypes) error {
var joinErr error
for _, et := range executorTypes {
if err := et.RemoveNamespace(ctx, ns); err != nil {
joinErr = errors.Join(joinErr, err)
}
}
logger.Info("multitenant.NSWatcher: deregistered namespace", ...)
return joinErr
}
```
**7b. Изменён `StartNSWatcher`:** стратегия `TrackOnly``DispatchRemove`:
```go
config := utils.NewDefaultManagedNamespaceWatcherConfig(...)
config.RemovalStrategy = utils.NamespaceRemovalStrategyDispatchRemove
```
**Почему:** Без `DispatchRemove` `RemoveFunc` подписчика никогда не вызывается. `TrackOnly` вызывает только `DefaultNSResolver().RemoveNamespace()` (что сделано в `HandleWatcherNamespaceRemoval`), но не диспетчирует событие подписчикам.
---
### Шаг 8: `pkg/buildermgr/envwatcher.go`
- Добавлен `nsCancels map[string]context.CancelFunc` в struct `environmentWatcher`
- Инициализирован в `makeEnvironmentWatcher` (там же где `envWatchInformer`)
- `AddNamespace` переключён на per-NS context:
```go
nsCtx, nsCancel := context.WithCancel(ctx)
envw.nsCancels[ns] = nsCancel
factory.Start(nsCtx.Done())
```
- Добавлен `RemoveNamespace(ns string)`:
```go
func (envw *environmentWatcher) RemoveNamespace(ns string) {
if cancel, ok := envw.nsCancels[ns]; ok { cancel(); delete(...) }
delete(envw.envWatchInformer, ns)
}
```
**Ошибка при первой попытке:** replace_string_in_file добавил `nsCancels` с тройным отступом (три таба вместо двух) и без закрывающего `}` struct literal — синтаксическая ошибка компиляции. Исправлено вторым вызовом replace.
---
### Шаг 9: `pkg/buildermgr/pkgwatcher.go`
Аналогично envwatcher:
- `nsCancels` в struct `packageWatcher`
- Инициализация в `makePackageWatcher`
- `AddNamespace` → per-NS ctx для `fissionFactory.Start()` и `podFactory.Start()`
- `RemoveNamespace(ns string)` очищает `pkgInformer[ns]`, `podInformer[ns]`
---
### Шаг 10: `pkg/buildermgr/namespace_subscriber.go`
Добавлены два новых интерфейса:
```go
type builderEnvNamespaceRemover interface {
RemoveNamespace(ns string)
}
type builderPkgNamespaceRemover interface {
RemoveNamespace(ns string)
}
```
Добавлен `RemoveFunc`:
```go
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
deregisterBuilderNamespace(record.Name, envw, pkgw)
return nil
},
```
`deregisterBuilderNamespace` через type assertion вызывает `RemoveNamespace` если интерфейс реализован:
```go
func deregisterBuilderNamespace(namespace string, envw, pkgw) {
utils.DefaultNSResolver().RemoveNamespace(namespace)
if r, ok := envw.(builderEnvNamespaceRemover); ok { r.RemoveNamespace(namespace) }
if r, ok := pkgw.(builderPkgNamespaceRemover); ok { r.RemoveNamespace(namespace) }
}
```
**Почему type assertion:** `builderEnvNamespaceAdder` — интерфейс-параметр функции `NewNamespaceSubscriber`. Вместо добавления `RemoveNamespace` в существующий интерфейс (что сломало бы тестовые фейки) используем опциональный интерфейс через type assertion.
---
### Шаг 11: `pkg/buildermgr/ns_watcher.go`
Стратегия изменена на `DispatchRemove` аналогично executor.
---
### Шаг 12: `pkg/router/httpTriggers.go`
**12a. Добавлен `nsCancels` в struct:**
```go
// nsCancels holds per-namespace context cancel functions for informer lifecycle.
nsCancels map[string]context.CancelFunc
```
**12b. Инициализация в `makeHTTPTriggerSet`:**
```go
nsCancels: make(map[string]context.CancelFunc),
```
**12c. Изменён `AddNamespace`:** per-NS ctx:
```go
nsCtx, nsCancel := context.WithCancel(ctx)
ts.nsCancels[ns] = nsCancel
factory.Start(nsCtx.Done())
k8sCache.WaitForCacheSync(nsCtx.Done(), ...) // тоже nsCtx
```
**12d. Новый метод `RemoveNamespace`:**
```go
func (ts *HTTPTriggerSet) RemoveNamespace(ns string) {
if cancel, ok := ts.nsCancels[ns]; ok { cancel(); delete(...) }
ts.informerMu.Lock()
delete(ts.triggerInformer, ns)
delete(ts.funcInformer, ns)
ts.informerMu.Unlock()
ts.syncTriggers() // немедленно перестраивает routing table без удалённого NS
}
```
**Почему `informerMu.Lock()`:** `HTTPTriggerSet` уже имеет `informerMu sync.RWMutex` для защиты `triggerInformer`/`funcInformer`. Используем его — не добавляем новые мьютексы.
---
### Шаг 13: `pkg/router/namespace_subscriber.go`
Добавлен `routerNamespaceRemover` interface и `RemoveFunc`:
```go
type routerNamespaceRemover interface {
RemoveNamespace(ns string)
}
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
if r, ok := ts.(routerNamespaceRemover); ok {
r.RemoveNamespace(record.Name)
}
return nil
},
```
---
### Шаг 14: `pkg/router/ns_watcher.go`
Стратегия → `DispatchRemove`.
---
### Шаг 15: Тест-фейк `pkg/executor/multitenant/ns_watcher_test.go`
`fakeExecutorType` не реализовывал новый метод → ошибка компиляции:
```
*fakeExecutorType does not implement executortype.ExecutorType (missing method RemoveNamespace)
```
Добавлена заглушка:
```go
func (f *fakeExecutorType) RemoveNamespace(ctx context.Context, ns string) error { return nil }
```
---
## 4. Результат компиляции и тестов
```
go build ./pkg/... ./cmd/... → нет вывода (успех)
go test ./pkg/utils/...
./pkg/executor/...
./pkg/buildermgr/...
./pkg/router/...
ok github.com/fission/fission/pkg/utils
ok github.com/fission/fission/pkg/executor/executortype/newdeploy
ok github.com/fission/fission/pkg/executor/executortype/poolmgr
ok github.com/fission/fission/pkg/executor/fscache
ok github.com/fission/fission/pkg/executor/multitenant
ok github.com/fission/fission/pkg/executor/util
ok github.com/fission/fission/pkg/buildermgr
ok github.com/fission/fission/pkg/router
```
---
## 5. Схема потока при удалении NS (после всех правок)
```
k8s: Namespace label fission.io/managed=true удалён/NS удалён
ManagedNamespaceWatcher (DispatchRemove стратегия)
├─► HandleWatcherNamespaceRemoval()
│ DefaultNSResolver().RemoveNamespace(ns) ← сброс глобального guard
│ manager.DispatchRemove(ctx, ns)
inMemoryNamespaceManager.DispatchRemove()
├─► goroutine: subscriber[executor].OnNamespaceRemove(record)
│ deregisterNamespace(ctx, logger, ns, executorTypes)
│ gpm.RemoveNamespace(ctx, ns)
│ nsCancel() ← останавливает informer goroutines
│ delete(podLister[ns])
│ delete(podListerSynced[ns])
│ poolPodC.RemoveNamespace(ns)
│ delete(envLister[ns])
│ delete(envListerSynced[ns])
│ delete(podLister[ns])
│ delete(podListerSynced[ns])
│ deploy.RemoveNamespace(ctx, ns)
│ nsCancel()
│ delete(deplLister[ns])
│ delete(deplListerSynced[ns])
│ delete(svcLister[ns])
│ delete(svcListerSynced[ns])
│ container.RemoveNamespace(ctx, ns)
│ nsCancel()
│ delete(deplLister[ns])
│ delete(svcLister[ns])
├─► goroutine: subscriber[buildermgr].OnNamespaceRemove(record)
│ deregisterBuilderNamespace(ns, envw, pkgw)
│ DefaultNSResolver().RemoveNamespace(ns) ← повторно (безопасно)
│ envw.RemoveNamespace(ns)
│ nsCancel()
│ delete(envWatchInformer[ns])
│ pkgw.RemoveNamespace(ns)
│ nsCancel()
│ delete(pkgInformer[ns])
│ delete(podInformer[ns])
└─► goroutine: subscriber[router].OnNamespaceRemove(record)
ts.RemoveNamespace(ns)
nsCancel() ← останавливает triggerInf/funcInf goroutines
informerMu.Lock()
delete(triggerInformer[ns])
delete(funcInformer[ns])
informerMu.Unlock()
syncTriggers() ← немедленно убирает routes для удалённого NS
```
---
## 6. Что НЕ было реализовано и почему
**`FunctionServiceCache.DeleteByNamespace(ns string)`** — не реализовано.
Причина: `idleObjectReaper` периодически вызывает `IsValid()` для всех записей. Для удалённого NS k8s API возвращает 404/403 → `IsValid()` вернёт `false` → запись будет удалена reaperом естественным образом. Это создаёт несколько минут "грязных" записей и 404 ошибки в логах, но не влияет на корректность: для удалённого NS новые запросы не придут (router очистил routes), а reaper уберёт старые записи.
Реализация `DeleteByNamespace` потребовала бы добавления namespace-индекса в `byFunction`/`byAddress`/`byFunctionUID` кэшах (нетривиально), или дорогого линейного прохода по всем записям. Не было делать без явного запроса.
---
## 7. Затронутые файлы (17 изменённых)
| Файл | Тип изменения |
|------|---------------|
| `pkg/executor/executortype/executortype.go` | +метод в interface |
| `pkg/executor/executortype/poolmgr/gpm.go` | +поле nsCancels, modify AddNamespace, +RemoveNamespace |
| `pkg/executor/executortype/poolmgr/poolpodcontroller.go` | +RemoveNamespace |
| `pkg/executor/executortype/newdeploy/newdeploymgr.go` | +поле nsCancels, modify AddNamespace, +RemoveNamespace |
| `pkg/executor/executortype/container/containermgr.go` | +поле nsCancels, modify AddNamespace, +RemoveNamespace |
| `pkg/executor/multitenant/namespace_subscriber.go` | +RemoveFunc |
| `pkg/executor/multitenant/ns_watcher.go` | +deregisterNamespace, DispatchRemove |
| `pkg/executor/multitenant/ns_watcher_test.go` | +RemoveNamespace в fakeExecutorType |
| `pkg/buildermgr/namespace_subscriber.go` | +интерфейсы remover, +RemoveFunc, +deregisterBuilderNamespace |
| `pkg/buildermgr/envwatcher.go` | +nsCancels, modify AddNamespace, +RemoveNamespace |
| `pkg/buildermgr/pkgwatcher.go` | +nsCancels, modify AddNamespace, +RemoveNamespace |
| `pkg/buildermgr/ns_watcher.go` | DispatchRemove |
| `pkg/router/httpTriggers.go` | +nsCancels, modify AddNamespace, +RemoveNamespace |
| `pkg/router/namespace_subscriber.go` | +routerNamespaceRemover, +RemoveFunc |
| `pkg/router/ns_watcher.go` | DispatchRemove |
| `doc/FORENSIC_ARCHITECTURE_AUDIT.md` | перемещён из корня (git rename) |
| `doc/console-compat-2026-05-15.md` | создан (отдельная задача) |
---
## 8. Ошибки в процессе
### Ошибка 1: Синтаксическая ошибка в envwatcher.go
**Что случилось:** При попытке заменить блок инициализации struct добавился `nsCancels:` с тройным отступом и без закрывающей `}`:
```
// Стало (неверно):
enableOwnerReferences: utils.IsOwnerReferencesEnabled(),
nsCancels: make(map[string]context.CancelFunc),
err := envWatcher.EnvWatchEventHandlers(ctx)
// ← пропущена } закрывающая struct literal
```
**Причина:** replace_string_in_file не нашёл точное совпадение с нужным whitespace и применил замену частично некорректно.
**Исправление:** второй вызов replace_string_in_file с правильным контекстом (включая соседние строки для однозначного совпадения).
**Вывод компилятора:**
```
pkg/buildermgr/envwatcher.go:117:6: syntax error: unexpected := in composite literal; possibly missing comma or }
```
### Ошибка 2: Тест-фейк не реализует интерфейс
**Что случилось:** После добавления `RemoveNamespace` в interface `ExecutorType`, тест `ns_watcher_test.go` не компилировался:
```
*fakeExecutorType does not implement executortype.ExecutorType (missing method RemoveNamespace)
```
**Исправление:** добавлена заглушка в `fakeExecutorType`.
---
## 9. Инварианты безопасности
1. `nsCancel()` идемпотентен: повторный вызов не паникует (context package гарантирует это)
2. `RemoveNamespace("")` защищён early return во всех реализациях
3. `deregisterBuilderNamespace` через type assertion — безопасно если интерфейс не реализован (просто пропускает)
4. `routerNamespaceRemover` через type assertion в router subscriber — аналогично
5. Goroutines informer factories останавливаются асинхронно после `cancel()` — это нормально, k8s client-go гарантирует graceful shutdown при отмене контекста
6. После `RemoveNamespace` и до следующего `AddNamespace` — любые события от k8s для этого NS будут проигнорированы (informers остановлены, listers удалены)
-279
View File
@@ -1,279 +0,0 @@
# Forensic Architecture Audit: Fission Fork (multitenant, May 2026)
> Актуальная редакция. Легаси-версия: `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md`
> Обновлено: 2026-05-18 после реализации namespace lifecycle hardening.
---
## Статус исправлений
| Риск | Статус | Коммит |
|------|--------|--------|
| Informer goroutine/FD leak при TrackOnly removal | ✅ ЗАКРЫТ | `4eedf95f` |
| Router stale routes при повторном добавлении NS | ✅ ЗАКРЫТ | `4eedf95f` |
| Executor dedup dirty state при re-add NS | ✅ ЗАКРЫТ | `4eedf95f` |
| Синхронный subscriber dispatch (onboarding latency) | ✅ ЗАКРЫТ | предыдущая сессия |
| `DefaultNSResolver` только append (нет RemoveNamespace) | ✅ ЗАКРЫТ | предыдущая сессия |
| Stuck-failed namespace без auto-recovery | ✅ ЗАКРЫТ | `919e8439` |
| AdoptExistingResources race при rolling update | ✅ ЗАКРЫТ | `2a7d6101` |
| No Explicit State Machine (implicit phase transitions) | ⚠️ СМЯГЧЕНО | `919e8439` |
| Sharded mutex (bottleneck при >500 concurrent tenant) | ⏳ BACKLOG | не актуально при текущей нагрузке |
---
## Architectural Decisions (реально принятые)
- **Dynamic Namespace Discovery**: Механизм динамического обнаружения и подключения tenant-namespace через label `fission.io/managed=true` (`pkg/utils/namespace_manager.go`, `pkg/executor/multitenant/ns_watcher.go`).
- **Namespace Lifecycle Management**: Жизненный цикл namespace централизован через интерфейс `NamespaceManager` с подписчиками (executor, router, buildermgr).
- **Decoupled Registration**: Каждый компонент подписывается как `NamespaceSubscriber` и реализует свою логику инициализации/чистки ресурсов.
- **Backward Compatibility**: Поддержка статического списка через env (`FISSION_RESOURCE_NAMESPACES`) с динамическим расширением.
- **No-Restart Onboarding**: Добавление tenant не требует рестарта pod-ов.
- **RBAC/SA Provisioning**: Автоматическое создание SA и RBAC для новых namespace (`EnsureNamespaceSA`).
- **Informer Factories Per Namespace**: Отдельная informer factory для каждого NS, с per-NS context cancellation.
- **Explicit Namespace Removal Strategy**: `DispatchRemove` — при удалении NS вызываются `RemoveFunc` у всех подписчиков, останавливаются informer-ы через `context.CancelFunc`.
- **Parallel Subscriber Dispatch**: Подписчики вызываются параллельно через `errgroup` — onboarding не блокируется медленным SA provisioning.
---
## Core Complexity Centers
- **NamespaceManager & Watcher**: Центр всей динамики — координация событий, фаз, подписчиков.
- **ExecutorType Subsystems**: Poolmgr, NewDeploy, Container — каждый хранит собственный per-NS кэш, lister-ы, логику adoption и reaping.
- **Informer Lifecycle**: Динамическое создание/остановка informer-ов через per-NS `context.CancelFunc`. Чистка `envLister[ns]`/`deplLister[ns]`/`triggerInformer[ns]` при `RemoveNamespace`.
- **FunctionServiceCache**: Кэширование и lifecycle function pod-ов, синхронизация с событиями из разных источников. **Не очищается при RemoveNamespace**`idleObjectReaper` убирает устаревшие записи через `IsValid()` check.
---
## Hidden Coupling & Accidental Complexity
- **Implicit Contract**: Все компоненты обязаны реализовывать `NamespaceSubscriber` симметрично (и `AddFunc`, и `RemoveFunc`). Нарушение → silent drift.
- **Global vs Local State**: Глобальный `DefaultNSResolver` + локальные lister-ы в каждом executor type. `RemoveNamespace` в NSResolver и в каждом executor type должны быть вызваны согласованно.
- **Deduplication Responsibility**: `AddNamespace` дедупликация — через `DefaultNSResolver().AddNamespace()` возвращающий `bool`, и через проверку локального lister-а (`envLister[ns] != nil`). После `RemoveNamespace` оба guard сбрасываются → re-add корректно создаёт новые informer-ы.
- **Event Handler Ordering**: Порядок подписчиков в `Subscribe` влияет на side-effects, но `errgroup` делает их параллельными — ordering больше не определяет latency, но всё ещё влияет на приоритет ошибок.
- **RBAC Drift**: Provisioning SA/RBAC в `registerNamespace`, cleanup — в `deregisterNamespace`. При сбое cleanup — dangling SA/ClusterRoleBinding.
---
## Workaround-Driven Decisions
- ~~**Track-Only Removal**~~ → **ЗАМЕНЕНО** на `DispatchRemove` — cleanup вызывается всегда.
- **Manual Adoption**: При старте executor-ы делают adopt orphaned ресурсов. Закрыто: `PreRegisterManagedNamespaces` обеспечивает полный NS snapshot до adopt/cleanup (§1.3).
- **Explicit Reaper Loops**: `idleObjectReaper` чистит `FunctionServiceCache` вместо event-driven подхода. Приемлемо: `IsValid()` check достаточен при корректной работе per-NS informer-ов.
---
## Fragile Operational Components
- **RBAC/SA Drift**: Неконсистентность между созданием и удалением SA/ролей при сбое в `deregisterNamespace`.
- **Cache Invalidation**: `FunctionServiceCache` не очищается при `RemoveNamespace` — расчёт на `idleObjectReaper`. При высоком churn rate может накапливать stale записи быстрее, чем reaper убирает.
- **Adoption Race**: ~~`AdoptExistingResources` vs `namespace_subscriber` — активная проблема~~ — закрыто: `PreRegisterManagedNamespaces` перед adopt/cleanup (`2a7d6101`).
- **Stuck Failed Phase**: ~~Namespace в `failed` не восстанавливается без рестарта~~ — закрыто: `RunReconciler` + полная цепочка error propagation (§1.4).
---
## Poor Scalability Risks
- **Informer Explosion**: ~15002000 goroutine при 100 tenant (см. §2). **Частично смягчено**: goroutine-ы корректно останавливаются при `RemoveNamespace` — нет накопления при churn. Но в steady-state 100 NS — линейный рост горутин остаётся.
- ~~**Synchronous Dispatch**~~ → **ИСПРАВЛЕНО**: параллельный dispatch через `errgroup`.
- **Centralized Locking**: Глобальный mutex на NamespaceManager. При текущей нагрузке (<50 ns) — не узкое место. При >500 concurrent tenant — backlog (sharded mutex, §5).
- **Thundering Herd на resync**: 100 NS × 5 informer-типов × LIST каждые 30 мин — 500 concurrent LIST к API.
---
## Future Maintenance Problems
- **Hidden State Machines**: Фазы namespace реализованы неявно — сложно дебажить stuck state. Нет формализованной машины состояний с explicit transitions.
- **Implicit Error Handling**: Ошибки в `deregisterNamespace` логируются, но NS может остаться в некорректном состоянии. Нет `NamespaceCondition` на k8s-объекте.
- **Contract Drift**: Изменение интерфейса `NamespaceSubscriber` (например, добавление `ResyncFunc`) требует синхронного обновления всех компонентов.
- **FunctionServiceCache без per-NS cleanup**: если `idleObjectReaper` будет отключён/изменён — stale cache может накапливаться.
---
## Risky / Hard-to-Maintain Decisions
| Решение | Статус | Примечание |
|---------|--------|------------|
| Informer Lifecycle Management | ✅ Hardened | per-NS context cancel + RemoveNamespace во всех компонентах |
| Centralized Mutex | ⚠️ Приемлемо | sharding в backlog, не актуально до >500 NS |
| Manual Adoption | ✅ Закрыто | race при rolling update (`2a7d6101`) |
| No Explicit State Machine | ✅ Частично закрыто | stuck-failed закрыт (`919e8439`); явная state machine в backlog |
| Eventual Consistency | ⚠️ Смягчено | параллельный dispatch уменьшает окно, но не устраняет |
---
## Multi-Tenancy, Isolation, Orchestration, Lifecycle, State, Reconciliation
- **Multi-Tenancy**: Label-based discovery, каждый tenant — отдельный namespace, изоляция на уровне k8s.
- **Isolation Model**: Namespace-level isolation, per-NS SA/RBAC, per-NS informer factory.
- **Lifecycle Management**: Фазы (discovered → registering → active → deregistering → removed / failed) реализованы. Auto-recovery из failed работает через `RunReconciler`. Явная state machine в backlog.
- **State Handling**: Глобальный `DefaultNSResolver` + локальные lister-ы. После `RemoveNamespace` — оба синхронизованы. После re-add — оба корректно инициализируются заново.
- **Reconciliation Logic**: Каждый компонент через subscribe. Отсутствует reconcile-очередь для failed state.
- **Operational Burden**: Средний — goroutine leak устранён, stale informer устранён, stuck-failed закрыт. Требуется мониторинг: orphaned SA/RBAC при неудачном deregister.
---
## Engineering Maturity
- **Maturity**: Архитектурно зрелый, хорошо документированный, с явным reasoning и поэтапным внедрением.
- **Complexity**: Высокая в синхронизации и lifecycle. Снижена за счёт формализации `RemoveNamespace` контракта.
- **Maintainability**: Среднесрочная — без явной state machine сложность будет расти. Auto-recovery из failed работает.
- **Production-Grade**: Близко — informer lifecycle корректен, dispatch параллелен, cleanup симметричен, stuck-failed закрыт, AdoptExistingResources race закрыт.
---
# Deep Risk Analysis (актуальная, May 2026)
---
## 1. Сценарии отказа
### 1.1 Informer Lifecycle Management — ✅ ЗАКРЫТ
**Что было:** relabel-цикл NS создавал phantom-состояние: informer-ы не останавливались при track-only removal, `DefaultNSResolver` не очищал запись → re-add возвращал `false` → новые informer-ы не создавались.
**Что сделано (коммит `4eedf95f`):**
- `RemoveNamespace(ns)` добавлен в интерфейс `ExecutorType` и реализован в poolmgr, newdeploy, container.
- В каждом executor type: per-NS context cancel (`nsCancels map[string]context.CancelFunc`). `AddNamespace` создаёт `nsCtx, nsCancel := context.WithCancel(ctx)`, передаёт `nsCtx` в `factory.Start()`. `RemoveNamespace` вызывает `nsCancel()` и удаляет lister-ы из карт.
- Router: `HTTPTriggerSet.RemoveNamespace()` отменяет per-NS ctx, удаляет `triggerInformer[ns]`/`funcInformer[ns]` под `informerMu.Lock()`, вызывает `syncTriggers()`.
- Buildermgr: `envWatcher.RemoveNamespace()` и `pkgWatcher.RemoveNamespace()` — аналогично.
- `DefaultNSResolver.RemoveNamespace(ns)` удаляет NS из глобального map → re-add корректно проходит guard.
- Стратегия `DispatchRemove` во всех 3 компонентах → `RemoveFunc` вызывается при удалении NS.
**Текущий статус:** informer goroutine/FD корректно останавливаются; re-add NS создаёт чистые informer-ы; router не видит stale routes.
---
### 1.2 Centralized Mutex — ⚠️ ПРИЕМЛЕМО
**Сценарий:** высокая churn + concurrent Snapshot.
`dispatch()` отпускает mutex перед вызовом каждого subscriber, берёт снова для следующего. При батч-онбординге 10+ NS параллельно: конкуренция за mutex, latency spike на `Snapshot()` в `idleObjectReaper`.
**Смягчено:** `dispatch()` теперь параллельный (errgroup) — подписчики не вызываются последовательно, время блокировки mutex между подписчиками устранено. `Snapshot()` конкурирует только с `Upsert` — при текущей нагрузке (<50 NS) практически нет.
**Остаётся:** при >500 concurrent tenant с >1 onboarding/sec — sharded mutex даст выигрыш. В backlog.
---
### 1.3 Manual Adoption (AdoptExistingResources) — ✅ ЗАКРЫТ (коммит `2a7d6101`)
**Сценарий: гонка adoption vs watcher при старте**
**Что было:** `AdoptExistingResources` и `CleanupOldExecutorObjects` запускались до `StartNSWatcher`. `DefaultNSResolver().Snapshot()` возвращал только статические NS из `FISSION_RESOURCE_NAMESPACES` → managed NS не покрывались:
- Pods от предыдущего executor в managed NS не adoptировались (сохраняли старый `instanceID`) → poolmgr создавал новые pool pods → cold start.
- Старые RS/deployments в managed NS не чистились → накапливались.
**Что сделано:** `multitenant.PreRegisterManagedNamespaces(ctx, logger, kubernetesClient)` — синхронный `Namespaces.List` с label `fission.io/managed=true` вызывается в `executor.go` **до** goroutines adopt+cleanup. Добавляет все managed NS в `DefaultNSResolver`. Идемпотентен с последующим `AddFunc` из watcher. Не ломает при ошибке API (warn + proceed).
**End-to-end после фикса:**
1. `PreRegisterManagedNamespaces``DefaultNSResolver` содержит static + managed NS
2. `AdoptExistingResources` → патчит pods в managed NS с новым `instanceID`
3. `CleanupOldExecutorObjects` / `GetReaperNamespace()` → видит managed NS → чистит стale объекты
4. `StartNSWatcher``AddFunc` срабатывает для тех же NS — `DefaultNSResolver().AddNamespace()` idempotent, `AddNamespace` executor types dedup-protected
---
### 1.4 No Explicit State Machine — ✅ ЗАКРЫТ (коммит `919e8439`)
**Сценарий: stuck в `failed` без auto-recovery**
**Что было:** `EnsureNamespaceSA` и `registerNamespace` были void-функциями — ошибки только логировались, до `MarkPartFailed` не доходили. Executor subscriber всегда возвращал nil → namespace никогда не попадал в `NamespacePhaseFailed``RunReconciler` для executor был мёртвым кодом.
**Что сделано:**
- `setupSAAndRoleBindings` → возвращает `error`
- `EnsureNamespaceSA` → возвращает `error`, пробрасывает
- `registerNamespace` → возвращает `error` (SA + executorTypes) с `fmt.Errorf` wrapping
- Executor `AddFunc`/`ResyncFunc` → пробрасывают ошибку вместо `return nil`
- `RunReconciler` → принимает `*zap.Logger`, логирует каждый retry и исход
**End-to-end flow:**
1. `EnsureNamespaceSA` fails (k8s 503) → `registerNamespace` returns error
2. Executor AddFunc returns error → `dispatch()``MarkPartFailed("executor")`
3. `deriveNamespacePhase``NamespacePhaseFailed`
4. `RunReconciler` tick (30s) находит namespace → `DispatchResync` → retry
5. Если API восстановился: `MarkPartActive``NamespacePhaseActive` → лог `resync succeeded`
**Накопление при churn:** ликвидировано — failed NS автоматически выходят из этой фазы при восстановлении API.
**Ограничение:** нет max-retries. Namespace, у которого SA создать принципиально невозможно (например, удалённый k8s namespace), будет ретраиться вечно. Приемлемо на текущем масштабе.
---
### 1.5 Eventual Consistency — ⚠️ СМЯГЧЕНО
**Сценарий:** HTTPTrigger создан в окне до готовности informer.
**Было:** последовательный dispatch → если executor делал SA provisioning 1030 сек, router не начинал `WaitForCacheSync`. Trigger, созданный в этом окне, пропускался до следующего resync (30 мин).
**Смягчено:** параллельный dispatch через errgroup → router и executor стартуют `AddNamespace` одновременно. Окно уязвимости = время `WaitForCacheSync` в router (~2–5 сек), а не время SA provisioning (~30 сек).
**Остаётся:** trigger, созданный за 2–5 сек до `WaitForCacheSync` в router → нормально обрабатывается через `AddFunc` после sync. Фактически проблема устранена для практических сценариев.
---
## 2. Анализ при 50100 tenant с churn 10 ns/час
### Informer Count (steady-state)
При 100 активных tenant:
- **Poolmgr**: 2 factory × 100 NS × ~35 goroutine = **6001000 goroutine**
- **NewDeploy**: аналогично ~6001000 goroutine
- **Router**: 1 factory × 100 NS × ~2 goroutine = **200 goroutine**
- **Buildermgr**: ~200 goroutine
Итого: **~16002400 goroutine** от informer-ов. **Линейный рост с числом NS — неизбежен при текущей архитектуре.**
**Что изменилось после hardening:** при churn goroutine-ы корректно останавливаются при `RemoveNamespace` — нет накопления мёртвых goroutine. Steady-state = ~O(active_NS), а не O(total_NS_ever_seen).
### Thundering Herd на resync
100 NS × 5 informer-типов × LIST каждые 30 мин = **500 concurrent LIST** к Kubernetes API. Не изменилось, не исправлено.
### Stuck Failed Accumulation — ✅ ЗАКРЫТ
Failed NS автоматически ретраятся `RunReconciler` каждые 30с и выходят из `failed` при восстановлении API. Накопления больше не происходит.
### AdoptExistingResources Race — ✅ ЗАКРЫТ
`PreRegisterManagedNamespaces` синхронно добавляет managed NS в `DefaultNSResolver` до adopt/cleanup. Старые pods adoptируются, stale объекты чистятся. Подробно — §1.3.
---
## 3. Рекомендации (приоритизированные)
### P1 — Reconcile-очередь для failed NS — ✅ ЗАКРЫТ (`919e8439`)
Error propagation исправлена во всей цепочке: `setupSAAndRoleBindings``EnsureNamespaceSA``registerNamespace` → executor subscriber. `RunReconciler` логирует retry и исход.
### P2 — AdoptExistingResources после BootstrapAndDispatch — ✅ ЗАКРЫТ (`2a7d6101`)
`PreRegisterManagedNamespaces` вызывается синхронно до adopt/cleanup. Делает один `Namespaces.List(label=fission.io/managed=true)` → добавляет все managed NS в `DefaultNSResolver`. После этого adopt и cleanup покрывают полный tenant NS set.
**Влияние:** устранены orphaned pods при холодном старте и resource leak (stale RS/deployments).
### P3 — NamespaceCondition на k8s Namespace объекте
Пометить Namespace через `kubectl annotate` или через status subresource при failed phase → оператор видит причину без чтения логов.
### Backlog — Sharded mutex
Актуально при >500 concurrent tenant с >1 onboarding/sec. Технически feasible без breaking interface change (см. `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`).
---
## 4. FunctionServiceCache — текущий инвариант
`FunctionServiceCache` (`fsCache` в gpm и newdeploy) **не очищается** при `RemoveNamespace`. Это осознанное решение:
- `idleObjectReaper` периодически вызывает `fsCache.ListOldForPool()` → для каждой записи проверяет `podLister[ns]` → если NS удалён, `podLister[ns]` == nil → pod не найден → запись считается expired → `fsCache.DeleteEntry()`.
- Временной лаг = интервал reaper-а (по умолчанию ~1 мин). При высоком churn возможно накопление stale записей, но они не вызывают функциональных ошибок (только небольшой overhead на reaper iteration).
**Когда станет проблемой:** при отключении/изменении reaper-а или при >10 000 stale записей (O(n) iteration).
---
## 5. Sharded Mutex — вердикт
**Технически реализуемо** без breaking interface change. Полный код — в `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`.
**Вердикт:** не оправдано при текущей нагрузке. Реальный bottleneck — AdoptExistingResources race — закрыт (`2a7d6101`). Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.
@@ -1,385 +0,0 @@
# Forensic Architecture Audit: Fission Fork (feature/multitenant, May 2026)
---
## Architectural Decisions (реально принятые)
- **Dynamic Namespace Discovery**: Введён механизм динамического обнаружения и подключения tenant-namespace через label `fission.io/managed=true` (см. `pkg/utils/namespace_manager.go`, `pkg/executor/multitenant/ns_watcher.go`).
- **Namespace Lifecycle Management**: Весь жизненный цикл namespace теперь централизован через интерфейс `NamespaceManager` с подписчиками (executor, router, buildermgr).
- **Decoupled Registration**: Каждый компонент (executor, router, buildermgr) подписывается как subscriber и реализует свою логику инициализации/чистки ресурсов при появлении/удалении namespace.
- **Backward Compatibility**: Сохраняется поддержка статического списка через env (`FISSION_RESOURCE_NAMESPACES`), но теперь он расширяется динамически.
- **No-Restart Onboarding**: Добавление нового tenant не требует рестарта pod-ов — watcher реагирует на label, триггерит регистрацию во всех подсистемах.
- **RBAC/SA Provisioning**: Автоматическое создание service account и RBAC для новых namespace (см. `EnsureNamespaceSA`).
- **Informer Factories Per Namespace**: Для каждого нового namespace создаются отдельные informer factory для CRD и core-ресурсов.
- **Explicit Namespace Removal Strategy**: Поддержка двух стратегий удаления: track-only (по умолчанию) и dispatch-remove (с вызовом OnNamespaceRemove у подписчиков).
## Core Complexity Centers
- **NamespaceManager & Watcher**: Центр всей динамики — сложная координация событий, фаз, подписчиков, race-conditions.
- **ExecutorType Subsystems**: Poolmgr, NewDeploy, Container — каждый хранит собственное состояние, кэш, логику adoption и reaping.
- **Informer Lifecycle**: Динамическое создание/удаление informer-ов на лету для каждого namespace.
- **FunctionServiceCache**: Кэширование и lifecycle function pod-ов, синхронизация с событиями из разных источников.
## Hidden Coupling & Accidental Complexity
- **Implicit Contract**: Все компоненты обязаны корректно реализовать NamespaceSubscriber — нарушение приводит к silent drift.
- **Global vs Local State**: Есть глобальный NamespaceResolver и локальные состояния в каждом executor type — возможны рассинхронизации.
- **Deduplication Responsibility**: Deduplication namespace размазан между глобальным резолвером и локальными структурами.
- **Event Handler Ordering**: Порядок подписчиков влияет на фазу и side-effects, но не гарантируется явно.
- **RBAC Drift**: Provisioning SA/RBAC делается в одном месте, но cleanup — в другом, возможны dangling ресурсы.
## Iterative Growth
- **Layered Refactor**: Ветка развивается через серию малых шагов (см. doc/thinking/2026-04-26-namespace-manager-step*.md), каждый шаг — отдельный инвариант.
- **Hybrid Model**: Некоторое время coexist старый статический и новый динамический pipeline, с явным разделением путей.
- **Feature Flags via Env**: Многое управляется через env-переменные, что позволяет поэтапно включать/выключать новые механики.
## Workaround-Driven Decisions
- **Track-Only Removal**: По умолчанию удаление namespace не вызывает cleanup в подписчиках — workaround против race-condition при массовых удалениях.
- **Manual Adoption**: При старте executor-ы делают adopt orphaned ресурсов (pods, deployments) — workaround для несовершенного lifecycle.
- **Explicit Reaper Loops**: Для чистки orphaned объектов используются отдельные циклы (object reaper), а не event-driven подход.
## Fragile Operational Components
- **Informer Factory Lifecycle**: Ошибки в динамическом создании/удалении informer-ов приводят к memory leak или stale watchers.
- **RBAC/SA Drift**: Неконсистентность между созданием и удалением сервисных аккаунтов и ролей.
- **Cache Invalidation**: FunctionServiceCache может рассинхронизироваться при сбоях в event flow.
- **Adoption Loops**: AdoptExistingResources может не покрыть все edge-case, особенно при race между startup и watcher.
## Poor Scalability Risks
- **Informer Explosion**: На сотнях/тысячах namespace число informer-ов и goroutine растёт линейно, возможен memory/FD exhaustion.
- **Synchronous Dispatch**: Все подписчики вызываются синхронно, при долгой инициализации одного — блокируются остальные.
- **Centralized Locking**: NamespaceManager держит глобальный mutex на все операции — bottleneck при высокой churn rate.
- **No Sharding**: Нет горизонтального масштабирования NamespaceManager — всё в одном процессе.
## Future Maintenance Problems
- **Hidden State Machines**: Фазы namespace и частей (part state) реализованы неявно, без явной state machine — сложно дебажить stuck state.
- **Implicit Error Handling**: Ошибки в подписчиках часто логируются, но не эскалируются — возможна silent failure.
- **Contract Drift**: Любое изменение интерфейса NamespaceSubscriber требует синхронного обновления всех компонентов.
- **Complex Test Surface**: Много интеграционных точек, сложно покрыть тестами все сценарии гонок и отказов.
## Deepest Upstream Divergence
- **Полная замена статической модели discovery на динамическую через watcher и NamespaceManager.**
- **Весь lifecycle tenant-namespace теперь event-driven, а не env-driven.**
- **Введён централизованный интерфейс подписки на события namespace для всех core-компонентов.**
- **Механика adopt orphaned ресурсов и явная поддержка rollback/cleanup.**
## Surprisingly Mature Parts
- **Интерфейс NamespaceManager**: Чётко выделен, покрыт тестами, поддерживает snapshot, summary, phase tracking.
- **Event Handler Abstraction**: Все watcher-ы используют единый event handler contract, легко расширять.
- **Backward Compatibility Layer**: Старый pipeline не сломан, coexist с новым.
- **Документация и коммиты**: Подробные шаги, объяснения, reasoning — видно зрелый инженерный подход.
## Risky / Hard-to-Maintain Decisions
- **Informer Lifecycle Management**: Очень сложно гарантировать отсутствие leak/stale при динамике.
- **Centralized Mutex**: Один mutex на NamespaceManager — риск блокировок.
- **Manual Adoption**: AdoptExistingResources — временное решение, не покрывает все сценарии.
- **No Explicit State Machine**: Фазы и переходы не формализованы, возможны stuck state.
- **Eventual Consistency**: Нет гарантии моментальной консистентности между компонентами.
---
## Multi-Tenancy, Isolation, Orchestration, Lifecycle, State, Reconciliation
- **Multi-Tenancy**: Реализовано через label-based discovery, каждый tenant — отдельный namespace, все ресурсы изолированы на уровне k8s.
- **Isolation Model**: Namespace-level isolation, автоматическое создание SA/RBAC, informer-ы и кэш на каждый tenant.
- **Orchestration**: NamespaceManager + подписчики — централизованный event bus для всех core-компонентов.
- **Lifecycle Management**: Поддержка всех фаз (discovered, registering, active, deregistering, removed, failed), но state machine неявная.
- **State Handling**: Гибрид глобального и локального состояния, возможны рассинхронизации.
- **Reconciliation Logic**: Каждый компонент реализует свою reconcile-логику через подписку на события.
- **Controller Complexity**: Высокая, много слоёв абстракции, много точек гонок.
- **Deployment Reproducibility**: Helm-чарты поддерживают все новые env, backward compatibility сохранён.
- **Operational Burden**: Высокий — требуется мониторинг leak, race, orphaned ресурсов, ручной контроль за adoption.
---
## Engineering Maturity, Complexity, Maintainability Horizon
- **Maturity**: Архитектурно зрелый, хорошо документированный, с явным reasoning и поэтапным внедрением.
- **Complexity**: Высокая, особенно в динамике и синхронизации между компонентами.
- **Maintainability**: Среднесрочная — без явной state machine и горизонтального масштабирования возможны проблемы при росте нагрузки.
- **Production-Grade**: Ближе к production-grade platform engineering, чем к эксперименту, но требует доработки по масштабированию и явной формализации state transitions.
---
## Architectural Drift / Entropy / Hazards
- **Drift**: Возможен drift между глобальным и локальным состоянием, если подписчики реализованы несимметрично.
- **Entropy**: Много точек входа, implicit contract, нет явной state machine — сложность будет расти.
- **Hazards**: Memory leak, race-condition, orphaned ресурсы, silent failure при ошибках в подписчиках.
---
## Summary
Этот форк — зрелая попытка перевести Fission на event-driven multi-tenant архитектуру с динамическим discovery и централизованным lifecycle management. Основные сложности и риски — в управлении состоянием, синхронизации и масштабируемости. Требует дальнейшей формализации state machine, горизонтального масштабирования и усиления тестового покрытия для production-grade эксплуатации.
---
# Deep Risk Analysis (May 2026)
> Конкретные сценарии отказа, оценка при 50–100 tenant, предложения по исправлению.
---
## 1. Сценарии отказа для каждого "Risky Decision"
### 1.1 Informer Lifecycle Management
**Сценарий: повторная регистрация namespace через relabel**
1. Оператор снимает label `fission.io/managed=true` с namespace `tenant-42`.
2. Namespace-watcher вызывает `HandleWatcherNamespaceRemoval()`. Стратегия `TrackOnly`: NamespaceManager помечает запись как `removed` и **не вызывает** `OnNamespaceRemove` у подписчиков.
3. Informer-ы executor (gpm, newdeploy) и router продолжают работать — pool для tenant-42 жив, функции маршрутизируются.
4. Оператор возвращает label — kubernetes генерирует `MODIFIED`-событие.
5. `RunManagedNamespaceWatcher` (resync 30 мин) может не вызвать Add снова для уже известного NS.
6. **Router**: `AddNamespace` вызывает `DefaultNSResolver().AddNamespace(ns)`. Глобальный resolver уже содержит tenant-42 (его никто не удалял из-за track-only) → возвращает `false` → router делает **early return без создания новых informer-ов** (строка 460 `httpTriggers.go`). Router считает namespace активным (старые informer-ы ещё работают) — но если они были остановлены контекстом — тихое 404.
7. **Executor**: `gpm.AddNamespace` проверяет `poolPodC.envLister[ns]` — если старый lister жив, возвращает nil сразу (дедупликация). Всё выглядит нормально, но фактически используются **устаревшие informer-ы** с застрявшим кэшем.
**Итог**: relabel-цикл создаёт phantom-состояние: компоненты думают что NS активен, но его lifecycle разорван.
---
### 1.2 Centralized Mutex
**Сценарий: высокая churn + concurrent Snapshot**
`dispatch()` снимает write-lock перед вызовом каждого subscriber-а, затем берёт его снова для следующего. Структура:
```
mu.Lock() → читаем список subs →
mu.Unlock() → вызываем handler(sub1) [k8s API call, может занять сотни мс]
mu.Lock() → читаем следующий sub →
mu.Unlock() → вызываем handler(sub2)
```
Параллельно: router каждые 20 мс делает `syncTriggers()``updateRouter()` → итерирует `snapshotFuncInformers()` → берёт `informerMu.RLock`. Это другой mutex, но `DefaultNSResolver().Snapshot()` вызывается из `idleObjectReaper` каждые 5 сек под глобальным `RWMutex` NamespaceManager.
При 100 tenant с churn 10 ns/час: в среднем каждые 6 мин добавляется namespace. Само по себе безвредно. Но при пике (батч-онбординг 10 tenant за 1 минуту): `dispatch()` держит write-lock с паузами на unlock/relock для каждого subscriber × 10 параллельных dispatch → конкуренция за mutex возрастает. `Snapshot()` в `idleObjectReaper` (каждые 5 сек) и в `AdoptExistingResources` (каждый рестарт) будут ждать.
**Итог**: не deadlock, но latency spike на Snapshot на старте и при батч-онбординге — 200–500 мс при 10+ concurrent dispatch.
---
### 1.3 Manual Adoption (AdoptExistingResources)
**Сценарий: гонка adoption vs watcher**
1. Executor стартует. `AdoptExistingResources` запускается, берёт `DefaultNSResolver().Snapshot()` — snapshot содержит только статические NS из `FISSION_RESOURCE_NAMESPACES`.
2. Параллельно запускается `RunManagedNamespaceWatcher`. Watcher вызывает `BootstrapAndDispatch()`, который регистрирует managed NS и вызывает `registerNamespace()` у executor-подписчика.
3. `registerNamespace()` вызывает `DefaultNSResolver().AddNamespace(ns)` (глобальный guard), затем `gpm.AddNamespace()`.
4. **Но `AdoptExistingResources` уже завершила свой loop** — managed NS не попал в snapshot. Orphaned pods в tenant NS не приняты.
5. Функции в этих pod-ах будут вызываться ещё раз через cold start — лишний latency spike и потеря статуса `instanceID` у подов (старый instanceID в annotation не перезаписан → `CleanupOldExecutorObjects` сочтёт их orphaned → удалит).
**Hardcoded 30s timeout**: `AdoptExistingResources` в poolmgr не имеет явного timeout, но `k8sCache.WaitForCacheSync` в `Run()` блокирует до готовности — только после этого запускается `service()`. Если namespace watcher опередил, poolmgr получит env-события до того как `AdoptExistingResources` завершится → гонка на `gpm.pools` map (не защищена mutex вне `service()` goroutine).
---
### 1.4 No Explicit State Machine
**Сценарий: stuck в `failed` без auto-recovery**
1. Namespace `tenant-99` помечен `fission.io/managed=true`.
2. `registerNamespace()` вызывает `EnsureNamespaceSA()` — Kubernetes API momentarily unavailable (503).
3. `EnsureNamespaceSA()` возвращает ошибку → вызывающий код (предположительно) пишет в лог и помечает часть как `NamespacePartStateFailed`.
4. `deriveNamespacePhase()` выставляет namespace в `NamespacePhaseFailed`.
5. **Нет reconcile-цикла**: нет горутины, которая периодически проверяет failed namespace и пытается повторить. Phase останется `failed` до рестарта процесса.
6. Router был вызван следующим в цепочке dispatch. Т.к. dispatch вызывается подписчики последовательно без barrier, router **уже создал свои informer-ы** до того как executor завершился с ошибкой.
7. **Dirty state**: router видит `tenant-99` как активный (informer-ы есть), executor — нет (SA/RBAC не создан). Любой вызов функции из tenant-99 → executor не может специализировать pod (нет fetcher SA) → 503.
Лог покажет ошибку, но namespace останется в `failed` навсегда (до рестарта). Оператор не получит никакого k8s-статуса — ни condition на Namespace объекте, ни event.
---
### 1.5 Eventual Consistency
**Сценарий: HTTPTrigger создан в окне до ready informer**
1. Tenant создаёт namespace с label → namespace добавляется в NamespaceManager.
2. `dispatch()` вызывает router subscriber → `AddNamespace()`:
```go
k8sCache.WaitForCacheSync(ctx.Done(), triggerInf.HasSynced, funcInf.HasSynced)
ts.syncTriggers()
```
Router ждёт sync и перестраивает роутинг. Это занимает несколько секунд.
3. Tenant **немедленно** после создания namespace создаёт HTTPTrigger через API.
4. Если trigger создан **до** завершения `WaitForCacheSync` в router → informer ещё не синхронизирован, но trigger уже в etcd.
5. После sync informer получит это событие через `AddFunc` → `syncTriggers()`. Это нормально.
6. **Проблема в другом**: `dispatch()` вызывает подписчиков **последовательно**. Если executor (первый в списке) занимается `EnsureNamespaceSA` + `registerExecutorTypes` (10–30 сек при медленном API) → router subscriber не вызывается всё это время. HTTPTrigger, созданный в этом окне, попадёт в informer, но router ещё не начал слушать → `AddFunc` для этого trigger не вызовется никогда (resync через 30 мин).
7. Результат: trigger существует в etcd, но **отсутствует в роутере 30 минут**.
---
## 2. Анализ при 50100 tenant с churn 10 ns/час
### Informer Explosion
При 100 активных tenant:
- **Executor (poolmgr)**: 1 `SharedInformerFactory` (Fission CRD) + 1 `SharedInformerFactory` (k8s pods/RS) на NS = 200 factory. Каждая factory запускает горутины на каждый informer (~35 горутин). **~6001000 goroutine** только от poolmgr.
- **Executor (newdeploy)**: аналогично — ещё 200 factory, ~600 goroutин.
- **Router**: 1 factory на NS = 100 factory, ~200 goroutин.
- **buildermgr**: 1 factory на NS = 100 goroutин.
Итого: **~15002000 goroutine** только от informer-ов. При пике churn (10 ns/час) — каждые 6 минут добавляется NS, создаётся ~20 новых горутин, они не убираются при track-only removal.
При **100 NS × 30 мин resync**: каждые 30 мин каждый informer делает LIST всех объектов в своём NS. 100 × 5 informer-типов × LIST = **500 concurrent LIST-запросов** к Kubernetes API раз в 30 минут — возможный thundering herd.
### Stuck Failed State
10 ns/час churn с 1% API error rate = ~2.4 failed namespace/сутки. Каждый остаётся в `failed` навсегда. За 30 дней = ~72 "мёртвых" записи в NamespaceManager. `Snapshot()` возвращает их в `idleObjectReaper` → лишние LIST к k8s API для несуществующих/неактивных NS → ошибки, логи, load.
### AdoptExistingResources Race
Каждый рестарт executor-а — race. При rolling update в k8s (новый pod стартует, старый ещё жив): оба executor-а параллельно делают `AdoptExistingResources` → оба патчат `instanceID` на одних и тех же pod-ах → `CleanupOldExecutorObjects` нового экземпляра удаляет pod-ы старого (ожидаемо), но при race может удалить pod, который новый экземпляр уже adoptировал.
### Router Dedup Gap — критический сценарий при рестарте
При рестарте executor + router одновременно:
1. `FISSION_RESOURCE_NAMESPACES` содержит `fission-fn` (статический NS).
2. `namespace.go` `init()` добавляет его в `DefaultNSResolver`.
3. `BootstrapAndDispatch()` в NamespaceManager вызывает dispatch для всех managed NS, включая `fission-fn`.
4. **Router** `AddNamespace("fission-fn")` → `DefaultNSResolver().AddNamespace("fission-fn")` → **false** (уже добавлен в `init()`!) → **early return, informer для fission-fn НЕ создан**.
5. Executor (gpm, newdeploy) — используют own dedup (envLister/deplLister), `fission-fn` там нет → создают informer.
6. Router слеп к HTTPTrigger и Function событиям из `fission-fn` при динамическом пути. Спасает только то, что `GetInformersForNamespaces` вызывается в `MakeHTTPTriggerSet` при старте — но только для NS из env.
**Вывод**: если `fission-fn` включён в `FISSION_RESOURCE_NAMESPACES` И помечен `fission.io/managed=true` — возможна ситуация, когда после рестарта router использует startup-informer, а executor использует watcher-informer с другим lifecycle → рассинхронизация при следующем relabel-цикле.
---
## 3. Минимальное изменение: явная state machine без полного рефакторинга
Текущая проблема: `failed` namespace остаётся в `failed` навсегда — нет retry.
**Изменение**: добавить reconcile-очередь в `inMemoryNamespaceManager` без изменения публичного интерфейса.
```go
// В inMemoryNamespaceManager добавить:
type reconcileRequest struct {
ns string
attempt int
}
reconcileQueue chan reconcileRequest // небуферизованный или с буфером 64
// В MarkPartFailed (или в dispatch при возврате ошибки от subscriber):
func (m *inMemoryNamespaceManager) enqueueReconcile(ns string, attempt int) {
select {
case m.reconcileQueue <- reconcileRequest{ns: ns, attempt: attempt}:
default: // уже в очереди, skip
}
}
// Новая горутина, запускается в BootstrapAndDispatch или отдельным методом:
func (m *inMemoryNamespaceManager) RunReconciler(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case req := <-m.reconcileQueue:
if req.attempt >= 5 { // max retries
m.logger.Error("namespace reconcile exhausted", zap.String("ns", req.ns))
continue
}
backoff := time.Duration(1<<req.attempt) * time.Second // 1, 2, 4, 8, 16 сек
time.AfterFunc(backoff, func() {
// Повторить dispatch только для failed-частей:
m.mu.RLock()
rec, ok := m.records[req.ns]
m.mu.RUnlock()
if !ok || rec.Phase != NamespacePhaseFailed {
return // уже исправлено или удалено
}
// Вызвать только тех подписчиков, у кого часть в FailedState:
m.dispatchRetry(ctx, req.ns, req.attempt+1)
})
}
}
}
```
**Изменения интерфейса**: `NamespaceManager` получает метод `RunReconciler(ctx)` — добавляется в интерфейс, но не breaking change для существующих вызывающих (можно добавить как опциональный метод или вызвать из `BootstrapAndDispatch`).
**Что НЕ меняется**: `NamespaceSubscriber`, `NamespaceRecord`, публичные методы `Upsert`/`Snapshot`/`Subscribe` — всё прежнее.
---
## 4. Track-Only Removal: скрытые допущения и dirty state
### Допущение 1: `DefaultNSResolver` — только append
`pkg/utils/namespace.go`: метод `AddNamespace` добавляет NS в глобальный map, метода `RemoveNamespace` не существует. Последствия:
- Namespace, удалённый через label-снятие, **навсегда остаётся** в глобальном resolver-е.
- `idleObjectReaper` в poolmgr и newdeploy делает `DefaultNSResolver().Snapshot()` → итерирует удалённые NS → делает LIST Environments/Functions в уже несуществующем (или чужом) namespace → получает k8s 403/404 → логирует ошибку → возвращает из reaper-а (!) — `return` на ошибке прерывает весь цикл reaper-а для текущей итерации.
### Допущение 2: Informer-ы продолжают работать
После track-only removal informer-ы executor-а и router-а **не останавливаются**. Для poolmgr: env-events из удалённого namespace продолжают триггерить создание пулов. Пулы создаются в k8s (или пытаются) — для namespace, который более не является managed. RBAC мог быть уже удалён оператором → pod-ы не могут pull fetcher image → CrashLoopBackOff в "удалённом" namespace.
### Допущение 3: FunctionServiceCache не очищается
`fsCache` (в gpm и newdeploy) содержит записи с `Function.Namespace = "tenant-42"`. После track-only removal записи не удаляются. `idleObjectReaper` находит их через `fsCache.ListOldForPool()` → пытается найти pod в `gpm.podLister["tenant-42"]` → lister ещё жив (informer работает) → pod может быть найден → считается "valid" → не reaped → запись в кэше живёт вечно.
### Допущение 4 (критическое): повторное добавление того же NS → router слеп
Последовательность:
1. NS `tenant-42` добавлен → `DefaultNSResolver().AddNamespace("tenant-42")` → **true** → router создаёт informer.
2. NS удалён (track-only) → resolver не очищен → informer router-а продолжает работать.
3. NS добавлен снова (новый tenant с тем же именем, например после namespace-переименования).
4. `AddNamespace("tenant-42")` на router-е → `DefaultNSResolver().AddNamespace("tenant-42")` → **false** (уже в map!) → **early return**.
5. Router **не создаёт новый informer** — считает что уже обслуживает namespace. Но старый informer работает с **кэшем от предыдущего tenants** — старые Function и HTTPTrigger объекты (с другими UID) видны в `funcInformer.GetStore()`.
6. Executor (gpm): `poolPodC.envLister["tenant-42"]` тоже существует → own dedup → early return → executor тоже не создаёт новый informer.
7. Новые HTTPTrigger-ы нового tenant-42 **никогда не попадут в router** (resync через 30 мин принесёт их, но с кэшем старого tenanta!).
**Результат**: dirty state — оба компонента убеждены что всё нормально, но фактически обслуживают кэш несуществующего tenant с объектами с устаревшими UID. Вызовы функций нового tenant → 404 или выполнение **функций старого tenant** если имена совпадают.
---
## 5. Оценка замены centralized mutex на sharded lock
### Техническая реализация (feasible)
```go
const numShards = 16
type shardedNamespaceManager struct {
shards [numShards]nsShard
subsMu sync.RWMutex
subs map[string]NamespaceSubscriber
// ... остальные поля
}
type nsShard struct {
mu sync.RWMutex
records map[string]NamespaceRecord // только NS принадлежащие этому шарду
}
func shardIndex(ns string) int {
h := fnv.New32a()
h.Write([]byte(ns))
return int(h.Sum32()) % numShards
}
```
`Upsert(ns, ...)` → берёт lock только шарда `shardIndex(ns)`.
`Get(ns)` → RLock только нужного шарда.
`Snapshot()` → **последовательно** берёт RLock каждого шарда, копирует, освобождает, переходит к следующему. N=16 последовательных lock-acquisitions.
### Сохранение интерфейса
Публичный интерфейс `NamespaceManager` (Upsert, Get, Snapshot, Subscribe, Dispatch) не меняется. Подписчики (`NamespaceSubscriber`) не меняются.
### Анализ выгоды
При 10 ns/час churn: **одно upsert каждые 6 минут**. Текущий bottleneck — не mutex, а:
1. Synchronous subscriber dispatch (каждый делает k8s API calls)
2. Informer resync thundering herd
3. AdoptExistingResources race
Sharded lock убирает конкуренцию за mutex при **параллельных per-namespace операциях**. Но `dispatch()` сам снимает/берёт lock несколько раз — sharding не помогает здесь (dispatch по одному NS всегда один шард).
`Snapshot()` становится чуть медленнее (16 lock-acquisitions вместо 1 RLock) при маленьком числе NS, и сопоставима при большом.
### Вердикт
**Технически реализуемо с сохранением интерфейса. Не оправдано при текущей нагрузке.**
Sharded mutex даст реальный выигрыш только если `Upsert` и `Get` вызываются **параллельно для разных NS** с частотой > 100 ops/sec. При 10 ns/час это недостижимо. Реальные bottleneck-и — в subscriber dispatch и informer lifecycle, не в mutex.
Приоритет вместо sharding:
1. Сделать subscriber dispatch **параллельным** (goroutine per subscriber с errgroup) — немедленное ускорение онбординга.
2. Добавить `RemoveNamespace` в `DefaultNSResolver` — закрывает класс dirty-state багов.
3. Добавить reconcile-очередь (см. п. 3) — закрывает stuck-failed.
Sharded lock — в backlog, актуально при > 500 concurrent tenant с > 1 onboarding/sec.
-62
View File
@@ -1,62 +0,0 @@
# Интеграционный тест: P1/P2 (namespace lifecycle hardening)
## Контекст
Ветка: `fix/namespace-lifecycle-hardening` (fission-src)
Коммиты: P1 (auto-recovery failed NS), P2 (adopt orphaned pods)
Образ executor: `naeel/fission-bundle:v1.22.1` (задеплоен 2026-05-18)
Тестирование — через **fission-console** UI.
---
## 1. Проверка P1: auto-recovery failed NS
1. Открыть fission-console, создать tenant (новый managed namespace)
2. Нарочно сломать ServiceAccount или RoleBinding:
```bash
kubectl delete sa fission-fetcher -n <tenant-ns>
```
3. В UI убедиться, что namespace ушёл в фазу `failed`
4. Через ~30 сек namespace должен автоматически восстановиться (`active`)
- SA/RB пересозданы
- Функции снова работают (запустить любую тестовую функцию)
---
## 2. Проверка P2: adopt orphaned pods после рестарта executor
1. Через fission-console вызвать несколько функций (чтобы были warm pods)
2. Рестартовать executor:
```bash
kubectl rollout restart deployment/executor -n fission
kubectl rollout status deployment/executor -n fission
```
3. Убедиться что:
- Функции продолжают работать без cold start задержки
- В логах executor есть строки `PreRegisterManagedNamespaces`, `adopt`, `cleanup`:
```bash
kubectl logs -n fission -l svc=executor --tail=100 | grep -E "PreRegister|adopt|cleanup"
```
---
## 3. Проверить отсутствие регрессий (через fission-console)
- Создание/удаление tenant работает
- Функции создаются, редактируются, выполняются
- Логи функций доступны в UI
- Нет ошибок в UI и в логах executor/router
---
## 4. Команды для диагностики
```bash
# Текущий образ executor
kubectl get deployment executor -n fission -o jsonpath="{.spec.template.spec.containers[0].image}"
# Логи executor (последние 200 строк)
kubectl logs -n fission -l svc=executor --tail=200
# Статус managed NS
kubectl get ns -l managed-by=fission
```
-644
View File
@@ -1,644 +0,0 @@
# Fission Console API — Руководство пользователя
> Версия: актуальна для модернизированного Fission с мультитенантностью (ngcloud).
---
## Базовый URL
```
https://fission.kube5s.ru/console/api
```
---
## Аутентификация
### Где взять токен
Сервер поддерживает два типа токенов — определяет автоматически по форме:
| Форма токена | Тип | Описание |
|---|---|---|
| JWT (три части через `.`) | **Production** | JWT из личного кабинета NUBES (Профиль → Токены). Валидируется через Deck API облака |
| Любая строка ≥ 6 символов | **Demo** | Любой произвольный логин — без внешней проверки. Удобно для разработки и тестирования |
| Строка < 6 символов | — | 401 |
**Production (NUBES):** JWT-токен берётся в личном кабинете NUBES → Профиль → Токены.
**Demo:** любая строка ≥ 6 символов — например `myuser@example.com` или `dev-user-1`.
### Передача токена
Два способа — оба равнозначны:
```http
X-Auth-Token: <токен>
```
```http
Authorization: Bearer <токен>
```
### POST /auth
Проверка токена и получение информации о своём namespace.
```bash
curl -X POST https://fission.kube5s.ru/console/api/auth \
-H "Content-Type: application/json" \
-d '{"token": "myuser@example.com", "env": "test"}'
```
**Параметры:**
| Поле | Описание |
|---|---|
| `token` | Токен (JWT или demo-строка) |
| `env` | Стенд: `prod`, `dev`, `test` (только для JWT; по умолчанию `test`) |
**Ответ 200:**
```json
{
"ok": true,
"env": "test",
"namespace": "fission-a3f9c1b2d4e6f8a1",
"email": "user@example.com"
}
```
**Ошибки:**
| Код | Причина |
|-----|---------|
| 400 | Тело не JSON или `token` пустой |
| 401 | Токен < 6 символов или JWT не прошёл валидацию в Deck API |
| 405 | GET вместо POST |
> **Namespace детерминирован**: `fission-` + hex(SHA256(sub)[:8]) — одинаковый токен → всегда один namespace.
> Namespace и RBAC создаются автоматически при первом обращении.
---
## Мультитенантность ★ КЛЮЧЕВОЕ ОТЛИЧИЕ
- Каждый пользователь работает в **изолированном K8s namespace**: `fission-<hash(token)>`
- Все операции (создание, список, вызов, удаление) **автоматически ограничены своим namespace**
- Указать namespace вручную **невозможно**
- Функции другого пользователя **не видны и не доступны** — любая операция над чужим объектом возвращает **404** (не 403, чтобы не раскрывать факт существования)
- **Routes изолированы**: функции разных пользователей с одинаковым именем получают разные HTTP-маршруты
### Квоты (применяются автоматически, значения по умолчанию)
| Ресурс | Лимит |
|--------|-------|
| Функции (`count/functions.fission.io`) | 20 |
| Пакеты (`count/packages.fission.io`) | 40 |
| HTTP Triggers (`count/httptriggers.fission.io`) | 20 |
| Pods | 30 |
| CPU requests (суммарно) | 1 |
| CPU limits (суммарно) | 12 |
| RAM requests (суммарно) | 2 Gi |
| RAM limits (суммарно) | 6 Gi |
> Значения настраиваются env vars (`QUOTA_REQ_CPU`, `QUOTA_PODS`, и т.д.) без пересборки.
---
## Функции
### POST /functions — Создать функцию из кода (JSON)
```bash
curl -X POST https://fission.kube5s.ru/console/api/functions \
-H "X-Auth-Token: user@domain.com" \
-H "Content-Type: application/json" \
-d '{
"name": "my-fn",
"language": "nodejs",
"code": "module.exports = async function(ctx) { return { status: 200, body: \"hello\" }; }"
}'
```
**Параметры запроса:**
| Поле | Тип | Обязательно | Описание |
|------|-----|:-----------:|---------|
| `name` | string | ✓ | Имя функции (см. правила ниже) |
| `language` | string | ✓ | Среда выполнения: `nodejs`, `python`, `go`, `php`, `ruby` |
| `code` | string | ✓ | Исходный код (строка). Максимум 1 MB |
| `entrypoint` | string | — | Точка входа (по умолчанию — зависит от языка) |
| `route` | string | — | HTTP-маршрут (по умолчанию `/<ns-suffix>/<name>`) |
| `methods` | []string | — | HTTP-методы (по умолчанию `["GET"]`) |
| `timeout` | int64 | — | Таймаут функции в секундах |
| `ttl` | string | — | Время жизни функции: `15m`, `1h`, `2d` и т.д. ★ |
**Правила именования (`name`):**
- Только строчные буквы, цифры, дефис
- Не начинается и не заканчивается дефисом
- Максимум **57 символов**
**Ответ 201:**
```json
{
"name": "my-fn",
"package": "my-fn-pkg",
"httptrigger": "my-fn-route",
"route": "/a3f9c1b2d4e6/my-fn",
"expires_at": null
}
```
> `expires_at` — время удаления функции (RFC3339), `null` если TTL не задан.
**Ошибки:**
| Код | Причина |
|-----|---------|
| 400 | Нет `name`/`language`/`code`, невалидное имя, неизвестный язык, код > 1 MB, невалидный TTL |
| 409 | Функция с таким именем уже существует у этого пользователя |
---
### POST /functions — Создать функцию из zip-архива (multipart)
Альтернативный способ: передать архив напрямую при создании функции.
```bash
curl -X POST https://fission.kube5s.ru/console/api/functions \
-H "X-Auth-Token: user@domain.com" \
-F "name=my-fn" \
-F "language=python" \
-F "entrypoint=main.handler" \
-F "archive=@my-function.zip"
```
**Параметры формы (multipart/form-data):**
| Поле | Тип | Обязательно | Описание |
|------|-----|:-----------:|---------|
| `name` | string | ✓ | Имя функции |
| `language` | string | ✓* | Язык (`python`, `nodejs`, `go`, `php`, `ruby`) — или `environment` |
| `environment` | string | ✓* | Явное имя environment (вместо `language`) |
| `archive` | file | ✓ | zip-архив с кодом. Максимум 100 KB |
| `entrypoint` | string | — | Точка входа |
| `route` | string | — | HTTP-маршрут |
| `methods` | string | — | HTTP-методы через запятую (`GET,POST`) |
| `timeout` | string | — | Таймаут в секундах |
| `ttl` | string | — | Время жизни: `15m`, `1h`, `2d` и т.д. |
> Архив должен быть валидным zip (magic bytes `PK`). Максимальный суммарный распакованный размер — 100 KB (защита от zip bomb).
**Ответ 201:**
```json
{
"name": "my-fn",
"namespace": "fission-a3f9c1b2d4e6f8a1",
"environment": "python-env",
"route": "/a3f9c1b2d4e6/my-fn",
"source_type": "archive"
}
```
---
### GET /functions — Список функций
```bash
curl https://fission.kube5s.ru/console/api/functions \
-H "X-Auth-Token: user@domain.com"
```
**Ответ 200** — массив сырых K8s объектов типа `Function`. Новый пользователь → `[]`.
Возвращает **только функции текущего пользователя**.
---
### GET /functions/{name} — Описание функции
```bash
curl https://fission.kube5s.ru/console/api/functions/my-fn \
-H "X-Auth-Token: user@domain.com"
```
**Ответ 200:**
```json
{
"name": "my-fn",
"namespace": "fission-a3f9c1b2d4e6f8a1",
"environment": "nodejs-env",
"package": "my-fn-pkg",
"entrypoint": "main",
"timeout": 60,
"created_at": "2026-05-01T10:00:00Z",
"updated_at": "2026-05-01T12:00:00Z",
"code": "module.exports = async function(ctx) { ... }",
"source_type": "code",
"archive_filename": "",
"route": "/a3f9c1b2d4e6/my-fn",
"methods": ["GET", "POST"],
"raw": {}
}
```
> `code` — исходный код (если хранится как literal). Для функций из архива может быть пустым.
> `source_type` — `"code"` или `"archive"`.
> `raw` — полный K8s объект Function.
**Ошибки:**
| Код | Причина |
|-----|---------|
| 404 | Функция не существует или принадлежит другому пользователю |
---
### POST /functions/{name}/invoke — Вызов функции
```bash
curl -X POST https://fission.kube5s.ru/console/api/functions/my-fn/invoke \
-H "X-Auth-Token: user@domain.com" \
-H "Content-Type: application/json" \
-d '{}'
```
**Ответ 200:**
```json
{
"status": 200,
"latency_ms": 42,
"response_raw": "hello"
}
```
> `response_raw` — тело ответа функции как строка.
> `status` — HTTP-статус ответа функции.
> `latency_ms` — время выполнения в миллисекундах.
> Cold start (первый вызов после создания) может занять **10-60 секунд** — Pod создаётся и прогревается. Последующие вызовы быстрые.
**Ошибки:**
| Код | Причина |
|-----|---------|
| 404 | Функция не существует или принадлежит другому пользователю |
| 502 | Fission router недоступен или функция завершилась с timeout |
---
### PUT /functions/{name}/code — Обновить код функции ★
Обновляет код существующей функции. Создаётся новый Package, executor подхватывает его при следующем вызове.
```bash
curl -X PUT https://fission.kube5s.ru/console/api/functions/my-fn/code \
-H "X-Auth-Token: user@domain.com" \
-H "Content-Type: application/json" \
-d '{
"code": "module.exports = async function(ctx) { return { status: 200, body: \"v2\" }; }"
}'
```
**Параметры:**
| Поле | Тип | Обязательно | Описание |
|------|-----|:-----------:|---------|
| `code` | string | ✓ | Новый исходный код |
| `timeout` | int64 | — | Новый таймаут в секундах |
**Ответ 200:**
```json
{
"updated": true,
"package": "my-fn-pkg-xxxxxx"
}
```
**Ошибки:**
| Код | Причина |
|-----|---------|
| 400 | `code` пустой или только пробелы |
| 404 | Функция не существует или принадлежит другому пользователю |
---
### PUT /functions/{name}/archive — Обновить архив функции ★
Обновляет функцию новым zip-архивом (multipart/form-data, поле `archive`).
```bash
curl -X PUT https://fission.kube5s.ru/console/api/functions/my-fn/archive \
-H "X-Auth-Token: user@domain.com" \
-F "archive=@my-function-v2.zip"
```
**Ответ 200:**
```json
{
"updated": true,
"package": "my-fn-pkg-xxxxxx"
}
```
---
### PUT /functions/{name}/timeout — Обновить таймаут функции ★
Обновляет только таймаут (и опционально entrypoint) без замены кода или архива.
```bash
curl -X PUT https://fission.kube5s.ru/console/api/functions/my-fn/timeout \
-H "X-Auth-Token: user@domain.com" \
-H "Content-Type: application/json" \
-d '{"timeout": 120}'
```
**Параметры:**
| Поле | Тип | Описание |
|------|-----|---------|
| `timeout` | int64 | Новый таймаут в секундах |
| `entrypoint` | string | Новая точка входа (опционально) |
**Ответ 200:**
```json
{
"updated": true,
"timeout": 120
}
```
---
### DELETE /functions/{name} — Удалить функцию
```bash
curl -X DELETE https://fission.kube5s.ru/console/api/functions/my-fn \
-H "X-Auth-Token: user@domain.com"
```
**Ответ 200:**
```json
{
"deleted": true,
"name": "my-fn",
"package": "my-fn-pkg"
}
```
> Удаляются также связанные HTTPTrigger, TimeTrigger и Package.
> Архив в S3 удаляется асинхронно.
> Если язык больше не используется ни одной функцией — environment Pod'ы убираются автоматически.
**Ошибки:**
| Код | Причина |
|-----|---------|
| 404 | Функция не существует или принадлежит другому пользователю |
> Повторное удаление той же функции → **404**.
---
## Прямой вызов по route — GET|POST /fn/{route}
Вызов функции напрямую по HTTP-маршруту без обёртки invoke. Ответ проксируется как есть — без JSON-обёртки.
```bash
curl https://fission.kube5s.ru/fn/a3f9c1b2d4e6/my-fn \
-H "X-Auth-Token: user@domain.com"
```
> Используйте этот endpoint когда нужно получить чистый HTTP-ответ функции, а не JSON-обёртку с `response_raw`.
> Метод запроса (GET/POST/…) проксируется без изменений.
---
## Time Triggers (расписание)
### GET /timetriggers — Список
```bash
curl https://fission.kube5s.ru/console/api/timetriggers \
-H "X-Auth-Token: user@domain.com"
```
Возвращает массив сырых K8s объектов TimeTrigger.
### POST /timetriggers — Создать
```bash
curl -X POST https://fission.kube5s.ru/console/api/timetriggers \
-H "X-Auth-Token: user@domain.com" \
-H "Content-Type: application/json" \
-d '{
"name": "my-cron",
"functionName": "my-fn",
"cron": "*/5 * * * *"
}'
```
**Параметры:**
| Поле | Тип | Обязательно | Описание |
|------|-----|:-----------:|---------|
| `name` | string | ✓ | Имя trigger'а |
| `functionName` | string | ✓ | Имя функции |
| `cron` | string | ✓ | Cron-выражение (стандартный формат) |
| `method` | string | — | HTTP-метод для вызова (по умолчанию `POST`) |
| `subpath` | string | — | Дополнительный путь |
**Ответ 201:**
```json
{
"name": "my-cron",
"namespace": "fission-a3f9c1b2d4e6f8a1",
"cron": "*/5 * * * *",
"method": "POST",
"subpath": "",
"function": "my-fn",
"raw": {}
}
```
### GET /timetriggers/{name} — Описание
**Ответ 200** — та же структура что и при создании.
### PUT /timetriggers/{name} — Обновить
**Ответ 200:**
```json
{
"updated": true,
"trigger": { ...та же структура... }
}
```
### DELETE /timetriggers/{name} — Удалить
**Ответ 200:**
```json
{
"deleted": true,
"name": "my-cron"
}
```
---
## AI-линтер архивов ★
Проверяет zip-архив на синтаксические ошибки до деплоя. Не создаёт функцию.
Поддерживаемые файлы: `.py`, `.js`, `.rb`, `.php`.
### POST /ai/lint-archive
```bash
curl -X POST https://fission.kube5s.ru/console/api/ai/lint-archive \
-H "X-Auth-Token: user@domain.com" \
-F "archive=@my-function.zip" \
-F "entrypoint=main.handler" \
-F "language=python"
```
**Параметры формы:**
| Поле | Описание |
|------|---------|
| `archive` | zip-архив (обязательно) |
| `entrypoint` | Точка входа `module.function` — проверяется что файл и функция существуют в архиве |
| `language` | Язык — проверяется что архив содержит файлы нужного расширения |
**Ответ 200:**
```json
{
"ok": true,
"results": [
{"file": "main.py", "ok": true},
{"file": "helper.py", "ok": false, "output": "SyntaxError: invalid syntax (helper.py, line 5)"},
{"file": "main.handler", "ok": true, "output": "entrypoint 'main.handler' найден"}
]
}
```
> `ok: false` в корне объекта означает что хотя бы один файл не прошёл проверку.
> `output` содержит вывод линтера — присутствует только при ошибке (для entrypoint — всегда).
**Ошибки:**
| Код | Причина |
|-----|---------|
| 400 | Нет поля `archive`, нет поддерживаемых файлов (.py/.js/.rb/.php) в архиве |
| 413 | Архив > 100 KB или суммарный распакованный размер > 100 KB (zip bomb protection) |
---
## TTL — Время жизни функции ★
Функция может быть создана с ограниченным временем жизни. После истечения TTL функция удаляется автоматически.
**Формат:** число + суффикс: `m` (минуты), `h` (часы), `d` (дни).
**Примеры:** `15m`, `1h`, `2d`, `12h`
```bash
curl -X POST .../functions \
-H "X-Auth-Token: user@domain.com" \
-H "Content-Type: application/json" \
-d '{
"name": "temp-fn",
"language": "python",
"code": "def main(event, context): return \"hi\"",
"ttl": "1h"
}'
```
В ответе будет поле `expires_at` (формат RFC3339):
```json
{
"name": "temp-fn",
"package": "temp-fn-pkg",
"httptrigger": "temp-fn-route",
"route": "/...",
"expires_at": "2026-05-06T14:00:00Z"
}
```
Невалидные значения TTL (`0d`, `-1h`, `99z`, `abc`) → **400**.
---
## HTTP-коды — сводная таблица
| Код | Значение |
|-----|---------|
| 200 | Успех (GET, DELETE, PUT) |
| 201 | Объект создан (POST /functions, POST /timetriggers) |
| 400 | Ошибка валидации параметров |
| 401 | Не авторизован (нет токена или < 6 символов) |
| 404 | Объект не найден (или чужой) |
| 405 | Неверный HTTP-метод |
| 409 | Конфликт (дубликат имени) |
| 413 | Тело слишком большое (код > 1 MB, архив > 100 KB) |
| 502 | Ошибка взаимодействия с Fission (router/executor недоступен) |
**Формат ошибки:**
```json
{ "error": "описание ошибки" }
```
---
## Поддерживаемые языки
| `language` | Расширение файла | Entrypoint по умолчанию |
|------------|-----------------|------------------------|
| `python` | `.py` | `main.main` |
| `nodejs` | `.js` | зависит от runtime |
| `go` | `.go` | зависит от runtime |
| `php` | `.php` | зависит от runtime |
| `ruby` | `.rb` | зависит от runtime |
---
## Примеры сценариев
### Быстрый старт (inline-код)
```bash
BASE="https://fission.kube5s.ru/console/api"
TOKEN="myuser@example.com"
# Создать функцию
curl -X POST "$BASE/functions" \
-H "X-Auth-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"hello","language":"python","code":"def main(event, context): return \"hello world\""}'
# Вызвать
curl -X POST "$BASE/functions/hello/invoke" \
-H "X-Auth-Token: $TOKEN" -H "Content-Type: application/json" -d '{}'
# Обновить код
curl -X PUT "$BASE/functions/hello/code" \
-H "X-Auth-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"code":"def main(event, context): return \"v2\""}'
# Удалить
curl -X DELETE "$BASE/functions/hello" -H "X-Auth-Token: $TOKEN"
```
### Создать из архива напрямую
```bash
curl -X POST "$BASE/functions" \
-H "X-Auth-Token: $TOKEN" \
-F "name=my-fn" \
-F "language=python" \
-F "entrypoint=main.handler" \
-F "archive=@my-fn.zip"
```
### Проверить архив перед деплоем
```bash
curl -X POST "$BASE/ai/lint-archive" \
-H "X-Auth-Token: $TOKEN" \
-F "archive=@my-fn.zip" \
-F "entrypoint=main.handler" \
-F "language=python"
```
### Создать временную функцию (исчезнет через 30 минут)
```bash
curl -X POST "$BASE/functions" \
-H "X-Auth-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"temp-fn","language":"nodejs","code":"module.exports = async () => ({status:200,body:\"tmp\"})","ttl":"30m"}'
```
### Настроить расписание
```bash
# Вызывать my-fn каждые 5 минут
curl -X POST "$BASE/timetriggers" \
-H "X-Auth-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"my-cron","functionName":"my-fn","cron":"*/5 * * * *"}'
```
-72
View File
@@ -1,72 +0,0 @@
# Console ↔ fission-src multitenant: compatibility check (2026-05-15)
## Что изменилось в fission-src (feature/multitenant)
| Изменение | Файл |
|---|---|
| Удалён старый partial RBAC | `deploy/executor-ns-watcher-rbac.yaml` |
| Добавлен полный RBAC для NSWatcher | `deploy/multitenant/rbac.yaml` |
| Добавлен `EnsureNamespaceSA` | `pkg/utils/serviceaccount.go` |
| NSWatcher вызывает `EnsureNamespaceSA` при обнаружении NS с `fission.io/managed=true` | `pkg/executor/multitenant/ns_watcher.go` |
| Добавлен тест NSWatcher с fake k8s | `pkg/utils/namespace_manager_test.go` |
---
## Что делает консоль при создании namespace
`SetupFissionNamespace` в `console/internal/fission/namespace.go`:
1. Создаёт Namespace с лейблами:
- `managed-by=fission-console`
- `fission.io/managed=true` ← триггер для NSWatcher
2. Создаёт ServiceAccounts: `fission-fetcher`, `fission-builder`
3. Создаёт RoleBindings с `cluster-admin` ClusterRole для всех Fission SA:
- `fission-executor`, `fission-router`, `fission-buildermgr`, `fission-kubewatcher`, `fission-timer`
- `fission-fetcher` (из fission NS + локально в user NS)
- `fission-builder` (из fission NS + локально в user NS)
---
## Взаимодействие с EnsureNamespaceSA
`EnsureNamespaceSA` вызывается NSWatcher **после** того как консоль создала NS.
Логика (в `setupSAAndRoleBindings`):
1. Создаёт/получает SA `fission-fetcher` → SA уже существует → `IsAlreadyExists` → OK
2. Для каждого permission из `fetcherCheck` вызывает `checkPermission` через `localsubjectaccessreviews`
3. Поскольку у `fission-fetcher` уже есть `cluster-admin` RoleBinding (создан консолью) →
**все проверки возвращают `exists=true`**`rules` остаётся пустым → Role и RoleBinding **не создаются**
**Итог: EnsureNamespaceSA является no-op если консоль уже настроила namespace. Никаких конфликтов.**
---
## Что нужно на кластере
Для работы NSWatcher нужен `deploy/multitenant/rbac.yaml` применён **один раз**:
```bash
kubectl apply -f ~/terra/fission-src/deploy/multitenant/rbac.yaml
```
Это даёт:
- `fission-executor``list/watch namespaces` (NSWatcher)
- `fission-router``list/watch namespaces` (NSWatcher)
- `fission-executor``create SA/Role/RoleBinding` в user NS (`fission-executor-sa-provisioner`)
Без этого RBAC `EnsureNamespaceSA` будет падать с Forbidden, но **консоль продолжит работать** — она создаёт SA/RoleBindings сама и не зависит от NSWatcher.
---
## Вердикт
| Сценарий | Статус |
|---|---|
| Новый NS создаётся через консоль | ✅ работает как раньше |
| NSWatcher обнаруживает NS по `fission.io/managed=true` | ✅ совместимо |
| `EnsureNamespaceSA` вызывается в уже настроенном NS | ✅ no-op, нет конфликтов |
| Старый NS (без нового fission-bundle) | ✅ консоль не зависит от NSWatcher |
| Сборка консоли (`go build ./...`) | ✅ BUILD OK |
**Код консоли менять не нужно.** Нужно только применить `deploy/multitenant/rbac.yaml` при деплое нового fission-bundle.
-349
View File
@@ -1,349 +0,0 @@
# Fission Multi-Tenant Porting Guide
> **Purpose:** This document exists so that any AI agent or engineer can fully
> understand what was changed to add multi-tenancy to this Fission fork, why
> each decision was made, and what needs to be ported when a new upstream
> Fission release arrives.
>
> Fork base: `github.com/fission/fission` tag `v1.22.0` (2025-12-16)
> Our branch: `feature/multitenant`
---
## 1. The Problem We Solved
In stock Fission v1.22.0 all resource namespaces must be listed in the
`FISSION_RESOURCE_NAMESPACES` environment variable **before** the process starts.
Adding a new tenant namespace requires:
1. Patching that env var on executor, router, buildermgr deployments
2. Triggering a rolling restart of all three components (~30 s downtime each)
At scale (hundreds of tenants created continuously) this causes a permanent
rolling-restart loop and cascading failures for existing users.
**Our solution:** hot namespace registration without pod restarts. Any platform
(console, operator, CI/CD) creates a Kubernetes Namespace with label
`fission.io/managed=true` — all three Fission components detect it within
milliseconds via k8s Watch and register it live.
---
## 2. Integration Contract (external platforms)
The entire contract between an external platform and Fission is a single label:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: tenant-abc123
labels:
fission.io/managed: "true"
```
No other coupling to Fission internals is required.
To remove a tenant namespace: delete the namespace or remove the label.
Current removal strategy is `track-only` (the manager records the event but does
not actively deregister — the executor types stop receiving events for deleted
resources naturally). `dispatch-remove` strategy exists in the model but is not
wired by default (see §8).
---
## 2a. How the Console (`../fission`) Integrates
The Fission Console (`github.com/naeel/fission`, package `console`) creates tenant
namespaces via `SetupFissionNamespace()` in
`console/internal/fission/namespace.go`.
That function does three things:
1. Creates the Namespace with two labels:
- `managed-by=fission-console` — console's own filter
- **`fission.io/managed=true`** — this is the NSWatcher trigger
2. Creates `fission-fetcher` and `fission-builder` ServiceAccounts in the new NS
3. Creates RoleBindings for all Fission system SAs (`fission-executor`,
`fission-router`, `fission-buildermgr`, etc.) using `cluster-admin` scoped to
the namespace
**The coupling is exactly one label.** The console does not call any Fission
internal API to register the namespace — it just sets `fission.io/managed=true`
and the NSWatcher in executor/router/buildermgr picks it up automatically within
~50ms.
**Before `v1.22.0-mt1` (today's deploy):** the console set the label but the
executor was running the official `ghcr.io/fission/fission-bundle:v1.22.0` image
which has no NSWatcher — so the label was silently ignored. Tenant namespaces
still worked because the console also created the SA/RoleBindings manually (step 2
and 3 above), so pool pods could start. But executor/router/buildermgr were not
dynamically aware of new namespaces — they relied on whatever was in
`FISSION_RESOURCE_NAMESPACES` at startup.
**After `v1.22.0-mt1`:** executor/router/buildermgr detect the label
automatically. The SA creation in `EnsureNamespaceSA` (our code in
`pkg/utils/serviceaccount.go`) now runs from the executor side as well — but since
the console already created the SA, `EnsureNamespaceSA` is a no-op (idempotent).
No conflict, no double work.
---
## 3. Backward Compatibility
`FISSION_RESOURCE_NAMESPACES` continues to work exactly as before. Namespaces
listed there are bootstrapped at startup with source `env` and do not require the
label. The NSWatcher layer adds **on top** of the existing mechanism — nothing
was removed.
---
## 4. File Map
### New files (did not exist in v1.22.0)
| File | Purpose | What breaks if removed |
|------|---------|------------------------|
| `pkg/utils/namespace_manager_model.go` | All types: `NamespaceRecord`, `NamespacePhase`, `NamespaceSource`, `NamespaceEvent`, `NamespaceRemovalStrategy`, `ManagedNamespaceWatcherConfig` | Everything — all other files import these types |
| `pkg/utils/namespace_manager.go` | `NamespaceManager` interface + `inMemoryNamespaceManager` implementation. `RunManagedNamespaceWatcher()` — the single entry point used by all three components. `NewNamespaceWatcherEventHandlers()` — k8s informer callbacks. `EnsureNamespaceSA` helper call site. | All NSWatcher functionality |
| `pkg/utils/namespace_manager_test.go` | Unit + integration tests for NamespaceManager | Tests only |
| `pkg/utils/namespace_manager_model_test.go` | Tests for model helpers | Tests only |
| `pkg/utils/serviceaccount.go` (was modified, `EnsureNamespaceSA` added at bottom) | `EnsureNamespaceSA(ctx, client, logger, ns)` — creates fission-fetcher SA/Role/RoleBinding in a new namespace idempotently | Pool pods in new namespaces fail to start (no SA to run fetcher) |
| `pkg/executor/multitenant/ns_watcher.go` | `StartNSWatcher()` — executor entry point. `registerNamespace()` — calls `AddNamespace` on global resolver + `EnsureNamespaceSA` + all executor types. | Executor never learns about new namespaces |
| `pkg/executor/multitenant/namespace_subscriber.go` | `NewNamespaceSubscriber()` — adapter from `NamespaceSubscriber` interface to executor `registerNamespace()` | Same as above |
| `pkg/executor/multitenant/ns_watcher_test.go` | Tests | Tests only |
| `pkg/executor/multitenant/namespace_subscriber_test.go` | Tests | Tests only |
| `pkg/router/ns_watcher.go` | `StartNSWatcher()` — router entry point, 5 lines | Router never learns about new namespaces |
| `pkg/router/namespace_subscriber.go` | `NewNamespaceSubscriber()` — adapter calling `HTTPTriggerSet.AddNamespace` | Same as above |
| `pkg/router/namespace_subscriber_test.go` | Tests | Tests only |
| `pkg/buildermgr/ns_watcher.go` | `StartNSWatcher()` — buildermgr entry point, 5 lines | Buildermgr never learns about new namespaces |
| `pkg/buildermgr/namespace_subscriber.go` | `NewNamespaceSubscriber()` — adapter calling `envWatcher.AddNamespace` + `pkgWatcher.AddNamespace` | Same as above |
| `pkg/buildermgr/namespace_subscriber_test.go` | Tests | Tests only |
| `deploy/multitenant/rbac.yaml` | ClusterRoles + ClusterRoleBindings for all three components (see §6) | Components crash at startup or fail to watch namespaces |
### Modified files (existed in v1.22.0, we changed them)
| File | What we added | What breaks if reverted |
|------|--------------|-------------------------|
| `pkg/utils/namespace.go` | `ManagedNamespaceLabelKey/Value` constants, `ManagedNamespaceLabelSelector()`, `IsManagedNamespace()`, `AddNamespace()` (thread-safe dedup), `Snapshot()` (sorted slice copy under read-lock), `SnapshotWithOptions()` | All callers of `Snapshot()` break — there are many; label constants used by informer filter |
| `pkg/utils/namespace_test.go` | Tests for new methods | Tests only |
| `pkg/utils/informer.go` | `NewSharedInformerFactoryForNamespaces(namespaces []string)` — creates informer factory filtered to a dynamic list of namespaces | Executor types cannot create per-NS informers for new namespaces |
| `pkg/executor/executor.go` | `StartNSWatcher(...)` call added after executor types are initialized | NSWatcher never starts in executor |
| `pkg/executor/executortype/executortype.go` | `AddNamespace(ctx, ns, mgr)` added to the `ExecutorType` interface | All three executor types must implement this; compilation fails |
| `pkg/executor/executortype/poolmgr/gpm.go` | `AddNamespace()` implementation — creates per-NS informer factory, pod lister, event handlers | poolmgr never picks up functions in new namespaces |
| `pkg/executor/executortype/poolmgr/poolpodcontroller.go` | Uses `Snapshot()` in runtime loop instead of static namespace list | Pool pods not created in new namespaces |
| `pkg/executor/executortype/newdeploy/newdeploymgr.go` | `AddNamespace()` implementation | newdeploy never picks up functions in new namespaces |
| `pkg/executor/executortype/container/containermgr.go` | `AddNamespace()` implementation | container executor never picks up functions in new namespaces |
| `pkg/router/router.go` | `StartNSWatcher(...)` call added | NSWatcher never starts in router |
| `pkg/router/httpTriggers.go` | `AddNamespace(ns string)` on `HTTPTriggerSet` — starts per-NS informers for HTTPTriggers and Functions | Router ignores HTTPTriggers in new namespaces |
| `pkg/router/functionReferenceResolver.go` | Uses `Snapshot()` in runtime loop | Router resolves functions only in statically-configured namespaces |
| `pkg/buildermgr/buildermgr.go` | `StartNSWatcher(...)` call added | NSWatcher never starts in buildermgr |
| `pkg/buildermgr/envwatcher.go` | `AddNamespace(ns string)` — starts per-NS Environment informer | Buildermgr ignores Environments in new namespaces |
| `pkg/buildermgr/pkgwatcher.go` | `AddNamespace(ns string)` — starts per-NS Package informer | Buildermgr ignores Packages in new namespaces |
| `pkg/storagesvc/archivePruner.go` | Uses `Snapshot()` instead of static namespace list | Archive pruner only cleans old namespaces |
| `.gitignore` | Added `*.token` | Minor — token files would be committed accidentally |
| `deploy/multitenant/rbac.yaml` | New file (see above) | — |
---
## 5. Data Flow: from label to HTTP 200
```
kubectl label ns tenant-abc123 fission.io/managed=true
k8s API server emits ADDED event on Namespace stream
▼ (within ~50ms)
utils.RunManagedNamespaceWatcher ← informer AddFunc
│ (all 3 components share this function)
NamespaceManager.DispatchAdd(ctx, "tenant-abc123")
├──► executor subscriber:
│ registerNamespace()
│ 1. DefaultNSResolver().AddNamespace("tenant-abc123")
│ 2. EnsureNamespaceSA(ctx, client, logger, "tenant-abc123")
│ └─ creates fission-fetcher SA + Role + RoleBinding
│ 3. poolmgr.AddNamespace("tenant-abc123")
│ └─ creates per-NS InformerFactory, pod lister, handlers
│ 4. newdeploy.AddNamespace("tenant-abc123")
│ 5. container.AddNamespace("tenant-abc123")
├──► router subscriber:
│ HTTPTriggerSet.AddNamespace("tenant-abc123")
│ └─ starts watching HTTPTriggers + Functions in that NS
└──► buildermgr subscriber:
envWatcher.AddNamespace("tenant-abc123")
pkgWatcher.AddNamespace("tenant-abc123")
└─ starts watching Environments + Packages in that NS
User creates: fission env create --namespace tenant-abc123 ...
fission fn create --namespace tenant-abc123 ...
fission httptrigger create --namespace tenant-abc123 ...
Router picks up HTTPTrigger → resolves Function → cold-starts pod in tenant-abc123
HTTP 200 ← function response
```
---
## 6. RBAC Explained
Three ClusterRoles in `deploy/multitenant/rbac.yaml`:
### `fission-executor-ns-watcher` (also for router: `fission-router-ns-watcher`)
```
namespaces: list, watch
```
Without this: informer fails to start with "Forbidden" — components never learn
about new namespaces.
### `fission-executor-sa-provisioner`
```
serviceaccounts: get, list, watch, create, update, patch
events: create
localsubjectaccessreviews: create
roles: get, list, watch, create, update, patch
rolebindings: get, list, watch, create, update, patch
```
Why `events.create`: Kubernetes forbids creating a Role that grants permissions
the caller does not currently hold. Since `fission-fetcher` gets `events.create`
in the Role we create for it, `fission-executor` must hold `events.create` itself
to be allowed to create that Role. This is a k8s RBAC escalation prevention rule.
Why `localsubjectaccessreviews.create`: `EnsureNamespaceSA` calls
`setupSAAndRoleBindings` which first checks if the SA already has each permission
before creating it — that check uses a `LocalSubjectAccessReview`.
---
## 7. Key Design Decisions and Why
### Decision: pub/sub via `NamespaceManager`, not direct calls
**Why not:** simply call `poolmgr.AddNamespace`, `router.AddNamespace` etc.
directly from a shared goroutine.
**Why pub/sub:** executor, router and buildermgr run as separate processes
(different pod). Each process has its own copy of the watcher. Subscribers are
registered in-process. This pattern makes each component fully self-contained and
testable in isolation. No cross-process coupling.
### Decision: `fission.io/managed=true` label as the only trigger
**Why not:** watch all namespaces, or use a CRD, or use annotations.
**Why label:** labels are the idiomatic k8s way to select resources. A label
selector in the informer factory (`fission.io/managed=true`) means the informer
only receives events for labeled namespaces — zero overhead for the hundreds of
system namespaces.
### Decision: `EnsureNamespaceSA` in executor, not in a separate operator
**Why:** the SA must exist before the first pool pod starts. The executor is
already in the hot path — it processes the NS event and then immediately triggers
pod scheduling. Doing it in a separate controller would introduce a race. Doing it
in the executor keeps the lifecycle coupled correctly.
### Decision: `NamespaceRemovalStrategy = track-only` (not `dispatch-remove`)
**Why:** removing a namespace in k8s is already an irreversible event — all
resources inside are cascade-deleted by k8s. The executor types detect the pod
deletions themselves. Dispatching an explicit "remove" to all subscribers would
require each subscriber to implement a cleanup path — added complexity for zero
operational benefit in our use case. Can be switched per-component via
`ManagedNamespaceWatcherConfig.RemovalStrategy`.
### Decision: `AddNamespace` is idempotent (safe to call multiple times)
**Why:** k8s informers can deliver the same event more than once (resync). Every
`AddNamespace` call is a no-op if the namespace is already registered. No locks
held across the whole function — `NamespaceResolver.AddNamespace` uses a write
lock only for the map write, checks for existence first under the same lock.
### Decision: global `NamespaceResolver` (`DefaultNSResolver()`) updated once in executor
**Why:** `DefaultNSResolver().AddNamespace(ns)` is called exactly once in
`registerNamespace()` — before the executor types are called. Each executor type
does NOT call it themselves. This avoids a subtle race: if executor type A adds
the NS to the global resolver first, and executor type B checks the global resolver
as its dedup mechanism, B would see it as already registered and skip — even
though B has not actually processed it yet. The correct dedup is per executor type.
---
## 8. What Is NOT Done (deliberately deferred)
| Missing feature | Why deferred | File/interface to extend |
|----------------|--------------|--------------------------|
| `dispatch-remove` full wiring | Not needed for current use case | `ManagedNamespaceWatcherConfig.RemovalStrategy` — just switch the constant |
| Buildermgr ClusterRole in rbac.yaml | Buildermgr uses the same SA as executor in our deployment; check your setup | Add a third ClusterRole/Binding to `deploy/multitenant/rbac.yaml` |
| Helm chart integration | We apply rbac.yaml manually. A proper Helm chart would include these RBAC objects | `charts/fission-all/templates/` |
| Layer 2 (tenant isolation: per-NS network policy, resource quotas) | Out of scope for Layer 1 | Not started |
| Layer 3 (per-tenant auth, billing hooks) | Out of scope | Not started |
| `NamespaceManager.DispatchRemove` subscriber wiring | Each subscriber has a `RemoveFunc` stub returning nil | Implement per subscriber |
---
## 9. Porting to a New Upstream Version
When `github.com/fission/fission` releases v1.23 or later, follow this order:
1. **Check the upstream changelog** for any changes to:
- `pkg/utils/namespace.go` — if they renamed or refactored `NamespaceResolver`, our `AddNamespace`/`Snapshot` additions need to be reapplied
- `pkg/executor/executortype/executortype.go` — if they changed the `ExecutorType` interface, our `AddNamespace` method needs to be reapplied
- `pkg/router/httpTriggers.go` — if `HTTPTriggerSet` changed, our `AddNamespace` method on it needs to be reapplied
- `pkg/buildermgr/envwatcher.go`, `pkgwatcher.go` — same
- `pkg/utils/informer.go` — if the informer factory pattern changed
2. **Apply in this order** (each depends on the previous):
1. `pkg/utils/namespace_manager_model.go` — pure types, no deps on other changed files
2. `pkg/utils/namespace.go` additions (`AddNamespace`, `Snapshot`, label constants)
3. `pkg/utils/namespace_manager.go` — depends on model + namespace.go
4. `pkg/utils/serviceaccount.go` — add `EnsureNamespaceSA` at the bottom
5. `pkg/utils/informer.go` — add `NewSharedInformerFactoryForNamespaces`
6. `pkg/executor/executortype/executortype.go` — add `AddNamespace` to interface
7. Executor types: `poolmgr/gpm.go`, `newdeploy/newdeploymgr.go`, `container/containermgr.go` — implement `AddNamespace`
8. `pkg/executor/multitenant/` — copy the whole package as-is
9. `pkg/executor/executor.go` — add `StartNSWatcher` call
10. `pkg/router/httpTriggers.go` — add `AddNamespace` method on `HTTPTriggerSet`
11. `pkg/router/namespace_subscriber.go`, `pkg/router/ns_watcher.go` — copy as-is
12. `pkg/router/router.go` — add `StartNSWatcher` call
13. `pkg/buildermgr/envwatcher.go`, `pkgwatcher.go` — add `AddNamespace` method
14. `pkg/buildermgr/namespace_subscriber.go`, `pkg/buildermgr/ns_watcher.go` — copy as-is
15. `pkg/buildermgr/buildermgr.go` — add `StartNSWatcher` call
16. `pkg/storagesvc/archivePruner.go` — replace static namespace list with `Snapshot()`
17. `deploy/multitenant/rbac.yaml` — apply unchanged
3. **Run tests:**
```bash
go test ./pkg/utils/... ./pkg/executor/... ./pkg/router/... ./pkg/buildermgr/...
```
4. **Build and deploy:**
```bash
# on VM:
docker run --rm -v $PWD:/src -w /src golang:1.26-alpine \
sh -c 'go build -o /src/fission-bundle-bin ./cmd/fission-bundle/'
docker build -f Dockerfile.mt -t naeel/fission-bundle:vNEW_TAG .
docker push naeel/fission-bundle:vNEW_TAG
kubectl set image deployment/executor -n fission executor=naeel/fission-bundle:vNEW_TAG
kubectl set image deployment/router -n fission router=naeel/fission-bundle:vNEW_TAG
kubectl set image deployment/buildermgr -n fission buildermgr=naeel/fission-bundle:vNEW_TAG
```
5. **Run e2e test:**
```bash
bash ~/terra/fission/scripts/test_layer1.sh
# Expected: PASS=5 FAIL=0
```
---
## 10. Test Coverage
| Test file | What it covers |
|-----------|---------------|
| `pkg/utils/namespace_test.go` | `AddNamespace` dedup, `Snapshot` sorted output, `IsManagedNamespace` |
| `pkg/utils/namespace_manager_test.go` | `Bootstrap`, `DispatchAdd`/`Remove`/`Resync`, subscriber dispatch, `TestStartManagedNamespaceWatcherIntegration` — full k8s fake informer → subscriber pipeline |
| `pkg/utils/namespace_manager_model_test.go` | Model helpers, `Clone`, `IsActive`, `IsTerminal` |
| `pkg/executor/multitenant/ns_watcher_test.go` | `registerNamespace` with fake k8s client |
| `pkg/executor/multitenant/namespace_subscriber_test.go` | Subscriber adapter |
| `pkg/router/namespace_subscriber_test.go` | Router subscriber adapter |
| `pkg/buildermgr/namespace_subscriber_test.go` | Buildermgr subscriber adapter |
| `~/terra/fission/scripts/test_layer1.sh` | End-to-end: create labeled NS → executor registers it → create env/fn/trigger → call function → HTTP 200 |
-191
View File
@@ -1,191 +0,0 @@
# Fission — Краткий справочник команд
> Базовый URL: `https://fission.kube5s.ru/console/api`
> Токен передаётся через `X-Auth-Token: <token>` или `Authorization: Bearer <token>`
```bash
BASE="https://fission.kube5s.ru/console/api"
T="X-Auth-Token: mylogin@example.com" # demo: любая строка ≥6 символов
```
---
## Стандартные операции
### Функции
```bash
# Создать функцию
curl -X POST "$BASE/functions" -H "$T" -H "Content-Type: application/json" \
-d '{"name":"hello","language":"python","code":"def main(event, context): return \"hi\""}'
# Список функций
curl "$BASE/functions" -H "$T"
# Описание функции
curl "$BASE/functions/hello" -H "$T"
# Вызвать функцию
curl -X POST "$BASE/functions/hello/invoke" -H "$T" \
-H "Content-Type: application/json" -d '{}'
# Обновить код
curl -X PUT "$BASE/functions/hello/code" -H "$T" -H "Content-Type: application/json" \
-d '{"code":"def main(event, context): return \"v2\""}'
# Удалить функцию
curl -X DELETE "$BASE/functions/hello" -H "$T"
```
**Языки:** `python`, `nodejs`, `go`, `php`, `ruby`
**Правила имени:** строчные буквы, цифры, дефис; не начинается/не заканчивается дефисом; максимум 57 символов.
---
### Environments
```bash
# Список environments
curl "$BASE/environments" -H "$T"
```
---
### Packages
```bash
# Список пакетов
curl "$BASE/packages" -H "$T"
```
---
### HTTP Triggers
```bash
# Список HTTP triggers
curl "$BASE/httptriggers" -H "$T"
```
---
### Time Triggers (cron)
```bash
# Создать cron
curl -X POST "$BASE/timetriggers" -H "$T" -H "Content-Type: application/json" \
-d '{"name":"my-cron","functionName":"hello","cron":"*/5 * * * *"}'
# Список
curl "$BASE/timetriggers" -H "$T"
# Обновить
curl -X PUT "$BASE/timetriggers/my-cron" -H "$T" -H "Content-Type: application/json" \
-d '{"cron":"0 * * * *"}'
# Удалить
curl -X DELETE "$BASE/timetriggers/my-cron" -H "$T"
```
---
### Прямой вызов по route
```bash
# Вызов без JSON-обёртки (чистый HTTP)
curl "https://fission.kube5s.ru/fn/<route>" -H "$T"
```
---
## Наши расширения
### Создание из zip-архива
```bash
# Создать функцию из архива напрямую
curl -X POST "$BASE/functions" -H "$T" \
-F "name=my-fn" -F "language=python" -F "entrypoint=main.handler" \
-F "archive=@my-function.zip"
# Обновить функцию новым архивом
curl -X PUT "$BASE/functions/my-fn/archive" -H "$T" \
-F "archive=@my-function-v2.zip"
```
---
### TTL — самоуничтожающиеся функции
```bash
# Функция исчезнет через 1 час
curl -X POST "$BASE/functions" -H "$T" -H "Content-Type: application/json" \
-d '{"name":"temp","language":"nodejs","code":"module.exports=async()=>({status:200,body:\"ok\"})","ttl":"1h"}'
```
Форматы TTL: `15m`, `2h`, `1d`, `7d`
---
### AI: проверить архив перед деплоем
```bash
curl -X POST "$BASE/ai/lint-archive" -H "$T" \
-F "archive=@my-fn.zip" \
-F "language=python" \
-F "entrypoint=main.handler"
```
Ответ:
```json
{
"ok": true,
"results": [{"file": "main.py", "ok": true}]
}
```
Поддерживает: `.py`, `.js`, `.rb`, `.php`
---
### Обновить только таймаут
```bash
curl -X PUT "$BASE/functions/hello/timeout" -H "$T" -H "Content-Type: application/json" \
-d '{"timeout": 120}'
```
---
### Статус namespace
```bash
# Готовность namespace (stages: создан → RBAC → control-plane)
curl "$BASE/ns/status" -H "$T"
```
---
### Аутентификация / получить namespace
```bash
curl -X POST "$BASE/auth" -H "Content-Type: application/json" \
-d '{"token":"mylogin@example.com"}'
# → {"ok":true,"namespace":"fission-a3f9c1b2...","email":"..."}
```
---
## HTTP-коды
| Код | Значение |
|-----|---------|
| 200 | OK |
| 201 | Создано |
| 400 | Ошибка валидации |
| 401 | Нет/невалидный токен |
| 404 | Не найдено (или чужое) |
| 409 | Уже существует |
| 413 | Слишком большой код/архив |
| 502 | Fission внутренняя ошибка |
@@ -0,0 +1,94 @@
# 2026-04-26 — Layer2 chat handoff
## Что уже сделано
Layer1 завершён в ветке rewrite/layer1-namespace-manager-step1 и перенесён в новую рабочую ветку:
`rewrite/layer2-namespace-manager-api-step1`
Layer1 означает, что внутренняя адаптация Fission под multi-tenant namespace onboarding уже готова:
1. Вынесен общий `NamespaceManager`.
2. Buildermgr, router и executor/multitenant переведены на общий watcher/helper layer.
3. Summary/debug contract стабилизирован.
4. Logging path усилен и покрыт тестами.
Последняя точка закрытия layer1:
- commit `63ce6ea``layer1: close namespace manager step1`
## Какие тесты уже были прогнаны
Финальный целевой прогон для layer1:
`go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant`
Он прошёл зелёным.
## На какой ветке продолжать
Продолжать работу нужно на ветке:
`rewrite/layer2-namespace-manager-api-step1`
## Что является целью layer2
Layer2 — это уже не перепись watcher-ов, а внешний read-only consumption поверх готового `NamespaceManager` слоя.
Практическая цель:
1. Дать безопасный read-only status/debug/API surface для состояния multi-tenant namespace onboarding.
2. Не менять runtime behavior watcher-ов.
3. Не дублировать логику manager-а в service-level коде.
4. Использовать уже существующий `NamespaceManagerSummary`, а не придумывать вторую модель состояния.
## Что делать в новом чате
Новый чат должен стартовать не с переписывания layer1 заново, а с аккуратного поиска лучшей точки интеграции для layer2.
Предпочтительный порядок:
1. Проверить текущую ветку и чистоту дерева.
2. Найти существующий service-level debug/status/API contour в buildermgr, router или executor.
3. Выбрать один самый безопасный read-only endpoint или status surface.
4. Протащить наружу `NamespaceManagerSummary` без изменения watcher semantics.
5. Добавить unit/integration tests именно на внешний consumer-side path.
6. Документировать каждый шаг в новых файлах в `doc/thinking/`.
## Чего НЕ надо делать
1. Не продолжать внутреннюю консолидацию watcher layer ради самой консолидации.
2. Не ломать существующий runtime flow add/resync/remove.
3. Не вводить второй независимый источник правды о namespace state.
4. Не менять старые doc-файлы — только новые файлы с новыми шагами.
## Важные файлы для продолжения
- `pkg/utils/namespace_manager.go`
- `pkg/utils/namespace_manager_model.go`
- `pkg/utils/namespace_manager_test.go`
- `pkg/buildermgr/ns_watcher.go`
- `pkg/router/ns_watcher.go`
- `pkg/executor/multitenant/ns_watcher.go`
## Как начать с другого компьютера
Если работа продолжается в том же репозитории на той же VM, достаточно открыть репозиторий и проверить ветку:
`cd ~/terra/fission-src && git branch --show-current && git log --oneline -8`
Если ветка не выбрана, переключиться на неё:
`git checkout rewrite/layer2-namespace-manager-api-step1`
Если новый чат работает через VS Code tools над sshfs mount, локальный путь будет соответствовать смонтированной папке, а команды всё равно нужно запускать через SSH на VM.
## Готовый текст для первого сообщения в новом чате
Ниже текст, который можно вставить почти без изменений:
"Продолжаем в repo `fission-src` на ветке `rewrite/layer2-namespace-manager-api-step1`. Layer1 завершён и закрыт commit-ом `63ce6ea`. Внутренний `NamespaceManager` layer готов, buildermgr/router/executor уже сидят на общих watcher helper-ах, summary/debug contract стабилизирован и целевой прогон `go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant` уже был зелёным. Теперь нужен layer2: аккуратно найти лучший существующий read-only status/debug/API surface и начать вынос наружу `NamespaceManagerSummary` без изменения runtime semantics watcher-ов. Работай маленькими шагами, с новыми doc-файлами в `doc/thinking/`, без background команд, все команды только через SSH на VM и всегда с timeout."
## Ожидаемый первый технический шаг в новом чате
Не писать код сразу. Сначала найти реальный существующий endpoint или status surface, куда summary можно встроить безопасно и без архитектурного мусора.
@@ -1,231 +0,0 @@
# Мультитенантный 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 тега
+2 -19
View File
@@ -76,8 +76,6 @@ type (
podSpecPatch *apiv1.PodSpec
envWatchInformer map[string]k8sCache.SharedIndexInformer
enableOwnerReferences bool
// nsCancels holds per-namespace context cancel functions.
nsCancels map[string]context.CancelFunc
}
)
@@ -113,8 +111,8 @@ func makeEnvironmentWatcher(
podSpecPatch: podSpecPatch,
envWatchInformer: utils.GetInformersForNamespaces(fissionClient, time.Minute*30, fv1.EnvironmentResource),
enableOwnerReferences: utils.IsOwnerReferencesEnabled(),
nsCancels: make(map[string]context.CancelFunc),
}
err := envWatcher.EnvWatchEventHandlers(ctx)
if err != nil {
return nil, err
@@ -542,21 +540,6 @@ func (envw *environmentWatcher) AddNamespace(ctx context.Context, ns string, mgr
envw.envWatchInformer[ns] = envInf
mgr.AddInformers(ctx, map[string]k8sCache.SharedIndexInformer{ns: envInf})
// Create a per-namespace cancellable context for informer lifecycle.
nsCtx, nsCancel := context.WithCancel(ctx)
envw.nsCancels[ns] = nsCancel
factory.Start(nsCtx.Done())
factory.Start(ctx.Done())
envw.logger.Info("buildermgr.envWatcher.AddNamespace: done", zap.String("namespace", ns))
}
// RemoveNamespace deregisters a namespace from the environment watcher.
func (envw *environmentWatcher) RemoveNamespace(ns string) {
if cancel, ok := envw.nsCancels[ns]; ok {
cancel()
delete(envw.nsCancels, ns)
}
delete(envw.envWatchInformer, ns)
envw.logger.Info("buildermgr.envWatcher.RemoveNamespace: cleaned up", zap.String("namespace", ns))
}
-25
View File
@@ -11,18 +11,10 @@ type builderEnvNamespaceAdder interface {
AddNamespace(ctx context.Context, ns string, mgr manager.Interface)
}
type builderEnvNamespaceRemover interface {
RemoveNamespace(ns string)
}
type builderPkgNamespaceAdder interface {
AddNamespace(ctx context.Context, ns string, mgr manager.Interface)
}
type builderPkgNamespaceRemover interface {
RemoveNamespace(ns string)
}
func NewNamespaceSubscriber(envw builderEnvNamespaceAdder, pkgw builderPkgNamespaceAdder, mgr manager.Interface) utils.NamespaceSubscriber {
return utils.NamespaceSubscriberFuncs{
SubscriberName: "buildermgr",
@@ -30,10 +22,6 @@ func NewNamespaceSubscriber(envw builderEnvNamespaceAdder, pkgw builderPkgNamesp
registerBuilderNamespace(ctx, record.Name, envw, pkgw, mgr)
return nil
},
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
deregisterBuilderNamespace(record.Name, envw, pkgw)
return nil
},
ResyncFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
registerBuilderNamespace(ctx, record.Name, envw, pkgw, mgr)
return nil
@@ -53,16 +41,3 @@ func registerBuilderNamespace(ctx context.Context, namespace string, envw builde
pkgw.AddNamespace(ctx, namespace, mgr)
}
}
func deregisterBuilderNamespace(namespace string, envw builderEnvNamespaceAdder, pkgw builderPkgNamespaceAdder) {
if namespace == "" {
return
}
utils.DefaultNSResolver().RemoveNamespace(namespace)
if r, ok := envw.(builderEnvNamespaceRemover); ok {
r.RemoveNamespace(namespace)
}
if r, ok := pkgw.(builderPkgNamespaceRemover); ok {
r.RemoveNamespace(namespace)
}
}
+1 -3
View File
@@ -25,9 +25,7 @@ func StartNSWatcher(
pkgw *packageWatcher,
mgr manager.Interface,
) {
config := utils.NewDefaultManagedNamespaceWatcherConfig("buildermgr.NSWatcher", NewNamespaceSubscriber(envw, pkgw, mgr))
config.RemovalStrategy = utils.NamespaceRemovalStrategyDispatchRemove
_, err := utils.RunManagedNamespaceWatcher(ctx, logger, kubeClient, mgr, config)
_, err := utils.RunManagedNamespaceWatcher(ctx, logger, kubeClient, mgr, utils.NewDefaultManagedNamespaceWatcherConfig("buildermgr.NSWatcher", NewNamespaceSubscriber(envw, pkgw, mgr)))
if err != nil {
logger.Error("buildermgr.NSWatcher: BootstrapAndDispatch failed", zap.Error(err))
}
+2 -21
View File
@@ -49,8 +49,6 @@ type (
pkgInformer map[string]k8sCache.SharedIndexInformer
storageSvcUrl string
buildCache *cache.Cache[crd.CacheKeyUR, *fv1.Package]
// nsCancels holds per-namespace context cancel functions.
nsCancels map[string]context.CancelFunc
}
)
@@ -66,7 +64,6 @@ func makePackageWatcher(logger *zap.Logger, fissionClient versioned.Interface, k
pkgInformer: pkgInformer,
storageSvcUrl: storageSvcUrl,
buildCache: cache.MakeCache[crd.CacheKeyUR, *fv1.Package](0, 0),
nsCancels: make(map[string]context.CancelFunc),
}
return pkgw
}
@@ -366,23 +363,7 @@ func (pkgw *packageWatcher) AddNamespace(ctx context.Context, ns string, mgr man
ns + "/pkg": pkgInf,
ns + "/pod": podInf,
})
// Create a per-namespace cancellable context for informer lifecycle.
nsCtx, nsCancel := context.WithCancel(ctx)
pkgw.nsCancels[ns] = nsCancel
fissionFactory.Start(nsCtx.Done())
podFactory.Start(nsCtx.Done())
fissionFactory.Start(ctx.Done())
podFactory.Start(ctx.Done())
pkgw.logger.Info("buildermgr.pkgWatcher.AddNamespace: done", zap.String("namespace", ns))
}
// RemoveNamespace deregisters a namespace from the package watcher.
func (pkgw *packageWatcher) RemoveNamespace(ns string) {
if cancel, ok := pkgw.nsCancels[ns]; ok {
cancel()
delete(pkgw.nsCancels, ns)
}
delete(pkgw.pkgInformer, ns)
delete(pkgw.podInformer, ns)
pkgw.logger.Info("buildermgr.pkgWatcher.RemoveNamespace: cleaned up", zap.String("namespace", ns))
}
+8 -1
View File
@@ -111,7 +111,13 @@ func (c *client) GetServiceForFunction(ctx context.Context, fn *fv1.Function) (s
func (c *client) UnTapService(ctx context.Context, fnMeta metav1.ObjectMeta, executorType fv1.ExecutorType, serviceURL *url.URL) error {
url := c.executorURL + "/v2/unTapService"
tapSvc := TapServiceRequest{
FnMetadata: fnMeta,
FnMetadata: metav1.ObjectMeta{
Name: fnMeta.Name,
Namespace: fnMeta.Namespace,
ResourceVersion: fnMeta.ResourceVersion,
Generation: fnMeta.Generation,
UID: fnMeta.UID,
},
FnExecutorType: executorType,
ServiceURL: strings.TrimPrefix(serviceURL.String(), "http://"),
}
@@ -175,6 +181,7 @@ func (c *client) TapService(fnMeta metav1.ObjectMeta, executorType fv1.ExecutorT
Name: fnMeta.Name,
Namespace: fnMeta.Namespace,
ResourceVersion: fnMeta.ResourceVersion,
Generation: fnMeta.Generation,
UID: fnMeta.UID,
},
FnExecutorType: executorType,
-5
View File
@@ -347,11 +347,6 @@ func StartExecutor(ctx context.Context, clientGen crd.ClientGeneratorInterface,
executorTypes[ndm.GetTypeName(ctx)] = ndm
executorTypes[cnm.GetTypeName(ctx)] = cnm
// Pre-populate DefaultNSResolver with managed (labeled) namespaces so that
// AdoptExistingResources and CleanupOldExecutorObjects cover the full tenant NS set.
// Must run before the adopt/cleanup goroutines below. Non-fatal on error.
multitenant.PreRegisterManagedNamespaces(ctx, logger, kubernetesClient)
adoptExistingResources, _ := strconv.ParseBool(os.Getenv("ADOPT_EXISTING_RESOURCES"))
wg := &sync.WaitGroup{}
@@ -90,10 +90,6 @@ type (
objectReaperIntervalSecond time.Duration
enableOwnerReferences bool
// nsCancels holds per-namespace context cancel functions so informer
// factories started in AddNamespace can be stopped on RemoveNamespace.
nsCancels map[string]context.CancelFunc
}
)
@@ -136,7 +132,8 @@ func MakeContainer(
deplLister: make(map[string]appslisters.DeploymentLister),
deplListerSynced: make(map[string]k8sCache.InformerSynced),
svcLister: make(map[string]corelisters.ServiceLister),
svcListerSynced: make(map[string]k8sCache.InformerSynced), nsCancels: make(map[string]context.CancelFunc),
svcListerSynced: make(map[string]k8sCache.InformerSynced),
enableOwnerReferences: utils.IsOwnerReferencesEnabled(),
}
@@ -836,36 +833,9 @@ func (caaf *Container) AddNamespace(ctx context.Context, ns string, mgr manager.
return fmt.Errorf("AddNamespace %s (container): add function handler: %w", ns, err)
}
// Create a per-namespace cancellable context so RemoveNamespace can stop
// these specific informer factories without affecting the whole process.
nsCtx, nsCancel := context.WithCancel(ctx)
caaf.nsCancels[ns] = nsCancel
finformer.Start(nsCtx.Done())
cnmInformer.Start(nsCtx.Done())
finformer.Start(ctx.Done())
cnmInformer.Start(ctx.Done())
caaf.logger.Info("AddNamespace: done (container)", zap.String("namespace", ns))
return nil
}
// RemoveNamespace deregisters a namespace from the container executor.
// Cancels the per-namespace informer context and clears all lister maps so that
// a subsequent AddNamespace call will re-register the namespace correctly.
func (caaf *Container) RemoveNamespace(ctx context.Context, ns string) error {
if ns == "" {
return nil
}
caaf.logger.Info("RemoveNamespace: cleaning up namespace (container)", zap.String("namespace", ns))
if cancel, ok := caaf.nsCancels[ns]; ok {
cancel()
delete(caaf.nsCancels, ns)
}
delete(caaf.deplLister, ns)
delete(caaf.deplListerSynced, ns)
delete(caaf.svcLister, ns)
delete(caaf.svcListerSynced, ns)
return nil
}
@@ -74,9 +74,4 @@ type ExecutorType interface {
// starts watching Fission CRDs and K8s resources in it without a pod restart.
// Called when a Namespace with label fission.io/managed=true appears.
AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error
// RemoveNamespace deregisters a namespace from the executor, cancelling its
// informer goroutines and clearing dedup state so that a re-add works correctly.
// Called when a Namespace with label fission.io/managed=true is removed.
RemoveNamespace(ctx context.Context, ns string) error
}
@@ -94,10 +94,6 @@ type (
objectReaperIntervalSecond time.Duration
enableOwnerReferences bool
// nsCancels holds per-namespace context cancel functions so informer
// factories started in AddNamespace can be stopped on RemoveNamespace.
nsCancels map[string]context.CancelFunc
}
)
@@ -144,7 +140,8 @@ func MakeNewDeploy(
deplLister: make(map[string]appslisters.DeploymentLister),
deplListerSynced: make(map[string]k8sCache.InformerSynced),
svcLister: make(map[string]corelisters.ServiceLister),
svcListerSynced: make(map[string]k8sCache.InformerSynced), nsCancels: make(map[string]context.CancelFunc),
svcListerSynced: make(map[string]k8sCache.InformerSynced),
enableOwnerReferences: utils.IsOwnerReferencesEnabled(),
}
@@ -952,36 +949,9 @@ func (deploy *NewDeploy) AddNamespace(ctx context.Context, ns string, mgr manage
return fmt.Errorf("AddNamespace %s (newdeploy): add environment handler: %w", ns, err)
}
// Create a per-namespace cancellable context so RemoveNamespace can stop
// these specific informer factories without affecting the whole process.
nsCtx, nsCancel := context.WithCancel(ctx)
deploy.nsCancels[ns] = nsCancel
finformer.Start(nsCtx.Done())
ndmInformer.Start(nsCtx.Done())
finformer.Start(ctx.Done())
ndmInformer.Start(ctx.Done())
deploy.logger.Info("AddNamespace: done (newdeploy)", zap.String("namespace", ns))
return nil
}
// RemoveNamespace deregisters a namespace from the newdeploy executor.
// Cancels the per-namespace informer context and clears all lister maps so that
// a subsequent AddNamespace call will re-register the namespace correctly.
func (deploy *NewDeploy) RemoveNamespace(ctx context.Context, ns string) error {
if ns == "" {
return nil
}
deploy.logger.Info("RemoveNamespace: cleaning up namespace (newdeploy)", zap.String("namespace", ns))
if cancel, ok := deploy.nsCancels[ns]; ok {
cancel()
delete(deploy.nsCancels, ns)
}
delete(deploy.deplLister, ns)
delete(deploy.deplListerSynced, ns)
delete(deploy.svcLister, ns)
delete(deploy.svcListerSynced, ns)
return nil
}
+6 -36
View File
@@ -98,10 +98,6 @@ type (
podSpecPatch *apiv1.PodSpec
objectReaperIntervalSecond time.Duration
// nsCancels holds per-namespace context cancel functions so informer
// factories started in AddNamespace can be stopped on RemoveNamespace.
nsCancels map[string]context.CancelFunc
}
request struct {
requestType
@@ -163,7 +159,6 @@ func MakeGenericPoolManager(ctx context.Context,
objectReaperIntervalSecond: time.Duration(executorUtils.GetObjectReaperInterval(logger, fv1.ExecutorTypePoolmgr, 5)) * time.Second,
podLister: make(map[string]corelisters.PodLister),
podListerSynced: make(map[string]k8sCache.InformerSynced),
nsCancels: make(map[string]context.CancelFunc),
}
for ns, informerFactory := range gpmInformerFactory {
gpm.podLister[ns] = informerFactory.Core().V1().Pods().Lister()
@@ -838,40 +833,15 @@ func (gpm *GenericPoolManager) AddNamespace(ctx context.Context, ns string, mgr
return fmt.Errorf("AddNamespace %s: register informers: %w", ns, err)
}
// Create a per-namespace cancellable context so RemoveNamespace can stop
// these specific informer factories without affecting the whole process.
nsCtx, nsCancel := context.WithCancel(ctx)
gpm.nsCancels[ns] = nsCancel
// Also update gpm.podLister so IsValid can resolve pods in this namespace.
// Without this line gpm.podLister[ns] is nil for dynamically-added user namespaces,
// causing a nil-pointer panic in IsValid, which leaves activeRequests permanently stuck at 1.
gpm.podLister[ns] = gpmInformer.Core().V1().Pods().Lister()
// Start the factories — they will begin syncing immediately.
finformer.Start(nsCtx.Done())
gpmInformer.Start(nsCtx.Done())
finformer.Start(ctx.Done())
gpmInformer.Start(ctx.Done())
gpm.logger.Info("AddNamespace: informers started for namespace", zap.String("namespace", ns))
return nil
}
// RemoveNamespace deregisters a namespace from the poolmgr executor.
// Cancels the per-namespace informer context and clears all lister maps so that
// a subsequent AddNamespace call will re-register the namespace correctly.
func (gpm *GenericPoolManager) RemoveNamespace(ctx context.Context, ns string) error {
if ns == "" {
return nil
}
gpm.logger.Info("RemoveNamespace: cleaning up namespace (poolmgr)", zap.String("namespace", ns))
// Stop informer factories for this namespace.
if cancel, ok := gpm.nsCancels[ns]; ok {
cancel()
delete(gpm.nsCancels, ns)
}
// Clear gpm-level lister maps so dedup passes on next AddNamespace.
delete(gpm.podLister, ns)
delete(gpm.podListerSynced, ns)
// Clear PoolPodController lister maps.
gpm.poolPodC.RemoveNamespace(ns)
return nil
}
@@ -528,14 +528,3 @@ func (p *PoolPodController) AddNamespaceInformers(
p.logger.Info("AddNamespaceInformers: registered informers for namespace", zap.String("namespace", ns))
return nil
}
// RemoveNamespace clears all per-namespace lister state in the PoolPodController.
// Called from GenericPoolManager.RemoveNamespace so the dedup check in AddNamespace
// will pass if the namespace is re-added later.
func (p *PoolPodController) RemoveNamespace(ns string) {
delete(p.envLister, ns)
delete(p.envListerSynced, ns)
delete(p.podLister, ns)
delete(p.podListerSynced, ns)
p.logger.Info("PoolPodController.RemoveNamespace: cleared lister state", zap.String("namespace", ns))
}
@@ -21,16 +21,12 @@ func NewNamespaceSubscriber(
return utils.NamespaceSubscriberFuncs{
SubscriberName: "executor",
AddFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
return registerNamespace(ctx, logger, kubernetesClient, record.Name, executorTypes, mgr)
},
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
return deregisterNamespace(ctx, logger, record.Name, executorTypes)
registerNamespace(ctx, logger, kubernetesClient, record.Name, executorTypes, mgr)
return nil
},
ResyncFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
// Reconciler calls this for namespaces in NamespacePhaseFailed.
// registerNamespace is idempotent: SA creation is a no-op if SA exists,
// executor type AddNamespace guards against duplicate informer creation.
return registerNamespace(ctx, logger, kubernetesClient, record.Name, executorTypes, mgr)
registerNamespace(ctx, logger, kubernetesClient, record.Name, executorTypes, mgr)
return nil
},
}
}
+4 -68
View File
@@ -62,10 +62,8 @@ package multitenant
import (
"context"
"errors"
"fmt"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/core/v1"
@@ -89,9 +87,7 @@ func StartNSWatcher(
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
mgr manager.Interface,
) {
config := utils.NewDefaultManagedNamespaceWatcherConfig("multitenant.NSWatcher", NewNamespaceSubscriber(logger, kubernetesClient, executorTypes, mgr))
config.RemovalStrategy = utils.NamespaceRemovalStrategyDispatchRemove
_, err := utils.RunManagedNamespaceWatcher(ctx, logger, kubernetesClient, mgr, config)
_, err := utils.RunManagedNamespaceWatcher(ctx, logger, kubernetesClient, mgr, utils.NewDefaultManagedNamespaceWatcherConfig("multitenant.NSWatcher", NewNamespaceSubscriber(logger, kubernetesClient, executorTypes, mgr)))
if err != nil {
logger.Error("multitenant.NSWatcher: BootstrapAndDispatch failed", zap.Error(err))
}
@@ -102,10 +98,6 @@ func StartNSWatcher(
// Each executor type uses its own internal state for deduplication instead of the
// global resolver, so all executor types receive the AddNamespace call regardless of
// iteration order.
//
// Returns an error if SA provisioning or any executor-type initialization fails.
// The error is propagated to the NamespaceSubscriber so the NamespaceManager can
// mark the namespace as NamespacePhaseFailed and the reconciler will retry automatically.
func registerNamespace(
ctx context.Context,
logger *zap.Logger,
@@ -113,21 +105,14 @@ func registerNamespace(
ns string,
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
mgr manager.Interface,
) error {
) {
// Update the global resolver once here. Each executor type must NOT call
// DefaultNSResolver().AddNamespace() for dedup — they have their own checks.
utils.DefaultNSResolver().AddNamespace(ns)
// Ensure fission-fetcher SA exists in the new namespace so pool pods can start.
// A failure here means function pods will crash (no SA to pull fetcher image) —
// propagate so the reconciler retries until the API is available again.
if err := utils.EnsureNamespaceSA(ctx, kubernetesClient, logger, ns); err != nil {
return fmt.Errorf("EnsureNamespaceSA: %w", err)
}
if err := registerExecutorTypes(ctx, logger, ns, executorTypes, mgr); err != nil {
return fmt.Errorf("registerExecutorTypes: %w", err)
}
utils.EnsureNamespaceSA(ctx, kubernetesClient, logger, ns)
registerExecutorTypes(ctx, logger, ns, executorTypes, mgr)
logger.Info("multitenant.NSWatcher: registered namespace", zap.String("namespace", ns))
return nil
}
func registerExecutorTypes(
@@ -150,52 +135,3 @@ func registerExecutorTypes(
}
return joinErr
}
// deregisterNamespace calls RemoveNamespace on every executor type for the given namespace.
// Called when a Namespace with label fission.io/managed=true is removed.
func deregisterNamespace(
ctx context.Context,
logger *zap.Logger,
ns string,
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
) error {
var joinErr error
for _, et := range executorTypes {
if err := et.RemoveNamespace(ctx, ns); err != nil {
logger.Error("multitenant.NSWatcher: RemoveNamespace failed",
zap.String("namespace", ns),
zap.Error(err),
)
joinErr = errors.Join(joinErr, err)
}
}
logger.Info("multitenant.NSWatcher: deregistered namespace", zap.String("namespace", ns))
return joinErr
}
// PreRegisterManagedNamespaces does a one-time synchronous List of all Namespaces
// labeled fission.io/managed=true and adds them to the global DefaultNSResolver.
//
// Called at executor startup BEFORE AdoptExistingResources and CleanupOldExecutorObjects
// so that adopt and cleanup cover managed (dynamic) namespaces, not just static ones
// from FISSION_RESOURCE_NAMESPACES. This closes the P2 race where old executor pods
// in managed NS were never adopted (causing unnecessary cold starts) and never cleaned
// (causing orphaned pod accumulation).
//
// Failure is non-fatal: a warning is logged and the executor proceeds with static NS only.
func PreRegisterManagedNamespaces(ctx context.Context, logger *zap.Logger, client kubernetes.Interface) {
nsList, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{
LabelSelector: utils.ManagedNamespaceLabelSelector(),
})
if err != nil {
logger.Warn("PreRegisterManagedNamespaces: failed to list managed namespaces; adopt/cleanup will use static NS only",
zap.Error(err))
return
}
for i := range nsList.Items {
utils.DefaultNSResolver().AddNamespace(nsList.Items[i].Name)
}
logger.Info("PreRegisterManagedNamespaces: pre-registered managed namespaces",
zap.Int("count", len(nsList.Items)))
}
@@ -46,7 +46,6 @@ func (f *fakeExecutorType) AddNamespace(ctx context.Context, ns string, mgr mana
f.lastNamespace = ns
return f.addErr
}
func (f *fakeExecutorType) RemoveNamespace(ctx context.Context, ns string) error { return nil }
var _ executortype.ExecutorType = (*fakeExecutorType)(nil)
+12 -43
View File
@@ -50,18 +50,16 @@ type HTTPTriggerSet struct {
*functionServiceMap
*mutableRouter
logger *zap.Logger
fissionClient versioned.Interface
kubeClient kubernetes.Interface
executor eclient.ClientInterface
resolver *functionReferenceResolver
triggers []fv1.HTTPTrigger
triggerInformer map[string]k8sCache.SharedIndexInformer
functions []fv1.Function
funcInformer map[string]k8sCache.SharedIndexInformer
informerMu sync.RWMutex
// nsCancels holds per-namespace context cancel functions for informer lifecycle.
nsCancels map[string]context.CancelFunc
logger *zap.Logger
fissionClient versioned.Interface
kubeClient kubernetes.Interface
executor eclient.ClientInterface
resolver *functionReferenceResolver
triggers []fv1.HTTPTrigger
triggerInformer map[string]k8sCache.SharedIndexInformer
functions []fv1.Function
funcInformer map[string]k8sCache.SharedIndexInformer
informerMu sync.RWMutex
updateRouterRequestChannel chan struct{}
tsRoundTripperParams *tsRoundTripperParams
isDebugEnv bool
@@ -86,7 +84,6 @@ func makeHTTPTriggerSet(logger *zap.Logger, fmap *functionServiceMap, fissionCli
svcAddrUpdateThrottler: actionThrottler,
unTapServiceTimeout: unTapServiceTimeout,
syncDebouncer: debounce.New(time.Millisecond * 20),
nsCancels: make(map[string]context.CancelFunc),
}
httpTriggerSet.triggerInformer = utils.GetInformersForNamespaces(fissionClient, time.Minute*30, fv1.HttpTriggerResource)
httpTriggerSet.funcInformer = utils.GetInformersForNamespaces(fissionClient, time.Minute*30, fv1.FunctionResource)
@@ -529,39 +526,11 @@ func (ts *HTTPTriggerSet) AddNamespace(ctx context.Context, ns string, mgr manag
ns + "/trigger": triggerInf,
ns + "/func": funcInf,
})
// Create a per-namespace cancellable context for informer lifecycle.
nsCtx, nsCancel := context.WithCancel(ctx)
ts.nsCancels[ns] = nsCancel
factory.Start(nsCtx.Done())
factory.Start(ctx.Done())
// Wait for cache to sync before rebuilding the router, so triggers are visible.
k8sCache.WaitForCacheSync(nsCtx.Done(), triggerInf.HasSynced, funcInf.HasSynced)
k8sCache.WaitForCacheSync(ctx.Done(), triggerInf.HasSynced, funcInf.HasSynced)
ts.logger.Info("router.AddNamespace: done", zap.String("namespace", ns))
ts.syncTriggers()
return nil
}
// RemoveNamespace deregisters a namespace from the router.
// Cancels the per-namespace informer context, removes informers from internal maps,
// and triggers a syncTriggers so stale routes are removed immediately.
func (ts *HTTPTriggerSet) RemoveNamespace(ns string) {
if ns == "" {
return
}
ts.logger.Info("router.RemoveNamespace: cleaning up namespace", zap.String("namespace", ns))
if cancel, ok := ts.nsCancels[ns]; ok {
cancel()
delete(ts.nsCancels, ns)
}
ts.informerMu.Lock()
delete(ts.triggerInformer, ns)
delete(ts.funcInformer, ns)
ts.informerMu.Unlock()
ts.syncTriggers()
ts.logger.Info("router.RemoveNamespace: done", zap.String("namespace", ns))
}
-10
View File
@@ -11,22 +11,12 @@ type routerNamespaceAdder interface {
AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error
}
type routerNamespaceRemover interface {
RemoveNamespace(ns string)
}
func NewNamespaceSubscriber(ts routerNamespaceAdder, mgr manager.Interface) utils.NamespaceSubscriber {
return utils.NamespaceSubscriberFuncs{
SubscriberName: "router",
AddFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
return registerRouterNamespace(ctx, record.Name, ts, mgr)
},
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
if r, ok := ts.(routerNamespaceRemover); ok {
r.RemoveNamespace(record.Name)
}
return nil
},
ResyncFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
return registerRouterNamespace(ctx, record.Name, ts, mgr)
},
+1 -3
View File
@@ -25,9 +25,7 @@ func StartNSWatcher(
ts *HTTPTriggerSet,
mgr manager.Interface,
) {
config := utils.NewDefaultManagedNamespaceWatcherConfig("router.NSWatcher", NewNamespaceSubscriber(ts, mgr))
config.RemovalStrategy = utils.NamespaceRemovalStrategyDispatchRemove
_, err := utils.RunManagedNamespaceWatcher(ctx, logger, kubeClient, mgr, config)
_, err := utils.RunManagedNamespaceWatcher(ctx, logger, kubeClient, mgr, utils.NewDefaultManagedNamespaceWatcherConfig("router.NSWatcher", NewNamespaceSubscriber(ts, mgr)))
if err != nil {
logger.Error("router.NSWatcher: BootstrapAndDispatch failed", zap.Error(err))
}
+29 -28
View File
@@ -26,6 +26,7 @@ import (
"github.com/fission/fission/pkg/crd"
"github.com/fission/fission/pkg/generated/clientset/versioned"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
@@ -90,36 +91,36 @@ func (pruner *ArchivePruner) getOrphanArchives(ctx context.Context) {
archivesRefByPkgs := make([]string, 0)
var archiveID string
// get all pkgs from kubernetes across ALL namespaces (including tenant namespaces)
// Fix: DefaultNSResolver().Snapshot() returns only fission-registered namespaces,
// not tenant namespaces (fission-*). Use NamespaceAll to scan everything.
pkgList, err := pruner.crdClient.CoreV1().Packages(metav1.NamespaceAll).List(ctx, metav1.ListOptions{})
if err != nil {
pruner.logger.Error("error getting package list from kubernetes", zap.Error(err))
return
}
// extract archives referenced by these pkgs
for _, pkg := range pkgList.Items {
if pkg.Spec.Deployment.URL != "" {
archiveID, err = getQueryParamValue(pkg.Spec.Deployment.URL, "id")
if err != nil {
pruner.logger.Error("error extracting value of archiveID from deployment url",
zap.Error(err),
zap.String("url", pkg.Spec.Deployment.URL))
return
}
archivesRefByPkgs = append(archivesRefByPkgs, archiveID)
// get all pkgs from kubernetes
for _, namespace := range utils.DefaultNSResolver().Snapshot() {
pkgList, err := pruner.crdClient.CoreV1().Packages(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
pruner.logger.Error("error getting package list from kubernetes", zap.Error(err))
return
}
if pkg.Spec.Source.URL != "" {
archiveID, err = getQueryParamValue(pkg.Spec.Source.URL, "id")
if err != nil {
pruner.logger.Error("error extracting value of archiveID from source url",
zap.Error(err),
zap.String("url", pkg.Spec.Source.URL))
return
// extract archives referenced by these pkgs
for _, pkg := range pkgList.Items {
if pkg.Spec.Deployment.URL != "" {
archiveID, err = getQueryParamValue(pkg.Spec.Deployment.URL, "id")
if err != nil {
pruner.logger.Error("error extracting value of archiveID from deployment url",
zap.Error(err),
zap.String("url", pkg.Spec.Deployment.URL))
return
}
archivesRefByPkgs = append(archivesRefByPkgs, archiveID)
}
if pkg.Spec.Source.URL != "" {
archiveID, err = getQueryParamValue(pkg.Spec.Source.URL, "id")
if err != nil {
pruner.logger.Error("error extracting value of archiveID from source url",
zap.Error(err),
zap.String("url", pkg.Spec.Source.URL))
return
}
archivesRefByPkgs = append(archivesRefByPkgs, archiveID)
}
archivesRefByPkgs = append(archivesRefByPkgs, archiveID)
}
}
-17
View File
@@ -113,23 +113,6 @@ func (nsr *NamespaceResolver) AddNamespace(ns string) bool {
return true
}
// RemoveNamespace removes a namespace from FissionResourceNS.
// Returns true if the namespace was present and removed, false if it was not found.
// Thread-safe. Used when a namespace loses the fission.io/managed=true label so that
// Snapshot() and idleObjectReaper loops no longer iterate over deleted namespaces.
func (nsr *NamespaceResolver) RemoveNamespace(ns string) bool {
nsr.mu.Lock()
defer nsr.mu.Unlock()
if _, exists := nsr.FissionResourceNS[ns]; !exists {
return false
}
delete(nsr.FissionResourceNS, ns)
if nsr.Logger != nil {
nsr.Logger.Info("dynamically removed namespace from resolver", zap.String("namespace", ns))
}
return true
}
// Snapshot returns a stable copy of the currently registered resource namespaces.
// The returned slice is detached from the internal mutable map and safe to iterate.
func (nsr *NamespaceResolver) Snapshot() []string {
+15 -118
View File
@@ -74,12 +74,6 @@ type NamespaceManager interface {
MarkPartActive(namespace string, part string) (NamespaceRecord, bool)
MarkPartFailed(namespace string, part string, err error) (NamespaceRecord, bool)
Remove(name string) bool
// RunReconciler periodically retries namespaces stuck in NamespacePhaseFailed (every 30 s)
// and performs a health-check on Active namespaces (every 60 s) by calling DispatchResync,
// which re-ensures SA/RoleBindings and executor-type registration are intact.
// Must be started as a goroutine; exits when ctx is cancelled.
// logger is used to report retry attempts and outcomes; pass zap.NewNop() to silence.
RunReconciler(ctx context.Context, logger *zap.Logger)
}
type inMemoryNamespaceManager struct {
@@ -148,12 +142,6 @@ func RunManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, kubeCli
logger = namespaceManagerLogger(logger)
manager, handlers, err := PrepareManagedNamespaceWatcher(ctx, logger, config)
StartManagedNamespaceWatcher(ctx, logger, config.Component, kubeClient, mgr, handlers)
// Start reconciler: retries namespaces stuck in NamespacePhaseFailed every 30s.
mgr.Add(ctx, func(ctx context.Context) {
logger.Info(config.Component + ": namespace reconciler started")
manager.RunReconciler(ctx, logger)
logger.Info(config.Component + ": namespace reconciler stopped")
})
LogNamespaceManagerSummary(logger, config.Component+": started namespace watcher", manager.Summary())
return manager, err
}
@@ -248,11 +236,6 @@ func HandleWatcherNamespaceRemoval(ctx context.Context, logger *zap.Logger, comp
if !ok {
return
}
// Always remove from global resolver so Snapshot() and idleObjectReaper
// stop iterating this namespace. This is safe: if the same name is re-added
// later, AddNamespace will return true and all subscribers will re-register.
DefaultNSResolver().RemoveNamespace(record.Name)
if strategy == NamespaceRemovalStrategyDispatchRemove {
if _, _, err := manager.DispatchRemove(ctx, record.Name); err != nil {
logger.Error(component+": DispatchRemove failed", zap.String("namespace", record.Name), zap.Error(err))
@@ -526,44 +509,28 @@ func (m *inMemoryNamespaceManager) DispatchResync(ctx context.Context, namespace
}
func (m *inMemoryNamespaceManager) dispatch(ctx context.Context, namespace string, handler func(NamespaceSubscriber, NamespaceRecord) error) (NamespaceRecord, bool, error) {
_, ok := m.Get(namespace)
record, ok := m.Get(namespace)
if !ok {
return NamespaceRecord{}, false, nil
}
subscribers := m.snapshotSubscriberObjects()
for _, sub := range subscribers {
_, _ = m.MarkPartRegistering(namespace, sub.Name())
}
// Run all subscriber handlers in parallel — each handler makes independent k8s API calls.
// MarkPart* methods are internally mutex-protected and safe for concurrent calls.
var (
wg sync.WaitGroup
mu sync.Mutex
errs error
)
for _, sub := range subscribers {
sub := sub
wg.Add(1)
go func() {
defer wg.Done()
currentRecord, _ := m.Get(namespace)
err := handler(sub, currentRecord)
if err != nil {
_, _ = m.MarkPartFailed(namespace, sub.Name(), err)
mu.Lock()
errs = errors.Join(errs, err)
mu.Unlock()
return
var firstErr error
for _, subscriber := range m.snapshotSubscriberObjects() {
_, _ = m.MarkPartRegistering(namespace, subscriber.Name())
currentRecord, _ := m.Get(namespace)
err := handler(subscriber, currentRecord)
if err != nil {
_, _ = m.MarkPartFailed(namespace, subscriber.Name(), err)
if firstErr == nil {
firstErr = err
}
_, _ = m.MarkPartActive(namespace, sub.Name())
}()
continue
}
_, _ = m.MarkPartActive(namespace, subscriber.Name())
}
wg.Wait()
record, _ := m.Get(namespace)
return record, true, errs
record, _ = m.Get(namespace)
return record, true, firstErr
}
func (m *inMemoryNamespaceManager) MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool) {
@@ -614,76 +581,6 @@ func (m *inMemoryNamespaceManager) Remove(name string) bool {
return true
}
// RunReconciler periodically:
// - retries namespaces stuck in NamespacePhaseFailed via DispatchResync (every 30 s)
// - health-checks Active namespaces by calling DispatchResync (every 60 s); since
// registerNamespace is idempotent, this is a no-op when SA/RoleBindings are intact
// and silently restores them if they were deleted.
//
// logger receives one log line per retry attempt and per outcome.
// Exits when ctx is cancelled.
func (m *inMemoryNamespaceManager) RunReconciler(ctx context.Context, logger *zap.Logger) {
if logger == nil {
logger = zap.NewNop()
}
failedTicker := time.NewTicker(30 * time.Second)
defer failedTicker.Stop()
activeTicker := time.NewTicker(60 * time.Second)
defer activeTicker.Stop()
for {
select {
case <-ctx.Done():
return
case <-failedTicker.C:
m.mu.RLock()
var failedNS []string
for ns, rec := range m.records {
if rec.Phase == NamespacePhaseFailed {
failedNS = append(failedNS, ns)
}
}
m.mu.RUnlock()
if len(failedNS) == 0 {
continue
}
logger.Info("namespace reconciler: retrying failed namespaces",
zap.Int("count", len(failedNS)),
zap.Strings("namespaces", failedNS),
)
for _, ns := range failedNS {
logger.Info("namespace reconciler: dispatching resync", zap.String("namespace", ns))
_, _, err := m.DispatchResync(ctx, ns)
if err != nil {
logger.Error("namespace reconciler: resync still failing, will retry",
zap.String("namespace", ns),
zap.Error(err),
)
} else {
logger.Info("namespace reconciler: resync succeeded", zap.String("namespace", ns))
}
}
case <-activeTicker.C:
m.mu.RLock()
var activeNS []string
for ns, rec := range m.records {
if rec.Phase == NamespacePhaseActive {
activeNS = append(activeNS, ns)
}
}
m.mu.RUnlock()
for _, ns := range activeNS {
_, _, err := m.DispatchResync(ctx, ns)
if err != nil {
logger.Warn("namespace reconciler: active NS health-check failed, marking failed for retry",
zap.String("namespace", ns),
zap.Error(err),
)
}
}
}
}
}
func deriveNamespacePhase(record NamespaceRecord) NamespacePhase {
if len(record.RegisteredParts) == 0 {
return record.Phase
-71
View File
@@ -10,7 +10,6 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sfake "k8s.io/client-go/kubernetes/fake"
k8sCache "k8s.io/client-go/tools/cache"
@@ -819,76 +818,6 @@ func TestNamespaceManagerDispatchRemoveFailure(t *testing.T) {
}
}
// TestStartManagedNamespaceWatcherIntegration проверяет полный маршрут:
// fake k8s client → информер → AddFunc → subscriber.OnNamespaceAdd.
// Это интеграционный тест без real cluster — использует k8sfake.
func TestStartManagedNamespaceWatcherIntegration(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
fakeClient := k8sfake.NewSimpleClientset()
mgr := managerPkg.New()
router := &testNamespaceSubscriber{name: "router"}
// Запускаем watcher — не Bootstrap, только informer.
// config.Namespaces пусто, чтобы не было pre-loaded namespace-ов.
manager, err := RunManagedNamespaceWatcher(ctx, zap.NewNop(), fakeClient, mgr, ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: nil,
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: router,
})
if err != nil {
t.Fatalf("RunManagedNamespaceWatcher failed: %v", err)
}
// Создаём namespace с managed label в fake client.
ns := &corev1.Namespace{}
ns.Name = "tenant-integration-a"
ns.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
if _, err := fakeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil {
t.Fatalf("failed to create test namespace in fake client: %v", err)
}
// Ждём, пока informer обработает event и subscriber получит вызов.
// Timeout намеренно короткий (3 секунды) — fake client синхронный.
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if router.addCalls >= 1 {
break
}
time.Sleep(10 * time.Millisecond)
}
if router.addCalls == 0 {
t.Fatalf("expected subscriber.OnNamespaceAdd to be called after namespace creation, got 0 calls")
}
if router.addCalls != 1 {
t.Fatalf("expected exactly 1 add call (namespace is new), got %d", router.addCalls)
}
// Namespace должен быть в manager snapshot.
snapshot := manager.Snapshot()
found := false
for _, name := range snapshot {
if name == ns.Name {
found = true
break
}
}
if !found {
t.Fatalf("expected %s in manager snapshot after informer event, got %v", ns.Name, snapshot)
}
record, ok := manager.Get(ns.Name)
if !ok {
t.Fatalf("expected record for %s in manager", ns.Name)
}
if record.Phase != NamespacePhaseActive {
t.Fatalf("expected active phase after informer add, got %s", record.Phase)
}
}
func TestNamespaceSubscriberFuncs(t *testing.T) {
addCalls := 0
removeCalls := 0
+7 -11
View File
@@ -121,8 +121,7 @@ func (sa *ServiceAccount) runSACheck(ctx context.Context) {
for _, baseNS := range sa.nsResolver.Snapshot() {
for _, permission := range sa.permissions {
targetNS := sa.resolveSANamespace(baseNS, permission.saName)
// Errors are already logged inside setupSAAndRoleBindings; periodic loop ignores them.
_ = setupSAAndRoleBindings(ctx, sa.kubernetesClient, sa.logger, targetNS, permission)
setupSAAndRoleBindings(ctx, sa.kubernetesClient, sa.logger, targetNS, permission)
}
}
}
@@ -135,14 +134,14 @@ func (sa *ServiceAccount) resolveSANamespace(baseNS, saName string) string {
return sa.nsResolver.GetFunctionNS(baseNS)
}
func setupSAAndRoleBindings(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, namespace string, ps *ServiceAccountPermissions) error {
func setupSAAndRoleBindings(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, namespace string, ps *ServiceAccountPermissions) {
SAObj, err := createGetSA(ctx, client, ps.saName, namespace)
if err != nil {
logger.Error("error while creating or getting service account",
zap.String("sa_name", ps.saName),
zap.String("namespace", namespace),
zap.Error(err))
return err
return
}
var rules []rbac.PolicyRule
@@ -178,15 +177,14 @@ func setupSAAndRoleBindings(ctx context.Context, client kubernetes.Interface, lo
role, err := setupRoles(ctx, client, logger, SAObj, rules, suffix)
if err != nil {
logger.Error("error while creating roles", zap.Error(err))
return err
return
}
_, err = setupRoleBinding(ctx, client, logger, SAObj, role, suffix)
if err != nil {
logger.Error("error while creating role bindings", zap.Error(err))
return err
return
}
}
return nil
}
func setupRoles(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, sa *v1.ServiceAccount, rules []rbac.PolicyRule, suffix string) (*rbac.Role, error) {
@@ -316,10 +314,8 @@ func getSAInterval() time.Duration {
}
// EnsureNamespaceSA creates the fission-fetcher ServiceAccount and its Role/RoleBinding
// in the given namespace. Returns an error if SA or RoleBinding creation fails so callers
// can propagate it to the namespace lifecycle manager and trigger a reconcile retry.
// in the given namespace if they do not already exist. Safe to call repeatedly.
// Used by the multi-tenant NS watcher to provision per-namespace SA on NS registration.
func EnsureNamespaceSA(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, ns string) error {
return setupSAAndRoleBindings(ctx, client, logger, ns, fetcherCheck)
func EnsureNamespaceSA(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, ns string) {
setupSAAndRoleBindings(ctx, client, logger, ns, fetcherCheck)
}