Compare commits

..
Author SHA1 Message Date
“Naeel” 847595a1b3 wip: все изменения на 2026-05-06 2026-05-06 13:45:25 +04:00
Naeel 63ce6ea135 layer1: close namespace manager step1 2026-04-26 16:27:53 +03:00
Naeel 7b6ff84188 layer1: harden namespace watcher logging path 2026-04-26 16:27:20 +03:00
Naeel 55d0b5a9e7 layer1: log namespace watcher startup summary 2026-04-26 16:24:29 +03:00
Naeel 813617ffd1 layer1: log watcher namespace transitions 2026-04-26 11:52:06 +03:00
Naeel 7d7fe561a8 layer1: log namespace summary active flag 2026-04-26 11:51:03 +03:00
Naeel fed69da335 layer1: add namespace summary active helper 2026-04-26 11:46:47 +03:00
Naeel 9f0e911b9d layer1: stabilize namespace summary contract 2026-04-26 11:46:15 +03:00
Naeel f4a3bffc6b layer1: test namespace manager summary logging 2026-04-26 11:45:13 +03:00
Naeel 90924cdec7 layer1: checkpoint namespace manager runtime series 2026-04-26 11:44:36 +03:00
Naeel 6e037a506d layer1: add default watcher config helper 2026-04-26 11:03:39 +03:00
Naeel d24605a8b8 layer1: fix managed watcher config wiring 2026-04-26 11:02:54 +03:00
Naeel d2ff55f9e0 layer1: add managed watcher config 2026-04-26 11:02:09 +03:00
Naeel b9236698f3 layer1: run managed namespace watchers 2026-04-26 11:01:05 +03:00
Naeel 073f2c1504 layer1: add live namespace counts 2026-04-26 10:59:39 +03:00
Naeel b93e720e12 layer1: add namespace source counts 2026-04-26 10:59:01 +03:00
Naeel f16aa030db layer1: log namespace manager summary 2026-04-26 10:57:29 +03:00
Naeel 5c481b7293 layer1: add namespace manager summary 2026-04-26 10:55:56 +03:00
Naeel 67db8d71f1 layer1: fix watcher preparation imports 2026-04-26 10:54:51 +03:00
Naeel 2bbed95c2a layer1: prepare managed namespace watchers 2026-04-26 10:54:24 +03:00
Naeel a1517ba4b2 layer1: share managed namespace watcher startup 2026-04-26 10:51:31 +03:00
Naeel 49be1db3a0 layer1: share namespace watcher event handlers 2026-04-26 10:50:28 +03:00
Naeel c3b161da83 layer1: share update removal policy 2026-04-26 10:49:26 +03:00
Naeel 0755319fac layer1: formalize namespace removal strategy 2026-04-26 10:48:42 +03:00
Naeel 340b9cae84 layer1: centralize namespace watcher handlers 2026-04-26 10:47:12 +03:00
Naeel 4cd4bc9507 layer1: share namespace watcher lifecycle helpers 2026-04-26 10:45:03 +03:00
Naeel 0e08664ef6 layer1: share watcher namespace manager bootstrap 2026-04-26 10:41:14 +03:00
Naeel d7497dcd34 layer1: remove old namespace watcher helpers 2026-04-26 10:40:36 +03:00
Naeel 447133d5b2 layer1: track namespace removals in manager 2026-04-26 10:39:59 +03:00
Naeel b6f3640bbe layer1: add namespace tombstone helper step 34 2026-04-26 10:39:11 +03:00
Naeel d09bee3431 layer1: add namespace remove dispatch step 33 2026-04-26 10:36:48 +03:00
Naeel 488157963a layer1: bootstrap executor namespace manager 2026-04-26 10:35:50 +03:00
Naeel 804bc533db layer1: bootstrap router namespace manager 2026-04-26 10:35:29 +03:00
Naeel 65837610a1 layer1: bootstrap builder namespace manager 2026-04-26 10:35:09 +03:00
Naeel dd7470922c layer1: hook executor watcher to namespace manager 2026-04-26 10:34:34 +03:00
Naeel 0135a93a30 layer1: hook router watcher to namespace manager 2026-04-26 10:34:04 +03:00
Naeel b12a8e5e75 layer1: hook builder watcher to namespace manager 2026-04-26 10:33:36 +03:00
Naeel ad0f83fd4b layer1: add namespace bootstrap dispatch step 26 2026-04-26 10:32:43 +03:00
Naeel 6f77fa5a9a layer1: add executor namespace subscriber step 25 2026-04-26 10:31:54 +03:00
Naeel 331f531962 layer1: factor executor namespace registration step 24 2026-04-26 10:30:59 +03:00
Naeel 346399d35f layer1: align router watcher flow step 23 2026-04-26 10:29:43 +03:00
Naeel 33a08f00b3 layer1: fix router namespace subscriber syntax 2026-04-26 10:29:19 +03:00
Naeel 2971029c42 layer1: add router namespace subscriber step 22 2026-04-26 10:28:57 +03:00
Naeel 3159fba65b layer1: align builder watcher flow step 21 2026-04-26 10:27:09 +03:00
Naeel b200b8bb5b layer1: fix builder namespace subscriber syntax 2026-04-26 10:26:40 +03:00
Naeel 126f7cc51c layer1: add builder namespace subscriber step 20 2026-04-26 10:26:13 +03:00
Naeel bcf34d6b1a layer1: add namespace subscriber adapter step 19 2026-04-26 10:24:01 +03:00
Naeel 022960ade7 layer1: add namespace event bridge step 18 2026-04-26 10:23:21 +03:00
Naeel b1e2e6462d layer1: add namespace dispatch step 17 2026-04-26 10:22:35 +03:00
Naeel ae8275d0a7 layer1: add namespace lifecycle subscribers step 16 2026-04-26 10:21:26 +03:00
Naeel 2857398e11 layer1: checkpoint pending utils changes 2026-04-26 10:20:56 +03:00
Naeel 834c0de941 layer1: add namespace event helpers step 15 2026-04-26 10:05:51 +03:00
Naeel db4499d8c7 layer1: add namespace part helpers step 14 2026-04-26 10:05:12 +03:00
Naeel b3f99b2b6c layer1: centralize managed namespace labels step 13 2026-04-26 10:04:18 +03:00
Naeel 5e5058ba0e layer1: add namespace manager bridge step 12 2026-04-26 10:03:09 +03:00
Naeel 42acce308f layer1: add namespace bootstrap step 11 2026-04-26 10:02:37 +03:00
Naeel 66a3dc2a3c layer1: derive namespace phases step 10 2026-04-26 10:02:03 +03:00
Naeel 910f65b6d4 layer1: add namespace manager subscribers step 9 2026-04-26 10:01:21 +03:00
Naeel 114d5b99af layer1: add namespace manager skeleton step 8 2026-04-26 10:00:45 +03:00
Naeel ac2638f17d layer1: add namespace manager model step 7 2026-04-26 09:54:23 +03:00
Naeel 97b13a13c2 doc: add namespace manager target design 2026-04-26 09:53:06 +03:00
Naeel 1f53bc1fb7 doc: add detailed namespace rewrite logic 2026-04-26 09:50:06 +03:00
Naeel 87477d4529 layer1: guard router informer maps step 6 2026-04-26 09:37:09 +03:00
Naeel 94f26b69ee layer1: fix newdeploy namespace parity step 5 2026-04-26 09:35:39 +03:00
Naeel 56a499a59f layer1: fix buildermgr namespace dedup step 4 2026-04-26 09:34:46 +03:00
Naeel 6102b277c8 layer1: migrate runtime loops to snapshots step 3 2026-04-26 09:33:36 +03:00
Naeel 9ce9829f3b layer1: fix sa namespace routing step 2 2026-04-26 09:32:08 +03:00
Naeel c987fa07e8 layer1: add namespace snapshot api step 1 2026-04-26 09:30:51 +03:00
Naeel 27a280bc03 doc: record debugging comparison notes 2026-04-26 09:11:18 +03:00
Naeel e2dff8db09 doc: add detailed layer1 multi-tenant fix report 2026-04-26 09:06:44 +03:00
Naeel 7faaa9dc1f rbac: allow router namespace watch in multi-tenant mode 2026-04-26 09:03:15 +03:00
Naeel f617913ad9 rbac: allow full fetcher role provisioning in dynamic namespaces 2026-04-26 08:59:02 +03:00
Naeel 8ccc9fb342 rbac: add fission-executor-sa-provisioner for SA/Role/RoleBinding creation in user NS
Fixes EnsureNamespaceSA getting 403 Forbidden when provisioning fission-fetcher
SA in dynamically registered namespaces. Adds ClusterRole + ClusterRoleBinding
with create/update/patch for serviceaccounts, roles, rolebindings.

