Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc0c5637f | ||
|
|
0cae275dbd | ||
|
|
775845b55c | ||
|
|
c9036f716f | ||
|
|
4349d0b239 | ||
|
|
63ce6ea135 | ||
|
|
7b6ff84188 | ||
|
|
55d0b5a9e7 | ||
|
|
813617ffd1 | ||
|
|
7d7fe561a8 | ||
|
|
fed69da335 | ||
|
|
9f0e911b9d | ||
|
|
f4a3bffc6b | ||
|
|
90924cdec7 | ||
|
|
6e037a506d | ||
|
|
d24605a8b8 | ||
|
|
d2ff55f9e0 | ||
|
|
b9236698f3 | ||
|
|
073f2c1504 | ||
|
|
b93e720e12 | ||
|
|
f16aa030db | ||
|
|
5c481b7293 | ||
|
|
67db8d71f1 | ||
|
|
2bbed95c2a | ||
|
|
a1517ba4b2 | ||
|
|
49be1db3a0 | ||
|
|
c3b161da83 | ||
|
|
0755319fac | ||
|
|
340b9cae84 | ||
|
|
4cd4bc9507 | ||
|
|
0e08664ef6 | ||
|
|
d7497dcd34 | ||
|
|
447133d5b2 | ||
|
|
b6f3640bbe | ||
|
|
d09bee3431 | ||
|
|
488157963a | ||
|
|
804bc533db | ||
|
|
65837610a1 | ||
|
|
dd7470922c | ||
|
|
0135a93a30 | ||
|
|
b12a8e5e75 | ||
|
|
ad0f83fd4b | ||
|
|
6f77fa5a9a | ||
|
|
331f531962 | ||
|
|
346399d35f | ||
|
|
33a08f00b3 | ||
|
|
2971029c42 | ||
|
|
3159fba65b | ||
|
|
b200b8bb5b | ||
|
|
126f7cc51c | ||
|
|
bcf34d6b1a | ||
|
|
022960ade7 | ||
|
|
b1e2e6462d | ||
|
|
ae8275d0a7 | ||
|
|
2857398e11 | ||
|
|
834c0de941 | ||
|
|
db4499d8c7 | ||
|
|
b3f99b2b6c | ||
|
|
5e5058ba0e | ||
|
|
42acce308f | ||
|
|
66a3dc2a3c | ||
|
|
910f65b6d4 | ||
|
|
114d5b99af | ||
|
|
ac2638f17d | ||
|
|
97b13a13c2 | ||
|
|
1f53bc1fb7 | ||
|
|
87477d4529 | ||
|
|
94f26b69ee | ||
|
|
56a499a59f | ||
|
|
6102b277c8 | ||
|
|
9ce9829f3b | ||
|
|
c987fa07e8 | ||
|
|
27a280bc03 | ||
|
|
e2dff8db09 | ||
|
|
7faaa9dc1f | ||
|
|
f617913ad9 | ||
|
|
8ccc9fb342 | ||
|
|
161de70576 | ||
|
|
82e1ff76a5 |
@@ -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:
|
||||
- "*"
|
||||
@@ -0,0 +1,79 @@
|
||||
# NEXT CHAT: LAYER2 START HERE
|
||||
|
||||
Если ты новый агент в новом чате, сначала прочитай этот файл целиком.
|
||||
|
||||
## Где работать
|
||||
|
||||
Репозиторий: `fission-src`
|
||||
|
||||
Ветка:
|
||||
|
||||
`rewrite/layer2-namespace-manager-api-step1`
|
||||
|
||||
## Что уже готово
|
||||
|
||||
Layer1 завершён.
|
||||
|
||||
Это значит:
|
||||
|
||||
1. Внутренний `NamespaceManager` layer уже реализован.
|
||||
2. Buildermgr, router и executor/multitenant уже переведены на общий watcher/helper layer.
|
||||
3. Summary/debug contract стабилизирован.
|
||||
4. Logging path усилен.
|
||||
5. Layer1 закрыт commit-ом:
|
||||
|
||||
`63ce6ea`
|
||||
`layer1: close namespace manager step1`
|
||||
|
||||
## Что уже было проверено
|
||||
|
||||
Целевой прогон для layer1 уже был зелёным:
|
||||
|
||||
`go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant`
|
||||
|
||||
## Что нужно делать теперь
|
||||
|
||||
Нужен layer2.
|
||||
|
||||
Layer2 = не переписывать watcher-ы заново, а дать внешний read-only status/debug/API surface поверх уже готового `NamespaceManager` слоя.
|
||||
|
||||
Цель:
|
||||
|
||||
1. Найти лучший существующий read-only endpoint/status/debug surface.
|
||||
2. Начать аккуратно выносить наружу `NamespaceManagerSummary`.
|
||||
3. Не менять runtime semantics watcher-ов.
|
||||
4. Не плодить второй источник правды о namespace state.
|
||||
|
||||
## Как работать
|
||||
|
||||
1. Работай маленькими шагами.
|
||||
2. Перед кодом сначала найди правильную точку интеграции.
|
||||
3. Все новые заметки пиши только в новые файлы в `doc/thinking/`.
|
||||
4. Не трогай старые doc-файлы.
|
||||
5. Не запускай background-команды.
|
||||
6. Все команды запускай только через SSH на VM и всегда с timeout.
|
||||
|
||||
## Важные файлы
|
||||
|
||||
- `pkg/utils/namespace_manager.go`
|
||||
- `pkg/utils/namespace_manager_model.go`
|
||||
- `pkg/utils/namespace_manager_test.go`
|
||||
- `pkg/buildermgr/ns_watcher.go`
|
||||
- `pkg/router/ns_watcher.go`
|
||||
- `pkg/executor/multitenant/ns_watcher.go`
|
||||
|
||||
## Первый шаг в новом чате
|
||||
|
||||
Сначала не писать код.
|
||||
|
||||
Сначала:
|
||||
|
||||
1. проверить текущую ветку и чистоту дерева;
|
||||
2. найти существующий service-level status/debug/API contour;
|
||||
3. выбрать один безопасный read-only entrypoint для первого шага layer2.
|
||||
|
||||
## Текст первого сообщения нового чата
|
||||
|
||||
Можно просто вставить это:
|
||||
|
||||
"Прочитай файл NEXT_CHAT_LAYER2.md и продолжай работу строго по нему. Нужен layer2: safe read-only API/status/debug surface поверх NamespaceManager без изменения runtime semantics watcher-ов. Сначала найди правильную точку интеграции, потом делай маленькие шаги с документированием в новых файлах doc/thinking/."
|
||||
@@ -1,3 +1,18 @@
|
||||
> [!IMPORTANT]
|
||||
> ## Это форк Fission с поддержкой мультитенантности (multi-tenant)
|
||||
>
|
||||
> **Автор доработок:** Naeel / ngcloud
|
||||
> **Базовая версия:** Fission v1.22.0 (официальный)
|
||||
> **Репозиторий:** https://gitea.services.ngcloud.ru/Nail/fission-src
|
||||
>
|
||||
> ### Что добавлено по сравнению с официальным Fission:
|
||||
> - **Динамический multi-tenant:** namespace с меткой `fission.io/managed=true` подхватываются без рестарта Fission
|
||||
> - **Автоматический SA provisioning:** при появлении нового namespace автоматически создаются ServiceAccount, Role, RoleBinding для fetcher/builder
|
||||
> - **Namespace Manager:** новый компонент в `pkg/utils/` для отслеживания namespace в реальном времени
|
||||
> - **Обратная совместимость:** полная, поведение идентично официальному если меток нет
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="https://fission.io/images/logo-gh.svg" width="300" />
|
||||
<br>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM cgr.dev/chainguard/static:latest@sha256:a301031ffd4ed67f35ca7fa6cf3dad9937b5fa47d7493955a18d9b4ca5412d1a
|
||||
COPY fission-bundle /
|
||||
ENTRYPOINT ["/fission-bundle"]
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
FROM cgr.dev/chainguard/static:latest@sha256:a301031ffd4ed67f35ca7fa6cf3dad9937b5fa47d7493955a18d9b4ca5412d1a
|
||||
COPY fission-bundle /
|
||||
ENTRYPOINT ["/fission-bundle"]
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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-*`
|
||||
@@ -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,94 @@
|
||||
# 2026-04-26 — Layer2 chat handoff
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
Layer1 завершён в ветке rewrite/layer1-namespace-manager-step1 и перенесён в новую рабочую ветку:
|
||||
|
||||
`rewrite/layer2-namespace-manager-api-step1`
|
||||
|
||||
Layer1 означает, что внутренняя адаптация Fission под multi-tenant namespace onboarding уже готова:
|
||||
|
||||
1. Вынесен общий `NamespaceManager`.
|
||||
2. Buildermgr, router и executor/multitenant переведены на общий watcher/helper layer.
|
||||
3. Summary/debug contract стабилизирован.
|
||||
4. Logging path усилен и покрыт тестами.
|
||||
|
||||
Последняя точка закрытия layer1:
|
||||
|
||||
- commit `63ce6ea` — `layer1: close namespace manager step1`
|
||||
|
||||
## Какие тесты уже были прогнаны
|
||||
|
||||
Финальный целевой прогон для layer1:
|
||||
|
||||
`go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant`
|
||||
|
||||
Он прошёл зелёным.
|
||||
|
||||
## На какой ветке продолжать
|
||||
|
||||
Продолжать работу нужно на ветке:
|
||||
|
||||
`rewrite/layer2-namespace-manager-api-step1`
|
||||
|
||||
## Что является целью layer2
|
||||
|
||||
Layer2 — это уже не перепись watcher-ов, а внешний read-only consumption поверх готового `NamespaceManager` слоя.
|
||||
|
||||
Практическая цель:
|
||||
|
||||
1. Дать безопасный read-only status/debug/API surface для состояния multi-tenant namespace onboarding.
|
||||
2. Не менять runtime behavior watcher-ов.
|
||||
3. Не дублировать логику manager-а в service-level коде.
|
||||
4. Использовать уже существующий `NamespaceManagerSummary`, а не придумывать вторую модель состояния.
|
||||
|
||||
## Что делать в новом чате
|
||||
|
||||
Новый чат должен стартовать не с переписывания layer1 заново, а с аккуратного поиска лучшей точки интеграции для layer2.
|
||||
|
||||
Предпочтительный порядок:
|
||||
|
||||
1. Проверить текущую ветку и чистоту дерева.
|
||||
2. Найти существующий service-level debug/status/API contour в buildermgr, router или executor.
|
||||
3. Выбрать один самый безопасный read-only endpoint или status surface.
|
||||
4. Протащить наружу `NamespaceManagerSummary` без изменения watcher semantics.
|
||||
5. Добавить unit/integration tests именно на внешний consumer-side path.
|
||||
6. Документировать каждый шаг в новых файлах в `doc/thinking/`.
|
||||
|
||||
## Чего НЕ надо делать
|
||||
|
||||
1. Не продолжать внутреннюю консолидацию watcher layer ради самой консолидации.
|
||||
2. Не ломать существующий runtime flow add/resync/remove.
|
||||
3. Не вводить второй независимый источник правды о namespace state.
|
||||
4. Не менять старые doc-файлы — только новые файлы с новыми шагами.
|
||||
|
||||
## Важные файлы для продолжения
|
||||
|
||||
- `pkg/utils/namespace_manager.go`
|
||||
- `pkg/utils/namespace_manager_model.go`
|
||||
- `pkg/utils/namespace_manager_test.go`
|
||||
- `pkg/buildermgr/ns_watcher.go`
|
||||
- `pkg/router/ns_watcher.go`
|
||||
- `pkg/executor/multitenant/ns_watcher.go`
|
||||
|
||||
## Как начать с другого компьютера
|
||||
|
||||
Если работа продолжается в том же репозитории на той же VM, достаточно открыть репозиторий и проверить ветку:
|
||||
|
||||
`cd ~/terra/fission-src && git branch --show-current && git log --oneline -8`
|
||||
|
||||
Если ветка не выбрана, переключиться на неё:
|
||||
|
||||
`git checkout rewrite/layer2-namespace-manager-api-step1`
|
||||
|
||||
Если новый чат работает через VS Code tools над sshfs mount, локальный путь будет соответствовать смонтированной папке, а команды всё равно нужно запускать через SSH на VM.
|
||||
|
||||
## Готовый текст для первого сообщения в новом чате
|
||||
|
||||
Ниже текст, который можно вставить почти без изменений:
|
||||
|
||||
"Продолжаем в repo `fission-src` на ветке `rewrite/layer2-namespace-manager-api-step1`. Layer1 завершён и закрыт commit-ом `63ce6ea`. Внутренний `NamespaceManager` layer готов, buildermgr/router/executor уже сидят на общих watcher helper-ах, summary/debug contract стабилизирован и целевой прогон `go test ./pkg/utils/... ./pkg/buildermgr/... ./pkg/router/... ./pkg/executor/multitenant` уже был зелёным. Теперь нужен layer2: аккуратно найти лучший существующий read-only status/debug/API surface и начать вынос наружу `NamespaceManagerSummary` без изменения runtime semantics watcher-ов. Работай маленькими шагами, с новыми doc-файлами в `doc/thinking/`, без background команд, все команды только через SSH на VM и всегда с timeout."
|
||||
|
||||
## Ожидаемый первый технический шаг в новом чате
|
||||
|
||||
Не писать код сразу. Сначала найти реальный существующий endpoint или status surface, куда summary можно встроить безопасно и без архитектурного мусора.
|
||||
@@ -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 слоем с понятным жизненным циклом.
|
||||
@@ -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
Binary file not shown.
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -111,7 +111,13 @@ func (c *client) GetServiceForFunction(ctx context.Context, fn *fv1.Function) (s
|
||||
func (c *client) UnTapService(ctx context.Context, fnMeta metav1.ObjectMeta, executorType fv1.ExecutorType, serviceURL *url.URL) error {
|
||||
url := c.executorURL + "/v2/unTapService"
|
||||
tapSvc := TapServiceRequest{
|
||||
FnMetadata: fnMeta,
|
||||
FnMetadata: metav1.ObjectMeta{
|
||||
Name: fnMeta.Name,
|
||||
Namespace: fnMeta.Namespace,
|
||||
ResourceVersion: fnMeta.ResourceVersion,
|
||||
Generation: fnMeta.Generation,
|
||||
UID: fnMeta.UID,
|
||||
},
|
||||
FnExecutorType: executorType,
|
||||
ServiceURL: strings.TrimPrefix(serviceURL.String(), "http://"),
|
||||
}
|
||||
@@ -175,6 +181,7 @@ func (c *client) TapService(fnMeta metav1.ObjectMeta, executorType fv1.ExecutorT
|
||||
Name: fnMeta.Name,
|
||||
Namespace: fnMeta.Namespace,
|
||||
ResourceVersion: fnMeta.ResourceVersion,
|
||||
Generation: fnMeta.Generation,
|
||||
UID: fnMeta.UID,
|
||||
},
|
||||
FnExecutorType: executorType,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,64 @@ 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)
|
||||
}
|
||||
|
||||
// Also update gpm.podLister so IsValid can resolve pods in this namespace.
|
||||
// Without this line gpm.podLister[ns] is nil for dynamically-added user namespaces,
|
||||
// causing a nil-pointer panic in IsValid, which leaves activeRequests permanently stuck at 1.
|
||||
gpm.podLister[ns] = gpmInformer.Core().V1().Pods().Lister()
|
||||
|
||||
// Start the factories — they will begin syncing immediately.
|
||||
finformer.Start(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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8sCache "k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func TestNamespaceRecordClone(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
record := NamespaceRecord{
|
||||
Name: "tenant-a",
|
||||
Source: NamespaceSourceWatcher,
|
||||
Phase: NamespacePhaseRegistering,
|
||||
Generation: 7,
|
||||
UpdatedAt: now,
|
||||
Labels: map[string]string{
|
||||
"fission.io/managed": "true",
|
||||
},
|
||||
RegisteredParts: map[string]NamespacePartState{
|
||||
"router": {
|
||||
State: "active",
|
||||
UpdatedAt: now,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
clone := record.Clone()
|
||||
clone.Labels["fission.io/managed"] = "false"
|
||||
clone.RegisteredParts["router"] = NamespacePartState{State: "failed", LastError: "boom"}
|
||||
|
||||
if record.Labels["fission.io/managed"] != "true" {
|
||||
t.Fatalf("expected labels to be deep copied")
|
||||
}
|
||||
|
||||
if record.RegisteredParts["router"].State != "active" {
|
||||
t.Fatalf("expected registered parts to be deep copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceRecordIsActive(t *testing.T) {
|
||||
record := NamespaceRecord{Phase: NamespacePhaseActive}
|
||||
if !record.IsActive() {
|
||||
t.Fatalf("expected active phase to be reported as active")
|
||||
}
|
||||
|
||||
record.Phase = NamespacePhaseRegistering
|
||||
if record.IsActive() {
|
||||
t.Fatalf("expected non-active phase to be reported as inactive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceRecordIsTerminal(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
phase NamespacePhase
|
||||
expected bool
|
||||
}{
|
||||
{phase: NamespacePhaseRemoved, expected: true},
|
||||
{phase: NamespacePhaseFailed, expected: true},
|
||||
{phase: NamespacePhaseActive, expected: false},
|
||||
{phase: NamespacePhaseRegistering, expected: false},
|
||||
} {
|
||||
record := NamespaceRecord{Phase: test.phase}
|
||||
if record.IsTerminal() != test.expected {
|
||||
t.Fatalf("expected terminal=%v for phase %s", test.expected, test.phase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNamespaceEventClonesLabels(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
labels := map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
|
||||
event := NewNamespaceEvent(NamespaceEventAdd, "tenant-a", labels, NamespaceSourceWatcher, now)
|
||||
labels[ManagedNamespaceLabelKey] = "false"
|
||||
|
||||
if event.Labels[ManagedNamespaceLabelKey] != ManagedNamespaceLabelValue {
|
||||
t.Fatalf("expected namespace event labels to be cloned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedNamespaceEvent(t *testing.T) {
|
||||
event := ManagedNamespaceEvent(NamespaceEventAdd, "tenant-a", NamespaceSourceWatcher, time.Now().UTC())
|
||||
if event.Labels[ManagedNamespaceLabelKey] != ManagedNamespaceLabelValue {
|
||||
t.Fatalf("expected managed namespace label in event")
|
||||
}
|
||||
if event.Name != "tenant-a" {
|
||||
t.Fatalf("expected tenant-a event name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceEventFromNamespace(t *testing.T) {
|
||||
namespace := &corev1.Namespace{}
|
||||
namespace.Name = "tenant-a"
|
||||
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
|
||||
|
||||
event := NamespaceEventFromNamespace(NamespaceEventAdd, namespace, NamespaceSourceWatcher, time.Now().UTC())
|
||||
if event.Name != "tenant-a" {
|
||||
t.Fatalf("expected tenant-a event name")
|
||||
}
|
||||
if event.Labels[ManagedNamespaceLabelKey] != ManagedNamespaceLabelValue {
|
||||
t.Fatalf("expected namespace labels to be copied into event")
|
||||
}
|
||||
|
||||
namespace.Labels[ManagedNamespaceLabelKey] = "false"
|
||||
if event.Labels[ManagedNamespaceLabelKey] != ManagedNamespaceLabelValue {
|
||||
t.Fatalf("expected event labels to stay detached from original namespace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceFromObject(t *testing.T) {
|
||||
namespace := &corev1.Namespace{}
|
||||
namespace.Name = "tenant-a"
|
||||
|
||||
extracted, ok := NamespaceFromObject(namespace)
|
||||
if !ok || extracted.Name != "tenant-a" {
|
||||
t.Fatalf("expected namespace object to be extracted directly")
|
||||
}
|
||||
|
||||
extracted, ok = NamespaceFromObject(k8sCache.DeletedFinalStateUnknown{Obj: namespace})
|
||||
if !ok || extracted.Name != "tenant-a" {
|
||||
t.Fatalf("expected namespace tombstone to be extracted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceEventFromObject(t *testing.T) {
|
||||
namespace := &corev1.Namespace{}
|
||||
namespace.Name = "tenant-a"
|
||||
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
|
||||
|
||||
event := NamespaceEventFromObject(NamespaceEventRemove, k8sCache.DeletedFinalStateUnknown{Obj: namespace}, NamespaceSourceWatcher, time.Now().UTC())
|
||||
if event.Type != NamespaceEventRemove {
|
||||
t.Fatalf("expected remove event type")
|
||||
}
|
||||
if event.Name != "tenant-a" {
|
||||
t.Fatalf("expected tenant-a event name")
|
||||
}
|
||||
if event.Labels[ManagedNamespaceLabelKey] != ManagedNamespaceLabelValue {
|
||||
t.Fatalf("expected labels copied from tombstone namespace")
|
||||
}
|
||||
|
||||
emptyEvent := NamespaceEventFromObject(NamespaceEventRemove, "not-a-namespace", NamespaceSourceWatcher, time.Now().UTC())
|
||||
if emptyEvent.Name != "" {
|
||||
t.Fatalf("expected empty name for unsupported object")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceRemovalStrategyValues(t *testing.T) {
|
||||
if NamespaceRemovalStrategyTrackOnly != "track-only" {
|
||||
t.Fatalf("unexpected track-only strategy value")
|
||||
}
|
||||
if NamespaceRemovalStrategyDispatchRemove != "dispatch-remove" {
|
||||
t.Fatalf("unexpected dispatch-remove strategy value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceManagerSummaryZeroValue(t *testing.T) {
|
||||
summary := NamespaceManagerSummary{}
|
||||
if summary.TotalNamespaces != 0 {
|
||||
t.Fatalf("expected zero total namespaces")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceManagerSummaryHasActiveNamespaces(t *testing.T) {
|
||||
summary := NamespaceManagerSummary{LiveNamespaces: 1}
|
||||
if !summary.HasActiveNamespaces() {
|
||||
t.Fatalf("expected positive live count to report active namespaces")
|
||||
}
|
||||
summary.LiveNamespaces = 0
|
||||
if summary.HasActiveNamespaces() {
|
||||
t.Fatalf("expected zero live count to report no active namespaces")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user