Also adds doc/progress.md and doc/thinking/2026-04-26-rbac-fix.md.
2026-04-26 07:46:33 +03:00
Naeel 161de70576 multi-tenant: EnsureNamespaceSA + ns_watcher SA provisioning (v8) 2026-04-26 07:41:46 +03:00
82e1ff76a5 Add npm dependabot rules (#3293)
* Add npm dependabot rules

Signed-off-by: Sanket Sudake <sanketsudake@gmail.com>

* Update .github/dependabot.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by:  Sanket Sudake <sanketsudake@gmail.com>

---------

Signed-off-by: Sanket Sudake <sanketsudake@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-16 10:33:52 +05:30
104 changed files with 7249 additions and 30 deletions
+61
View File
@@ -0,0 +1,61 @@
# Правила
## ⛔ ОТВЕЧАТЬ КРАТКО — АБСОЛЮТНОЕ ПРАВИЛО
- Вопрос → короткий ответ → СТОП.
- Ничего лишнего.
- Код — только по запросу.
## ⛔⛔⛔ ВОПРОС = СТОП
**Если в сообщении есть вопрос в ЛЮБОЙ форме** ("так ?", "верно ?", "почему ?", "как ?", "так же ?" и т.д.):
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
- СТОП — жду команды дальше
- **ЗАПРЕЩЕНО:** обновлять манифесты, заливать образы, применять на кластер, запускать тесты
**Исключение:** только если приказ явно включает цепочку ("собери И залей И тесты")
+12
View File
@@ -48,3 +48,15 @@ updates:
helm-charts:
patterns:
- "*"
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
exclude-paths:
- "test/**"
groups:
npm-dependencies:
patterns:
- "*"
+99
View File
@@ -0,0 +1,99 @@
# Правила работы агента
## ⛔⛔⛔ 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,6 +20,8 @@ environments/php7/vendor/
*.tfstate
*.backup
*.token
# Common backup files
*.swp
*.bak
+3
View File
@@ -0,0 +1,3 @@
FROM cgr.dev/chainguard/static:latest@sha256:a301031ffd4ed67f35ca7fa6cf3dad9937b5fa47d7493955a18d9b4ca5412d1a
COPY fission-bundle /
ENTRYPOINT ["/fission-bundle"]
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
FROM cgr.dev/chainguard/static:latest@sha256:a301031ffd4ed67f35ca7fa6cf3dad9937b5fa47d7493955a18d9b4ca5412d1a
COPY fission-bundle /
ENTRYPOINT ["/fission-bundle"]
Binary file not shown.
+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
+122
View File
@@ -0,0 +1,122 @@
# deploy/multitenant/rbac.yaml
#
# RBAC required for the Fission multi-tenant NSWatcher components.
#
# Both fission-executor and fission-router must be allowed to list and watch
# Namespaces at the cluster scope so that their NSWatchers can detect newly-
# labeled Namespaces.
#
# The executor also needs additional write permissions to provision the
# fission-fetcher ServiceAccount/Role/RoleBinding in new namespaces.
# Apply once per cluster after installing Fission:
#
# kubectl apply -f deploy/multitenant/rbac.yaml
#
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fission-executor-ns-watcher
labels:
app.kubernetes.io/name: fission
app.kubernetes.io/component: executor
app.kubernetes.io/part-of: fission-multitenant
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fission-executor-ns-watcher
labels:
app.kubernetes.io/name: fission
app.kubernetes.io/component: executor
app.kubernetes.io/part-of: fission-multitenant
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fission-executor-ns-watcher
subjects:
- kind: ServiceAccount
name: fission-executor
namespace: fission
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fission-router-ns-watcher
labels:
app.kubernetes.io/name: fission
app.kubernetes.io/component: router
app.kubernetes.io/part-of: fission-multitenant
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fission-router-ns-watcher
labels:
app.kubernetes.io/name: fission
app.kubernetes.io/component: router
app.kubernetes.io/part-of: fission-multitenant
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fission-router-ns-watcher
subjects:
- kind: ServiceAccount
name: fission-router
namespace: fission
---
# ClusterRole: allows fission-executor to create/update fission-fetcher SA,
# Role and RoleBinding in any user namespace managed by NSWatcher.
#
# It also needs two less-obvious permissions:
# 1. localsubjectaccessreviews.create — setupSAAndRoleBindings checks whether
# the target SA already has each permission before creating missing rules.
# 2. events.create — Kubernetes forbids creating a Role that grants permissions
# the caller does not currently hold. Since fission-fetcher gets events.create,
# fission-executor must hold it too in order to create that Role.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fission-executor-sa-provisioner
labels:
app.kubernetes.io/name: fission
app.kubernetes.io/component: executor
app.kubernetes.io/part-of: fission-multitenant
rules:
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create"]
- apiGroups: ["authorization.k8s.io"]
resources: ["localsubjectaccessreviews"]
verbs: ["create"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["roles", "rolebindings"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fission-executor-sa-provisioner
labels:
app.kubernetes.io/name: fission
app.kubernetes.io/component: executor
app.kubernetes.io/part-of: fission-multitenant
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fission-executor-sa-provisioner
subjects:
- kind: ServiceAccount
name: fission-executor
namespace: fission
+644
View File
@@ -0,0 +1,644 @@
# 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 * * * *"}'
```
+52
View File
@@ -0,0 +1,52 @@
# Fission Multi-Tenant — Progress
## Задача
Добиться 5/5 PASS в `test_layer1.sh`: динамически добавленный NS с меткой `fission.io/managed=true` должен работать без рестарта Fission.
---
## Статус задач
| # | Задача | Статус |
|---|--------|--------|
| 1 | Добавить `EnsureNamespaceSA` в `pkg/utils/serviceaccount.go` | ✅ DONE |
| 2 | Вызов `EnsureNamespaceSA` из `ns_watcher.go` при регистрации NS | ✅ DONE |
| 3 | Сборка образа `naeel/fission-bundle:v1.22.0-multi-ns-8` | ✅ DONE |
| 4 | Деплой образа v8 в кластер (executor/router/buildermgr) | ✅ DONE |
| 5 | Коммит `161de70` "multi-tenant: EnsureNamespaceSA + ns_watcher SA provisioning (v8)" | ✅ DONE |
| 6 | Исправить RBAC: добавить полный набор прав для SA provisioning в `deploy/multitenant/rbac.yaml` | ✅ DONE |
| 7 | Применить RBAC через `kubectl apply`, верифицировать SA/Role/RoleBinding | ✅ DONE |
| 8 | Коммит RBAC fix | 🔄 IN PROGRESS |
| 9 | Запустить `test_layer1.sh`, добиться 5/5 PASS | ⏳ TODO |
---
## Текущий результат теста
`test_layer1.sh` — 4/5:
- Шаг 5 падает: `serviceaccount "fission-fetcher" not found` в NS `l1-test-77773`
## Диагностика (2026-04-26)
- Код `EnsureNamespaceSA` присутствует в `serviceaccount.go`
- `ns_watcher.go` строка 168 вызывает `EnsureNamespaceSA`
- RBAC: `kubectl auth can-i create serviceaccounts --as=...fission-executor -n l1-test-77773`**`no`** ❌
- ClusterRole `fission-executor-multi-ns` не имеет `create` для `serviceaccounts`, и нет rules для `roles`/`rolebindings`
- Вывод: `setupSAAndRoleBindings` вызывается, но получает 403 Forbidden и тихо фейлится → SA не создаётся → pod не стартует
## Решение
Добавить в `deploy/multitenant/rbac.yaml` новый ClusterRole + ClusterRoleBinding с правами:
- `serviceaccounts`: `get/list/watch/create/update/patch`
- `roles`, `rolebindings`: `get/list/watch/create/update/patch`
- `events`: `create`
- `localsubjectaccessreviews.authorization.k8s.io`: `create`
Применить через `kubectl apply`.
**Пересборка образа НЕ нужна** — логика правильная, проблема только в RBAC.
## Последняя верификация
- `kubectl auth can-i create events --as=system:serviceaccount:fission:fission-executor``yes`
- `kubectl auth can-i create localsubjectaccessreviews.authorization.k8s.io --as=system:serviceaccount:fission:fission-executor``yes`
- В новом NS `rbac-verify-83117` автоматически созданы:
- `ServiceAccount/fission-fetcher`
- `Role/fission-fetcher-role-*`
- `RoleBinding/fission-fetcher-rolebinding-*`
+191
View File
@@ -0,0 +1,191 @@
# 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,248 @@
# 2026-04-26 - Layer1 multi-tenant NSWatcher: полный разбор до 5/5 PASS
## Цель
Довести `test_layer1.sh` до `PASS=5 FAIL=0` для сценария:
1. создаётся новый namespace
2. namespace получает label `fission.io/managed=true`
3. Fission без рестарта подхватывает namespace
4. в namespace создаются `Environment`, `Function`, `HTTPTrigger`
5. функция успешно вызывается через router
Ключевое требование: всё должно происходить без rolling restart Fission-компонентов.
## Исходный симптом
Первый устойчивый симптом был таким:
- `test_layer1.sh` стабильно доходил до `4/5`
- шаг вызова функции падал
- в user namespace наблюдалось:
- `FailedCreate`
- `serviceaccount "fission-fetcher" not found`
Это означало, что poolmgr deployment для environment уже создаётся, но pod не может стартовать без `fission-fetcher` ServiceAccount.
## Что уже было исправлено до RBAC-этапа
Кодовая часть hot-registration была уже внедрена ранее:
- `pkg/utils/serviceaccount.go`
- добавлена `EnsureNamespaceSA(...)`
- `pkg/executor/multitenant/ns_watcher.go`
- при регистрации нового namespace вызывается `EnsureNamespaceSA(...)`
- образ `naeel/fission-bundle:v1.22.0-multi-ns-8` уже был собран и задеплоен
То есть логика в коде уже существовала; сбой был не в отсутствии вызова, а в невозможности выполнить его успешно в кластере.
## Диагностика 1: executor не может создать ServiceAccount/Role/RoleBinding
Была проведена проверка прав service account `fission-executor`.
Подтверждено:
- код `EnsureNamespaceSA` вызывается
- `ns_watcher` регистрирует namespace
- executor не имеет достаточных RBAC-прав для provisioning ресурсов в новом namespace
Первый явный пробел:
- отсутствовали права на:
- `serviceaccounts`
- `roles`
- `rolebindings`
После начального RBAC fix было видно, что `ServiceAccount/fission-fetcher` уже создаётся, но этого оказалось недостаточно.
## Диагностика 2: initial RBAC fix оказался неполным
После расширения прав на `serviceaccounts/roles/rolebindings` тест перестал падать на отсутствии SA, но при детальной диагностике выяснилось, что `EnsureNamespaceSA` всё ещё не может полностью создать `Role` для fetcher.
Ключевой лог executor:
```text
error while creating role for sa fission-fetcher in namespace diag-ns-82702
... is attempting to grant RBAC permissions not currently held:
{APIGroups:[""], Resources:["events"], Verbs:["create"]}
```
И дополнительный лог перед этим:
```text
localsubjectaccessreviews.authorization.k8s.io is forbidden
```
### Что это означает
Функция `setupSAAndRoleBindings()` делает две важные вещи:
1. пытается проверить уже существующие права через `LocalSubjectAccessReview`
2. если прав нет, создаёт `Role` с нужными permission-ами
Следовательно executor должен иметь не только право создавать `Role/RoleBinding`, но и:
- `authorization.k8s.io/localsubjectaccessreviews:create`
- все permission-ы, которые он пытается делегировать через создаваемую `Role`
В нашем случае fetcher получает право:
- `events:create`
По правилам Kubernetes нельзя создать `Role`, выдающую право, которого нет у самого вызывающего субъекта. Поэтому executor должен был сам иметь `events:create`.
### Реальный root cause на этом этапе
`fission-executor` не имел:
- `events.create`
- `localsubjectaccessreviews.create`
Из-за этого:
- `ServiceAccount` создавался
- но `Role` и `RoleBinding` создавались не полностью или не создавались вовсе
- downstream specialization ломалась
## Исправление 1: полный executor RBAC для dynamic SA provisioning
В `deploy/multitenant/rbac.yaml` был добавлен и затем расширен `ClusterRole`:
- `fission-executor-sa-provisioner`
Итоговый набор прав для него:
- core:
- `serviceaccounts`: `get`, `list`, `watch`, `create`, `update`, `patch`
- `events`: `create`
- `authorization.k8s.io`:
- `localsubjectaccessreviews`: `create`
- `rbac.authorization.k8s.io`:
- `roles`: `get`, `list`, `watch`, `create`, `update`, `patch`
- `rolebindings`: `get`, `list`, `watch`, `create`, `update`, `patch`
После применения этого манифеста было подтверждено:
- `kubectl auth can-i create events --as=system:serviceaccount:fission:fission-executor` -> `yes`
- `kubectl auth can-i create localsubjectaccessreviews.authorization.k8s.io --as=system:serviceaccount:fission:fission-executor` -> `yes`
И в новом test namespace автоматически появлялись:
- `ServiceAccount/fission-fetcher`
- `Role/fission-fetcher-role-*`
- `RoleBinding/fission-fetcher-rolebinding-*`
## Изменение симптома после executor-fix
После полного executor RBAC fix шаг 5 перестал падать с `500` timeout от executor.
Новый симптом:
- постоянный `HTTP 404`
- router не видел route/function в новом namespace
Это был важный индикатор того, что executor-path уже работает лучше, а оставшаяся проблема находится в router-path.
## Диагностика 3: router NSWatcher не мог watch/list namespaces
Лог router показал прямую ошибку:
```text
failed to list *v1.Namespace: namespaces is forbidden:
User "system:serviceaccount:fission:fission-router" cannot list resource
"namespaces" at the cluster scope
```
При этом код router уже содержал dynamic namespace watcher:
- `pkg/router/ns_watcher.go`
То есть логика была, но RBAC для `fission-router` отсутствовал.
### Реальный root cause на этом этапе
`fission-router` не имел cluster-scope прав:
- `namespaces:list`
- `namespaces:watch`
Из-за этого:
- router не подхватывал новые labeled namespaces
- `HTTPTriggerSet.AddNamespace(...)` не вызывался
- HTTP trigger не попадал в router runtime map
- вызов функции возвращал `404`
## Исправление 2: router RBAC для NSWatcher
В тот же `deploy/multitenant/rbac.yaml` добавлены:
- `ClusterRole/fission-router-ns-watcher`
- `ClusterRoleBinding/fission-router-ns-watcher`
С правами:
- core `namespaces`: `list`, `watch`
После применения подтверждено:
- `kubectl auth can-i list namespaces --as=system:serviceaccount:fission:fission-router` -> `yes`
- `kubectl auth can-i watch namespaces --as=system:serviceaccount:fission:fission-router` -> `yes`
## Финальная проверка
После обоих RBAC fixes повторный запуск `test_layer1.sh` дал:
```text
ИТОГ: PASS=5 FAIL=0
```
На шаге 5 функция успешно ответила:
```text
HTTP 200 - hello from layer1
```
## Что именно оказалось правдой по итогу
Итоговая проблема состояла из двух последовательных RBAC-дырок:
1. executor не мог полностью provision-ить `fission-fetcher` в динамическом namespace
2. router не мог подхватить новый namespace из-за отсутствия namespace watch/list
То есть код hot-registration в целом был правильный, но runtime contract в Kubernetes RBAC был реализован не полностью.
## Итоговые изменения
### Код и манифесты
- `deploy/multitenant/rbac.yaml`
- executor namespace watch
- executor SA provisioning RBAC
- router namespace watch RBAC
### Документация
- `doc/progress.md`
- `doc/thinking/2026-04-26-rbac-fix.md`
- `doc/thinking/2026-04-26-layer1-pass-detailed.md`
### Коммиты по ходу исправления
- `161de70` - `multi-tenant: EnsureNamespaceSA + ns_watcher SA provisioning (v8)`
- `8ccc9fb` - первый RBAC commit
- `f617913` - полный executor RBAC fix для fetcher role provisioning
- `7faaa9d` - router namespace watch RBAC
## Практический вывод
Для hot namespace onboarding в Fission недостаточно просто добавить informer-ы в коде.
Нужно обеспечить весь runtime contract:
- executor видит namespace
- executor может provision-ить service accounts и RBAC в tenant namespace
- executor может делегировать все требуемые permission-ы
- router видит namespace и подписывается на triggers/functions в нём
Если хотя бы одно из этих звеньев отсутствует, поведение выглядит как "код вроде есть, но dynamic namespace не работает".
@@ -0,0 +1,589 @@
# 2026-04-26 — Layer 1 namespace rewrite: подробная логика правок
## Зачем этот документ
Нужен не просто список коммитов, а объяснение инженерной логики:
- что именно было не так в коде;
- почему исправление выбрано именно таким;
- почему изменения разбиты на маленькие шаги;
- какие инварианты я старался сохранить;
- что уже исправлено, а что еще нет.
Этот документ описывает серию маленьких безопасных шагов в ветке
`rewrite/layer1-namespace-manager-step1`.
Основной принцип серии:
1. Не делать большой взрывной rewrite.
2. Сначала сузить race-surface и разъединить старую статическую модель от новой динамической.
3. Исправлять реальные дефекты отдельно от mechanical refactor.
4. После каждого шага отдельно проверять соответствующий пакет тестами.
---
## Исходная архитектурная проблема
Переделанный Layer 1 жил в гибридном состоянии.
Старая модель Fission:
- список resource namespaces задается один раз на старте;
- компоненты считают этот список immutable;
- informer factories строятся из startup configuration.
Новая multi-tenant модель:
- namespace появляется позже, уже после старта процесса;
- watcher видит label `fission.io/managed=true`;
- компоненты должны подключить новый namespace на лету.
Из-за этого в коде образовался разрыв между двумя мирами:
1. Часть кода уже работает как dynamic system.
2. Часть кода все еще читает глобальную map namespace-ов напрямую, как будто она immutable.
3. В некоторых компонентах startup-path и dynamic-path оказались несимметричными.
4. В некоторых местах общий global dedup конфликтует с локальной логикой конкретного компонента.
Это и есть корневой дефект всей подсистемы: не один конкретный баг, а отсутствие единого namespace lifecycle contract.
---
## Что было решено не делать сразу
Я сознательно не пошел в большой rewrite в один коммит.
Почему:
1. Слишком много точек входа: executor, router, buildermgr, storagesvc, utils.
2. Если переписать все сразу, невозможно будет локализовать регрессию.
3. Уже были реальные functional дефекты в нескольких местах, их удобнее чинить изолированно.
4. Пользователь отдельно попросил идти последовательно и проверять после каждого изменения.
Поэтому выбран bounded rewrite: сначала вычищать старые опасные предположения, затем исправлять функциональные несовпадения, и только потом идти к более крупному NamespaceManager.
---
## Инварианты серии
Во всех шагах я старался держать одинаковые правила.
### 1. Не ломать действующий onboarding contract
Если namespace приходит через label watcher, компоненты должны продолжать подключать его без рестарта. Нельзя было ради рефактора возвращаться к статической модели.
### 2. Не менять лишние контракты одновременно
Если шаг про snapshot API, он не должен заодно переписывать cleanup semantics.
### 3. Сначала механические и безопасные сдвиги, потом functional fixes
Это нужно, чтобы понимать, баг возник из-за новой логики или уже существовал ранее.
### 4. Каждый шаг должен быть проверяем локально
После каждого шага запускались тесты по затронутому пакету, а не абстрактное «кажется, всё нормально».
---
## Step 1 — Snapshot API для namespace resolver
Коммит: `c987fa0`
### Что было не так
`NamespaceResolver` уже имел mutex для записи через `AddNamespace`, но многие потребители читали `FissionResourceNS` напрямую.
Это означало следующее:
1. Запись в map уже динамическая.
2. Чтение в части мест по-прежнему не thread-safe.
3. Код внешне выглядел как безопасный, потому что mutex в структуре есть, но контракт чтения не был централизован.
То есть защита существовала только наполовину.
### Что я сделал
В `pkg/utils/namespace.go` добавлены:
- `Snapshot()`
- `SnapshotWithOptions()`
Их логика:
1. Под read lock взять текущее состояние.
2. Скопировать его в detached slice.
3. Отсортировать, чтобы получить стабильный детерминированный порядок.
Почему именно slice snapshot, а не снова map:
1. Читателям в основном нужен именно проход по namespace-ам.
2. Slice удобнее для безопасной итерации.
3. Сортировка убирает дрожание порядка и делает поведение более предсказуемым в тестах и логике startup factory generation.
### Почему это был правильный первый шаг
Этот шаг почти не меняет бизнес-логику. Он не трогает watchers, RBAC, cleanup, lifecycle events. Он вводит базовый безопасный API, на который потом можно переводить потребителей.
### Что было переведено сразу
Чтобы snapshot API не оставался мертвым кодом, на него были переведены:
- `pkg/utils/informer.go`
- startup factory creation в `pkg/executor/executor.go`
Логика этого выбора:
1. Это общие helper path.
2. Они касаются большого числа компонентов.
3. Но при этом change поверхностный: вместо прямой итерации по map берется snapshot.
### Отдельный мелкий дефект, найденный на шаге 1
Новые тесты создали локальный `NamespaceResolver` без logger. Выяснилось, что часть методов предполагает ненулевой logger. Это нехорошо само по себе: utility object не должен падать только потому, что его используют вне global singleton.
Поэтому были добавлены nil checks вокруг debug/info логов в resolver.
### Проверка шага
Проверялось:
- `go test ./pkg/utils/...`
- `go test ./pkg/executor/...`
Смысл проверки:
1. Убедиться, что snapshot API корректен как utility layer.
2. Убедиться, что startup path executor не поменял поведение.
---
## Step 2 — Исправление namespace routing в serviceaccount checker
Коммит: `9ce9829`
### Что было не так
В `pkg/utils/serviceaccount.go` был более тонкий дефект, чем просто прямое чтение map.
В `runSACheck()` одна и та же переменная `ns` переиспользовалась внутри цикла по permission groups.
Смысл проблемы:
1. Есть исходный base namespace.
2. Для fetcher нужен путь через `GetFunctionNS(baseNS)`.
3. Для builder нужен путь через `GetBuilderNS(baseNS)`.
4. Но код мутировал саму переменную `ns` по мере обхода permission sets.
Это опасно, потому что builder resolution начинает зависеть от предыдущего шага цикла, а не от исходного namespace.
Если `FunctionNamespace` и `BuilderNamespace` различаются, route builder SA может поехать.
### Что я сделал
Изменение было разбито на две части:
1. Итерироваться не по `FissionResourceNS` напрямую, а по `Snapshot()`.
2. Явно вычислять `targetNS` из `baseNS` через отдельный метод `resolveSANamespace(baseNS, saName)`.
Почему выделен отдельный метод:
1. Логика namespace routing становится читаемой как отдельный контракт.
2. Её можно тестировать отдельно.
3. В коде исчезает скрытая мутация переменной цикла.
### Почему я не переписывал весь serviceaccount.go сразу
В файле еще остаются спорные места:
- глобальные `fetcherCheck` / `builderCheck`;
- мутация `permission.exists`;
- runtime provisioning через `LocalSubjectAccessReview`.
Но если решать всё сразу, шаг становится слишком широким. На этом этапе была цель исправить именно namespace routing bug и убрать прямую итерацию по общей map.
### Какой тест был добавлен
Добавлен unit test на `resolveSANamespace()`:
- fetcher на default namespace должен идти в function namespace;
- builder на default namespace должен идти в builder namespace;
- tenant namespace должен сохраняться как tenant namespace.
Тест важен не из-за синтаксиса, а потому что он фиксирует смысловую развязку между двумя namespace path.
### Проверка шага
Проверялось:
- `go test ./pkg/utils/...`
- `go test ./pkg/executor/...`
---
## Step 3 — Перевод runtime loops на snapshot API
Коммит: `6102b27`
### Что было не так
Даже после появления snapshot API ещё оставались runtime loops, которые напрямую читали общую map namespace-ов в горячих путях:
- adopt existing resources;
- idle object reaper;
- orphan archive pruning.
Это плохо не только из-за race. Это также концептуально закрепляет старую модель «список namespace-ов — это просто глобальная map, в которую можно смотреть отовсюду».
### Что я сделал
Перевёл на `Snapshot()` следующие места:
- `pkg/executor/executortype/container/containermgr.go`
- `pkg/executor/executortype/newdeploy/newdeploymgr.go`
- `pkg/executor/executortype/poolmgr/gpm.go`
- `pkg/storagesvc/archivePruner.go`
### Почему именно эти места были хорошим кандидатом
Потому что это mechanical refactor:
1. Логика списков не меняется.
2. Namespace source меняется с raw map на stable snapshot.
3. Поведение должно оставаться тем же, кроме устранения unsafe read.
### Что это дало
1. Уменьшило площадь прямого доступа к глобальному mutable состоянию.
2. Подготовило код к следующему этапу, когда namespace registry станет ещё более централизованным.
3. Сделало background loops более предсказуемыми при одновременном dynamic onboarding.
### Проверка шага
Проверялось:
- `go test ./pkg/executor/... ./pkg/storagesvc/...`
---
## Step 4 — Исправление buildermgr dedup bug
Коммит: `56a499a`
### Это уже не mechanical refactor, а реальный functional fix
### Что было не так
`buildermgr.StartNSWatcher()` при появлении нового namespace делал:
1. `envw.AddNamespace()`
2. `pkgw.AddNamespace()`
Но оба watcher-а использовали один и тот же глобальный dedup через `nsResolver.AddNamespace()`.
Фактический эффект:
1. Первый вызов успешно добавляет namespace в global resolver.
2. Второй вызов видит, что namespace уже «есть».
3. И просто выходит.
То есть в buildermgr динамический namespace мог получить только часть подписок.
Это уже не theoretical risk, а реальный дефект логики.
### Почему проблема архитектурная
Здесь смешались два уровня ответственности:
1. Global registry должен знать, что namespace существует.
2. Конкретный компонент должен знать, подписался ли он уже на этот namespace.
Это разные виды dedup.
Один глобальный dedup не может корректно заменить локальный dedup для двух разных subcomponents.
### Что я сделал
Логику развёл по уровням:
1. В `pkg/buildermgr/ns_watcher.go` global resolver обновляется один раз.
2. `environmentWatcher` dedup делает по своей map `envWatchInformer`.
3. `packageWatcher` dedup делает по своей map `pkgInformer`.
### Почему это правильнее
Теперь структура похожа на executor path:
1. Глобальный реестр говорит: namespace известен системе.
2. Каждый компонент сам решает: свои informers он уже поднял или нет.
Именно так должен выглядеть multi-component dynamic onboarding.
### Что я сознательно не делал
Не добавлял remove/cleanup и не переделывал buildermgr lifecycle целиком. На шаге требовалось только убрать ошибку дедупликации.
### Проверка шага
Проверялось:
- `go test ./pkg/buildermgr/...`
Тестов в пакете немного, но для этого шага важно было хотя бы подтвердить, что wiring собирается и не поломан compile-time.
---
## Step 5 — Исправление parity gap в newdeploy
Коммит: `94f26b6`
### Что было не так
`MakeNewDeploy()` на старте процесса регистрировал оба типа handler-ов:
- `FunctionEventHandlers()`
- `EnvEventHandlers()`
Но `AddNamespace()` для динамически появившегося namespace регистрировал только `FunctionEventHandlers()`.
Это значит, что два namespace-а с одинаковым содержимым вели себя по-разному только из-за времени появления:
1. startup namespace обслуживается полным code path;
2. dynamic namespace обслуживается урезанным code path.
Это очень плохое свойство для Layer 1, потому что поведение перестаёт зависеть только от данных и начинает зависеть от истории запуска процесса.
### Что я сделал
В `newdeploy.AddNamespace()` добавил регистрацию `EnvEventHandlers()` рядом с `FunctionEventHandlers()`.
### Почему fix именно такой
Потому что это минимальное исправление семантической несимметрии.
Я не придумывал новую абстракцию, а привёл dynamic path к уже существующему startup contract.
### Инженерный смысл шага
Это важный принцип всей серии: если startup-path и late onboarding-path делают похожую работу, они должны проходить через один и тот же контракт, а не через два слегка разных набора side effects.
### Проверка шага
Проверялось:
- `go test ./pkg/executor/executortype/newdeploy`
---
## Step 6 — Защита router informer maps от гонок
Коммит: `87477d4`
### Что было не так
В router динамический namespace добавляет новые informer-ы в две map:
- `triggerInformer`
- `funcInformer`
Параллельно `updateRouter()` итерируется по тем же map, собирая триггеры и функции для rebuild router-а.
Плюс `functionReferenceResolver` получает `funcInformer` и тоже читает его напрямую.
Это создаёт классическую проблему:
1. одна goroutine пишет в map;
2. другая одновременно по ней итерируется;
3. третья читает её через resolver.
Результат может быть от паники `concurrent map iteration and map write` до тихого чтения неполного состояния.
### Почему шаг стал чуть шире
Простой mutex только вокруг `HTTPTriggerSet.AddNamespace()` не решал бы проблему полностью, потому что `functionReferenceResolver` держал свою ссылку на ту же mutable структуру.
Поэтому понадобилось сделать две вещи одновременно:
1. Защитить maps в `HTTPTriggerSet` через `RWMutex` и snapshot helpers.
2. Дать `functionReferenceResolver` собственный thread-safe путь доступа к informer registry.
### Что я сделал
В `HTTPTriggerSet`:
- добавлен `RWMutex`;
- добавлены `snapshotTriggerInformers()`;
- добавлены `snapshotFuncInformers()`;
- `updateRouter()` и setup handlers теперь работают по snapshot-спискам.
В `functionReferenceResolver`:
- добавлен `RWMutex`;
- чтение informer-а по namespace теперь под read lock;
- добавлен `addInformer()` для безопасного добавления нового namespace.
В `router.AddNamespace()`:
- запись в `triggerInformer` и `funcInformer` идёт под lock;
- resolver получает новый informer через собственный безопасный метод.
### Почему именно snapshot-helpers, а не держать lock во время всей итерации
Потому что rebuild router-а и чтение store-ов могут быть относительно дорогими. Держать глобальный lock на всё это время было бы лишним. Нам нужен был не coarse lock на длинный процесс, а короткий lock на получение стабильного снимка ссылок на informer-ы.
То есть стратегия такая:
1. Быстро снять snapshot ссылок.
2. Отпустить lock.
3. Работать со snapshot уже без блокировки записи.
Это лучше и по безопасности, и по latency.
### Проверка шага
Проверялось:
- `go test ./pkg/router/...`
---
## Почему шаги документировались отдельно
Я сохранял отдельный thinking-файл на каждый шаг не ради бюрократии, а ради трассируемости.
Когда изменения маленькие, отдельные документы позволяют понять:
1. какой дефект исправлял именно этот коммит;
2. что было осознанно оставлено за рамками;
3. какой тест подтверждал именно этот шаг;
4. где functional fix, а где только mechanical safety refactor.
Именно это позволяет потом анализировать regressions не по памяти, а по истории.
---
## Что осталось нерешённым после step 6
Несмотря на шесть шагов, это ещё не финальный NamespaceManager rewrite.
Остаются важные вопросы.
### 1. Нет remove/cleanup semantics
Система умеет add, но почти не умеет delete/relabel cleanup.
Что это значит practically:
- informer-ы и локальные registry entries живут вечно;
- once onboarded, always onboarded;
- короткоживущие tenant namespace-ы будут оставлять мусор.
### 2. `serviceaccount.go` всё ещё не идеален
Текущий `serviceaccount.go` уже лучше, чем до step 2, но файл всё ещё сложный:
- глобальные `fetcherCheck` / `builderCheck` живут как process-wide mutable objects;
- `permission.exists` мутируется в runtime;
- provisioning и permission-check тесно сцеплены.
Это отдельный кандидат на следующий bounded refactor, но уже не маленький mechanical шаг.
### 3. Глобальный resolver всё ещё остаётся transitional abstraction
`NamespaceResolver` теперь безопаснее для чтения, но это пока ещё не полноценный NamespaceManager с событиями, remove lifecycle и подписками.
Он всё ещё ближе к thread-safe registry, чем к полной orchestration layer.
### 4. Cleanup/restart/backfill lifecycle ещё не централизован
Часть компонентов уже ближе к единообразию, но по-прежнему нет одного центрального orchestration contract вида:
- add existing namespaces on startup;
- reconcile on relabel;
- remove on delete;
- rebuild after restart;
- re-register late component safely.
---
## Почему я не стал сразу делать remove/cleanup
Потому что это уже следующая категория сложности.
До step 6 изменения укладывались в схему:
- локальный и понятный дефект;
- ограниченный blast radius;
- тестируемый пакет;
- отдельный маленький commit.
Remove/cleanup меняет уже жизненный цикл системы и затрагивает много мест одновременно:
- watcher behavior;
- manager lifecycle;
- informer shutdown semantics;
- cache invalidation;
- resolver state.
Это не тот шаг, который разумно смешивать с небольшими safety fixes.
---
## Почему такая стратегия лучше, чем «переписать всё сразу»
Потому что сейчас уже есть видимый результат с низким риском:
1. Уменьшено число прямых доступов к общей mutable map.
2. Исправлен реальный functional bug в buildermgr.
3. Исправлена реальная логическая ошибка в serviceaccount namespace routing.
4. Исправлена несимметрия в newdeploy dynamic path.
5. Закрыта явная router race-surface.
И всё это не одним большим коммитом, а серией шагов с локальной верификацией.
Для инфраструктурного кода это важнее, чем «красивый большой rewrite», который сложно раскладывать при регрессиях.
---
## Какие проверки были прогнаны по ходу серии
После шагов запускались:
- `go test ./pkg/utils/...`
- `go test ./pkg/executor/...`
- `go test ./pkg/storagesvc/...`
- `go test ./pkg/buildermgr/...`
- `go test ./pkg/router/...`
Логика была такая:
1. Не гонять каждый раз всю репу, если шаг локальный.
2. Но обязательно проверять затронутый пакет и соседний пакет, если change касается shared utility layer.
---
## Текущее состояние после серии
Серия шагов 1-6 не завершает rewrite, но заметно улучшает базу для следующего этапа.
Что теперь стало лучше:
1. Namespace reads стали заметно более дисциплинированными.
2. Dynamic namespace onboarding стал логически ровнее между компонентами.
3. В router исчезла наиболее явная race-surface на informer maps.
4. Buildermgr больше не теряет часть подписок на новый namespace из-за неправильного dedup.
Что остаётся следующим осмысленным этапом:
1. Вынесение уже полноценного NamespaceManager как orchestration layer.
2. Remove/cleanup lifecycle.
3. Разделение discovery, registry и provisioning.
4. Дополнительные тесты на restart/relabel/delete/burst onboarding.
---
## Отдельная заметка про `serviceaccount.go`
На момент написания этого документа файл `pkg/utils/serviceaccount.go` был заново перечитан по текущему содержимому. Документ описывает актуальную логику файла в его текущем состоянии, а не только то состояние, которое было в момент коммита step 2.
Это важно, потому что именно в этом файле пользовательский контекст отдельно предупредил о возможных дополнительных изменениях между сообщениями.
@@ -0,0 +1,44 @@
# 2026-04-26 — NamespaceManager rewrite, step 1
## Цель шага
Начать bounded rewrite Layer 1 без большого взрыва по коду.
Первый шаг deliberately узкий:
- не менять lifecycle namespace onboarding;
- не трогать watcher-ы executor/router/buildermgr;
- не менять контракты `AddNamespace`;
- убрать первые прямые проходы по общей mutable map `FissionResourceNS`.
## Почему именно так
Сейчас multi-tenant логика уже динамическая, но многие старые code path все еще читают
`DefaultNSResolver().FissionResourceNS` напрямую. Это опасно по двум причинам:
1. map общая и mutable, а dynamic onboarding меняет ее во время работы процесса;
2. часть helper-ов и startup path продолжают жить как будто список namespace-ов immutable.
Полный rewrite в один шаг дал бы слишком большой blast radius. Поэтому сначала вводится
thread-safe snapshot API в namespace layer, а затем существующие потребители переводятся
на него по одному.
## План шага 1
1. Добавить в `pkg/utils/namespace.go` методы snapshot для plain namespaces и namespaces with options.
2. Перевести `pkg/utils/informer.go` на snapshot API.
3. Перевести startup factory path в `pkg/executor/executor.go` на snapshot API.
4. Добавить unit tests для snapshot behavior.
5. Прогнать `go test ./pkg/utils/... ./pkg/executor/...`.
## Ожидаемый эффект
- меньше прямых чтений общей map;
- появление базового API, через который дальше можно выносить единый NamespaceManager;
- нулевое изменение внешнего поведения на этом шаге.
## Что НЕ делаем на этом шаге
- не исправляем watcher lifecycle;
- не добавляем remove/delete semantics;
- не трогаем router race и buildermgr dedup bug;
- не меняем RBAC.
@@ -0,0 +1,22 @@
# 2026-04-26 — NamespaceManager rewrite, step 10
## Цель шага
Научить skeleton manager выводить общую phase namespace-а из part states.
## Что меняем
1. Добавляем константы состояний частей:
- `registering`
- `active`
- `failed`
2. После `MarkPartState()` manager пересчитывает общую phase namespace-а.
3. Добавляем unit tests на переходы:
- registering -> active
- failed -> NamespacePhaseFailed
## Что НЕ меняем
- не запускаем реальный reconcile loop;
- не вызываем subscriber-ов автоматически;
- не подключаем manager к runtime.
@@ -0,0 +1,18 @@
# 2026-04-26 — NamespaceManager rewrite, step 11
## Цель шага
Добавить bootstrap helper для массовой загрузки initial namespace set в manager.
## Что меняем
1. Добавляем `Bootstrap()` в manager interface и реализацию.
2. Метод принимает список namespace-ов и `NamespaceSource`.
3. Метод прогоняет namespaces через `Upsert()` как initial discovered set.
4. Добавляем unit tests на bootstrap.
## Что НЕ меняем
- не подключаем bootstrap к runtime startup path;
- не меняем watcher-ы;
- не трогаем resolver/SA/runtime.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 12
## Цель шага
Добавить bridge helper между legacy `NamespaceResolver` и новым `NamespaceManager`.
## Что меняем
1. Добавляем helper `NewBootstrappedNamespaceManager()`.
2. Helper берёт snapshot из resolver и bootstraps manager.
3. Добавляем unit test на bootstrap from resolver.
## Что НЕ меняем
- не подключаем helper к production startup path;
- не меняем watcher-ы;
- не меняем runtime components.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 13
## Цель шага
Централизовать managed namespace label contract в `utils`.
## Что меняем
1. Добавляем в `utils`:
- `ManagedNamespaceLabelKey`
- `ManagedNamespaceLabelValue`
- `ManagedNamespaceLabelSelector()`
- `IsManagedNamespace()`
2. Переводим watcher-ы executor/router/buildermgr на единый helper.
## Что НЕ меняем
- не подключаем новый manager к watcher-ам;
- не меняем поведение onboarding;
- не трогаем runtime reconcile.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 14
## Цель шага
Добавить удобные helper-методы для part-state transitions.
## Что меняем
1. В manager interface добавляем:
- `MarkPartRegistering()`
- `MarkPartActive()`
- `MarkPartFailed()`
2. Реализуем их поверх `MarkPartState()`.
3. Добавляем unit tests.
## Что НЕ меняем
- не подключаем helpers к runtime reconcile;
- не трогаем watcher-ы и runtime components.
@@ -0,0 +1,16 @@
# 2026-04-26 — NamespaceManager rewrite, step 15
## Цель шага
Добавить utility helper-методы для построения `NamespaceEvent`.
## Что меняем
1. Добавляем `NewNamespaceEvent()`.
2. Добавляем `ManagedNamespaceEvent()`.
3. Добавляем unit tests.
## Что НЕ меняем
- не подключаем event helpers к watcher-ам;
- не меняем runtime behavior.
@@ -0,0 +1,18 @@
# 2026-04-26 — NamespaceManager rewrite, step 16
## Цель шага
Подготовить lifecycle subscriber contract для будущего reconcile path.
## Что меняем
1. Расширяем `NamespaceSubscriber` методами:
- `OnNamespaceAdd()`
- `OnNamespaceRemove()`
- `OnNamespaceResync()`
2. Обновляем тестовую заглушку subscriber-а.
## Что НЕ меняем
- не вызываем subscriber-ов из manager;
- не подключаем contract к runtime components.
@@ -0,0 +1,22 @@
# 2026-04-26 — NamespaceManager rewrite, step 17
## Цель шага
Добавить dispatch helper для прогона namespace через subscriber-ов в add/resync path.
## Что меняем
1. В manager interface добавляем:
- `DispatchAdd()`
- `DispatchResync()`
2. Manager вызывает subscriber-ов последовательно.
3. Для каждого subscriber-а manager проставляет part state:
- `registering`
- `active` или `failed`
4. Добавляем unit tests на success и failure path.
## Что НЕ меняем
- не подключаем dispatch к production watcher-ам;
- не добавляем remove dispatch;
- не меняем runtime components.
@@ -0,0 +1,15 @@
# 2026-04-26 — NamespaceManager rewrite, step 18
## Цель шага
Подготовить watcher-friendly helper для преобразования Kubernetes Namespace в `NamespaceEvent`.
## Что меняем
1. Добавляем `NamespaceEventFromNamespace()`.
2. Добавляем unit tests на перенос имени и labels.
## Что НЕ меняем
- не подключаем helper к watcher-ам;
- не меняем runtime behavior.
@@ -0,0 +1,16 @@
# 2026-04-26 — NamespaceManager rewrite, step 19
## Цель шага
Добавить functional adapter для `NamespaceSubscriber`.
## Что меняем
1. Добавляем `NamespaceSubscriberFuncs`.
2. Добавляем `Name()/OnNamespaceAdd()/OnNamespaceRemove()/OnNamespaceResync()`.
3. Добавляем unit tests.
## Что НЕ меняем
- не подключаем adapter к runtime;
- не меняем production watcher-ы.
@@ -0,0 +1,33 @@
# 2026-04-26 — NamespaceManager rewrite, step 2
## Цель шага
Убрать еще один прямой проход по `FissionResourceNS` и закрыть конкретный баг в
`pkg/utils/serviceaccount.go`.
## Проблема
`runSACheck()` сейчас:
1. итерируется по `sa.nsResolver.FissionResourceNS` напрямую;
2. переиспользует переменную `ns` внутри внутреннего цикла по permissions.
Из-за этого код выглядит безобидно, но фактически смешивает два разных namespace path:
- fetcher path через `GetFunctionNS()`;
- builder path через `GetBuilderNS()`.
Если `FunctionNamespace` и `BuilderNamespace` различаются, builder SA может начать
резолвиться уже не от исходного namespace, а от результата предыдущего шага цикла.
## Что меняем
1. Берем base namespaces через thread-safe `Snapshot()`.
2. Для каждого permission вычисляем `targetNS` из исходного `baseNS`, а не из мутированной переменной.
3. Добавляем unit test на routing function/builder namespace.
## Что НЕ меняем на этом шаге
- не трогаем глобальные `fetcherCheck` / `builderCheck` структуры;
- не меняем `LocalSubjectAccessReview` path;
- не делаем большой refactor всего SA provisioning.
@@ -0,0 +1,21 @@
# 2026-04-26 — NamespaceManager rewrite, step 20
## Цель шага
Сделать первый реальный runtime adapter для `NamespaceManager` в `buildermgr`.
## Что меняем
1. Добавляем buildermgr namespace subscriber.
2. Adapter переиспользует существующие `envWatcher.AddNamespace()` и `packageWatcher.AddNamespace()`.
3. `add/resync` path повторяет текущую логику watcher-а:
- добавить namespace в resolver;
- вызвать env watcher;
- вызвать package watcher.
4. Добавляем unit test на вызов обоих watcher-ов.
## Что НЕ меняем
- не подключаем subscriber к `StartNSWatcher()`;
- не меняем remove behavior;
- не ломаем текущий production flow.
@@ -0,0 +1,15 @@
# 2026-04-26 — NamespaceManager rewrite, step 21
## Цель шага
Свести текущий watcher flow и новый subscriber flow `buildermgr` к одному helper.
## Что меняем
1. `buildermgr/ns_watcher.go` больше не дублирует логику add/resync.
2. Watcher вызывает `registerBuilderNamespace()`.
## Что НЕ меняем
- не меняем внешний API watcher-а;
- не переключаем `StartNSWatcher()` на `NamespaceManager`.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 22
## Цель шага
Добавить первый runtime adapter для `router` по тому же шаблону, что и для `buildermgr`.
## Что меняем
1. Добавляем router namespace subscriber.
2. Adapter переиспользует существующий `HTTPTriggerSet.AddNamespace()`.
3. `add/resync` path прогоняется через общий helper.
## Что НЕ меняем
- не подключаем subscriber к `StartNSWatcher()`;
- не меняем remove path;
- не меняем текущий production flow.
@@ -0,0 +1,15 @@
# 2026-04-26 — NamespaceManager rewrite, step 23
## Цель шага
Свести текущий watcher flow и новый subscriber flow `router` к одному helper.
## Что меняем
1. `router/ns_watcher.go` больше не дублирует add/resync логику.
2. Watcher вызывает `registerRouterNamespace()`.
## Что НЕ меняем
- не переключаем `StartNSWatcher()` на `NamespaceManager`;
- не меняем внешний API watcher-а.
@@ -0,0 +1,16 @@
# 2026-04-26 — NamespaceManager rewrite, step 24
## Цель шага
Подготовить `executor/multitenant` к subscriber adapter без смены текущего watcher behavior.
## Что меняем
1. Выделяем отдельный helper для прогона `AddNamespace()` по executor type-ам.
2. Оставляем `EnsureNamespaceSA()` в текущем `registerNamespace()`.
3. Добавляем unit test на успешный прогон и propagation ошибок.
## Что НЕ меняем
- не подключаем `NamespaceManager`;
- не меняем внешний API watcher-а.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 25
## Цель шага
Добавить runtime adapter для `executor/multitenant` поверх уже выделенного helper-а.
## Что меняем
1. Добавляем executor namespace subscriber.
2. `add/resync` path переиспользует `registerNamespace()`.
3. Добавляем unit test на вызов executor type-ов.
## Что НЕ меняем
- не подключаем subscriber к watcher-у;
- не меняем remove path;
- не меняем внешний API watcher-а.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 26
## Цель шага
Добавить единый startup bridge для manager: bootstrap model + dispatch в subscriber-ы.
## Что меняем
1. В `NamespaceManager` добавляем `BootstrapAndDispatch()`.
2. Helper сначала делает `Bootstrap()`, потом вызывает `DispatchAdd()` по каждому namespace.
3. Ошибки агрегируются и не останавливают остальные namespace.
4. Добавляем unit tests на success и partial-failure.
## Что НЕ меняем
- не подключаем helper к production startup path;
- не меняем watcher behavior.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 27
## Цель шага
Сделать первый реальный runtime hook на `NamespaceManager` в `buildermgr` watcher.
## Что меняем
1. `buildermgr.StartNSWatcher()` поднимает локальный `NamespaceManager`.
2. В manager заранее bootstrapped текущий snapshot resolver-а.
3. Watcher `Add/Update` события прогоняет через:
- `Upsert()`
- `DispatchAdd()` или `DispatchResync()`
4. Подписчиком manager-а становится уже существующий `buildermgr` subscriber adapter.
## Что НЕ меняем
- не меняем `registerBuilderNamespace()`;
- не добавляем remove path;
- не меняем остальные компоненты.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 28
## Цель шага
Сделать такой же runtime hook на `NamespaceManager` в `router` watcher.
## Что меняем
1. `router.StartNSWatcher()` поднимает локальный `NamespaceManager`.
2. Manager bootstrapped из текущего resolver snapshot.
3. Watcher `Add/Update` события прогоняет через:
- `Upsert()`
- `DispatchAdd()` или `DispatchResync()`
4. Подписчиком manager-а становится router subscriber adapter.
## Что НЕ меняем
- не добавляем remove path;
- не меняем `HTTPTriggerSet.AddNamespace()`.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 29
## Цель шага
Перевести `executor/multitenant` watcher на тот же manager flow, что уже используется в `buildermgr` и `router`.
## Что меняем
1. `StartNSWatcher()` поднимает локальный `NamespaceManager`.
2. Manager bootstrapped из resolver snapshot.
3. Watcher `Add/Update` события прогоняет через:
- `Upsert()`
- `DispatchAdd()` или `DispatchResync()`
4. Подписчиком manager-а становится executor subscriber adapter.
## Что НЕ меняем
- не добавляем remove path;
- не меняем `registerNamespace()` и низкоуровневый executor registration helper.
@@ -0,0 +1,28 @@
# 2026-04-26 — NamespaceManager rewrite, step 3
## Цель шага
Срезать еще один слой прямых чтений `DefaultNSResolver().FissionResourceNS` в runtime code path.
## Почему это отдельный шаг
После step 1 snapshot API уже существует, но runtime loops в executor и storagesvc все еще
читают общую mutable map напрямую. Это не архитектурный rewrite, а чистый safety refactor:
- `container.AdoptExistingResources()`
- `newdeploy.AdoptExistingResources()`
- `newdeploy.doIdleObjectReaper()`
- `poolmgr.AdoptExistingResources()`
- `poolmgr.doIdleObjectReaper()`
- `storagesvc.ArchivePruner.getOrphanArchives()`
## Что меняем
В этих местах цикл переводится на `DefaultNSResolver().Snapshot()`.
## Что НЕ меняем
- не меняем семантику cleanup;
- не меняем behavior watcher-ов;
- не добавляем remove semantics;
- не исправляем router race и buildermgr dedup на этом шаге.
@@ -0,0 +1,15 @@
# 2026-04-26 — NamespaceManager rewrite, step 30
## Цель шага
Закрыть startup gap в `buildermgr`: manager должен отражать и существующие namespace-ы, а не только новые события watcher-а.
## Что меняем
1. `buildermgr.StartNSWatcher()` создаёт пустой `NamespaceManager`.
2. После `Subscribe()` выполняется `BootstrapAndDispatch()` по текущему snapshot resolver-а.
## Что НЕ меняем
- не меняем low-level registration helper;
- не меняем remove path.
@@ -0,0 +1,15 @@
# 2026-04-26 — NamespaceManager rewrite, step 31
## Цель шага
Закрыть startup gap в `router`: локальный manager должен отражать существующие namespace-ы уже на старте.
## Что меняем
1. `router.StartNSWatcher()` создаёт пустой `NamespaceManager`.
2. После `Subscribe()` выполняется `BootstrapAndDispatch()` по snapshot resolver-а.
## Что НЕ меняем
- не меняем `HTTPTriggerSet.AddNamespace()`;
- не добавляем remove path.
@@ -0,0 +1,15 @@
# 2026-04-26 — NamespaceManager rewrite, step 32
## Цель шага
Закрыть startup gap в `executor/multitenant`: manager должен отражать стартовые namespace-ы и прогонять их через тот же subscriber path.
## Что меняем
1. `StartNSWatcher()` создаёт пустой `NamespaceManager`.
2. После `Subscribe()` выполняется `BootstrapAndDispatch()` по snapshot resolver-а.
## Что НЕ меняем
- не меняем `registerNamespace()`;
- не добавляем remove path.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 33
## Цель шага
Убрать несоответствие между contract и manager implementation: `OnNamespaceRemove()` уже есть, а `DispatchRemove()` ещё нет.
## Что меняем
1. В `NamespaceManager` добавляем `DispatchRemove()`.
2. Manager вызывает `OnNamespaceRemove()` у всех subscriber-ов.
3. После dispatch namespace переводится в `removed` через `NamespaceEventRemove`.
4. Добавляем unit tests на success и failure path.
## Что НЕ меняем
- не подключаем remove events в watcher-ы;
- не реализуем physical cleanup в runtime components.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 34
## Цель шага
Подготовить безопасный helper для delete/tombstone событий Namespace informer-а.
## Что меняем
1. Добавляем `NamespaceFromObject()`.
2. Helper поддерживает:
- `*corev1.Namespace`
- `cache.DeletedFinalStateUnknown`
3. Добавляем `NamespaceEventFromObject()`.
4. Добавляем unit tests.
## Что НЕ меняем
- не подключаем delete handling в watcher-ы на этом шаге;
- не меняем runtime behavior.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 35
## Цель шага
Научить watcher-ы фиксировать label-drop/delete в локальном `NamespaceManager`, не трогая реальные runtime регистрации.
## Что меняем
1. Во все три namespace watcher-а добавляем:
- `DeleteFunc`
- обработку `managed -> unmanaged` в `UpdateFunc`
2. При таком событии watcher:
- создаёт `NamespaceEventRemove`
- записывает его в manager через `Upsert()`
- пишет явный log, что runtime cleanup НЕ выполняется
## Что НЕ меняем
- не вызываем `DispatchRemove()` из watcher-ов;
- не удаляем informer-ы, resolver state или runtime registrations.
@@ -0,0 +1,18 @@
# 2026-04-26 — NamespaceManager rewrite, step 36
## Цель шага
Убрать мёртвый код после перевода watcher-ов на `NamespaceManager` flow.
## Что меняем
1. Удаляем неиспользуемые helper-ы:
- `builderNSName()`
- `routerNSName()`
- `namespaceName()`
2. Убираем ставшие неиспользуемыми imports.
## Что НЕ меняем
- не меняем runtime behavior;
- не меняем watcher logic.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 37
## Цель шага
Убрать дублирование startup manager flow в трёх namespace watcher-ах.
## Что меняем
1. В `utils` добавляем helper `NewWatcherNamespaceManager()`.
2. Helper:
- создаёт `NamespaceManager`
- подписывает subscriber-ов
- выполняет `BootstrapAndDispatch()`
3. `buildermgr`, `router`, `executor/multitenant` используют новый helper.
## Что НЕ меняем
- не меняем semantics dispatch;
- не меняем runtime cleanup policy.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 38
## Цель шага
Убрать повторяющуюся lifecycle логiku namespace watcher-ов.
## Что меняем
1. В `utils` добавляем helpers:
- `NamespaceBecameUnmanaged()`
- `DispatchNamespaceAdd()`
- `DispatchNamespaceResync()`
- `RecordNamespaceRemoval()`
2. `buildermgr`, `router`, `executor/multitenant` используют эти helpers.
## Что НЕ меняем
- не меняем runtime semantics;
- remove по-прежнему только bookkeeping, без cleanup.
@@ -0,0 +1,43 @@
# 2026-04-26 — NamespaceManager rewrite, step 39
## Цель шага
Свести три namespace watcher-а к одинаковому lifecycle поведению через общие handlers в `utils`.
## Что меняем
1. Добавляем helpers:
- `HandleWatcherNamespaceAdd()`
- `HandleWatcherNamespaceUpdate()`
- `HandleWatcherNamespaceDelete()`
2. Helpers централизуют:
- dispatch add/resync;
- remove bookkeeping;
- стандартное logging-сообщение.
3. `buildermgr`, `router`, `executor/multitenant` переходят на эти helpers.
## Что НЕ меняем
- не меняем runtime cleanup policy;
- не меняем manager state model.# 2026-04-26 — NamespaceManager rewrite, step 39
## Цель шага
Свести три namespace watcher-а к одинаковому lifecycle поведению через общие handlers в `utils`.
## Что меняем
1. Добавляем helpers:
- `HandleWatcherNamespaceAdd()`
- `HandleWatcherNamespaceUpdate()`
- `HandleWatcherNamespaceDelete()`
2. Helpers централизуют:
- dispatch add/resync;
- remove bookkeeping;
- стандартное logging-сообщение.
3. `buildermgr`, `router`, `executor/multitenant` переходят на эти helpers.
## Что НЕ меняем
- не меняем runtime cleanup policy;
- не меняем manager state model.
@@ -0,0 +1,32 @@
# 2026-04-26 — NamespaceManager rewrite, step 4
## Цель шага
Исправить реальный functional bug в dynamic onboarding buildermgr.
## Дефект
`buildermgr.StartNSWatcher()` вызывает:
1. `envw.AddNamespace()`
2. `pkgw.AddNamespace()`
Но оба watcher-а используют один и тот же глобальный `nsResolver.AddNamespace()` для dedup.
Из-за этого первый вызов добавляет namespace, а второй считает его уже обработанным и
выходит раньше времени. В результате у динамического tenant namespace может подняться только
Environment informer без Package informer.
## Исправление
1. Глобальный resolver обновляется один раз в `buildermgr/ns_watcher.go`.
2. `environmentWatcher` dedup делает только по своей map `envWatchInformer`.
3. `packageWatcher` dedup делает только по своим map `pkgInformer` / `podInformer`.
Так buildermgr становится симметричнее executor path: общий registry обновляется один раз,
а конкретные компоненты сами решают, подписаны ли они уже на namespace.
## Что НЕ меняем
- не добавляем cleanup/remove semantics;
- не меняем router;
- не трогаем newdeploy parity gap на этом шаге.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 40
## Цель шага
Зафиксировать lifecycle policy для namespace removal в коде явно, а не только комментариями и log-сообщениями.
## Что меняем
1. Добавляем `NamespaceRemovalStrategy`.
2. Поддерживаем два режима:
- `track-only`
- `dispatch-remove`
3. Общие watcher handlers принимают strategy.
4. Текущий production flow использует `track-only`.
5. Добавляем unit tests на оба режима.
## Что НЕ меняем
- не включаем реальный remove dispatch в watcher-ах;
- не меняем runtime cleanup policy по умолчанию.
@@ -0,0 +1,16 @@
# 2026-04-26 — NamespaceManager rewrite, step 41
## Цель шага
Довести explicit removal strategy до полного покрытия watcher lifecycle paths.
## Что меняем
1. `HandleWatcherNamespaceUpdate()` теперь тоже принимает `NamespaceRemovalStrategy`.
2. `managed -> unmanaged` path использует ту же policy, что и `DeleteFunc`.
3. Добавляем unit test на update-path с `dispatch-remove`.
## Что НЕ меняем
- текущие watcher-ы остаются на `track-only`;
- runtime cleanup policy по умолчанию не меняется.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 42
## Цель шага
Убрать последний крупный слой дублирования в namespace watcher-ах: сами `ResourceEventHandlerFuncs`.
## Что меняем
1. В `utils` добавляем `NewNamespaceWatcherEventHandlers()`.
2. Конструктор собирает общий `Add/Update/Delete` flow на базе уже существующих handler helper-ов.
3. `buildermgr`, `router`, `executor/multitenant` используют общий конструктор.
## Что НЕ меняем
- не меняем label selector;
- не меняем manager semantics;
- не меняем removal policy по умолчанию.
@@ -0,0 +1,19 @@
# 2026-04-26 — NamespaceManager rewrite, step 43
## Цель шага
Убрать оставшуюся копипасту старта namespace informer-а из `buildermgr`, `router`, `executor/multitenant`.
## Что меняем
1. В `utils` добавляем `StartManagedNamespaceWatcher()`.
2. Helper централизует:
- informer factory с label selector;
- регистрацию event handlers;
- start/cache sync/stop logging через `mgr`.
3. Три watcher-а переходят на общий helper.
## Что НЕ меняем
- не меняем lifecycle logic;
- не меняем selector contract `fission.io/managed=true`.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 44
## Цель шага
Убрать последний дублирующийся orchestration-код из `StartNSWatcher()` в трёх компонентах.
## Что меняем
1. В `utils` добавляем `PrepareManagedNamespaceWatcher()`.
2. Helper:
- создаёт `NamespaceManager`;
- делает bootstrap+dispatch;
- собирает общие event handlers.
3. `buildermgr`, `router`, `executor/multitenant` используют этот helper.
## Что НЕ меняем
- не меняем subscriber logic;
- не меняем managed namespace watcher startup helper;
- не меняем removal strategy по умолчанию.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 45
## Цель шага
Подготовить компактный status/debug surface для `NamespaceManager`.
## Что меняем
1. Добавляем `NamespaceManagerSummary`.
2. В `NamespaceManager` добавляем `Summary()`.
3. Summary считает:
- общее число namespace-ов;
- число по phase;
- список subscriber-ов.
4. Добавляем unit tests.
## Что НЕ меняем
- не публикуем summary наружу через HTTP;
- не меняем watcher behavior.
@@ -0,0 +1,33 @@
# 2026-04-26 — NamespaceManager rewrite, step 46
## Цель шага
Закрыть маленький пробел в debug surface: `LogNamespaceManagerSummary()` уже используется, но отдельно не тестируется.
## Что меняем
1. Добавляем unit test на `LogNamespaceManagerSummary()`.
2. Проверяем, что helper безопасен на `nil` logger и не паникует на заполненном summary.
## Что НЕ меняем
- не меняем runtime behavior;
- не публикуем summary наружу через HTTP.# 2026-04-26 — NamespaceManager rewrite, step 46
## Цель шага
Начать реальное использование `NamespaceManager.Summary()` в orchestration layer.
## Что меняем
1. Добавляем helper `LogNamespaceManagerSummary()`.
2. `PrepareManagedNamespaceWatcher()` пишет summary после bootstrap.
3. В лог попадают:
- общее число namespace-ов;
- subscriber-ы;
- phase counts.
## Что НЕ меняем
- не экспортируем summary наружу через HTTP;
- не меняем runtime behavior watcher-ов.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 47
## Цель шага
Сделать `NamespaceManagerSummary` информативнее для наблюдения за источниками namespace state.
## Что меняем
1. В summary добавляем `SourceCounts`.
2. `Summary()` считает namespace-ы по `NamespaceSource`.
3. `LogNamespaceManagerSummary()` пишет `source_counts`.
4. Обновляем unit tests.
## Что НЕ меняем
- не меняем watcher behavior;
- не меняем semantics state transitions.
@@ -0,0 +1,33 @@
# 2026-04-26 — NamespaceManager rewrite, step 48
## Цель шага
Добавить маленький, но полезный helper поверх summary/debug contract: проверку, есть ли вообще живые namespace-ы.
## Что меняем
1. В `NamespaceManagerSummary` добавляем `HasActiveNamespaces()`.
2. Добавляем unit tests на true/false path.
## Что НЕ меняем
- не меняем summary counters;
- не меняем watcher behavior.# 2026-04-26 — NamespaceManager rewrite, step 48
## Цель шага
Убрать двусмысленность в `NamespaceManagerSummary`: сейчас `TotalNamespaces` включает и removed-записи.
## Что меняем
1. Добавляем `LiveNamespaces`.
2. `Summary()` считает его по `Snapshot()`.
3. `LogNamespaceManagerSummary()` пишет оба значения:
- `total_namespaces`
- `live_namespaces`
4. Обновляем unit tests.
## Что НЕ меняем
- не меняем правила хранения removed records;
- не меняем watcher behavior.
@@ -0,0 +1,35 @@
# 2026-04-26 — NamespaceManager rewrite, step 49
## Цель шага
Довести `HasActiveNamespaces()` до реального use-site, чтобы helper не оставался чисто декларативным.
## Что изменено
1. `LogNamespaceManagerSummary()` теперь пишет флаг `has_active_namespaces`.
2. Добавлен unit test на presence и значение этого поля в structured log.
## Почему это безопасно
- watcher behavior не меняется;
- изменён только debug/logging contract;
- покрыто `go test ./pkg/utils/...`.# 2026-04-26 — NamespaceManager rewrite, step 49
## Цель шага
Собрать `prepare + start` managed namespace watcher в один общий entrypoint.
## Что меняем
1. Добавляем `RunManagedNamespaceWatcher()`.
2. Helper:
- готовит manager;
- строит handlers;
- запускает managed namespace informer.
3. Три `StartNSWatcher()` переходят на новый entrypoint.
4. Добавляем минимальный unit test с fake client.
## Что НЕ меняем
- не меняем subscriber logic;
- не меняем selector/strategy semantics.
@@ -0,0 +1,29 @@
# 2026-04-26 — NamespaceManager rewrite, step 5
## Цель шага
Исправить несимметрию между startup-path и dynamic namespace onboarding в `newdeploy` executor.
## Дефект
На старте `MakeNewDeploy()` регистрирует два вида обработчиков на Fission informers:
- `FunctionEventHandlers()`
- `EnvEventHandlers()`
Но dynamic `AddNamespace()` регистрировал только `FunctionEventHandlers()`.
Это означало, что namespace, появившийся после старта процесса, обслуживается не тем же
код-path, что namespace, известный на старте. Для multi-tenant Layer 1 это плохая семантика:
часть поведения newdeploy зависит не от namespace, а от момента его появления.
## Исправление
В `AddNamespace()` добавляется регистрация `EnvEventHandlers()` перед запуском informer factory.
## Что НЕ меняем
- не меняем container executor;
- не меняем poolmgr;
- не добавляем remove semantics;
- не меняем router.
@@ -0,0 +1,32 @@
# 2026-04-26 — NamespaceManager rewrite, step 50
## Цель шага
Сделать summary/debug surface полезным в реальном watcher lifecycle, а не только на этапе подготовки manager-а.
## Что изменено
1. После успешных add/resync/remove transitions watcher helpers теперь пишут компактный summary manager-а.
2. Добавлен unit test на add-handler path с проверкой structured-log полей.
## Что это даёт
- runtime behavior не меняется;
- появляется последовательный debug trail по изменению manager state;
- новый helper `HasActiveNamespaces()` теперь используется и в general logging path, и в watcher transition path.# 2026-04-26 — NamespaceManager rewrite, step 50
## Цель шага
Сделать orchestration API для managed namespace watcher-а жёстче и читабельнее.
## Что меняем
1. Добавляем `ManagedNamespaceWatcherConfig`.
2. `PrepareManagedNamespaceWatcher()` и `RunManagedNamespaceWatcher()` принимают config struct.
3. Если strategy не задана, используется `track-only`.
4. Обновляем unit tests и call sites.
## Что НЕ меняем
- не меняем runtime semantics;
- не меняем subscriber logic.
@@ -0,0 +1,33 @@
# 2026-04-26 — NamespaceManager rewrite, step 51
## Цель шага
Закрыть observability gap между `prepared namespace manager` и runtime transition logs.
## Что изменено
1. `RunManagedNamespaceWatcher()` теперь пишет единый summary log после старта watcher-а.
2. Добавлен unit test на startup logging path.
## Почему это полезно
- buildermgr, router и executor получают одинаковый startup debug signal без копипасты;
- видно состояние manager-а в момент, когда watcher уже реально подключён;
- runtime semantics не меняется.# 2026-04-26 — NamespaceManager rewrite, step 51
## Цель шага
Убрать из call sites повторение стандартного config для managed namespace watcher-а.
## Что меняем
1. Добавляем `NewDefaultManagedNamespaceWatcherConfig()`.
2. Helper подставляет:
- `DefaultNSResolver().Snapshot()`;
- `track-only` как default removal strategy.
3. `buildermgr`, `router`, `executor/multitenant` используют helper.
## Что НЕ меняем
- не меняем runtime semantics;
- не меняем subscriber logic.
@@ -0,0 +1,17 @@
# 2026-04-26 — NamespaceManager rewrite, step 52
## Цель шага
Убрать хрупкость общего watcher path, где `nil` logger мог привести к panic на error/info ветках.
## Что изменено
1. Введена централизованная нормализация logger-а к `zap.NewNop()`.
2. Hardening применён к prepare/run/start и watcher event handlers.
3. Добавлены regression tests на nil-logger path.
## Почему это важно
- это уже runtime hardening, а не декоративный cleanup;
- общий helper layer стал безопаснее для повторного использования;
- поведение watcher-ов не меняется, меняется только устойчивость logging path.
@@ -0,0 +1,24 @@
# 2026-04-26 — NamespaceManager rewrite, step 53
## Итог step1
`rewrite/layer1-namespace-manager-step1` можно считать завершённым как отдельный этап.
## Критерии, которые теперь выполнены
1. Общий `NamespaceManager` и watcher orchestration вынесены в `pkg/utils`.
2. Buildermgr, router и executor/multitenant используют общий helper layer вместо прежней разрозненной lifecycle-логики.
3. Summary/debug contract стабилизирован и покрыт тестами.
4. Logging path усилен: есть prepare/start/transition summary logs и nil-logger hardening.
## Финальная проверка этапа
Пройден целевой набор:
`go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant`
Все пакеты зелёные.
## Что дальше
Следующий этап должен быть уже не про внутреннюю консолидацию watcher layer, а про внешний consumption этой модели: status/debug surface, integration behavior или следующий слой rewrite.
@@ -0,0 +1,34 @@
# 2026-04-26 — NamespaceManager rewrite, step 6
## Цель шага
Закрыть race-surface в router вокруг динамического добавления namespace informer-ов.
## Проблема
В router есть два связанных mutable map:
- `HTTPTriggerSet.triggerInformer`
- `HTTPTriggerSet.funcInformer`
`AddNamespace()` пишет в них на лету, а `updateRouter()` одновременно итерируется по ним.
Кроме того, `functionReferenceResolver` получает `funcInformer` map и читает ее без синхронизации.
Это делает dynamic onboarding потенциальным источником:
- `concurrent map iteration and map write`;
- чтения неполного снимка informer-ов;
- гонок между router rebuild и resolver lookup.
## Исправление
1. В `HTTPTriggerSet` добавляется `RWMutex` для informer maps.
2. Чтение informer-ов переводится на snapshot helpers.
3. `functionReferenceResolver` получает собственный lock и метод `addInformer()`.
4. `router.AddNamespace()` обновляет router map и resolver map под контролируемым доступом.
## Что НЕ меняем
- не переписываем router lifecycle целиком;
- не добавляем remove semantics;
- не меняем trigger/function business logic.
@@ -0,0 +1,36 @@
# 2026-04-26 — NamespaceManager rewrite, step 7
## Цель шага
Добавить минимальную модель данных для будущего `NamespaceManager`, не меняя пока production wiring.
## Почему это отдельный шаг
После шагов 1-6 уже стало ясно, что следующая стадия — не ещё один patch по месту, а переход к явной модели lifecycle.
Но сразу подключать новый manager к watcher-ам и компонентам рано. Сначала нужна опорная модель:
- `NamespacePhase`
- `NamespaceSource`
- `NamespaceEventType`
- `NamespaceRecord`
- `NamespacePartState`
## Что меняем
1. Добавляем новый файл с типами model layer.
2. Добавляем helper-методы:
- `Clone()`
- `IsActive()`
- `IsTerminal()`
3. Добавляем unit tests на:
- корректный deep copy;
- active semantics;
- terminal semantics.
## Что НЕ меняем
- не подключаем manager к production path;
- не меняем watcher-ы;
- не меняем resolver;
- не затрагиваем текущее изменение в `serviceaccount.go`.
@@ -0,0 +1,25 @@
# 2026-04-26 — NamespaceManager rewrite, step 8
## Цель шага
Добавить skeleton `NamespaceManager` с in-memory state и unit tests.
## Что меняем
1. Добавляем interface `NamespaceManager`.
2. Добавляем in-memory реализацию с mutex.
3. Добавляем операции:
- `Snapshot()`
- `SnapshotRecords()`
- `Get()`
- `Upsert()`
- `MarkPartState()`
- `Remove()`
4. Добавляем unit tests на snapshot/get/upsert/remove/part-state.
## Что НЕ меняем
- не подключаем manager к watcher-ам;
- не меняем текущий resolver path;
- не трогаем runtime components;
- не затрагиваем отдельное незакоммиченное изменение в `serviceaccount.go`.
@@ -0,0 +1,20 @@
# 2026-04-26 — NamespaceManager rewrite, step 9
## Цель шага
Добавить subscriber contract в `NamespaceManager`, не подключая его пока к runtime.
## Что меняем
1. Добавляем interface `NamespaceSubscriber`.
2. Добавляем в manager операции:
- `Subscribe()`
- `SnapshotSubscribers()`
3. Добавляем unit tests на регистрацию и snapshot subscriber-ов.
## Что НЕ меняем
- не вызываем subscriber-ов из watcher-ов;
- не строим reconcile loop;
- не трогаем runtime components;
- не затрагиваем внешнее изменение в `serviceaccount.go`.
@@ -0,0 +1,697 @@
# 2026-04-26 — Target design: полноценный NamespaceManager для Layer 1
## Зачем нужен ещё один документ
Уже есть подробный документ про сделанные шаги 1-6.
Но этого недостаточно для следующего этапа, потому что:
1. История исправлений не равна целевой архитектуре.
2. Локальные фиксы уже уменьшили риск, но не дали единого lifecycle contract.
3. Следующий этап уже нельзя начинать как серию хаотичных патчей по месту.
Нужен отдельный документ, который отвечает на вопрос:
какой именно Layer 1 мы хотим получить в результате bounded rewrite.
---
## Коротко: что именно строим
Нужен не просто thread-safe registry namespace-ов, а orchestration layer с явным lifecycle.
То есть не объект вида:
- `map[string]string` + `AddNamespace()`
а объект вида:
- обнаружение namespace;
- нормализация состояния;
- единый жизненный цикл add/remove/reconcile;
- подписка компонентов на события;
- безопасный snapshot для background loops;
- backfill existing namespaces on startup;
- восстановление после restart.
Рабочее имя этой сущности: `NamespaceManager`.
---
## Какую проблему он решает
Сейчас логика размазана по нескольким слоям одновременно:
1. `NamespaceResolver` хранит registry.
2. watcher-ы executor/router/buildermgr сами решают, как регистрировать namespace.
3. components сами придумывают свой dedup.
4. часть background loops читают namespace snapshot.
5. provisioning SA/RBAC живёт как side effect watcher-а.
Из-за этого нет одного ответа на вопросы:
1. Когда namespace считается «принятым» системой?
2. Когда он считается «удалённым»?
3. Что должно происходить при restart компонента?
4. Кто отвечает за cleanup?
5. Кто отвечает за reconcile при расхождении локального и фактического состояния?
`NamespaceManager` нужен именно для того, чтобы эти вопросы получили один общий ответ.
---
## Какие свойства должны быть у новой подсистемы
### 1. Один вход для namespace lifecycle
Все namespace-ы, независимо от того, пришли они:
- из env на старте;
- из уже существующих labeled namespaces;
- из нового namespace event;
- из relabel existing namespace;
должны проходить через один и тот же pipeline.
### 2. Явный state machine
Нельзя больше жить в модели «namespace либо есть в map, либо нет». Нужны как минимум фазы:
- discovered;
- registering;
- active;
- deregistering;
- removed;
- failed.
Не обязательно все эти фазы сразу экспонировать наружу, но внутренняя модель должна понимать, на каком этапе lifecycle находится namespace.
### 3. Разделение ответственности
Нужно развести по слоям:
1. Discovery — кто узнал о namespace.
2. Registry — текущее состояние namespace в памяти процесса.
3. Reconcile — как довести локальное состояние до желаемого.
4. Subscription — как сообщить executor/router/buildermgr о событии.
5. Provisioning — отдельные side effects вроде SA/RBAC.
### 4. Thread-safe чтение и запись
Любой компонент должен иметь один безопасный способ получить:
- snapshot namespace-ов;
- текущее состояние конкретного namespace;
- stream событий.
### 5. Symmetry startup vs runtime
Если namespace был известен на старте или пришёл позже, конечный набор действий должен быть одинаковым.
Именно этот пункт был нарушен в `newdeploy`, и именно он должен стать жёстким архитектурным правилом нового дизайна.
---
## Что не должно быть в новой модели
### 1. Прямых чтений глобальной map из произвольных мест
Любой код, который напрямую читает внутреннюю структуру namespace registry, должен считаться legacy и подлежать выносу.
### 2. Глобального dedup вместо локального lifecycle
Global registry отвечает только на вопрос «namespace известен системе». Он не должен автоматически означать «каждый компонент уже подключил все свои informers».
### 3. Неявных side effects в watcher callback
Watcher должен сообщать о факте, а не выполнять пол-процесса orchestration сам по себе.
### 4. Скрытой зависимости от порядка вызовов
Сейчас уже был пойман дефект, когда второй компонент не регистрировался, потому что первый успел пометить namespace как «уже обработанный». Новая модель должна быть инвариантна к порядку subscriber-ов.
---
## Предлагаемая модель данных
Ниже не обязательно точный конечный код, но это целевая форма.
```go
type NamespacePhase string
const (
NamespacePhaseDiscovered NamespacePhase = "discovered"
NamespacePhaseRegistering NamespacePhase = "registering"
NamespacePhaseActive NamespacePhase = "active"
NamespacePhaseDeregistering NamespacePhase = "deregistering"
NamespacePhaseRemoved NamespacePhase = "removed"
NamespacePhaseFailed NamespacePhase = "failed"
)
type NamespaceRecord struct {
Name string
Source NamespaceSource
Labels map[string]string
Phase NamespacePhase
LastError string
Generation int64
UpdatedAt time.Time
RegisteredParts map[string]NamespacePartState
}
type NamespacePartState struct {
State string
LastError string
UpdatedAt time.Time
}
```
Важная идея: manager должен знать не только список namespace-ов, но и состояние регистрации по частям.
Например:
- executor.poolmgr: active
- executor.newdeploy: active
- router: active
- buildermgr.env: active
- buildermgr.pkg: failed
- provisioning.fetcher-sa: active
Это критично для reconcile. Иначе при частичном падении система знает только «namespace есть», но не знает, что именно недорегистрировано.
---
## Источники namespace-ов
Нужен явный тип источника, чтобы не смешивать namespace-ы с разным происхождением.
```go
type NamespaceSource string
const (
NamespaceSourceEnv NamespaceSource = "env"
NamespaceSourceWatcher NamespaceSource = "watcher"
NamespaceSourceBackfill NamespaceSource = "backfill"
)
```
Почему это важно:
1. Проще расследовать состояние системы.
2. Проще логировать, откуда namespace попал в менеджер.
3. Проще понять, что именно должно переживать restart и что должно исчезать при relabel/delete.
---
## Предлагаемый API NamespaceManager
Ниже не «идеальный forever API», а минимально полезный контракт.
```go
type NamespaceManager interface {
Snapshot() []string
SnapshotRecords() []NamespaceRecord
Get(name string) (NamespaceRecord, bool)
RegisterDesired(ctx context.Context, event NamespaceEvent) error
DeregisterDesired(ctx context.Context, name string, reason string) error
Subscribe(name string, subscriber NamespaceSubscriber)
Start(ctx context.Context)
}
```
И ещё важнее — не только sync API, но и события.
```go
type NamespaceEventType string
const (
NamespaceEventAdd NamespaceEventType = "add"
NamespaceEventUpdate NamespaceEventType = "update"
NamespaceEventRemove NamespaceEventType = "remove"
NamespaceEventResync NamespaceEventType = "resync"
)
type NamespaceEvent struct {
Type NamespaceEventType
Name string
Labels map[string]string
Source NamespaceSource
ObservedAt time.Time
}
type NamespaceSubscriber interface {
Name() string
OnNamespaceAdd(ctx context.Context, ns NamespaceRecord) error
OnNamespaceRemove(ctx context.Context, ns NamespaceRecord) error
OnNamespaceResync(ctx context.Context, ns NamespaceRecord) error
}
```
---
## Как должен работать startup
Это один из самых важных разделов. Сейчас именно startup/runtime symmetry остаётся центральным требованием.
### Текущий анти-pattern
Сначала что-то строится по env namespaces, потом dynamic path делает другой набор действий отдельно.
### Целевой startup
При старте процесса manager должен:
1. Собрать namespaces из env.
2. Сделать backfill всех существующих namespaces с label `fission.io/managed=true`.
3. Нормализовать список без дублей.
4. Сформировать initial desired set.
5. Пропустить весь этот set через тот же reconcile pipeline, что и поздние события.
6. Только потом считать manager готовым.
Иначе говоря:
startup — это просто массовый initial reconcile, а не отдельная логика «в обход».
---
## Как должен работать runtime add
Когда watcher видит новый namespace или relabel в `managed=true`, он не должен сам лезть во все компоненты.
Он должен только отправить event в manager:
```go
RegisterDesired(NamespaceEvent{Type: Add, Name: ns, Source: Watcher, Labels: ...})
```
Дальше manager:
1. Обновляет/создаёт `NamespaceRecord`.
2. Ставит phase `registering`.
3. По подписчикам запускает reconcile `OnNamespaceAdd`.
4. Фиксирует state каждой части.
5. Если все обязательные части успешны, переводит namespace в `active`.
6. Если часть упала, переводит в `failed` с возможностью повторной reconcile.
Это важно: add должен быть idempotent и retry-friendly.
---
## Как должен работать runtime remove
Это следующий большой пробел в текущем Layer 1.
Нужен единый remove path для двух случаев:
1. namespace удалён;
2. label `fission.io/managed=true` снят.
Пайплайн должен быть таким:
1. Watcher сообщает `remove` event.
2. Manager помечает namespace как `deregistering`.
3. Вызывает `OnNamespaceRemove` у подписчиков.
4. Каждый подписчик:
- останавливает локальные informers;
- удаляет namespace из локальных lister maps;
- очищает связанный cache state.
5. После успешного снятия подписок manager переводит namespace в `removed` или удаляет запись полностью.
Главная причина делать это централизованно:
если remove semantics будут разъезжаться по компонентам, получится новая версия текущей проблемы, только уже в lifecycle удаления.
---
## Как должен работать reconcile
Remove/add недостаточно. Нужен ещё reconcile.
Причины:
1. Компонент мог стартовать позже manager-а.
2. Подписчик мог упасть на середине регистрации namespace.
3. Restart процесса может привести к тому, что локальная память пуста, а кластерное состояние уже существует.
Поэтому manager должен уметь периодически или по событию заново прогонять namespace через subscriber-ов.
Например:
```go
OnNamespaceResync(ctx, ns)
```
Или через тот же `OnNamespaceAdd`, если он строго idempotent.
Инженерно я бы предпочёл следующее правило:
1. `OnNamespaceAdd` и `OnNamespaceResync` могут быть одной реализацией.
2. Но семантически различать их всё равно полезно для логов и метрик.
---
## Кто должен быть subscriber-ами
### 1. Executor subscriber
Внутри него можно уже вызывать внутренние add/remove/resync по типам:
- poolmgr
- newdeploy
- container
Но для manager это один subscriber уровня executor.
Почему это лучше:
1. Manager не должен знать детали каждого executor type.
2. Executor сам лучше знает, что для него является complete registration.
### 2. Router subscriber
Отвечает за:
- func informer;
- trigger informer;
- resolver informer registry;
- invalidate/rebuild path.
### 3. BuilderMgr subscriber
Но внутри него стоит сделать внутреннее разделение частей:
- env watcher part;
- pkg watcher part.
Именно потому, что на этом месте уже был пойман баг локального dedup.
### 4. Provisioning subscriber
Отдельный subscriber для:
- `fission-fetcher` SA;
- возможно builder SA;
- связанных Role/RoleBinding path.
Почему это должен быть отдельный subscriber:
сейчас provisioning встроен как side effect watcher-а, а это делает sequencing слишком хрупким и плохо наблюдаемым.
---
## Почему provisioning нужно вынести отдельно
Сейчас логика «namespace зарегистрирован» и логика «в namespace создан нужный service account + RBAC» слишком слеплены.
Это вредно по нескольким причинам:
1. Трудно диагностировать, что именно сломалось: discovery, informer wiring или RBAC provisioning.
2. Нельзя отдельно повторить provisioning без повторного полного namespace registration.
3. Нельзя нормально отслеживать частичный success.
Целевой дизайн:
- manager знает, что provisioning — это отдельная обязательная или полуобязательная часть namespace lifecycle;
- provisioning subscriber отдаёт свой статус отдельно;
- при необходимости его можно повторно reconcile без переинициализации router/executor/buildermgr.
---
## Нужен ли новый объект вместо NamespaceResolver
Да, но не обязательно удалять `NamespaceResolver` в один момент.
Реалистичная стратегия:
### Этап A
Сделать `NamespaceResolver` внутренней реализацией snapshot/compat layer.
### Этап B
Поверх него построить `NamespaceManager` как orchestration layer.
### Этап C
Постепенно вычистить прямые зависимости компонентов от `NamespaceResolver` и перевести их на manager/subscriber contract.
Почему так, а не сразу delete old resolver:
1. Слишком много мест уже используют текущие helper-ы.
2. Нужен период совместного существования старого snapshot API и нового orchestration API.
3. Иначе blast radius снова станет слишком большим.
---
## Минимальный состав внутренних методов manager-а
Ниже не внешний API, а то, что почти наверняка понадобится внутри.
```go
func (m *manager) upsertRecord(event NamespaceEvent) NamespaceRecord
func (m *manager) markPartState(ns string, subscriber string, state NamespacePartState)
func (m *manager) markPhase(ns string, phase NamespacePhase, err error)
func (m *manager) snapshotActiveNamespaces() []string
func (m *manager) emit(event internalEvent)
func (m *manager) reconcileNamespace(ctx context.Context, name string)
func (m *manager) removeNamespace(ctx context.Context, name string)
```
Причина: если manager не умеет хранить part-level state, он снова выродится в glorified map.
---
## Какой порядок вызовов нужен при add
Не просто «вызвать всех subscriber-ов подряд». Нужна осознанная последовательность.
Один из возможных вариантов:
1. Provisioning subscriber
2. BuilderMgr subscriber
3. Executor subscriber
4. Router subscriber
Но это не единственный вариант. Важно другое: порядок должен быть явным и объяснимым.
Почему provisioning логично раньше:
если namespace ещё не имеет нужного service account, часть runtime path может не подняться корректно.
Почему router можно позже:
он меньше зависит от SA provisioning, чем runtime execution path.
Но я бы не жёстко кодировал этот порядок как случайную последовательность callback-ов. Лучше иметь явно заданную subscriber order policy.
---
## Как manager должен вести себя при частичном падении
Это одна из самых важных деталей, потому что сейчас система часто мыслит бинарно: success/fail.
Нужно поведение такого типа:
1. Executor зарегистрировался успешно.
2. Router зарегистрировался успешно.
3. BuilderMgr не зарегистрировался.
4. Namespace получает phase `failed` или `active-with-errors`.
5. В record фиксируется, что именно сломалось.
6. Reconcile можно повторить только для buildermgr part.
Именно это позволит избегать режимов «namespace вроде есть, но реально не полностью обслуживается, а система этого не видит».
---
## Метрики и логирование
Без этого новый manager будет трудно отлаживать.
Нужно как минимум:
### Метрики
- число active namespaces;
- число failed namespaces;
- число reconcile attempts;
- число add/remove events;
- количество ошибок по subscriber-ам.
### Логи
На каждое важное событие должны быть логи такого класса:
- namespace discovered;
- namespace registration started;
- subscriber registration succeeded;
- subscriber registration failed;
- namespace active;
- namespace deregistering;
- namespace removed;
- resync started/completed.
Без этого следующая стадия дебага снова упрётся в разрозненные логи компонентов.
---
## Тестовая стратегия для нового этапа
Нельзя ограничиться только unit tests отдельных helper-ов.
Нужны как минимум четыре слоя проверок.
### 1. Unit tests manager state machine
- add нового namespace;
- повторный add идемпотентен;
- remove переводит в нужную фазу;
- partial failure отражается в part states.
### 2. Unit tests subscriber ordering / reconcile
- add вызывает всех нужных subscriber-ов;
- failure одного subscriber-а не портит состояние других;
- повторный resync догоняет незарегистрированную часть.
### 3. Component tests
- buildermgr add/remove;
- router add/remove;
- newdeploy add parity;
- executor resync.
### 4. End-to-end tests
- startup with existing managed namespaces;
- late namespace add;
- relabel add;
- label removal;
- namespace delete;
- process restart;
- burst onboarding.
---
## Как бы я разбил реализацию следующего этапа на коммиты
Это очень важно: не повторять ошибку большого rewrite.
### Commit A
Добавить скелет `NamespaceManager` и in-memory record model без подключения компонентов.
Цель:
- новый тип существует;
- есть unit tests state model;
- legacy path ещё не тронут.
### Commit B
Подключить discovery path: env + namespace watcher events начинают идти в manager.
Но subscribers пока можно ограничить одним compatibility subscriber.
### Commit C
Сделать provisioning отдельным subscriber-ом.
### Commit D
Перевести buildermgr на manager/subscriber contract.
Почему именно buildermgr первым:
там уже был пойман реальный dedup defect, и логика явно просит более чистый lifecycle.
### Commit E
Перевести router на manager/subscriber contract.
### Commit F
Перевести executor subscriber.
### Commit G
Добавить remove/relabel/delete lifecycle.
### Commit H
Вычистить legacy прямые обращения к resolver там, где это уже возможно.
---
## Что можно оставить совместимым на переходный период
Не всё нужно ломать сразу.
Можно временно оставить:
1. `Snapshot()` API у `NamespaceResolver` как compatibility layer.
2. Часть существующих helper-ов для informer factory creation.
3. Отдельные component-specific `AddNamespace()` методы, но вызывать их уже через manager subscriber.
Это позволит переподключать компоненты последовательно.
---
## Какие риски у самого NamespaceManager rewrite
Нужно честно фиксировать и риски новой архитектуры.
### 1. Over-centralization
Если сделать manager слишком умным, он начнёт знать внутренности каждого компонента, и получится новый монолит уже поверх старого.
Поэтому manager должен оркестрировать lifecycle, но не содержать доменную логику executor/router/buildermgr.
### 2. Deadlocks или долгие lock sections
Если state manager будет держать lock во время вызова subscriber-ов, это плохой дизайн.
Нужно правило:
- lock только на обновление внутреннего state;
- вызовы subscriber-ов делать вне глобального lock.
### 3. Excessive retries
Если reconcile не ограничить и не сделать наблюдаемым, можно получить noisy system с бесконечными повторными попытками.
### 4. Confused ownership
Если не определить, кто отвечает за remove/reconcile конкретной части, получится новая версия старой размазанной логики.
---
## Что я считаю правильным следующим шагом после этого документа
Не сразу кодить full manager.
Сначала нужен ещё один маленький подготовительный шаг:
1. Добавить новый package или файл со skeleton model `NamespaceRecord`, `NamespacePhase`, `NamespaceEvent`.
2. Покрыть его unit tests.
3. Не подключать пока к production lifecycle.
Почему:
это даст опорную модель данных, вокруг которой уже можно строить manager, не смешивая сразу storage, watchers и subscribers.
---
## Итог
Целевой `NamespaceManager` для Layer 1 — это не «один общий namespace» и не «ещё один helper над map`ой`».
Это должен быть orchestration слой с пятью обязательными свойствами:
1. единый lifecycle add/remove/resync;
2. state model с phase и part-level status;
3. подписчики-компоненты вместо хаотичных side effects;
4. symmetry startup и runtime onboarding;
5. безопасный reconcile после ошибок и restart.
Только после этого можно сказать, что Layer 1 действительно перестал быть монопользовательским Fission с набором динамических заплаток и стал многопользовательским control-plane слоем с понятным жизненным циклом.
+135
View File
@@ -0,0 +1,135 @@
# 2026-04-26 — RBAC fix для multi-tenant SA provisioning
## Симптом
`test_layer1.sh` шаг 5 падает: pod poolmgr не создаётся в динамически добавленном NS.
Event: `serviceaccount "fission-fetcher" not found`
## Путь диагностики
1. **Код есть**`EnsureNamespaceSA` добавлена в `serviceaccount.go`, вызывается из `ns_watcher.go:168`
2. **Образ задеплоен** — v8 работает, executor регистрирует NS (шаги 1-4 PASS)
3. **RBAC проверка**: `kubectl auth can-i create serviceaccounts --as=...fission-executor -n l1-test-77773`**`no`**
4. **ClusterRole `fission-executor-multi-ns`** имеет только `get/list/watch` для serviceaccounts, нет rules для `roles`/`rolebindings`
## Вывод
`setupSAAndRoleBindings` вызывается, но k8s отвечает 403 → функция тихо логирует ошибку и возвращает → SA не создаётся.
## Решение
Исправить `deploy/multitenant/rbac.yaml` — добавить ClusterRole с нужными правами + ClusterRoleBinding.
## Сделано
- Добавлен ClusterRole `fission-executor-sa-provisioner` с `create/update/patch` для `serviceaccounts`, `roles`, `rolebindings` (namespace-scoped через ClusterRole)
- Добавлен ClusterRoleBinding к SA `fission-executor` в NS `fission`
- `kubectl apply` — применено
- Верификация: `kubectl auth can-i create serviceaccounts/roles/rolebindings`**`yes/yes/yes`** ✅
## Результат после RBAC fix (2026-04-26)
Применено, RBAC проверка: `yes/yes/yes`
SA `fission-fetcher` создаётся в новом NS за 15 сек ✅
Тест `test_layer1.sh` всё равно 4/5 FAIL ❌
---
## Новая проблема — executor timeout при вызове функции
### Симптом
Шаг 5 (`вызываем функцию`): `HTTP 500 — error sending request to function`
Лог router:
```
function service entry timeout (60.000000)s exceeded
error posting to getting service for function: POST http://executor.fission/v2/getServiceForFunction
giving up after 4 attempt(s): context deadline exceeded
function: {namespace: l1-test-78841, name: hello}
```
### Что происходит
Router обращается к executor `/v2/getServiceForFunction`, executor не отвечает в течение 60 сек.
SA `fission-fetcher` уже есть (RBAC fix помог). Но poolmgr pod так и не запустился или executor не может создать service entry.
### Что нужно проверить
1. Есть ли pod poolmgr в NS `l1-test-78841`?
2. Если pod не создаётся — события в NS (`kubectl get events -n l1-test-78841`)
3. Если pod есть — логи executor (`kubectl logs -n fission deploy/executor`)
4. Может ли executor вообще видеть функции в динамически добавленном NS?
### Гипотезы
A. **Executor не видит функцию** — NS зарегистрирован в NSWatcher, но executor informer не получил Function объект → `getServiceForFunction` не знает о функции → timeout.
B. **poolmgr pod не стартует** — новая RBAC проблема или другой ресурс отсутствует.
C. **Executor видит функцию, но pool не готов** — cold start > 60 сек (маловероятно для Python hello).
---
## Обновление анализа — найден реальный RBAC root cause
### Подтверждённые факты
- Pool pod в новом NS создаётся и выходит в `Running`.
- `readyPod controller started` есть в логах executor.
- Ошибка возникает раньше/ниже: при `EnsureNamespaceSA` executor создаёт `ServiceAccount`, но не может создать `Role` полностью.
### Точный лог ошибки
```
error while creating role for sa fission-fetcher in namespace diag-ns-82702
roles.rbac.authorization.k8s.io ... is forbidden: user "system:serviceaccount:fission:fission-executor"
is attempting to grant RBAC permissions not currently held:
{APIGroups:[""], Resources:["events"], Verbs:["create"]}
```
Также перед этим:
```
localsubjectaccessreviews.authorization.k8s.io is forbidden
User "system:serviceaccount:fission:fission-executor" cannot create resource
"localsubjectaccessreviews"
```
### Вывод
Предыдущий RBAC fix был неполным.
Для динамического SA provisioning executor нужны не только:
- `serviceaccounts.create/update/patch`
- `roles.create/update/patch`
- `rolebindings.create/update/patch`
Но и ещё:
- `events.create` — иначе Kubernetes запрещает executor создавать Role, которая выдаёт `events.create` fetcher-у.
- `authorization.k8s.io/localsubjectaccessreviews.create` — иначе `checkPermission()` не может проверить текущие права SA.
### Исправление
Расширить `deploy/multitenant/rbac.yaml` для `fission-executor-sa-provisioner`:
- core `events`: `create`
- `authorization.k8s.io` `localsubjectaccessreviews`: `create`
После этого нужно:
1. `kubectl apply -f deploy/multitenant/rbac.yaml`
2. Создать новый test NS
3. Убедиться, что `Role` и `RoleBinding` для `fission-fetcher` создаются
4. Повторить `test_layer1.sh`
---
## Следующий найденный blocker — router RBAC
После полного executor RBAC fix `test_layer1.sh` изменил симптом:
- раньше шаг 5 падал с `500` и timeout на `executor /v2/getServiceForFunction`
- теперь шаг 5 падает с постоянным `404`
Лог router:
```
Failed to watch err="failed to list *v1.Namespace: namespaces is forbidden:
User \"system:serviceaccount:fission:fission-router\" cannot list resource
\"namespaces\" in API group \"\" at the cluster scope"
```
### Вывод
Executor-path уже починен, но router NSWatcher не работает, потому что у SA
`fission-router` нет cluster-scope прав `list/watch` на `namespaces`.
### Исправление
Добавить в `deploy/multitenant/rbac.yaml` ещё один набор ресурсов:
- `ClusterRole/fission-router-ns-watcher`
- `ClusterRoleBinding/fission-router-ns-watcher`
С правами:
- core `namespaces`: `list`, `watch`
@@ -0,0 +1,115 @@
# 2026-04-26 - Почему Sonnet 4.6 мог застрять на Layer1 и в чём он может быть сильнее
## Зачем этот документ
После успешного завершения кейса возник мета-вопрос:
- почему другая модель могла не дойти до рабочего решения
- в чём она всё же может быть объективно лучше
Документ нужен как заметка о процессе расследования, а не о самом кодовом fix.
## Почему Sonnet 4.6 мог не дожать именно этот кейс
### Кейс был каскадным
Здесь не было одного простого корня.
Последовательность была такой:
1. отсутствует `fission-fetcher`
2. потом выясняется недостаток прав на `Role/RoleBinding`
3. потом выясняется, что не хватает ещё и делегируемых permission-ов (`events.create`)
4. потом выясняется, что не хватает `localsubjectaccessreviews.create`
5. потом executor-path становится рабочим, но router-path всё ещё сломан
6. затем обнаруживается отсутствие namespace watch/list у `fission-router`
Модель, которая мыслит в режиме "нашёл корень -> исправил -> готово", на таком сценарии часто останавливается слишком рано.
### Симптомы менялись и маскировали прогресс
Промежуточные симптомы были:
- `serviceaccount not found`
- `500 timeout`
- `404`
- `200`
Это классический случай, где изменение симптома означает не провал, а смену активного bottleneck.
Если интерпретировать это неправильно, расследование начинает метаться.
### Нужно было понимать RBAC delegation, а не только RBAC access
Ключевая тонкость кейса:
- executor создаёт `Role` для fetcher
- эта `Role` выдаёт `events.create`
- Kubernetes запрещает создавать `Role`, делегирующую permission, которого нет у самого вызывающего субъекта
Следовательно надо было догадаться, что executor обязан получить `events.create`, хотя сам код падал не на "events usage", а на создании `Role`.
Это не самый очевидный вывод без жёсткой опоры на лог и знание RBAC semantics.
### Нужен был именно инструментальный debugging loop
Решение появилось не после одной сильной гипотезы, а после цикла:
1. найти симптом
2. проверить конкретное право
3. воспроизвести в новом namespace
4. подтвердить создание реальных объектов
5. перезапустить e2e test
6. перейти к следующему симптому
Без этого модель легко даёт хорошее объяснение, но не доводит задачу до зелёного результата.
## Что Sonnet 4.6 может делать лучше меня
### 1. Быстрый широкий синтез
Sonnet часто хорошо работает на старте, когда нужно быстро:
- разложить проблему по подсистемам
- набросать несколько гипотез
- предложить архитектурные альтернативы
- собрать большой черновик текста
### 2. High-level проектирование и brainstorming
На задачах вида:
- "какую архитектуру выбрать"
- "какие trade-off у подходов"
- "как разложить крупный рефактор"
он может давать очень сильный первый проход.
### 3. Большие гладкие черновики
Для первых версий:
- design-doc
- proposal
- API draft
- architecture summary
Sonnet нередко удобен именно скоростью и связностью первой версии.
## Что оказалось важнее в этом кейсе
В этом расследовании решающим было не качество первого explanation, а жёсткость процесса:
- не верить первому найденному root cause
- валидировать каждый шаг через cluster state
- считать fix завершённым только после `PASS=5/5`
- вносить изменения в код/манифесты, а не лечить кластер временными patch-командами
## Итоговая формулировка
Корректно говорить так:
- Sonnet может быть сильнее в широком синтезе, brainstorming, архитектурных черновиках и быстрых первых гипотезах
- в этом конкретном кейсе я оказался сильнее в последовательной инструментальной диагностике, удержании нескольких меняющихся симптомов и доведении расследования до рабочего e2e результата
То есть различие проявилось не в "умнее/глупее", а в типе задачи.
Executable
BIN
View File
Binary file not shown.
+132
View File
@@ -0,0 +1,132 @@
import re
# ── envwatcher.go ─────────────────────────────────────────────────────────────
with open("/home/naeel/terra/fission-src/pkg/buildermgr/envwatcher.go") as f:
src = f.read()
# добавляем genInformer import если нет
if "genInformer" not in src:
src = src.replace(
'"github.com/fission/fission/pkg/generated/clientset/versioned"',
'"github.com/fission/fission/pkg/generated/clientset/versioned"\n\t'
'genInformer "github.com/fission/fission/pkg/generated/informers/externalversions"',
1
)
# добавляем fmt если нет
if '"fmt"' not in src:
src = src.replace('"context"', '"context"\n\t"fmt"', 1)
addon = r'''
// AddNamespace dynamically registers a new namespace in environmentWatcher.
// Creates a per-NS Environment informer. Safe to call repeatedly — deduplicates via nsResolver.
func (envw *environmentWatcher) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) {
if !envw.nsResolver.AddNamespace(ns) {
return // already registered
}
envw.logger.Info("buildermgr.envWatcher.AddNamespace: setting up informer", zap.String("namespace", ns))
factory := genInformer.NewFilteredSharedInformerFactory(envw.fissionClient, 30*time.Minute, ns, nil)
envInf := factory.Core().V1().Environments().Informer()
_, err := envInf.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
envObj := obj.(*fv1.Environment)
envw.AddUpdateBuilder(ctx, envObj)
},
UpdateFunc: func(oldObj interface{}, newObj interface{}) {
oldEnvObj := oldObj.(*fv1.Environment)
newEnvObj := newObj.(*fv1.Environment)
if oldEnvObj.ResourceVersion == newEnvObj.ResourceVersion {
return
}
envw.AddUpdateBuilder(ctx, newEnvObj)
},
DeleteFunc: func(obj interface{}) {
envObj, ok := obj.(*fv1.Environment)
if !ok {
return
}
envw.deleteBuilder(ctx, envObj)
},
})
if err != nil {
envw.logger.Error("buildermgr.envWatcher.AddNamespace: add handler failed",
zap.String("namespace", ns), zap.Error(fmt.Errorf("%w", err)))
return
}
envw.envWatchInformer[ns] = envInf
mgr.AddInformers(ctx, map[string]k8sCache.SharedIndexInformer{ns: envInf})
envw.logger.Info("buildermgr.envWatcher.AddNamespace: done", zap.String("namespace", ns))
}
'''
with open("/home/naeel/terra/fission-src/pkg/buildermgr/envwatcher.go", "w") as f:
f.write(src + addon)
print("envwatcher.go: done")
# ── pkgwatcher.go ─────────────────────────────────────────────────────────────
with open("/home/naeel/terra/fission-src/pkg/buildermgr/pkgwatcher.go") as f:
src = f.read()
# добавляем genInformer import если нет
if "genInformer" not in src:
src = src.replace(
'"github.com/fission/fission/pkg/generated/clientset/versioned"',
'"github.com/fission/fission/pkg/generated/clientset/versioned"\n\t'
'genInformer "github.com/fission/fission/pkg/generated/informers/externalversions"',
1
)
# добавляем fmt если нет
if '"fmt"' not in src:
src = src.replace('"context"', '"context"\n\t"fmt"', 1)
# Добавляем k8sInformers если нет
if "k8sInformers" not in src:
src = src.replace(
'"k8s.io/client-go/kubernetes"',
'"k8s.io/client-go/kubernetes"\n\tk8sInformers "k8s.io/client-go/informers"',
1
)
addon2 = r'''
// AddNamespace dynamically registers a new namespace in packageWatcher.
// Creates per-NS Package and Pod informers. Safe to call repeatedly.
func (pkgw *packageWatcher) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) {
if !pkgw.nsResolver.AddNamespace(ns) {
return // already registered
}
pkgw.logger.Info("buildermgr.pkgWatcher.AddNamespace: setting up informers", zap.String("namespace", ns))
// Package informer
fissionFactory := genInformer.NewFilteredSharedInformerFactory(pkgw.fissionClient, 30*time.Minute, ns, nil)
pkgInf := fissionFactory.Core().V1().Packages().Informer()
_, err := pkgInf.AddEventHandler(pkgw.packageInformerHandler(ctx))
if err != nil {
pkgw.logger.Error("buildermgr.pkgWatcher.AddNamespace: pkg handler failed",
zap.String("namespace", ns), zap.Error(fmt.Errorf("%w", err)))
return
}
// Pod informer for build logs
podFactory := k8sInformers.NewSharedInformerFactoryWithOptions(pkgw.k8sClient, 30*time.Minute,
k8sInformers.WithNamespace(ns))
podInf := podFactory.Core().V1().Pods().Informer()
pkgw.pkgInformer[ns] = pkgInf
pkgw.podInformer[ns] = podInf
mgr.AddInformers(ctx, map[string]k8sCache.SharedIndexInformer{
ns + "/pkg": pkgInf,
ns + "/pod": podInf,
})
pkgw.logger.Info("buildermgr.pkgWatcher.AddNamespace: done", zap.String("namespace", ns))
}
'''
with open("/home/naeel/terra/fission-src/pkg/buildermgr/pkgwatcher.go", "w") as f:
f.write(src + addon2)
print("pkgwatcher.go: done")
+4
View File
@@ -74,5 +74,9 @@ func Start(ctx context.Context, clientGen crd.ClientGeneratorInterface, logger *
if err != nil {
return err
}
// Multi-tenant: watch namespaces labeled fission.io/managed=true
StartNSWatcher(ctx, logger, kubernetesClient, envWatcher, pkgWatcher, mgr)
return nil
}
+45
View File
@@ -39,6 +39,7 @@ import (
"github.com/fission/fission/pkg/executor/util"
fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
"github.com/fission/fission/pkg/generated/clientset/versioned"
genInformer "github.com/fission/fission/pkg/generated/informers/externalversions"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
@@ -498,3 +499,47 @@ func (envw *environmentWatcher) createBuilderDeployment(ctx context.Context, env
return deployment, nil
}
// AddNamespace dynamically registers a new namespace in environmentWatcher.
// Creates a per-NS Environment informer. Safe to call repeatedly — deduplicates via env informer map.
func (envw *environmentWatcher) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) {
if _, exists := envw.envWatchInformer[ns]; exists {
return // already registered
}
envw.logger.Info("buildermgr.envWatcher.AddNamespace: setting up informer", zap.String("namespace", ns))
factory := genInformer.NewFilteredSharedInformerFactory(envw.fissionClient, 30*time.Minute, ns, nil)
envInf := factory.Core().V1().Environments().Informer()
_, err := envInf.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
envObj := obj.(*fv1.Environment)
envw.AddUpdateBuilder(ctx, envObj)
},
UpdateFunc: func(oldObj interface{}, newObj interface{}) {
oldEnvObj := oldObj.(*fv1.Environment)
newEnvObj := newObj.(*fv1.Environment)
if oldEnvObj.ResourceVersion == newEnvObj.ResourceVersion {
return
}
envw.AddUpdateBuilder(ctx, newEnvObj)
},
DeleteFunc: func(obj interface{}) {
envObj, ok := obj.(*fv1.Environment)
if !ok {
return
}
envw.DeleteBuilder(ctx, envObj)
},
})
if err != nil {
envw.logger.Error("buildermgr.envWatcher.AddNamespace: add handler failed",
zap.String("namespace", ns), zap.Error(fmt.Errorf("%w", err)))
return
}
envw.envWatchInformer[ns] = envInf
mgr.AddInformers(ctx, map[string]k8sCache.SharedIndexInformer{ns: envInf})
factory.Start(ctx.Done())
envw.logger.Info("buildermgr.envWatcher.AddNamespace: done", zap.String("namespace", ns))
}
+43
View File
@@ -0,0 +1,43 @@
package buildermgr
import (
"context"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
type builderEnvNamespaceAdder interface {
AddNamespace(ctx context.Context, ns string, mgr manager.Interface)
}
type builderPkgNamespaceAdder interface {
AddNamespace(ctx context.Context, ns string, mgr manager.Interface)
}
func NewNamespaceSubscriber(envw builderEnvNamespaceAdder, pkgw builderPkgNamespaceAdder, mgr manager.Interface) utils.NamespaceSubscriber {
return utils.NamespaceSubscriberFuncs{
SubscriberName: "buildermgr",
AddFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
registerBuilderNamespace(ctx, record.Name, envw, pkgw, mgr)
return nil
},
ResyncFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
registerBuilderNamespace(ctx, record.Name, envw, pkgw, mgr)
return nil
},
}
}
func registerBuilderNamespace(ctx context.Context, namespace string, envw builderEnvNamespaceAdder, pkgw builderPkgNamespaceAdder, mgr manager.Interface) {
if namespace == "" {
return
}
utils.DefaultNSResolver().AddNamespace(namespace)
if envw != nil {
envw.AddNamespace(ctx, namespace, mgr)
}
if pkgw != nil {
pkgw.AddNamespace(ctx, namespace, mgr)
}
}
@@ -0,0 +1,52 @@
package buildermgr
import (
"context"
"testing"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
type fakeBuilderEnvNamespaceAdder struct {
lastNamespace string
calls int
}
func (f *fakeBuilderEnvNamespaceAdder) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) {
f.lastNamespace = ns
f.calls++
}
type fakeBuilderPkgNamespaceAdder struct {
lastNamespace string
calls int
}
func (f *fakeBuilderPkgNamespaceAdder) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) {
f.lastNamespace = ns
f.calls++
}
func TestNewNamespaceSubscriberAddAndResync(t *testing.T) {
envw := &fakeBuilderEnvNamespaceAdder{}
pkgw := &fakeBuilderPkgNamespaceAdder{}
subscriber := NewNamespaceSubscriber(envw, pkgw, nil)
record := utils.NamespaceRecord{Name: "tenant-builder-a"}
if subscriber.Name() != "buildermgr" {
t.Fatalf("expected buildermgr subscriber name")
}
if err := subscriber.OnNamespaceAdd(context.Background(), record); err != nil {
t.Fatalf("expected add to succeed: %v", err)
}
if err := subscriber.OnNamespaceResync(context.Background(), record); err != nil {
t.Fatalf("expected resync to succeed: %v", err)
}
if envw.calls != 2 || pkgw.calls != 2 {
t.Fatalf("expected both watchers to be called for add and resync")
}
if envw.lastNamespace != "tenant-builder-a" || pkgw.lastNamespace != "tenant-builder-a" {
t.Fatalf("expected namespace to be forwarded to both watchers")
}
}
+32
View File
@@ -0,0 +1,32 @@
// Package buildermgr — NSWatcher for multi-tenant mode.
//
// Listens for Namespaces labeled fission.io/managed=true and calls
// AddNamespace on envWatcher and packageWatcher so they pick up
// Environments and Packages in new tenant namespaces without a restart.
package buildermgr
import (
"context"
"go.uber.org/zap"
"k8s.io/client-go/kubernetes"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
// StartNSWatcher watches for Namespaces labeled fission.io/managed=true
// and immediately registers per-NS informers in envWatcher and pkgWatcher.
func StartNSWatcher(
ctx context.Context,
logger *zap.Logger,
kubeClient kubernetes.Interface,
envw *environmentWatcher,
pkgw *packageWatcher,
mgr manager.Interface,
) {
_, 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))
}
}
+38
View File
@@ -25,6 +25,7 @@ import (
apiv1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sInformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
k8sCache "k8s.io/client-go/tools/cache"
@@ -32,6 +33,7 @@ import (
"github.com/fission/fission/pkg/cache"
"github.com/fission/fission/pkg/crd"
"github.com/fission/fission/pkg/generated/clientset/versioned"
genInformer "github.com/fission/fission/pkg/generated/informers/externalversions"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
"github.com/fission/fission/pkg/utils/metrics"
@@ -329,3 +331,39 @@ func setInitialBuildStatus(ctx context.Context, fissionClient versioned.Interfac
// TODO: use UpdateStatus to update status
return fissionClient.CoreV1().Packages(pkg.Namespace).Update(ctx, pkg, metav1.UpdateOptions{})
}
// AddNamespace dynamically registers a new namespace in packageWatcher.
// Creates per-NS Package and Pod informers. Safe to call repeatedly — deduplicates via local informer maps.
func (pkgw *packageWatcher) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) {
if _, exists := pkgw.pkgInformer[ns]; exists {
return // already registered
}
pkgw.logger.Info("buildermgr.pkgWatcher.AddNamespace: setting up informers", zap.String("namespace", ns))
// Package informer
fissionFactory := genInformer.NewFilteredSharedInformerFactory(pkgw.fissionClient, 30*time.Minute, ns, nil)
pkgInf := fissionFactory.Core().V1().Packages().Informer()
_, err := pkgInf.AddEventHandler(pkgw.packageInformerHandler(ctx))
if err != nil {
pkgw.logger.Error("buildermgr.pkgWatcher.AddNamespace: pkg handler failed",
zap.String("namespace", ns), zap.Error(fmt.Errorf("%w", err)))
return
}
// Pod informer for build logs
podFactory := k8sInformers.NewSharedInformerFactoryWithOptions(pkgw.k8sClient, 30*time.Minute,
k8sInformers.WithNamespace(ns))
podInf := podFactory.Core().V1().Pods().Informer()
pkgw.pkgInformer[ns] = pkgInf
pkgw.podInformer[ns] = podInf
mgr.AddInformers(ctx, map[string]k8sCache.SharedIndexInformer{
ns + "/pkg": pkgInf,
ns + "/pod": podInf,
})
fissionFactory.Start(ctx.Done())
podFactory.Start(ctx.Done())
pkgw.logger.Info("buildermgr.pkgWatcher.AddNamespace: done", zap.String("namespace", ns))
}
+8 -1
View File
@@ -37,6 +37,7 @@ import (
"github.com/fission/fission/pkg/executor/executortype/newdeploy"
"github.com/fission/fission/pkg/executor/executortype/poolmgr"
"github.com/fission/fission/pkg/executor/fscache"
"github.com/fission/fission/pkg/executor/multitenant"
"github.com/fission/fission/pkg/executor/util"
fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
"github.com/fission/fission/pkg/generated/clientset/versioned"
@@ -293,7 +294,7 @@ func StartExecutor(ctx context.Context, clientGen crd.ClientGeneratorInterface,
logger.Info("Starting executor", zap.String("instanceID", executorInstanceID))
finformerFactory := make(map[string]genInformer.SharedInformerFactory, 0)
for _, ns := range utils.DefaultNSResolver().FissionResourceNS {
for _, ns := range utils.DefaultNSResolver().Snapshot() {
finformerFactory[ns] = genInformer.NewFilteredSharedInformerFactory(fissionClient, time.Minute*30, ns, nil)
}
@@ -399,6 +400,12 @@ func StartExecutor(ctx context.Context, clientGen crd.ClientGeneratorInterface,
utils.CreateMissingPermissionForSA(ctx, kubernetesClient, logger)
// Start multi-tenant Namespace watcher.
// Detects Namespaces labeled fission.io/managed=true and registers them in all
// executor types without a pod restart. Backward-compatible with FISSION_RESOURCE_NAMESPACES.
// See: pkg/executor/multitenant/ns_watcher.go
multitenant.StartNSWatcher(ctx, logger, kubernetesClient, executorTypes, mgr)
mgr.Add(ctx, func(ctx context.Context) {
metrics.ServeMetrics(ctx, "executor", logger, mgr)
})
@@ -292,7 +292,7 @@ func (caaf *Container) RefreshFuncPods(ctx context.Context, logger *zap.Logger,
func (caaf *Container) AdoptExistingResources(ctx context.Context) {
wg := &sync.WaitGroup{}
for _, namepsace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namepsace := range utils.DefaultNSResolver().Snapshot() {
fnList, err := caaf.fissionClient.CoreV1().Functions(namepsace).List(ctx, metav1.ListOptions{})
if err != nil {
caaf.logger.Error("error getting function list", zap.Error(err))
@@ -792,3 +792,50 @@ func getDeploymentObj(kubeobjs []apiv1.ObjectReference) *apiv1.ObjectReference {
func (caaf *Container) DumpDebugInfo(ctx context.Context) error {
return nil
}
// AddNamespace dynamically registers a new namespace in the container executor without a restart.
// Sets up deployment and service listers so the executor can manage container functions in the new NS.
func (caaf *Container) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error {
if ns == "" {
return nil
}
// Use container-specific dedup: check if deplLister is already set up for this NS.
// Do NOT use DefaultNSResolver().AddNamespace() — that is a global single-call guard
// shared by all executor types and is now called once in multitenant.registerNamespace.
if _, ok := caaf.deplLister[ns]; ok {
return nil // already registered
}
caaf.logger.Info("AddNamespace: setting up informers for new namespace (container)", zap.String("namespace", ns))
finformer := genInformer.NewFilteredSharedInformerFactory(caaf.fissionClient, 30*time.Minute, ns, nil)
executorLabel, err := utils.GetInformerLabelByExecutor(fv1.ExecutorTypeContainer)
if err != nil {
return fmt.Errorf("AddNamespace %s (container): get executor label: %w", ns, err)
}
cnmInformer := k8sInformers.NewSharedInformerFactoryWithOptions(
caaf.kubernetesClient,
30*time.Minute,
k8sInformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
opts.LabelSelector = executorLabel.String()
}),
k8sInformers.WithNamespace(ns),
)
caaf.deplLister[ns] = cnmInformer.Apps().V1().Deployments().Lister()
caaf.deplListerSynced[ns] = cnmInformer.Apps().V1().Deployments().Informer().HasSynced
caaf.svcLister[ns] = cnmInformer.Core().V1().Services().Lister()
caaf.svcListerSynced[ns] = cnmInformer.Core().V1().Services().Informer().HasSynced
_, err = finformer.Core().V1().Functions().Informer().AddEventHandler(caaf.FuncInformerHandler(ctx))
if err != nil {
return fmt.Errorf("AddNamespace %s (container): add function handler: %w", ns, err)
}
finformer.Start(ctx.Done())
cnmInformer.Start(ctx.Done())
caaf.logger.Info("AddNamespace: done (container)", zap.String("namespace", ns))
return nil
}
@@ -69,4 +69,9 @@ type ExecutorType interface {
// CleanupOldExecutorObjects cleans up resources created by old executor instances
CleanupOldExecutorObjects(context.Context)
// AddNamespace dynamically registers a new user namespace so the executor
// 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
}
@@ -312,7 +312,7 @@ func (deploy *NewDeploy) RefreshFuncPods(ctx context.Context, logger *zap.Logger
func (deploy *NewDeploy) AdoptExistingResources(ctx context.Context) {
wg := &sync.WaitGroup{}
for _, namepsace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namepsace := range utils.DefaultNSResolver().Snapshot() {
fnList, err := deploy.fissionClient.CoreV1().Functions(namepsace).List(ctx, metav1.ListOptions{})
if err != nil {
deploy.logger.Error("error getting function list", zap.Error(err))
@@ -782,7 +782,7 @@ func (deploy *NewDeploy) idleObjectReaper(ctx context.Context) {
func (deploy *NewDeploy) doIdleObjectReaper(ctx context.Context) {
envList := make(map[k8sTypes.UID]struct{})
for _, namespace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namespace := range utils.DefaultNSResolver().Snapshot() {
envs, err := deploy.fissionClient.CoreV1().Environments(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
deploy.logger.Fatal("failed to get environment list", zap.Error(err), zap.String("namespace", namespace))
@@ -898,3 +898,60 @@ func (deploy *NewDeploy) scaleDeployment(ctx context.Context, deplNS string, dep
func (deploy *NewDeploy) DumpDebugInfo(ctx context.Context) error {
return nil
}
// AddNamespace dynamically registers a new namespace in the newdeploy executor without a restart.
// Sets up deployment and service listers so the executor can manage functions in the new NS.
// Safe to call repeatedly — uses per-executor deplLister for deduplication.
func (deploy *NewDeploy) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error {
if ns == "" {
return nil
}
// Use newdeploy-specific dedup: check if deplLister is already set up for this NS.
// Do NOT use DefaultNSResolver().AddNamespace() — that is a global single-call guard
// shared by all executor types and is now called once in multitenant.registerNamespace.
if _, ok := deploy.deplLister[ns]; ok {
return nil // already registered
}
deploy.logger.Info("AddNamespace: setting up informers for new namespace (newdeploy)", zap.String("namespace", ns))
// Fission CRD informer factory for the new NS.
finformer := genInformer.NewFilteredSharedInformerFactory(deploy.fissionClient, 30*time.Minute, ns, nil)
// K8s deployment+service informer factory filtered by newdeploy executor label.
executorLabel, err := utils.GetInformerLabelByExecutor(fv1.ExecutorTypeNewdeploy)
if err != nil {
return fmt.Errorf("AddNamespace %s (newdeploy): get executor label: %w", ns, err)
}
ndmInformer := k8sInformers.NewSharedInformerFactoryWithOptions(
deploy.kubernetesClient,
30*time.Minute,
k8sInformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
opts.LabelSelector = executorLabel.String()
}),
k8sInformers.WithNamespace(ns),
)
// Register deployment and service listers — same as done at startup in MakeNewDeploy.
deploy.deplLister[ns] = ndmInformer.Apps().V1().Deployments().Lister()
deploy.deplListerSynced[ns] = ndmInformer.Apps().V1().Deployments().Informer().HasSynced
deploy.svcLister[ns] = ndmInformer.Core().V1().Services().Lister()
deploy.svcListerSynced[ns] = ndmInformer.Core().V1().Services().Informer().HasSynced
// Register function event handler so this NS's functions are adopted.
_, err = finformer.Core().V1().Functions().Informer().AddEventHandler(deploy.FunctionEventHandlers(ctx))
if err != nil {
return fmt.Errorf("AddNamespace %s (newdeploy): add function handler: %w", ns, err)
}
_, err = finformer.Core().V1().Environments().Informer().AddEventHandler(deploy.EnvEventHandlers(ctx))
if err != nil {
return fmt.Errorf("AddNamespace %s (newdeploy): add environment handler: %w", ns, err)
}
finformer.Start(ctx.Done())
ndmInformer.Start(ctx.Done())
deploy.logger.Info("AddNamespace: done (newdeploy)", zap.String("namespace", ns))
return nil
}
+60 -4
View File
@@ -357,7 +357,7 @@ func (gpm *GenericPoolManager) AdoptExistingResources(ctx context.Context) {
envMap := make(map[string]fv1.Environment)
wg := &sync.WaitGroup{}
for _, namespace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namespace := range utils.DefaultNSResolver().Snapshot() {
envs, err := gpm.fissionClient.CoreV1().Environments(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
gpm.logger.Error("error getting environment list", zap.Error(err))
@@ -389,7 +389,7 @@ func (gpm *GenericPoolManager) AdoptExistingResources(ctx context.Context) {
fv1.EXECUTOR_TYPE: string(fv1.ExecutorTypePoolmgr),
}
for _, namespace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namespace := range utils.DefaultNSResolver().Snapshot() {
podList, err := gpm.kubernetesClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: labels.Set(l).AsSelector().String(),
})
@@ -624,7 +624,7 @@ func (gpm *GenericPoolManager) idleObjectReaper(ctx context.Context) {
func (gpm *GenericPoolManager) doIdleObjectReaper(ctx context.Context) {
envList := make(map[k8sTypes.UID]struct{})
for _, namespace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namespace := range utils.DefaultNSResolver().Snapshot() {
envs, err := gpm.fissionClient.CoreV1().Environments(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
gpm.logger.Error("failed to get environment list", zap.Error(err), zap.String("namespace", namespace))
@@ -637,7 +637,7 @@ func (gpm *GenericPoolManager) doIdleObjectReaper(ctx context.Context) {
}
fnList := make(map[k8sTypes.UID]fv1.Function)
for _, namespace := range utils.DefaultNSResolver().FissionResourceNS {
for _, namespace := range utils.DefaultNSResolver().Snapshot() {
fns, err := gpm.fissionClient.CoreV1().Functions(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
gpm.logger.Error("failed to get environment list", zap.Error(err), zap.String("namespace", namespace))
@@ -784,3 +784,59 @@ func (gpm *GenericPoolManager) NoActiveConnectionEventChecker(ctx context.Contex
func (gpm *GenericPoolManager) DumpDebugInfo(ctx context.Context) error {
return gpm.fsCache.DumpDebugInfo(ctx)
}
// AddNamespace dynamically registers a new namespace in the poolmgr executor without a restart.
// Called by watchManagedNamespaces when a Namespace with label fission.io/managed=true appears.
//
// What it does:
// 1. Adds the namespace to the global NamespaceResolver (thread-safe, deduplicates)
// 2. Creates a Fission CRD informer factory for the NS (watches Environments, Functions, Packages)
// 3. Creates a K8s pod informer factory filtered by poolmgr executor label
// 4. Registers the informers with PoolPodController (envLister, podLister, RS watcher)
// 5. Starts both informer factories
//
// If the namespace was already registered, returns nil immediately (no-op).
func (gpm *GenericPoolManager) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error {
if ns == "" {
// Empty NS means "scan all" — for poolmgr this is a no-op; discovery is done by the caller.
return nil
}
// Use poolmgr-specific dedup: check if envLister is already set up for this NS.
// Do NOT use DefaultNSResolver().AddNamespace() — that is a global single-call guard
// shared by all executor types and is now called once in multitenant.registerNamespace.
if _, ok := gpm.poolPodC.envLister[ns]; ok {
return nil
}
gpm.logger.Info("AddNamespace: setting up informers for new namespace", zap.String("namespace", ns))
// Fission CRD informer factory — watches Environments, Functions, Packages in this NS.
finformer := genInformer.NewFilteredSharedInformerFactory(gpm.fissionClient, 30*time.Minute, ns, nil)
// K8s pod/RS informer factory filtered by poolmgr executor label.
// Same label used at startup in GetInformerFactoryByExecutor.
executorLabel, err := utils.GetInformerLabelByExecutor(fv1.ExecutorTypePoolmgr)
if err != nil {
return fmt.Errorf("AddNamespace %s: get executor label: %w", ns, err)
}
gpmInformer := k8sInformers.NewSharedInformerFactoryWithOptions(
gpm.kubernetesClient,
30*time.Minute,
k8sInformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
opts.LabelSelector = executorLabel.String()
}),
k8sInformers.WithNamespace(ns),
)
// Register the new informers with PoolPodController.
if err := gpm.poolPodC.AddNamespaceInformers(ctx, ns, finformer, gpmInformer); err != nil {
return fmt.Errorf("AddNamespace %s: register informers: %w", ns, err)
}
// Start the factories — they will begin syncing immediately.
finformer.Start(ctx.Done())
gpmInformer.Start(ctx.Done())
gpm.logger.Info("AddNamespace: informers started for namespace", zap.String("namespace", ns))
return nil
}
@@ -485,3 +485,46 @@ func (p *PoolPodController) spCleanupPodQueueProcessFunc(ctx context.Context) bo
p.spCleanupPodQueue.Forget(key)
return false
}
// AddNamespaceInformers registers informers for a newly-added namespace in PoolPodController.
// Called from GenericPoolManager.AddNamespace after the informer factories are created.
//
// Sets up:
// - Environment lister and synced func (for pool creation on env events)
// - Pod lister and synced func (for specialized pod tracking)
// - ReplicaSet event handler (for RS scale-down pod cleanup)
func (p *PoolPodController) AddNamespaceInformers(
ctx context.Context,
ns string,
finformer genInformer.SharedInformerFactory,
gpmInformer k8sInformers.SharedInformerFactory,
) error {
// Environment informer — triggers pool creation/deletion when envs change in this NS.
_, err := finformer.Core().V1().Environments().Informer().AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: p.enqueueEnvAdd,
UpdateFunc: p.enqueueEnvUpdate,
DeleteFunc: p.enqueueEnvDelete,
})
if err != nil {
return fmt.Errorf("AddNamespaceInformers %s: add env handler: %w", ns, err)
}
p.envLister[ns] = finformer.Core().V1().Environments().Lister()
p.envListerSynced[ns] = finformer.Core().V1().Environments().Informer().HasSynced
// Pod lister — used by processRS to find specialized pods in this NS.
p.podLister[ns] = gpmInformer.Core().V1().Pods().Lister()
p.podListerSynced[ns] = gpmInformer.Core().V1().Pods().Informer().HasSynced
// ReplicaSet informer — triggers cleanup of specialized pods when RS scales to 0.
_, err = gpmInformer.Apps().V1().ReplicaSets().Informer().AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: p.handleRSAdd,
UpdateFunc: p.handleRSUpdate,
DeleteFunc: p.handleRSDelete,
})
if err != nil {
return fmt.Errorf("AddNamespaceInformers %s: add RS handler: %w", ns, err)
}
p.logger.Info("AddNamespaceInformers: registered informers for namespace", zap.String("namespace", ns))
return nil
}
@@ -0,0 +1,32 @@
package multitenant
import (
"context"
"go.uber.org/zap"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/core/v1"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
func NewNamespaceSubscriber(
logger *zap.Logger,
kubernetesClient kubernetes.Interface,
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
mgr manager.Interface,
) utils.NamespaceSubscriber {
return utils.NamespaceSubscriberFuncs{
SubscriberName: "executor",
AddFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
registerNamespace(ctx, logger, kubernetesClient, record.Name, executorTypes, mgr)
return nil
},
ResyncFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
registerNamespace(ctx, logger, kubernetesClient, record.Name, executorTypes, mgr)
return nil
},
}
}
@@ -0,0 +1,37 @@
package multitenant
import (
"context"
"testing"
"go.uber.org/zap"
k8sfake "k8s.io/client-go/kubernetes/fake"
fv1 "github.com/fission/fission/pkg/apis/core/v1"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/utils"
)
func TestNewNamespaceSubscriberAddAndResync(t *testing.T) {
poolmgr := &fakeExecutorType{}
subscriber := NewNamespaceSubscriber(zap.NewNop(), k8sfake.NewSimpleClientset(), map[fv1.ExecutorType]executortype.ExecutorType{
fv1.ExecutorTypePoolmgr: poolmgr,
}, nil)
record := utils.NamespaceRecord{Name: "tenant-executor-b"}
if subscriber.Name() != "executor" {
t.Fatalf("expected executor subscriber name")
}
if err := subscriber.OnNamespaceAdd(context.Background(), record); err != nil {
t.Fatalf("expected add to succeed: %v", err)
}
if err := subscriber.OnNamespaceResync(context.Background(), record); err != nil {
t.Fatalf("expected resync to succeed: %v", err)
}
if poolmgr.addCalls != 2 {
t.Fatalf("expected executor type to receive add and resync calls")
}
if poolmgr.lastNamespace != "tenant-executor-b" {
t.Fatalf("expected namespace to be forwarded to executor type")
}
}
+137
View File
@@ -0,0 +1,137 @@
// Package multitenant provides utilities for running Fission in a multi-tenant
// Kubernetes environment where user namespaces are created dynamically at runtime.
//
// # The Problem
//
// In a standard Fission installation, all resource namespaces must be enumerated in the
// FISSION_RESOURCE_NAMESPACES environment variable before the process starts. Adding a
// new user namespace requires patching that env var and triggering a rolling restart of
// every Fission component (executor, router, buildermgr, etc.) — causing ~30 seconds of
// downtime per new tenant.
//
// At scale this becomes a severe operational problem: with hundreds of new tenants being
// created continuously, the executor is in a permanent rolling restart loop, causing
// cascading failures for all existing users.
//
// # The Solution
//
// This package implements hot namespace registration without pod restarts.
// The mechanism is intentionally simple and decoupled from any specific platform:
//
// 1. A platform (a cloud console, a CI/CD system, an operator) creates a Kubernetes
// Namespace and sets the label:
// fission.io/managed=true
//
// 2. StartNSWatcher registers a Kubernetes Namespace Informer that receives an event
// the moment a labeled Namespace is created or updated — no polling, no delay.
//
// 3. On the AddFunc / UpdateFunc callback NSWatcher calls AddNamespace on every
// registered executor type (poolmgr, newdeploy, container). Each type creates
// per-NS informer factories, pod listers, and event handlers — live, without restart.
//
// 4. NamespaceResolver.AddNamespace deduplicates — calling AddNamespace on an already-
// registered namespace is always a safe no-op.
//
// # Backward Compatibility
//
// The FISSION_RESOURCE_NAMESPACES environment variable continues to work as before.
// Namespaces listed there are registered at startup and do not require the label.
// This package adds on top of the existing mechanism — it does not replace it.
//
// # Required RBAC
//
// The fission-executor ServiceAccount must be granted permission to list and watch
// Namespaces at the cluster scope. Apply the manifest at:
//
// deploy/multitenant/rbac.yaml
//
// # Integration Contract
//
// The entire integration contract for external platforms is a single label on a Namespace:
//
// apiVersion: v1
// kind: Namespace
// metadata:
// name: tenant-abc123
// labels:
// fission.io/managed: "true"
//
// No other coupling to Fission internals is required or expected.
package multitenant
import (
"context"
"errors"
"go.uber.org/zap"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/core/v1"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
// StartNSWatcher registers a Kubernetes Namespace Informer that reacts immediately
// when a Namespace with label fission.io/managed=true is created or relabeled.
//
// Unlike a polling approach, this uses the standard k8s Watch mechanism — the executor
// receives the event within milliseconds of the Namespace appearing, with zero wasted
// API calls between events.
//
// The informer is managed via mgr and shuts down cleanly when ctx is cancelled.
func StartNSWatcher(
ctx context.Context,
logger *zap.Logger,
kubernetesClient kubernetes.Interface,
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
mgr manager.Interface,
) {
_, 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))
}
}
// registerNamespace calls AddNamespace on every executor type for the given namespace.
// The global NamespaceResolver is updated here — once, before any executor type is called.
// 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.
func registerNamespace(
ctx context.Context,
logger *zap.Logger,
kubernetesClient kubernetes.Interface,
ns string,
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
mgr manager.Interface,
) {
// 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.
utils.EnsureNamespaceSA(ctx, kubernetesClient, logger, ns)
registerExecutorTypes(ctx, logger, ns, executorTypes, mgr)
logger.Info("multitenant.NSWatcher: registered namespace", zap.String("namespace", ns))
}
func registerExecutorTypes(
ctx context.Context,
logger *zap.Logger,
ns string,
executorTypes map[fv1.ExecutorType]executortype.ExecutorType,
mgr manager.Interface,
) error {
var joinErr error
for _, et := range executorTypes {
if err := et.AddNamespace(ctx, ns, mgr); err != nil {
logger.Error("multitenant.NSWatcher: AddNamespace failed",
zap.String("namespace", ns),
zap.Error(err),
)
joinErr = errors.Join(joinErr, err)
}
}
return joinErr
}
@@ -0,0 +1,78 @@
package multitenant
import (
"context"
"errors"
"testing"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fv1 "github.com/fission/fission/pkg/apis/core/v1"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/executor/fscache"
"github.com/fission/fission/pkg/utils/manager"
)
type fakeExecutorType struct {
addCalls int
lastNamespace string
addErr error
}
func (f *fakeExecutorType) Run(context.Context, manager.Interface) {}
func (f *fakeExecutorType) GetTypeName(context.Context) fv1.ExecutorType {
return fv1.ExecutorTypePoolmgr
}
func (f *fakeExecutorType) GetFuncSvc(context.Context, *fv1.Function) (*fscache.FuncSvc, error) {
return nil, nil
}
func (f *fakeExecutorType) GetFuncSvcFromCache(context.Context, *fv1.Function) (*fscache.FuncSvc, error) {
return nil, nil
}
func (f *fakeExecutorType) DumpDebugInfo(context.Context) error { return nil }
func (f *fakeExecutorType) DeleteFuncSvcFromCache(context.Context, *fscache.FuncSvc) {}
func (f *fakeExecutorType) TapService(context.Context, string) error { return nil }
func (f *fakeExecutorType) UnTapService(context.Context, *metav1.ObjectMeta, string) {}
func (f *fakeExecutorType) MarkSpecializationFailure(context.Context, *metav1.ObjectMeta) {}
func (f *fakeExecutorType) IsValid(context.Context, *fscache.FuncSvc) bool { return true }
func (f *fakeExecutorType) RefreshFuncPods(context.Context, *zap.Logger, fv1.Function) error {
return nil
}
func (f *fakeExecutorType) AdoptExistingResources(context.Context) {}
func (f *fakeExecutorType) CleanupOldExecutorObjects(context.Context) {}
func (f *fakeExecutorType) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error {
f.addCalls++
f.lastNamespace = ns
return f.addErr
}
var _ executortype.ExecutorType = (*fakeExecutorType)(nil)
func TestRegisterExecutorTypes(t *testing.T) {
poolmgr := &fakeExecutorType{}
container := &fakeExecutorType{}
err := registerExecutorTypes(context.Background(), zap.NewNop(), "tenant-executor-a", map[fv1.ExecutorType]executortype.ExecutorType{
fv1.ExecutorTypePoolmgr: poolmgr,
fv1.ExecutorTypeContainer: container,
}, nil)
if err != nil {
t.Fatalf("expected registration success: %v", err)
}
if poolmgr.addCalls != 1 || container.addCalls != 1 {
t.Fatalf("expected all executor types to receive AddNamespace")
}
}
func TestRegisterExecutorTypesAggregatesErrors(t *testing.T) {
err := registerExecutorTypes(context.Background(), zap.NewNop(), "tenant-executor-a", map[fv1.ExecutorType]executortype.ExecutorType{
fv1.ExecutorTypePoolmgr: &fakeExecutorType{addErr: errors.New("poolmgr failed")},
fv1.ExecutorTypeContainer: &fakeExecutorType{},
}, nil)
if err == nil {
t.Fatalf("expected aggregated error")
}
if err.Error() != "poolmgr failed" {
t.Fatalf("unexpected error: %v", err)
}
}
+11
View File
@@ -18,6 +18,7 @@ package router
import (
"fmt"
"sync"
"time"
"go.uber.org/zap"
@@ -35,6 +36,7 @@ type (
// FunctionReference -> function metadata
refCache *cache.Cache[namespacedTriggerReference, resolveResult]
funcInformer map[string]k8sCache.SharedIndexInformer
mu sync.RWMutex
logger *zap.Logger
// store k8sCache.Store
}
@@ -120,12 +122,21 @@ func (frr *functionReferenceResolver) resolve(trigger fv1.HTTPTrigger) (*resolve
}
func (frr *functionReferenceResolver) getInformerByNamespace(namespace string) (k8sCache.SharedIndexInformer, error) {
frr.mu.RLock()
defer frr.mu.RUnlock()
if informer, ok := frr.funcInformer[namespace]; ok {
return informer, nil
}
return nil, fmt.Errorf("informer for namespace %s not found", namespace)
}
func (frr *functionReferenceResolver) addInformer(namespace string, informer k8sCache.SharedIndexInformer) {
frr.mu.Lock()
defer frr.mu.Unlock()
frr.funcInformer[namespace] = informer
}
// resolveByName simply looks up function by name in a namespace.
func (frr *functionReferenceResolver) resolveByName(namespace, name string) (*resolveResult, error) {
// get function from cache
+112 -4
View File
@@ -18,8 +18,10 @@ package router
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/bep/debounce"
@@ -35,6 +37,7 @@ import (
eclient "github.com/fission/fission/pkg/executor/client"
config "github.com/fission/fission/pkg/featureconfig"
"github.com/fission/fission/pkg/generated/clientset/versioned"
genInformer "github.com/fission/fission/pkg/generated/informers/externalversions"
"github.com/fission/fission/pkg/info"
"github.com/fission/fission/pkg/throttler"
"github.com/fission/fission/pkg/utils"
@@ -56,6 +59,7 @@ type HTTPTriggerSet struct {
triggerInformer map[string]k8sCache.SharedIndexInformer
functions []fv1.Function
funcInformer map[string]k8sCache.SharedIndexInformer
informerMu sync.RWMutex
updateRouterRequestChannel chan struct{}
tsRoundTripperParams *tsRoundTripperParams
isDebugEnv bool
@@ -308,7 +312,7 @@ func (ts *HTTPTriggerSet) updateTriggerStatusFailed(ht *fv1.HTTPTrigger, err err
}
func (ts *HTTPTriggerSet) addTriggerHandlers() error {
for _, triggerInformer := range ts.triggerInformer {
for _, triggerInformer := range ts.snapshotTriggerInformers() {
_, err := triggerInformer.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
trigger := obj.(*fv1.HTTPTrigger)
@@ -340,7 +344,7 @@ func (ts *HTTPTriggerSet) addTriggerHandlers() error {
}
func (ts *HTTPTriggerSet) addFunctionHandlers() error {
for _, funcInformer := range ts.funcInformer {
for _, funcInformer := range ts.snapshotFuncInformers() {
_, err := funcInformer.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
@@ -387,6 +391,28 @@ func (ts *HTTPTriggerSet) syncTriggers() {
})
}
func (ts *HTTPTriggerSet) snapshotTriggerInformers() []k8sCache.SharedIndexInformer {
ts.informerMu.RLock()
defer ts.informerMu.RUnlock()
informers := make([]k8sCache.SharedIndexInformer, 0, len(ts.triggerInformer))
for _, informer := range ts.triggerInformer {
informers = append(informers, informer)
}
return informers
}
func (ts *HTTPTriggerSet) snapshotFuncInformers() []k8sCache.SharedIndexInformer {
ts.informerMu.RLock()
defer ts.informerMu.RUnlock()
informers := make([]k8sCache.SharedIndexInformer, 0, len(ts.funcInformer))
for _, informer := range ts.funcInformer {
informers = append(informers, informer)
}
return informers
}
func (ts *HTTPTriggerSet) updateRouter(ctx context.Context) {
for {
select {
@@ -396,7 +422,7 @@ func (ts *HTTPTriggerSet) updateRouter(ctx context.Context) {
}
// get triggers
alltriggers := make([]fv1.HTTPTrigger, 0)
for _, triggerInformer := range ts.triggerInformer {
for _, triggerInformer := range ts.snapshotTriggerInformers() {
latestTriggers := triggerInformer.GetStore().List()
for _, t := range latestTriggers {
alltriggers = append(alltriggers, *t.(*fv1.HTTPTrigger))
@@ -407,7 +433,7 @@ func (ts *HTTPTriggerSet) updateRouter(ctx context.Context) {
// get functions
allfunctions := make([]fv1.Function, 0)
functionTimeout := make(map[types.UID]int, 0)
for _, funcInformer := range ts.funcInformer {
for _, funcInformer := range ts.snapshotFuncInformers() {
latestFunctions := funcInformer.GetStore().List()
for _, f := range latestFunctions {
fn := *f.(*fv1.Function)
@@ -426,3 +452,85 @@ func (ts *HTTPTriggerSet) updateRouter(ctx context.Context) {
ts.mutableRouter.updateRouter(router)
}
}
// AddNamespace dynamically registers a new namespace in the router without a restart.
// Creates per-NS informers for HTTPTriggers and Functions, wires up event handlers,
// and triggers a router rebuild. Safe to call repeatedly — deduplicates via NSResolver.
func (ts *HTTPTriggerSet) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error {
if !utils.DefaultNSResolver().AddNamespace(ns) {
return nil // already registered
}
ts.logger.Info("router.AddNamespace: setting up informers", zap.String("namespace", ns))
factory := genInformer.NewFilteredSharedInformerFactory(ts.fissionClient, 30*time.Minute, ns, nil)
triggerInf := factory.Core().V1().HTTPTriggers().Informer()
funcInf := factory.Core().V1().Functions().Informer()
_, err := triggerInf.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
trigger := obj.(*fv1.HTTPTrigger)
go createIngress(context.Background(), ts.logger, trigger, ts.kubeClient)
ts.syncTriggers()
},
DeleteFunc: func(obj interface{}) {
ts.syncTriggers()
trigger := obj.(*fv1.HTTPTrigger)
go deleteIngress(context.Background(), ts.logger, trigger, ts.kubeClient)
},
UpdateFunc: func(oldObj interface{}, newObj interface{}) {
oldTrigger := oldObj.(*fv1.HTTPTrigger)
newTrigger := newObj.(*fv1.HTTPTrigger)
if oldTrigger.ObjectMeta.ResourceVersion == newTrigger.ObjectMeta.ResourceVersion {
return
}
go updateIngress(context.Background(), ts.logger, oldTrigger, newTrigger, ts.kubeClient)
ts.syncTriggers()
},
})
if err != nil {
return fmt.Errorf("router.AddNamespace %s: trigger handler: %w", ns, err)
}
_, err = funcInf.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { ts.syncTriggers() },
DeleteFunc: func(obj interface{}) { ts.syncTriggers() },
UpdateFunc: func(oldObj interface{}, newObj interface{}) {
oldFn := oldObj.(*fv1.Function)
fn := newObj.(*fv1.Function)
if oldFn.ObjectMeta.ResourceVersion == fn.ObjectMeta.ResourceVersion {
return
}
for key, rr := range ts.resolver.copy() {
if key.namespace == fn.ObjectMeta.Namespace &&
rr.functionMap[fn.ObjectMeta.Name] != nil &&
rr.functionMap[fn.ObjectMeta.Name].ObjectMeta.ResourceVersion != fn.ObjectMeta.ResourceVersion {
ts.logger.Debug("invalidating resolver cache")
_ = ts.resolver.delete(key.namespace, key.triggerName, key.triggerResourceVersion)
break
}
}
ts.syncTriggers()
},
})
if err != nil {
return fmt.Errorf("router.AddNamespace %s: func handler: %w", ns, err)
}
ts.informerMu.Lock()
ts.triggerInformer[ns] = triggerInf
ts.funcInformer[ns] = funcInf
ts.informerMu.Unlock()
ts.resolver.addInformer(ns, funcInf)
mgr.AddInformers(ctx, map[string]k8sCache.SharedIndexInformer{
ns + "/trigger": triggerInf,
ns + "/func": funcInf,
})
factory.Start(ctx.Done())
// Wait for cache to sync before rebuilding the router, so triggers are visible.
k8sCache.WaitForCacheSync(ctx.Done(), triggerInf.HasSynced, funcInf.HasSynced)
ts.logger.Info("router.AddNamespace: done", zap.String("namespace", ns))
ts.syncTriggers()
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package router
import (
"context"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
type routerNamespaceAdder interface {
AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error
}
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)
},
ResyncFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
return registerRouterNamespace(ctx, record.Name, ts, mgr)
},
}
}
func registerRouterNamespace(ctx context.Context, namespace string, ts routerNamespaceAdder, mgr manager.Interface) error {
if namespace == "" || ts == nil {
return nil
}
return ts.AddNamespace(ctx, namespace, mgr)
}
+52
View File
@@ -0,0 +1,52 @@
package router
import (
"context"
"errors"
"testing"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
type fakeRouterNamespaceAdder struct {
lastNamespace string
calls int
err error
}
func (f *fakeRouterNamespaceAdder) AddNamespace(ctx context.Context, ns string, mgr manager.Interface) error {
f.lastNamespace = ns
f.calls++
return f.err
}
func TestNewNamespaceSubscriberAddAndResync(t *testing.T) {
adder := &fakeRouterNamespaceAdder{}
subscriber := NewNamespaceSubscriber(adder, nil)
record := utils.NamespaceRecord{Name: "tenant-router-a"}
if subscriber.Name() != "router" {
t.Fatalf("expected router subscriber name")
}
if err := subscriber.OnNamespaceAdd(context.Background(), record); err != nil {
t.Fatalf("expected add to succeed: %v", err)
}
if err := subscriber.OnNamespaceResync(context.Background(), record); err != nil {
t.Fatalf("expected resync to succeed: %v", err)
}
if adder.calls != 2 {
t.Fatalf("expected adder to be called for add and resync")
}
if adder.lastNamespace != "tenant-router-a" {
t.Fatalf("expected namespace to be forwarded")
}
}
func TestRegisterRouterNamespacePropagatesError(t *testing.T) {
adder := &fakeRouterNamespaceAdder{err: errors.New("add failed")}
err := registerRouterNamespace(context.Background(), "tenant-router-a", adder, nil)
if err == nil || err.Error() != "add failed" {
t.Fatalf("expected router add error to be propagated, got %v", err)
}
}
+32
View File
@@ -0,0 +1,32 @@
// Package router — NSWatcher for multi-tenant mode.
//
// Listens for Namespaces labeled fission.io/managed=true and calls
// HTTPTriggerSet.AddNamespace so the router picks up HTTPTriggers and
// Functions in tenant namespaces without a restart.
package router
import (
"context"
"go.uber.org/zap"
"k8s.io/client-go/kubernetes"
"github.com/fission/fission/pkg/utils"
"github.com/fission/fission/pkg/utils/manager"
)
// StartNSWatcher registers a Kubernetes Namespace Informer for the router.
// Whenever a Namespace with label fission.io/managed=true appears (or is relabeled),
// the router immediately subscribes to HTTPTriggers and Functions in that namespace.
func StartNSWatcher(
ctx context.Context,
logger *zap.Logger,
kubeClient kubernetes.Interface,
ts *HTTPTriggerSet,
mgr manager.Interface,
) {
_, 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))
}
}
+4
View File
@@ -213,6 +213,10 @@ func Start(ctx context.Context, clientGen crd.ClientGeneratorInterface, logger *
metrics.ServeMetrics(ctx, "router", logger, mgr)
})
// Multi-tenant: watch namespaces labeled fission.io/managed=true
// and dynamically register HTTPTrigger/Function informers without restart.
StartNSWatcher(ctx, logger, kubeClient, triggers, mgr)
logger.Info("starting router", zap.Int("port", port))
tracer := otel.Tracer("router")
+1 -1
View File
@@ -92,7 +92,7 @@ func (pruner *ArchivePruner) getOrphanArchives(ctx context.Context) {
var archiveID string
// get all pkgs from kubernetes
for _, namespace := range utils.DefaultNSResolver().FissionResourceNS {
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))
+4 -4
View File
@@ -23,7 +23,7 @@ import (
func GetInformersForNamespaces(client versioned.Interface, defaultSync time.Duration, kind string) map[string]cache.SharedIndexInformer {
informers := make(map[string]cache.SharedIndexInformer)
for _, ns := range DefaultNSResolver().FissionResourceNS {
for _, ns := range DefaultNSResolver().Snapshot() {
factory := genInformer.NewFilteredSharedInformerFactory(client, defaultSync, ns, nil).Core().V1()
switch kind {
case fv1.CanaryConfigResource:
@@ -52,7 +52,7 @@ func GetInformersForNamespaces(client versioned.Interface, defaultSync time.Dura
func GetK8sInformersForNamespaces(client kubernetes.Interface, defaultSync time.Duration, kind string) map[string]cache.SharedIndexInformer {
informers := make(map[string]cache.SharedIndexInformer)
namespaces := DefaultNSResolver()
for _, ns := range namespaces.FissionNSWithOptions(WithBuilderNs(), WithFunctionNs(), WithDefaultNs()) {
for _, ns := range namespaces.SnapshotWithOptions(WithBuilderNs(), WithFunctionNs(), WithDefaultNs()) {
factory := k8sInformers.NewSharedInformerFactoryWithOptions(client, defaultSync, k8sInformers.WithNamespace(ns))
switch kind {
case fv1.Deployments:
@@ -77,7 +77,7 @@ func GetK8sInformersForNamespaces(client kubernetes.Interface, defaultSync time.
func GetInformerEventChecker(ctx context.Context, client kubernetes.Interface, reason string) map[string]cache.SharedInformer {
informers := make(map[string]cache.SharedInformer)
namespaces := DefaultNSResolver()
for _, ns := range namespaces.FissionNSWithOptions(WithBuilderNs(), WithFunctionNs(), WithDefaultNs()) {
for _, ns := range namespaces.SnapshotWithOptions(WithBuilderNs(), WithFunctionNs(), WithDefaultNs()) {
informers[ns] = cache.NewSharedInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
@@ -100,7 +100,7 @@ func GetInformerFactoryByExecutor(client kubernetes.Interface, labels labels.Sel
informerFactory := make(map[string]k8sInformers.SharedInformerFactory)
namespaces := DefaultNSResolver()
for _, ns := range namespaces.FissionNSWithOptions(WithBuilderNs(), WithFunctionNs(), WithDefaultNs()) {
for _, ns := range namespaces.SnapshotWithOptions(WithBuilderNs(), WithFunctionNs(), WithDefaultNs()) {
factory := k8sInformers.NewSharedInformerFactoryWithOptions(client, defaultResync,
k8sInformers.WithTweakListOptions(func(options *metav1.ListOptions) {
options.LabelSelector = labels.String()
+60 -6
View File
@@ -2,7 +2,9 @@ package utils
import (
"os"
"sort"
"strings"
"sync"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -11,14 +13,26 @@ import (
)
const (
ENV_FUNCTION_NAMESPACE string = "FISSION_FUNCTION_NAMESPACE"
ENV_BUILDER_NAMESPACE string = "FISSION_BUILDER_NAMESPACE"
ENV_DEFAULT_NAMESPACE string = "FISSION_DEFAULT_NAMESPACE"
ENV_ADDITIONAL_NAMESPACE string = "FISSION_RESOURCE_NAMESPACES"
ENV_FUNCTION_NAMESPACE string = "FISSION_FUNCTION_NAMESPACE"
ENV_BUILDER_NAMESPACE string = "FISSION_BUILDER_NAMESPACE"
ENV_DEFAULT_NAMESPACE string = "FISSION_DEFAULT_NAMESPACE"
ENV_ADDITIONAL_NAMESPACE string = "FISSION_RESOURCE_NAMESPACES"
ManagedNamespaceLabelKey string = "fission.io/managed"
ManagedNamespaceLabelValue string = "true"
)
func ManagedNamespaceLabelSelector() string {
return ManagedNamespaceLabelKey + "=" + ManagedNamespaceLabelValue
}
func IsManagedNamespace(labels map[string]string) bool {
return labels[ManagedNamespaceLabelKey] == ManagedNamespaceLabelValue
}
type (
NamespaceResolver struct {
mu sync.RWMutex // protects FissionResourceNS
FunctionNamespace string
BuilderNamespace string
DefaultNamespace string
@@ -82,16 +96,46 @@ func WithDefaultNs() option {
}
}
// AddNamespace dynamically adds a namespace to FissionResourceNS without restarting the process.
// Returns true if the namespace was newly added, false if it was already present.
// Thread-safe — multiple goroutines may call this concurrently.
// The label fission.io/managed=true on the Namespace object is the trigger for this call.
func (nsr *NamespaceResolver) AddNamespace(ns string) bool {
nsr.mu.Lock()
defer nsr.mu.Unlock()
if _, exists := nsr.FissionResourceNS[ns]; exists {
return false
}
nsr.FissionResourceNS[ns] = ns
if nsr.Logger != nil {
nsr.Logger.Info("dynamically added namespace to 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 {
nsr.mu.RLock()
defer nsr.mu.RUnlock()
namespaces := listNamespaces(nsr.FissionResourceNS)
sort.Strings(namespaces)
return namespaces
}
func (nsr *NamespaceResolver) FissionNSWithOptions(option ...option) map[string]string {
var options options
for _, opt := range option {
options = *opt(&options)
}
fissionResourceNS := make(map[string]string)
nsr.mu.RLock()
fissionResourceNS := make(map[string]string, len(nsr.FissionResourceNS))
for k, v := range nsr.FissionResourceNS {
fissionResourceNS[k] = v
}
nsr.mu.RUnlock()
if options.functionNS && nsr.FunctionNamespace != "" {
fissionResourceNS[nsr.FunctionNamespace] = nsr.FunctionNamespace
@@ -102,10 +146,20 @@ func (nsr *NamespaceResolver) FissionNSWithOptions(option ...option) map[string]
if options.defaultNs && nsr.DefaultNamespace != "" {
fissionResourceNS[nsr.DefaultNamespace] = nsr.DefaultNamespace
}
nsr.Logger.Debug("fission resource namespaces", zap.Any("namespaces", listNamespaces(fissionResourceNS)))
if nsr.Logger != nil {
nsr.Logger.Debug("fission resource namespaces", zap.Any("namespaces", listNamespaces(fissionResourceNS)))
}
return fissionResourceNS
}
// SnapshotWithOptions returns a stable slice copy of Fission namespaces after applying
// optional builder/function/default namespace expansion.
func (nsr *NamespaceResolver) SnapshotWithOptions(option ...option) []string {
namespaces := listNamespaces(nsr.FissionNSWithOptions(option...))
sort.Strings(namespaces)
return namespaces
}
func GetNamespaces() map[string]string {
namespaces := make(map[string]string)
+608
View File
@@ -0,0 +1,608 @@
package utils
import (
"context"
"errors"
"sort"
"sync"
"time"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sInformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
k8sCache "k8s.io/client-go/tools/cache"
managerPkg "github.com/fission/fission/pkg/utils/manager"
)
type NamespaceSubscriber interface {
Name() string
OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error
OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error
OnNamespaceResync(ctx context.Context, record NamespaceRecord) error
}
type NamespaceSubscriberFuncs struct {
SubscriberName string
AddFunc func(ctx context.Context, record NamespaceRecord) error
RemoveFunc func(ctx context.Context, record NamespaceRecord) error
ResyncFunc func(ctx context.Context, record NamespaceRecord) error
}
func (s NamespaceSubscriberFuncs) Name() string {
return s.SubscriberName
}
func (s NamespaceSubscriberFuncs) OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error {
if s.AddFunc == nil {
return nil
}
return s.AddFunc(ctx, record)
}
func (s NamespaceSubscriberFuncs) OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error {
if s.RemoveFunc == nil {
return nil
}
return s.RemoveFunc(ctx, record)
}
func (s NamespaceSubscriberFuncs) OnNamespaceResync(ctx context.Context, record NamespaceRecord) error {
if s.ResyncFunc == nil {
return nil
}
return s.ResyncFunc(ctx, record)
}
type NamespaceManager interface {
Snapshot() []string
SnapshotRecords() []NamespaceRecord
Summary() NamespaceManagerSummary
Get(name string) (NamespaceRecord, bool)
Bootstrap(namespaces []string, source NamespaceSource, observedAt time.Time) []NamespaceRecord
BootstrapAndDispatch(ctx context.Context, namespaces []string, source NamespaceSource, observedAt time.Time) ([]NamespaceRecord, error)
Subscribe(subscriber NamespaceSubscriber)
SnapshotSubscribers() []string
Upsert(event NamespaceEvent) NamespaceRecord
DispatchAdd(ctx context.Context, namespace string) (NamespaceRecord, bool, error)
DispatchRemove(ctx context.Context, namespace string) (NamespaceRecord, bool, error)
DispatchResync(ctx context.Context, namespace string) (NamespaceRecord, bool, error)
MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool)
MarkPartRegistering(namespace string, part string) (NamespaceRecord, bool)
MarkPartActive(namespace string, part string) (NamespaceRecord, bool)
MarkPartFailed(namespace string, part string, err error) (NamespaceRecord, bool)
Remove(name string) bool
}
type inMemoryNamespaceManager struct {
mu sync.RWMutex
records map[string]NamespaceRecord
subs map[string]NamespaceSubscriber
}
func NewNamespaceManager() NamespaceManager {
return &inMemoryNamespaceManager{
records: make(map[string]NamespaceRecord),
subs: make(map[string]NamespaceSubscriber),
}
}
func NewBootstrappedNamespaceManager(resolver *NamespaceResolver, source NamespaceSource, observedAt time.Time) NamespaceManager {
manager := NewNamespaceManager()
if resolver == nil {
return manager
}
manager.Bootstrap(resolver.Snapshot(), source, observedAt)
return manager
}
func NewWatcherNamespaceManager(ctx context.Context, namespaces []string, source NamespaceSource, observedAt time.Time, subscribers ...NamespaceSubscriber) (NamespaceManager, error) {
manager := NewNamespaceManager()
for _, subscriber := range subscribers {
if subscriber == nil {
continue
}
manager.Subscribe(subscriber)
}
_, err := manager.BootstrapAndDispatch(ctx, namespaces, source, observedAt)
return manager, err
}
func NewDefaultManagedNamespaceWatcherConfig(component string, subscriber NamespaceSubscriber) ManagedNamespaceWatcherConfig {
return ManagedNamespaceWatcherConfig{
Component: component,
Namespaces: DefaultNSResolver().Snapshot(),
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: subscriber,
}
}
func namespaceManagerLogger(logger *zap.Logger) *zap.Logger {
if logger != nil {
return logger
}
return zap.NewNop()
}
func PrepareManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, config ManagedNamespaceWatcherConfig) (NamespaceManager, k8sCache.ResourceEventHandlerFuncs, error) {
logger = namespaceManagerLogger(logger)
strategy := config.RemovalStrategy
if strategy == "" {
strategy = NamespaceRemovalStrategyTrackOnly
}
manager, err := NewWatcherNamespaceManager(ctx, config.Namespaces, NamespaceSourceEnv, time.Now().UTC(), config.Subscriber)
handlers := NewNamespaceWatcherEventHandlers(ctx, logger, config.Component, manager, strategy)
LogNamespaceManagerSummary(logger, config.Component+": prepared namespace manager", manager.Summary())
return manager, handlers, err
}
func RunManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, kubeClient kubernetes.Interface, mgr managerPkg.Interface, config ManagedNamespaceWatcherConfig) (NamespaceManager, error) {
logger = namespaceManagerLogger(logger)
manager, handlers, err := PrepareManagedNamespaceWatcher(ctx, logger, config)
StartManagedNamespaceWatcher(ctx, logger, config.Component, kubeClient, mgr, handlers)
LogNamespaceManagerSummary(logger, config.Component+": started namespace watcher", manager.Summary())
return manager, err
}
func LogNamespaceManagerSummary(logger *zap.Logger, message string, summary NamespaceManagerSummary) {
if logger == nil {
return
}
logger.Info(message,
zap.Int("total_namespaces", summary.TotalNamespaces),
zap.Int("live_namespaces", summary.LiveNamespaces),
zap.Bool("has_active_namespaces", summary.HasActiveNamespaces()),
zap.Any("phase_counts", summary.PhaseCounts),
zap.Any("source_counts", summary.SourceCounts),
zap.Strings("subscribers", summary.Subscribers),
)
}
func NamespaceBecameUnmanaged(oldNamespace *corev1.Namespace, newNamespace *corev1.Namespace) bool {
if oldNamespace == nil || newNamespace == nil {
return false
}
return IsManagedNamespace(oldNamespace.Labels) && !IsManagedNamespace(newNamespace.Labels)
}
func DispatchNamespaceAdd(ctx context.Context, manager NamespaceManager, namespace *corev1.Namespace, source NamespaceSource, observedAt time.Time) (NamespaceRecord, bool, error) {
if manager == nil || namespace == nil || namespace.Name == "" {
return NamespaceRecord{}, false, nil
}
manager.Upsert(NamespaceEventFromNamespace(NamespaceEventAdd, namespace, source, observedAt))
return manager.DispatchAdd(ctx, namespace.Name)
}
func DispatchNamespaceResync(ctx context.Context, manager NamespaceManager, namespace *corev1.Namespace, source NamespaceSource, observedAt time.Time) (NamespaceRecord, bool, error) {
if manager == nil || namespace == nil || namespace.Name == "" {
return NamespaceRecord{}, false, nil
}
manager.Upsert(NamespaceEventFromNamespace(NamespaceEventUpdate, namespace, source, observedAt))
return manager.DispatchResync(ctx, namespace.Name)
}
func RecordNamespaceRemoval(manager NamespaceManager, obj interface{}, source NamespaceSource, observedAt time.Time) (NamespaceRecord, bool) {
if manager == nil {
return NamespaceRecord{}, false
}
event := NamespaceEventFromObject(NamespaceEventRemove, obj, source, observedAt)
if event.Name == "" {
return NamespaceRecord{}, false
}
record := manager.Upsert(event)
return record, true
}
func HandleWatcherNamespaceAdd(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, namespace *corev1.Namespace) {
logger = namespaceManagerLogger(logger)
if namespace == nil || namespace.Name == "" {
return
}
if _, _, err := DispatchNamespaceAdd(ctx, manager, namespace, NamespaceSourceWatcher, time.Now().UTC()); err != nil {
logger.Error(component+": DispatchAdd failed", zap.String("namespace", namespace.Name), zap.Error(err))
return
}
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
}
func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, oldNamespace *corev1.Namespace, newNamespace *corev1.Namespace, strategy NamespaceRemovalStrategy) {
logger = namespaceManagerLogger(logger)
if newNamespace == nil {
return
}
if NamespaceBecameUnmanaged(oldNamespace, newNamespace) {
HandleWatcherNamespaceRemoval(ctx, logger, component, manager, newNamespace, strategy)
return
}
if !IsManagedNamespace(newNamespace.Labels) {
return
}
if _, _, err := DispatchNamespaceResync(ctx, manager, newNamespace, NamespaceSourceWatcher, time.Now().UTC()); err != nil {
logger.Error(component+": DispatchResync failed", zap.String("namespace", newNamespace.Name), zap.Error(err))
return
}
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
}
func HandleWatcherNamespaceDelete(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, obj interface{}, strategy NamespaceRemovalStrategy) {
HandleWatcherNamespaceRemoval(ctx, logger, component, manager, obj, strategy)
}
func HandleWatcherNamespaceRemoval(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, obj interface{}, strategy NamespaceRemovalStrategy) {
logger = namespaceManagerLogger(logger)
record, ok := RecordNamespaceRemoval(manager, obj, NamespaceSourceWatcher, time.Now().UTC())
if !ok {
return
}
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))
return
}
logger.Info(component+": namespace removal dispatched through subscribers",
zap.String("namespace", record.Name))
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
return
}
logger.Info(component+": namespace removed from manager state; runtime registrations kept",
zap.String("namespace", record.Name))
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
}
func NewNamespaceWatcherEventHandlers(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, strategy NamespaceRemovalStrategy) k8sCache.ResourceEventHandlerFuncs {
logger = namespaceManagerLogger(logger)
return k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
namespace, ok := obj.(*corev1.Namespace)
if ok {
HandleWatcherNamespaceAdd(ctx, logger, component, manager, namespace)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldNamespace, _ := oldObj.(*corev1.Namespace)
newNamespace, ok := newObj.(*corev1.Namespace)
if ok {
HandleWatcherNamespaceUpdate(ctx, logger, component, manager, oldNamespace, newNamespace, strategy)
}
},
DeleteFunc: func(obj interface{}) {
HandleWatcherNamespaceDelete(ctx, logger, component, manager, obj, strategy)
},
}
}
func StartManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, component string, kubeClient kubernetes.Interface, mgr managerPkg.Interface, handlers k8sCache.ResourceEventHandlerFuncs) {
logger = namespaceManagerLogger(logger)
factory := k8sInformers.NewSharedInformerFactoryWithOptions(
kubeClient,
30*time.Minute,
k8sInformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
opts.LabelSelector = ManagedNamespaceLabelSelector()
}),
)
nsInformer := factory.Core().V1().Namespaces().Informer()
_, _ = nsInformer.AddEventHandler(handlers)
mgr.Add(ctx, func(ctx context.Context) {
logger.Info(component+": started", zap.String("label", ManagedNamespaceLabelSelector()))
factory.Start(ctx.Done())
factory.WaitForCacheSync(ctx.Done())
logger.Info(component + ": cache synced — watching for new namespaces")
<-ctx.Done()
logger.Info(component + ": stopped")
})
}
func (m *inMemoryNamespaceManager) Summary() NamespaceManagerSummary {
records := m.SnapshotRecords()
summary := NamespaceManagerSummary{
TotalNamespaces: len(records),
LiveNamespaces: len(m.Snapshot()),
PhaseCounts: make(map[NamespacePhase]int),
SourceCounts: make(map[NamespaceSource]int),
Subscribers: m.SnapshotSubscribers(),
}
if summary.Subscribers == nil {
summary.Subscribers = []string{}
}
for _, record := range records {
summary.PhaseCounts[record.Phase]++
summary.SourceCounts[record.Source]++
}
return summary
}
func (m *inMemoryNamespaceManager) Subscribe(subscriber NamespaceSubscriber) {
m.mu.Lock()
defer m.mu.Unlock()
m.subs[subscriber.Name()] = subscriber
}
func (m *inMemoryNamespaceManager) Bootstrap(namespaces []string, source NamespaceSource, observedAt time.Time) []NamespaceRecord {
records := make([]NamespaceRecord, 0, len(namespaces))
for _, namespace := range namespaces {
records = append(records, m.Upsert(NamespaceEvent{
Type: NamespaceEventAdd,
Name: namespace,
Source: source,
ObservedAt: observedAt,
}))
}
sort.Slice(records, func(i, j int) bool {
return records[i].Name < records[j].Name
})
return records
}
func (m *inMemoryNamespaceManager) BootstrapAndDispatch(ctx context.Context, namespaces []string, source NamespaceSource, observedAt time.Time) ([]NamespaceRecord, error) {
records := m.Bootstrap(namespaces, source, observedAt)
var joinErr error
for _, record := range records {
updatedRecord, ok, err := m.DispatchAdd(ctx, record.Name)
if ok {
record = updatedRecord
}
if err != nil {
joinErr = errors.Join(joinErr, err)
}
for index := range records {
if records[index].Name == record.Name {
records[index] = record
break
}
}
}
return records, joinErr
}
func (m *inMemoryNamespaceManager) SnapshotSubscribers() []string {
m.mu.RLock()
defer m.mu.RUnlock()
names := make([]string, 0, len(m.subs))
for name := range m.subs {
names = append(names, name)
}
sort.Strings(names)
return names
}
func (m *inMemoryNamespaceManager) snapshotSubscriberObjects() []NamespaceSubscriber {
m.mu.RLock()
defer m.mu.RUnlock()
names := make([]string, 0, len(m.subs))
for name := range m.subs {
names = append(names, name)
}
sort.Strings(names)
subscribers := make([]NamespaceSubscriber, 0, len(names))
for _, name := range names {
subscribers = append(subscribers, m.subs[name])
}
return subscribers
}
func (m *inMemoryNamespaceManager) Snapshot() []string {
m.mu.RLock()
defer m.mu.RUnlock()
namespaces := make([]string, 0, len(m.records))
for name, record := range m.records {
if record.Phase == NamespacePhaseRemoved {
continue
}
namespaces = append(namespaces, name)
}
sort.Strings(namespaces)
return namespaces
}
func (m *inMemoryNamespaceManager) SnapshotRecords() []NamespaceRecord {
m.mu.RLock()
defer m.mu.RUnlock()
records := make([]NamespaceRecord, 0, len(m.records))
for _, record := range m.records {
records = append(records, record.Clone())
}
sort.Slice(records, func(i, j int) bool {
return records[i].Name < records[j].Name
})
return records
}
func (m *inMemoryNamespaceManager) Get(name string) (NamespaceRecord, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
record, ok := m.records[name]
if !ok {
return NamespaceRecord{}, false
}
return record.Clone(), true
}
func (m *inMemoryNamespaceManager) Upsert(event NamespaceEvent) NamespaceRecord {
m.mu.Lock()
defer m.mu.Unlock()
now := event.ObservedAt
if now.IsZero() {
now = time.Now().UTC()
}
record, exists := m.records[event.Name]
if !exists {
record = NamespaceRecord{
Name: event.Name,
RegisteredParts: make(map[string]NamespacePartState),
}
if event.Type == NamespaceEventRemove {
record.Generation = 0
} else {
record.Generation = 1
}
} else if event.Type != NamespaceEventRemove {
record.Generation++
}
record.Name = event.Name
record.Source = event.Source
record.Labels = cloneStringMap(event.Labels)
record.UpdatedAt = now
switch event.Type {
case NamespaceEventAdd, NamespaceEventUpdate, NamespaceEventResync:
record.Phase = NamespacePhaseDiscovered
record.LastError = ""
case NamespaceEventRemove:
record.Phase = NamespacePhaseRemoved
}
if record.RegisteredParts == nil {
record.RegisteredParts = make(map[string]NamespacePartState)
}
m.records[event.Name] = record
return record.Clone()
}
func (m *inMemoryNamespaceManager) DispatchAdd(ctx context.Context, namespace string) (NamespaceRecord, bool, error) {
return m.dispatch(ctx, namespace, func(subscriber NamespaceSubscriber, record NamespaceRecord) error {
return subscriber.OnNamespaceAdd(ctx, record)
})
}
func (m *inMemoryNamespaceManager) DispatchRemove(ctx context.Context, namespace string) (NamespaceRecord, bool, error) {
record, ok := m.Get(namespace)
if !ok {
return NamespaceRecord{}, false, nil
}
var firstErr error
for _, subscriber := range m.snapshotSubscriberObjects() {
err := subscriber.OnNamespaceRemove(ctx, record)
if err != nil && firstErr == nil {
firstErr = err
}
}
removedRecord := m.Upsert(NamespaceEvent{
Type: NamespaceEventRemove,
Name: record.Name,
Labels: record.Labels,
Source: record.Source,
ObservedAt: time.Now().UTC(),
})
return removedRecord, true, firstErr
}
func (m *inMemoryNamespaceManager) DispatchResync(ctx context.Context, namespace string) (NamespaceRecord, bool, error) {
return m.dispatch(ctx, namespace, func(subscriber NamespaceSubscriber, record NamespaceRecord) error {
return subscriber.OnNamespaceResync(ctx, record)
})
}
func (m *inMemoryNamespaceManager) dispatch(ctx context.Context, namespace string, handler func(NamespaceSubscriber, NamespaceRecord) error) (NamespaceRecord, bool, error) {
record, ok := m.Get(namespace)
if !ok {
return NamespaceRecord{}, false, nil
}
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
}
continue
}
_, _ = m.MarkPartActive(namespace, subscriber.Name())
}
record, _ = m.Get(namespace)
return record, true, firstErr
}
func (m *inMemoryNamespaceManager) MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool) {
m.mu.Lock()
defer m.mu.Unlock()
record, ok := m.records[namespace]
if !ok {
return NamespaceRecord{}, false
}
if record.RegisteredParts == nil {
record.RegisteredParts = make(map[string]NamespacePartState)
}
if state.UpdatedAt.IsZero() {
state.UpdatedAt = time.Now().UTC()
}
record.RegisteredParts[part] = state
record.Phase = deriveNamespacePhase(record)
record.UpdatedAt = state.UpdatedAt
m.records[namespace] = record
return record.Clone(), true
}
func (m *inMemoryNamespaceManager) MarkPartRegistering(namespace string, part string) (NamespaceRecord, bool) {
return m.MarkPartState(namespace, part, NamespacePartState{State: NamespacePartStateRegistering})
}
func (m *inMemoryNamespaceManager) MarkPartActive(namespace string, part string) (NamespaceRecord, bool) {
return m.MarkPartState(namespace, part, NamespacePartState{State: NamespacePartStateActive})
}
func (m *inMemoryNamespaceManager) MarkPartFailed(namespace string, part string, err error) (NamespaceRecord, bool) {
lastError := ""
if err != nil {
lastError = err.Error()
}
return m.MarkPartState(namespace, part, NamespacePartState{State: NamespacePartStateFailed, LastError: lastError})
}
func (m *inMemoryNamespaceManager) Remove(name string) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.records[name]; !ok {
return false
}
delete(m.records, name)
return true
}
func deriveNamespacePhase(record NamespaceRecord) NamespacePhase {
if len(record.RegisteredParts) == 0 {
return record.Phase
}
hasRegistering := false
for _, part := range record.RegisteredParts {
switch part.State {
case NamespacePartStateFailed:
return NamespacePhaseFailed
case NamespacePartStateRegistering:
hasRegistering = true
case NamespacePartStateActive:
continue
default:
hasRegistering = true
}
}
if hasRegistering {
return NamespacePhaseRegistering
}
return NamespacePhaseActive
}
+181
View File
@@ -0,0 +1,181 @@
package utils
import (
"time"
corev1 "k8s.io/api/core/v1"
k8sCache "k8s.io/client-go/tools/cache"
)
const (
NamespacePartStateRegistering string = "registering"
NamespacePartStateActive string = "active"
NamespacePartStateFailed string = "failed"
)
type NamespacePhase string
const (
NamespacePhaseDiscovered NamespacePhase = "discovered"
NamespacePhaseRegistering NamespacePhase = "registering"
NamespacePhaseActive NamespacePhase = "active"
NamespacePhaseDeregistering NamespacePhase = "deregistering"
NamespacePhaseRemoved NamespacePhase = "removed"
NamespacePhaseFailed NamespacePhase = "failed"
)
type NamespaceSource string
const (
NamespaceSourceEnv NamespaceSource = "env"
NamespaceSourceWatcher NamespaceSource = "watcher"
NamespaceSourceBackfill NamespaceSource = "backfill"
)
type NamespaceEventType string
const (
NamespaceEventAdd NamespaceEventType = "add"
NamespaceEventUpdate NamespaceEventType = "update"
NamespaceEventRemove NamespaceEventType = "remove"
NamespaceEventResync NamespaceEventType = "resync"
)
type NamespaceRemovalStrategy string
const (
NamespaceRemovalStrategyTrackOnly NamespaceRemovalStrategy = "track-only"
NamespaceRemovalStrategyDispatchRemove NamespaceRemovalStrategy = "dispatch-remove"
)
type NamespacePartState struct {
State string
LastError string
UpdatedAt time.Time
}
type NamespaceRecord struct {
Name string
Source NamespaceSource
Labels map[string]string
Phase NamespacePhase
LastError string
Generation int64
UpdatedAt time.Time
RegisteredParts map[string]NamespacePartState
}
type NamespaceEvent struct {
Type NamespaceEventType
Name string
Labels map[string]string
Source NamespaceSource
ObservedAt time.Time
}
type NamespaceManagerSummary struct {
TotalNamespaces int
LiveNamespaces int
PhaseCounts map[NamespacePhase]int
SourceCounts map[NamespaceSource]int
Subscribers []string
}
func (summary NamespaceManagerSummary) HasActiveNamespaces() bool {
return summary.LiveNamespaces > 0
}
type ManagedNamespaceWatcherConfig struct {
Component string
Namespaces []string
RemovalStrategy NamespaceRemovalStrategy
Subscriber NamespaceSubscriber
}
func NewNamespaceEvent(eventType NamespaceEventType, name string, labels map[string]string, source NamespaceSource, observedAt time.Time) NamespaceEvent {
return NamespaceEvent{
Type: eventType,
Name: name,
Labels: cloneStringMap(labels),
Source: source,
ObservedAt: observedAt,
}
}
func ManagedNamespaceEvent(eventType NamespaceEventType, name string, source NamespaceSource, observedAt time.Time) NamespaceEvent {
return NewNamespaceEvent(eventType, name, map[string]string{
ManagedNamespaceLabelKey: ManagedNamespaceLabelValue,
}, source, observedAt)
}
func NamespaceEventFromNamespace(eventType NamespaceEventType, namespace *corev1.Namespace, source NamespaceSource, observedAt time.Time) NamespaceEvent {
if namespace == nil {
return NewNamespaceEvent(eventType, "", nil, source, observedAt)
}
return NewNamespaceEvent(eventType, namespace.Name, namespace.Labels, source, observedAt)
}
func NamespaceFromObject(obj interface{}) (*corev1.Namespace, bool) {
switch typed := obj.(type) {
case *corev1.Namespace:
return typed, true
case k8sCache.DeletedFinalStateUnknown:
namespace, ok := typed.Obj.(*corev1.Namespace)
return namespace, ok
case *k8sCache.DeletedFinalStateUnknown:
if typed == nil {
return nil, false
}
namespace, ok := typed.Obj.(*corev1.Namespace)
return namespace, ok
default:
return nil, false
}
}
func NamespaceEventFromObject(eventType NamespaceEventType, obj interface{}, source NamespaceSource, observedAt time.Time) NamespaceEvent {
namespace, ok := NamespaceFromObject(obj)
if !ok {
return NewNamespaceEvent(eventType, "", nil, source, observedAt)
}
return NamespaceEventFromNamespace(eventType, namespace, source, observedAt)
}
func (nr NamespaceRecord) Clone() NamespaceRecord {
clone := nr
clone.Labels = cloneStringMap(nr.Labels)
clone.RegisteredParts = cloneNamespacePartStates(nr.RegisteredParts)
return clone
}
func (nr NamespaceRecord) IsActive() bool {
return nr.Phase == NamespacePhaseActive
}
func (nr NamespaceRecord) IsTerminal() bool {
return nr.Phase == NamespacePhaseRemoved || nr.Phase == NamespacePhaseFailed
}
func cloneStringMap(input map[string]string) map[string]string {
if input == nil {
return nil
}
clone := make(map[string]string, len(input))
for key, value := range input {
clone[key] = value
}
return clone
}
func cloneNamespacePartStates(input map[string]NamespacePartState) map[string]NamespacePartState {
if input == nil {
return nil
}
clone := make(map[string]NamespacePartState, len(input))
for key, value := range input {
clone[key] = value
}
return clone
}

Some files were not shown because too many files have changed in this diff Show More