DefaultNSResolver().Snapshot() returns only namespaces registered via
AddNamespace(). Storagesvc does not listen to namespace events, so tenant
namespaces (fission-*) are never registered and their Package CRDs are
invisible to the pruner — causing all archives to be treated as orphans
and deleted.
Fix: use metav1.NamespaceAll to list Packages across all namespaces.
Remove unused pkg/utils import.
Deployed as naeel/fission-bundle:v1.23.1
RunReconciler now runs two tickers:
- 30s: retry Failed namespaces (existing behavior)
- 60s: DispatchResync on Active namespaces; since registerNamespace is
idempotent this is a no-op when SA/RoleBindings are intact and
silently restores them if deleted
Fixes P1-A from integration test 2026-05-18: SA deleted from Active NS
was not being restored because reconciler only processed Failed NS.
Problem:
executor starts → AdoptExistingResources + CleanupOldExecutorObjects run
against utils.DefaultNSResolver().Snapshot() which returns ONLY static NS
from FISSION_RESOURCE_NAMESPACES. Managed (labeled) namespaces are
registered later, asynchronously, by StartNSWatcher.
Result:
- Pods from a previous executor in managed NS are never adopted
(no instanceID patch) → poolmgr creates new pool pods → cold start
for first request after executor restart.
- Old executor objects (RS/deployments) in managed NS accumulate
without being cleaned up (resource leak).
Fix:
Add multitenant.PreRegisterManagedNamespaces(ctx, logger, kubernetesClient)
called synchronously in executor.go BEFORE the adopt/cleanup goroutines.
The function does a single Namespaces.List with label
fission.io/managed=true and calls DefaultNSResolver().AddNamespace() for
each result. This is idempotent with the later watcher AddFunc calls.
Failure is non-fatal: a warning is logged and startup proceeds with
static NS only (safe degraded mode).
After this call DefaultNSResolver().Snapshot() includes managed NS, so:
- AdoptExistingResources patches old pods in managed NS with new instanceID
- CleanupOldExecutorObjects removes stale objects from managed NS
- GetReaperNamespace() returns the full tenant NS set
Files:
pkg/executor/multitenant/ns_watcher.go — PreRegisterManagedNamespaces()
pkg/executor/executor.go — call before adopt/cleanup
Updated FORENSIC_ARCHITECTURE_AUDIT.md to reflect the fix from commit 919e8439:
Status table:
- 'Stuck-failed namespace без auto-recovery': ❌ ОТКРЫТ → ✅ ЗАКРЫТ (919e8439)
- 'No Explicit State Machine': ❌ ОТКРЫТ → ⚠️ СМЯГЧЕНО (stuck-failed закрыт;
явная state machine остаётся в backlog)
Sections updated:
- §1.4: полное описание что было (void-функции, мёртвый reconciler) и что
сделано (error propagation chain, end-to-end flow retry)
- Fragile Components: Stuck Failed Phase — вычеркнуто как закрытое
- Risky Decisions table: No Explicit State Machine → частично закрыто
- Lifecycle Management: добавлено что auto-recovery работает через RunReconciler
- Operational Burden: убрано упоминание stuck-failed как активной проблемы
- Maintainability/Production-Grade: обновлены под текущее состояние
- §2 Stuck Failed Accumulation: ✅ ЗАКРЫТ
- §3 P1 Reconcile-очередь: ✅ ЗАКРЫТ
- §5 Sharded mutex вердикт: убрано упоминание stuck-failed
Problem
-------
The namespace reconciler (RunReconciler, added previously) retries namespaces
in NamespacePhaseFailed every 30s by calling DispatchResync. But the phase
could never actually reach NamespacePhaseFailed for the executor component
because the executor's NamespaceSubscriber always returned nil — swallowing
any SA-provisioning or informer-init errors. The reconciler was dead code for
the executor path.
Root cause chain
----------------
1. setupSAAndRoleBindings() — void, errors only logged internally.
2. EnsureNamespaceSA() — void, just called setupSAAndRoleBindings.
3. registerNamespace() — void, errors from both functions lost.
4. Executor AddFunc/ResyncFunc — always returned nil to dispatch().
5. dispatch() marks parts Active unconditionally → NamespacePhaseFailed
is never triggered for executor → RunReconciler never fires for executor.
Consequence: if EnsureNamespaceSA failed (transient k8s 503, RBAC webhook
timeout, etc.) the namespace appeared Active in the manager but the fetcher
ServiceAccount was missing. Pool pods would CrashLoopBackOff on every call
to that namespace until a full process restart.
Changes
-------
pkg/utils/serviceaccount.go
- setupSAAndRoleBindings: void → error. Returns the first k8s API error
so callers can decide whether to retry.
- runSACheck: ignores the error with _ = (same behaviour as before, it's
a periodic background loop that already logs internally).
- EnsureNamespaceSA: void → error, propagates setupSAAndRoleBindings.
Updated godoc to explain the retry contract.
pkg/executor/multitenant/ns_watcher.go
- registerNamespace: void → error.
* EnsureNamespaceSA error → wrapped as 'EnsureNamespaceSA: ...' and returned.
* registerExecutorTypes error → wrapped as 'registerExecutorTypes: ...' and returned.
* Success log line only emitted when both succeed.
- Added 'fmt' import for error wrapping.
pkg/executor/multitenant/namespace_subscriber.go
- AddFunc: return registerNamespace(...) instead of ignoring its error.
- ResyncFunc: same — plus a comment explaining why it is safe to call
registerNamespace again (SA creation is idempotent, executor-type
AddNamespace guards against duplicate informer creation).
pkg/utils/namespace_manager.go
- RunReconciler interface signature: added *zap.Logger parameter.
Callers pass the component logger so retries are visible in prod logs.
- RunReconciler implementation:
* Accepts logger; falls back to zap.NewNop() if nil.
* Skips the tick entirely when no failed namespaces are found (no log spam).
* Logs 'retrying failed namespaces' with count + list when found.
* Logs per-namespace 'dispatching resync'.
* Logs 'resync succeeded' or 'resync still failing, will retry' with error.
- RunManagedNamespaceWatcher: passes logger to RunReconciler.
End-to-end flow after this fix
-------------------------------
1. EnsureNamespaceSA fails (k8s 503).
2. registerNamespace returns error.
3. Executor AddFunc returns error.
4. dispatch() calls MarkPartFailed("executor") → deriveNamespacePhase →
NamespacePhaseFailed.
5. RunReconciler tick (30s) finds the namespace → DispatchResync →
registerNamespace called again → EnsureNamespaceSA (idempotent) →
if API recovered: success → MarkPartActive → NamespacePhaseActive.
6. Log line 'namespace reconciler: resync succeeded' confirms recovery.
Backward compatibility
----------------------
- NamespaceManager interface: RunReconciler gained a *zap.Logger param.
There is exactly one implementation (inMemoryNamespaceManager) and one
call site (RunManagedNamespaceWatcher). No external mocks.
- EnsureNamespaceSA: callers outside this codebase (if any) that ignore
the error will still compile (Go allows ignoring return values).
- All 26 affected tests pass: go test ./pkg/utils/... ./pkg/executor/...
./pkg/buildermgr/... ./pkg/router/...
- Add RemoveNamespace(ctx, ns) to executortype.ExecutorType interface
- Implement RemoveNamespace in poolmgr, newdeploy, container executor types
- Add per-namespace context cancellation (nsCancels map) in all three types so
informer factories are stopped when namespace is removed (fixes goroutine leak)
- Add PoolPodController.RemoveNamespace to clear envLister/podLister maps
- Add deregisterNamespace() in executor multitenant subscriber
- Switch executor/router/buildermgr watcher strategy from TrackOnly to DispatchRemove
so RemoveFunc is called when fission.io/managed label is removed
- Add RemoveFunc to executor/router/buildermgr namespace subscribers
- Add RemoveNamespace to environmentWatcher and packageWatcher with per-NS cancel
- Add RemoveNamespace to HTTPTriggerSet: cancels informers, removes from maps, calls syncTriggers
- Fix ns_watcher_test.go fakeExecutorType to implement new RemoveNamespace method
Fixes:
- Executor dedup gap: re-added namespace was silently skipped (envLister/deplLister still present)
- Goroutine/FD leak: old informer factories ran forever after namespace removal
- Router stale routes: HTTPTriggers for removed namespace stayed in routing table
- DefaultNSResolver.RemoveNamespace(): removes NS from global map on label removal
so Snapshot() and idleObjectReaper stop iterating deleted namespaces.
Fixes class of dirty-state bugs when NS name is reused by new tenant.
- HandleWatcherNamespaceRemoval: call RemoveNamespace on both TrackOnly and
DispatchRemove strategies — global resolver cleanup is always required.
- dispatch(): parallel subscriber execution via goroutine per subscriber +
sync.WaitGroup. Reduces onboarding latency from O(N_subscribers × API_latency)
to O(max(API_latency)). Safe: MarkPart* are internally mutex-protected.
- inMemoryNamespaceManager.RunReconciler(): 30s ticker scans for
NamespacePhaseFailed records and retries via DispatchResync. Started
automatically by RunManagedNamespaceWatcher. Fixes permanent stuck-failed
state caused by transient k8s API errors.
Analysis source: FORENSIC_ARCHITECTURE_AUDIT.md §Deep Risk Analysis
TestStartManagedNamespaceWatcherIntegration проверяет полный маршрут
горячей регистрации namespace без real cluster:
1. RunManagedNamespaceWatcher запускается с k8sfake.NewSimpleClientset()
2. В fake client создаётся Namespace с label fission.io/managed=true
3. Kubernetes informer детектирует событие (без polling, через Watch)
4. SubscriberFuncs.OnNamespaceAdd вызывается
5. NamespaceManager содержит запись со статусом Active
Тест доказывает, что вся цепочка
fake k8s event → informer → AddFunc → subscriber → manager
работает корректно без rolling restart процесса.
Также добавлен import metav1 в test file (требовался для CreateOptions).
Единый сводный документ, описывающий полную архитектуру мультитенантного Fission.
Заменяет необходимость читать 50+ пошаговых thinking-файлов.
Содержит:
- Причина и концепция решения
- Архитектурная карта изменений (ASCII diagram)
- Таблица ключевых файлов с ролями
- Инженерные решения: Snapshot API, NamespaceManager event bus,
EnsureNamespaceSA, buildermgr dedup bug, router nil guard
- RBAC: что и почему (включая нетривиальные events:create и LSAR)
- Backward compatibility guarantees
- Описание test scenario (Layer 1, PASS=5)
- Порядок деплоя нового форка
- Направления дальнейшей работы
Файл deploy/executor-ns-watcher-rbac.yaml был создан на раннем этапе работы
над мультитенантностью. Он содержал только partial RBAC (только executor,
без router и без SA-provisioner прав).
Файл полностью покрыт deploy/multitenant/rbac.yaml который содержит:
- fission-executor-ns-watcher: list/watch namespaces
- fission-router-ns-watcher: list/watch namespaces
- fission-executor-sa-provisioner: create SA/Role/RoleBinding в user NS
Старый файл нигде не referenced — ни в charts, ни в коде.
Краткий справочник команд и концепций мультитенантного Fission.
Содержит: жизненный цикл namespace, CLI команды, схему RBAC,
структуру URL функций, типичные сценарии использования.
Полное руководство по REST API мультитенантного Fission Console.
Описывает все эндпоинты: создание namespace (tenant), деплой функций,
управление environment, триггеры, пакеты.
Актуально для нашего форка с мультитенантностью.
Добавлены файлы правил для GitHub Copilot:
- .github/copilot-instructions.md — краткие правила поведения ИИ в проекте:
отвечать кратко, не трогать рабочий код без явного указания, rsync на ВМ
после каждого изменения, git только локально.
- .github/pravila.md — расширенные правила проекта: порядок работы с SSH,
запреты на групповое удаление, правила docker build и деплоя.
Fixes EnsureNamespaceSA getting 403 Forbidden when provisioning fission-fetcher
SA in dynamically registered namespaces. Adds ClusterRole + ClusterRoleBinding
with create/update/patch for serviceaccounts, roles, rolebindings.
Also adds doc/progress.md and doc/thinking/2026-04-26-rbac-fix.md.
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `d44809c` to `a301031`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `d44809c` to `a301031`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `d44809c` to `a301031`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `d44809c` to `a301031`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `d44809c` to `a301031`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `939a132` to `d4c20db`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `939a132` to `d4c20db`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `939a132` to `d4c20db`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `939a132` to `d4c20db`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `939a132` to `d4c20db`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `b2e1c3d` to `b00a88c`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `b2e1c3d` to `b00a88c`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `b2e1c3d` to `b00a88c`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `b2e1c3d` to `b00a88c`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `b2e1c3d` to `b00a88c`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chart>fission-all/values.yaml: Fix typo in imageppullsecrets
Fix a typo in comments.
* Add support to customise storagesvc deployment strategy
Add suport to `values.yaml` to allow customisation of the storagesvc deployment
strategy. The default is a rolling update with `maxSurge` and `maxUnavailable`
of 25%. Users with ReadWriteOnce persistent storage can use the `Recreate`
strategy.
Issue 3195
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `9276a4e` to `2e3db16`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `9276a4e` to `2e3db16`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `9276a4e` to `2e3db16`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `9276a4e` to `2e3db16`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `9276a4e` to `2e3db16`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-version: latest
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `7a6456c` to `9276a4e`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7a6456c` to `9276a4e`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7a6456c` to `9276a4e`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7a6456c` to `9276a4e`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7a6456c` to `9276a4e`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `5497b01` to `853bfd4`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5497b01` to `853bfd4`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5497b01` to `853bfd4`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5497b01` to `853bfd4`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5497b01` to `853bfd4`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Update Go version to 1.24
* Update golangci-lint version
* Add envtest to tool
* Add dashboard linter as a tool
* Uset t.Cleanup
---------
Signed-off-by: Sanket Sudake <sanketsudake@gmail.com>
Fixes#3127
Update job names in helm chart templates to avoid conflicts.
* Change the `metadata.name` field in `charts/fission-all/templates/analytics/post-install-job.yaml` to `{{ template "fullname" . }}-{{ .Chart.Version }}-post-install`.
* Change the `metadata.name` field in `charts/fission-all/templates/analytics/post-upgrade-job.yaml` to `{{ template "fullname" . }}-{{ .Chart.Version }}-post-upgrade`.
---
For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/fission/fission/issues/3127?shareId=XXXX-XXXX-XXXX-XXXX).
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `7e1e8a0` to `5497b01`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7e1e8a0` to `5497b01`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7e1e8a0` to `5497b01`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7e1e8a0` to `5497b01`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `7e1e8a0` to `5497b01`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `f96b5a6` to `7e1e8a0`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f96b5a6` to `7e1e8a0`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f96b5a6` to `7e1e8a0`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f96b5a6` to `7e1e8a0`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f96b5a6` to `7e1e8a0`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `f5fe67a` to `f96b5a6`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f5fe67a` to `f96b5a6`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f5fe67a` to `f96b5a6`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f5fe67a` to `f96b5a6`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `f5fe67a` to `f96b5a6`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the docker-images group with 1 update in the /cmd/builder directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fetcher directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/fission-bundle directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/preupgradechecks directory: [chainguard/static](https://github.com/chainguard-images/images).
Bumps the docker-images group with 1 update in the /cmd/reporter directory: [chainguard/static](https://github.com/chainguard-images/images).
Updates `chainguard/static` from `5ff428f` to `f5fe67a`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5ff428f` to `f5fe67a`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5ff428f` to `f5fe67a`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5ff428f` to `f5fe67a`
- [Commits](https://github.com/chainguard-images/images/commits)
Updates `chainguard/static` from `5ff428f` to `f5fe67a`
- [Commits](https://github.com/chainguard-images/images/commits)
---
updated-dependencies:
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
- dependency-name: chainguard/static
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Use builder and container names when creating environment K8s object instead of keeping it empty.
Add skeleton for podSpec field to give user an idea of how to use podSpec field.
* Add a positive test for env podSpec
* Add the test to CI tests
* Remove duplicate wait_for_builder function
* Fix CI tests failure
* Fix CI tests failure
* Add a negative test for env podSpec
* Fix issues with negative test
* Removing negative test as it may break executor which will affect other tests
* Rebase with main as executor issue is fixed.
Add the negative test.
* Fix negative test
---------
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
The bug breaks the poolmgr service which stops the deletion and creation of new environments.
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
* Deprecation warning for cross namespace parameters `builderNamespace`, `functionNamespace`
and `disableOwnerReference` flag.
* Do not mention the version
---------
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
* Add DISABLE_OWNER_REFERENCES env variable to executor and buildermgr deployment.
Use this env var to decide adding ownerReferences to K8s resources created by fission CRD.
* Resolve review comments
* Fix lint failure
---------
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
* Fixed: Print pod log error when response status is 404 returned by test function
* Update util.go
---------
Signed-off-by: waitstory <waitstory@163.com>
* Update timetrigger crd and add method and subpath fields in spec.
Update fission-cli to accept user input for method and subpath fields.
Update publisher package to utilize these fields for triggering a function.
Update timer controller to use method and subpath fields for publishing a request.
Add a new test TestPublisherSubpath in pulisher package.
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
* Use kubebuilder default annotation.
Update test for fission-cli timetrigger create, update command to support method and subpath flags.
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
---------
Signed-off-by: Md Soharab Ansari <soharab.ansari@infracloud.io>
**Если в сообщении есть вопрос в ЛЮБОЙ форме** ("так ?", "верно ?", "почему ?", "как ?", "так же ?" и т.д.):
1. ТОЛЬКО ответить на вопрос
2. ОСТАНОВИТЬСЯ
3. ЖДАТЬ следующей команды
**ЗАПРЕЩЕНО** начинать работу, писать код, запускать команды — без явного "делай".
1.**⛔⛔⛔ АБСОЛЮТНЫЙ ЗАПРЕТ: не трогать и не читать рабочий код с целью подготовки к правке — без ПРЯМОГО указания "делай". Даже чтение файлов перед правкой — СТОП, сначала разрешение.**
2. Файлы редактируются локально:
~/fission-src (текущая рабочая папка)
После ЛЮБЫХ изменений ОБЯЗАТЕЛЬНО синхронизировать на ВМ командой:
**Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.**
## Поведение агента
- **⛔⛔⛔ АБСОЛЮТНЫЙ ЗАПРЕТ: не читать и не трогать код с целью подготовки к правке — без прямого "делай". Даже чтение файлов перед правкой — СТОП, сначала разрешение.**
- Не трогать рабочий код без явного указания
- Не делать НИЧЕГО сверх того, о чём явно приказали — ни git-команд, ни rebase, ни дополнительных шагов
- Если для продолжения нужен выбор — СПРОСИТЬ разрешения, не делать самостоятельно
- Деструктивные операции (`kubectl delete`, `rm -rf`, `terraform destroy` и др.) — только после явного подтверждения с указанием конкретных объектов
- Отвечать кратко, без вступлений, извинений, благодарностей и прочей воды
org.opencontainers.image.description:"Fetcher is a lightweight component used by environment and builder pods. Fetcher helps in fetch and upload of source/deployment packages and specializing environments."
- "--label=org.opencontainers.image.description=Fetcher is a lightweight component used by environment and builder pods. Fetcher helps in fetch and upload of source/deployment packages and specializing environments."
org.opencontainers.image.description:"fission-bundle is a component which is a single binary for all components. Most server side components running on server side are fission-bundle binary wrapped in container and used with different arguments."
- "--label=org.opencontainers.image.description=fission-bundle is a component which is a single binary for all components. Most server side components running on server side are fission-bundle binary wrapped in container and used with different arguments."
org.opencontainers.image.description:"Preupgradechecks ensures that Fission is ready for the targeted version upgrade by performing checks beforehand."
- "--label=org.opencontainers.image.description=Preupgradechecks ensures that Fission is ready for the targeted version upgrade by performing checks beforehand."
- "--label=org.opencontainers.image.description=Fetcher is a lightweight component used by environment and builder pods. Fetcher helps in fetch and upload of source/deployment packages and specializing environments."
- "--label=org.opencontainers.image.description=fission-bundle is a component which is a single binary for all components. Most server side components running on server side are fission-bundle binary wrapped in container and used with different arguments."
- "--label=org.opencontainers.image.description=Preupgradechecks ensures that Fission is ready for the targeted version upgrade by performing checks beforehand."
1.**`RemoveNamespace(ns string) bool`** — добавлен в `NamespaceResolver` (`pkg/utils/namespace.go`).
`HandleWatcherNamespaceRemoval` теперь вызывает его при любой стратегии, очищая глобальный resolver. Это исправляет дедупликацию router/buildermgr при re-add NS.
2.**Параллельный `dispatch()`** — `pkg/utils/namespace_manager.go`: заменён последовательный обход подписчиков на параллельный с `sync.WaitGroup`. Исправляет 30-минутное окно, когда HTTPTrigger не видел namespace из-за того что обход был последовательным.
3.**`RunReconciler()`** — добавлен в `NamespaceManager` interface и реализован в `inMemoryNamespaceManager`. Каждые 30 секунд сканирует namespace-ы в фазе `NamespacePhaseFailed` и вызывает `DispatchResync`. Исправляет постоянно stuck-failed namespace при транзиентных k8s API ошибках.
### Что оставалось нерешённым (из аудита)
Три проблемы, зафиксированные в `FORENSIC_ARCHITECTURE_AUDIT.md`:
**Проблема 1 — Executor dedup gap (Critical)**
При удалении NS и повторном добавлении executor молча пропускал его.
Причина: `gpm.poolPodC.envLister[ns]` и `deploy.deplLister[ns]` проверялись как дедупликация в `AddNamespace`, но никогда не очищались при удалении NS.
Результат: повторно добавленный namespace не получал informers в executor → функции не запускались.
**Проблема 2 — Goroutine/FD leak (High)**
При удалении NS старые informer factories продолжали работать (goroutines, file descriptors, LIST-запросы к k8s API каждые 30 минут).
Причина: informers запускались с `ctx.Done()` родительского контекста всего процесса, без механизма per-NS остановки.
**Проблема 3 — Router stale routes (Medium)**
После удаления NS router продолжал держать HTTPTrigger routes для этого namespace.
Причина: `triggerInformer[ns]` и `funcInformer[ns]` не чистились, `syncTriggers()` не вызывался.
`NewDefaultManagedNamespaceWatcherConfig` создаёт конфиг с`RemovalStrategy: NamespaceRemovalStrategyTrackOnly`.
При `TrackOnly` — `HandleWatcherNamespaceRemoval` вызывает `DefaultNSResolver().RemoveNamespace()` (наш предыдущий фикс), но **не** вызывает `manager.DispatchRemove()` → `subscriber.OnNamespaceRemove()` → `RemoveFunc` не срабатывает.
Для вызова `RemoveFunc` нужна стратегия `DispatchRemove`.
Существующий код в executor types не защищает lister maps мьютексами. Записи в них происходят только при `AddNamespace` (из subscriber goroutine). Добавление `RemoveNamespace` добавляет ещё одну запись из той же goroutine. Race condition с event handlers (которые читают эти maps) — известное ограничение существующего дизайна, не добавляем мьютексы чтобы не выходить за рамки задачи.
`HTTPTriggerSet` уже имеет `informerMu sync.RWMutex` — его и используем в `RemoveNamespace` при удалении из `triggerInformer`/`funcInformer`.
// RemoveNamespace deregisters a namespace from the executor, cancelling its
// informer goroutines and clearing dedup state so that a re-add works correctly.
// Called when a Namespace with label fission.io/managed=true is removed.
RemoveNamespace(ctxcontext.Context,nsstring)error
```
**Почему:** Все три executor type реализуют этот интерфейс. Добавление в интерфейс гарантирует, что новый тип executor не забудет реализовать метод (компилятор поймает).
**Почему отдельный метод:**`PoolPodController` — отдельная структура внутри poolmgr. Доступ к её полям из `GenericPoolManager.RemoveNamespace` требовал бы либо экспорта полей, либо метода. Метод — чище.
**Почему:** Без `DispatchRemove``RemoveFunc` подписчика никогда не вызывается. `TrackOnly` вызывает только `DefaultNSResolver().RemoveNamespace()` (что сделано в `HandleWatcherNamespaceRemoval`), но не диспетчирует событие подписчикам.
---
### Шаг 8: `pkg/buildermgr/envwatcher.go`
- Добавлен `nsCancels map[string]context.CancelFunc` в struct `environmentWatcher`
- Инициализирован в `makeEnvironmentWatcher` (там же где `envWatchInformer`)
if cancel, ok := envw.nsCancels[ns]; ok { cancel(); delete(...) }
delete(envw.envWatchInformer, ns)
}
```
**Ошибка при первой попытке:** replace_string_in_file добавил `nsCancels` с тройным отступом (три таба вместо двух) и без закрывающего `}` struct literal — синтаксическая ошибка компиляции. Исправлено вторым вызовом replace.
---
### Шаг 9: `pkg/buildermgr/pkgwatcher.go`
Аналогично envwatcher:
- `nsCancels` в struct `packageWatcher`
- Инициализация в `makePackageWatcher`
- `AddNamespace` → per-NS ctx для `fissionFactory.Start()` и `podFactory.Start()`
if r, ok := envw.(builderEnvNamespaceRemover); ok { r.RemoveNamespace(namespace) }
if r, ok := pkgw.(builderPkgNamespaceRemover); ok { r.RemoveNamespace(namespace) }
}
```
**Почему type assertion:** `builderEnvNamespaceAdder` — интерфейс-параметр функции `NewNamespaceSubscriber`. Вместо добавления `RemoveNamespace` в существующий интерфейс (что сломало бы тестовые фейки) используем опциональный интерфейс через type assertion.
---
### Шаг 11: `pkg/buildermgr/ns_watcher.go`
Стратегия изменена на `DispatchRemove` аналогично executor.
---
### Шаг 12: `pkg/router/httpTriggers.go`
**12a. Добавлен `nsCancels` в struct:**
```go
// nsCancels holds per-namespace context cancel functions for informer lifecycle.
nsCancels map[string]context.CancelFunc
```
**12b. Инициализация в `makeHTTPTriggerSet`:**
```go
nsCancels: make(map[string]context.CancelFunc),
```
**12c. Изменён `AddNamespace`:** per-NS ctx:
```go
nsCtx, nsCancel := context.WithCancel(ctx)
ts.nsCancels[ns] = nsCancel
factory.Start(nsCtx.Done())
k8sCache.WaitForCacheSync(nsCtx.Done(), ...) // тоже nsCtx
if cancel, ok := ts.nsCancels[ns]; ok { cancel(); delete(...) }
ts.informerMu.Lock()
delete(ts.triggerInformer, ns)
delete(ts.funcInformer, ns)
ts.informerMu.Unlock()
ts.syncTriggers() // немедленно перестраивает routing table без удалённого NS
}
```
**Почему `informerMu.Lock()`:** `HTTPTriggerSet` уже имеет `informerMu sync.RWMutex` для защиты `triggerInformer`/`funcInformer`. Используем его — не добавляем новые мьютексы.
---
### Шаг 13: `pkg/router/namespace_subscriber.go`
Добавлен `routerNamespaceRemover` interface и `RemoveFunc`:
```go
type routerNamespaceRemover interface {
RemoveNamespace(ns string)
}
RemoveFunc: func(ctx context.Context, record utils.NamespaceRecord) error {
syncTriggers() ← немедленно убирает routes для удалённого NS
```
---
## 6. Что НЕ было реализовано и почему
**`FunctionServiceCache.DeleteByNamespace(ns string)`** — не реализовано.
Причина: `idleObjectReaper` периодически вызывает `IsValid()` для всех записей. Для удалённого NS k8s API возвращает 404/403 → `IsValid()` вернёт `false` → запись будет удалена reaperом естественным образом. Это создаёт несколько минут "грязных" записей и 404 ошибки в логах, но не влияет на корректность: для удалённого NS новые запросы не придут (router очистил routes), а reaper уберёт старые записи.
Реализация `DeleteByNamespace` потребовала бы добавления namespace-индекса в `byFunction`/`byAddress`/`byFunctionUID` кэшах (нетривиально), или дорогого линейного прохода по всем записям. Не было делать без явного запроса.
---
## 7. Затронутые файлы (17 изменённых)
| Файл | Тип изменения |
|------|---------------|
| `pkg/executor/executortype/executortype.go` | +метод в interface |
2. `RemoveNamespace("")` защищён early return во всех реализациях
3. `deregisterBuilderNamespace` через type assertion — безопасно если интерфейс не реализован (просто пропускает)
4. `routerNamespaceRemover` через type assertion в router subscriber — аналогично
5. Goroutines informer factories останавливаются асинхронно после `cancel()` — это нормально, k8s client-go гарантирует graceful shutdown при отмене контекста
6. После `RemoveNamespace` и до следующего `AddNamespace` — любые события от k8s для этого NS будут проигнорированы (informers остановлены, listers удалены)
| AdoptExistingResources race при rolling update | ✅ ЗАКРЫТ | `2a7d6101` |
| No Explicit State Machine (implicit phase transitions) | ⚠️ СМЯГЧЕНО | `919e8439` |
| Sharded mutex (bottleneck при >500 concurrent tenant) | ⏳ BACKLOG | не актуально при текущей нагрузке |
---
## Architectural Decisions (реально принятые)
- **Dynamic Namespace Discovery**: Механизм динамического обнаружения и подключения tenant-namespace через label `fission.io/managed=true` (`pkg/utils/namespace_manager.go`, `pkg/executor/multitenant/ns_watcher.go`).
- **Namespace Lifecycle Management**: Жизненный цикл namespace централизован через интерфейс `NamespaceManager` с подписчиками (executor, router, buildermgr).
- **Decoupled Registration**: Каждый компонент подписывается как `NamespaceSubscriber` и реализует свою логику инициализации/чистки ресурсов.
- **Backward Compatibility**: Поддержка статического списка через env (`FISSION_RESOURCE_NAMESPACES`) с динамическим расширением.
- **No-Restart Onboarding**: Добавление tenant не требует рестарта pod-ов.
- **RBAC/SA Provisioning**: Автоматическое создание SA и RBAC для новых namespace (`EnsureNamespaceSA`).
- **Informer Factories Per Namespace**: Отдельная informer factory для каждого NS, с per-NS context cancellation.
- **Explicit Namespace Removal Strategy**: `DispatchRemove` — при удалении NS вызываются `RemoveFunc` у всех подписчиков, останавливаются informer-ы через `context.CancelFunc`.
- **Parallel Subscriber Dispatch**: Подписчики вызываются параллельно через `errgroup` — onboarding не блокируется медленным SA provisioning.
---
## Core Complexity Centers
- **NamespaceManager & Watcher**: Центр всей динамики — координация событий, фаз, подписчиков.
- **ExecutorType Subsystems**: Poolmgr, NewDeploy, Container — каждый хранит собственный per-NS кэш, lister-ы, логику adoption и reaping.
- **Informer Lifecycle**: Динамическое создание/остановка informer-ов через per-NS `context.CancelFunc`. Чистка `envLister[ns]`/`deplLister[ns]`/`triggerInformer[ns]` при `RemoveNamespace`.
- **FunctionServiceCache**: Кэширование и lifecycle function pod-ов, синхронизация с событиями из разных источников. **Не очищается при RemoveNamespace** — `idleObjectReaper` убирает устаревшие записи через `IsValid()` check.
---
## Hidden Coupling & Accidental Complexity
- **Implicit Contract**: Все компоненты обязаны реализовывать `NamespaceSubscriber` симметрично (и `AddFunc`, и `RemoveFunc`). Нарушение → silent drift.
- **Global vs Local State**: Глобальный `DefaultNSResolver` + локальные lister-ы в каждом executor type. `RemoveNamespace` в NSResolver и в каждом executor type должны быть вызваны согласованно.
- **Deduplication Responsibility**: `AddNamespace` дедупликация — через `DefaultNSResolver().AddNamespace()` возвращающий `bool`, и через проверку локального lister-а (`envLister[ns] != nil`). После `RemoveNamespace` оба guard сбрасываются → re-add корректно создаёт новые informer-ы.
- **Event Handler Ordering**: Порядок подписчиков в `Subscribe` влияет на side-effects, но `errgroup` делает их параллельными — ordering больше не определяет latency, но всё ещё влияет на приоритет ошибок.
- **RBAC Drift**: Provisioning SA/RBAC в `registerNamespace`, cleanup — в `deregisterNamespace`. При сбое cleanup — dangling SA/ClusterRoleBinding.
- **Manual Adoption**: При старте executor-ы делают adopt orphaned ресурсов. Закрыто: `PreRegisterManagedNamespaces` обеспечивает полный NS snapshot до adopt/cleanup (§1.3).
- **Explicit Reaper Loops**: `idleObjectReaper` чистит `FunctionServiceCache` вместо event-driven подхода. Приемлемо: `IsValid()` check достаточен при корректной работе per-NS informer-ов.
---
## Fragile Operational Components
- **RBAC/SA Drift**: Неконсистентность между созданием и удалением SA/ролей при сбое в `deregisterNamespace`.
- **Cache Invalidation**: `FunctionServiceCache` не очищается при `RemoveNamespace` — расчёт на `idleObjectReaper`. При высоком churn rate может накапливать stale записи быстрее, чем reaper убирает.
- **Adoption Race**: ~~`AdoptExistingResources` vs `namespace_subscriber` — активная проблема~~ — закрыто: `PreRegisterManagedNamespaces` перед adopt/cleanup (`2a7d6101`).
- **Stuck Failed Phase**: ~~Namespace в `failed` не восстанавливается без рестарта~~ — закрыто: `RunReconciler` + полная цепочка error propagation (§1.4).
---
## Poor Scalability Risks
- **Informer Explosion**: ~1500–2000 goroutine при 100 tenant (см. §2). **Частично смягчено**: goroutine-ы корректно останавливаются при `RemoveNamespace` — нет накопления при churn. Но в steady-state 100 NS — линейный рост горутин остаётся.
- ~~**Synchronous Dispatch**~~ → **ИСПРАВЛЕНО**: параллельный dispatch через `errgroup`.
- **Centralized Locking**: Глобальный mutex на NamespaceManager. При текущей нагрузке (<50 ns) — не узкое место. При >500 concurrent tenant — backlog (sharded mutex, §5).
- **Thundering Herd на resync**: 100 NS × 5 informer-типов × LIST каждые 30 мин — 500 concurrent LIST к API.
---
## Future Maintenance Problems
- **Hidden State Machines**: Фазы namespace реализованы неявно — сложно дебажить stuck state. Нет формализованной машины состояний с explicit transitions.
- **Implicit Error Handling**: Ошибки в `deregisterNamespace` логируются, но NS может остаться в некорректном состоянии. Нет `NamespaceCondition` на k8s-объекте.
- **Contract Drift**: Изменение интерфейса `NamespaceSubscriber` (например, добавление `ResyncFunc`) требует синхронного обновления всех компонентов.
- **FunctionServiceCache без per-NS cleanup**: если `idleObjectReaper` будет отключён/изменён — stale cache может накапливаться.
---
## Risky / Hard-to-Maintain Decisions
| Решение | Статус | Примечание |
|---------|--------|------------|
| Informer Lifecycle Management | ✅ Hardened | per-NS context cancel + RemoveNamespace во всех компонентах |
| Centralized Mutex | ⚠️ Приемлемо | sharding в backlog, не актуально до >500 NS |
| Manual Adoption | ✅ Закрыто | race при rolling update (`2a7d6101`) |
| No Explicit State Machine | ✅ Частично закрыто | stuck-failed закрыт (`919e8439`); явная state machine в backlog |
| Eventual Consistency | ⚠️ Смягчено | параллельный dispatch уменьшает окно, но не устраняет |
- **Lifecycle Management**: Фазы (discovered → registering → active → deregistering → removed / failed) реализованы. Auto-recovery из failed работает через `RunReconciler`. Явная state machine в backlog.
- **State Handling**: Глобальный `DefaultNSResolver` + локальные lister-ы. После `RemoveNamespace` — оба синхронизованы. После re-add — оба корректно инициализируются заново.
- **Reconciliation Logic**: Каждый компонент через subscribe. Отсутствует reconcile-очередь для failed state.
- **Operational Burden**: Средний — goroutine leak устранён, stale informer устранён, stuck-failed закрыт. Требуется мониторинг: orphaned SA/RBAC при неудачном deregister.
---
## Engineering Maturity
- **Maturity**: Архитектурно зрелый, хорошо документированный, с явным reasoning и поэтапным внедрением.
- **Complexity**: Высокая в синхронизации и lifecycle. Снижена за счёт формализации `RemoveNamespace` контракта.
- **Maintainability**: Среднесрочная — без явной state machine сложность будет расти. Auto-recovery из failed работает.
**Что было:** relabel-цикл NS создавал phantom-состояние: informer-ы не останавливались при track-only removal, `DefaultNSResolver` не очищал запись → re-add возвращал `false` → новые informer-ы не создавались.
**Что сделано (коммит `4eedf95f`):**
-`RemoveNamespace(ns)` добавлен в интерфейс `ExecutorType` и реализован в poolmgr, newdeploy, container.
-В каждом executor type: per-NS context cancel (`nsCancels map[string]context.CancelFunc`). `AddNamespace` создаёт `nsCtx, nsCancel := context.WithCancel(ctx)`, передаёт `nsCtx` в `factory.Start()`. `RemoveNamespace` вызывает `nsCancel()` и удаляет lister-ы из карт.
- Router: `HTTPTriggerSet.RemoveNamespace()` отменяет per-NS ctx, удаляет `triggerInformer[ns]`/`funcInformer[ns]` под `informerMu.Lock()`, вызывает `syncTriggers()`.
- Buildermgr: `envWatcher.RemoveNamespace()` и `pkgWatcher.RemoveNamespace()` — аналогично.
-`DefaultNSResolver.RemoveNamespace(ns)` удаляет NS из глобального map → re-add корректно проходит guard.
- Стратегия `DispatchRemove` во всех 3 компонентах → `RemoveFunc` вызывается при удалении NS.
**Текущий статус:** informer goroutine/FD корректно останавливаются; re-add NS создаёт чистые informer-ы; router не видит stale routes.
---
### 1.2 Centralized Mutex — ⚠️ ПРИЕМЛЕМО
**Сценарий:** высокая churn + concurrent Snapshot.
`dispatch()` отпускает mutex перед вызовом каждого subscriber, берёт снова для следующего. При батч-онбординге 10+ NS параллельно: конкуренция за mutex, latency spike на `Snapshot()` в `idleObjectReaper`.
**Смягчено:**`dispatch()` теперь параллельный (errgroup) — подписчики не вызываются последовательно, время блокировки mutex между подписчиками устранено. `Snapshot()` конкурирует только с `Upsert` — при текущей нагрузке (<50 NS) практически нет.
**Остаётся:** при >500 concurrent tenant с >1 onboarding/sec — sharded mutex даст выигрыш. В backlog.
**Сценарий: гонка adoption vs watcher при старте**
**Что было:**`AdoptExistingResources` и `CleanupOldExecutorObjects` запускались до `StartNSWatcher`. `DefaultNSResolver().Snapshot()` возвращал только статические NS из `FISSION_RESOURCE_NAMESPACES` → managed NS не покрывались:
- Pods от предыдущего executor в managed NS не adoptировались (сохраняли старый `instanceID`) → poolmgr создавал новые pool pods → cold start.
- Старые RS/deployments в managed NS не чистились → накапливались.
**Что сделано:**`multitenant.PreRegisterManagedNamespaces(ctx, logger, kubernetesClient)` — синхронный `Namespaces.List`с label `fission.io/managed=true` вызывается в `executor.go`**до** goroutines adopt+cleanup. Добавляет все managed NS в `DefaultNSResolver`. Идемпотентен с последующим `AddFunc` из watcher. Не ломает при ошибке API (warn + proceed).
**End-to-end после фикса:**
1.`PreRegisterManagedNamespaces` → `DefaultNSResolver` содержит static + managed NS
2.`AdoptExistingResources` → патчит pods в managed NS с новым `instanceID`
3.`CleanupOldExecutorObjects` / `GetReaperNamespace()` → видит managed NS → чистит стale объекты
4.`StartNSWatcher` → `AddFunc` срабатывает для тех же NS — `DefaultNSResolver().AddNamespace()` idempotent, `AddNamespace` executor types dedup-protected
---
### 1.4 No Explicit State Machine — ✅ ЗАКРЫТ (коммит `919e8439`)
**Сценарий: stuck в `failed` без auto-recovery**
**Что было:**`EnsureNamespaceSA` и `registerNamespace` были void-функциями — ошибки только логировались, до `MarkPartFailed` не доходили. Executor subscriber всегда возвращал nil → namespace никогда не попадал в `NamespacePhaseFailed` → `RunReconciler` для executor был мёртвым кодом.
4.`RunReconciler` tick (30s) находит namespace → `DispatchResync` → retry
5. Если API восстановился: `MarkPartActive` → `NamespacePhaseActive` → лог `resync succeeded`
**Накопление при churn:** ликвидировано — failed NS автоматически выходят из этой фазы при восстановлении API.
**Ограничение:** нет max-retries. Namespace, у которого SA создать принципиально невозможно (например, удалённый k8s namespace), будет ретраиться вечно. Приемлемо на текущем масштабе.
---
### 1.5 Eventual Consistency — ⚠️ СМЯГЧЕНО
**Сценарий:** HTTPTrigger создан в окне до готовности informer.
**Было:** последовательный dispatch → если executor делал SA provisioning 10–30 сек, router не начинал `WaitForCacheSync`. Trigger, созданный в этом окне, пропускался до следующего resync (30 мин).
**Смягчено:** параллельный dispatch через errgroup → router и executor стартуют `AddNamespace` одновременно. Окно уязвимости = время `WaitForCacheSync` в router (~2–5 сек), а не время SA provisioning (~30 сек).
**Остаётся:** trigger, созданный за 2–5 сек до `WaitForCacheSync` в router → нормально обрабатывается через `AddFunc` после sync. Фактически проблема устранена для практических сценариев.
Итого: **~1600–2400 goroutine** от informer-ов. **Линейный рост с числом NS — неизбежен при текущей архитектуре.**
**Что изменилось после hardening:** при churn goroutine-ы корректно останавливаются при `RemoveNamespace` — нет накопления мёртвых goroutine. Steady-state = ~O(active_NS), а не O(total_NS_ever_seen).
### Thundering Herd на resync
100 NS × 5 informer-типов × LIST каждые 30 мин = **500 concurrent LIST** к Kubernetes API. Не изменилось, не исправлено.
### Stuck Failed Accumulation — ✅ ЗАКРЫТ
Failed NS автоматически ретраятся `RunReconciler` каждые 30с и выходят из `failed` при восстановлении API. Накопления больше не происходит.
### AdoptExistingResources Race — ✅ ЗАКРЫТ
`PreRegisterManagedNamespaces` синхронно добавляет managed NS в `DefaultNSResolver` до adopt/cleanup. Старые pods adoptируются, stale объекты чистятся. Подробно — §1.3.
Error propagation исправлена во всей цепочке: `setupSAAndRoleBindings` → `EnsureNamespaceSA` → `registerNamespace` → executor subscriber. `RunReconciler` логирует retry и исход.
### P2 — AdoptExistingResources после BootstrapAndDispatch — ✅ ЗАКРЫТ (`2a7d6101`)
`PreRegisterManagedNamespaces` вызывается синхронно до adopt/cleanup. Делает один `Namespaces.List(label=fission.io/managed=true)` → добавляет все managed NS в `DefaultNSResolver`. После этого adopt и cleanup покрывают полный tenant NS set.
**Влияние:** устранены orphaned pods при холодном старте и resource leak (stale RS/deployments).
### P3 — NamespaceCondition на k8s Namespace объекте
Пометить Namespace через `kubectl annotate` или через status subresource при failed phase → оператор видит причину без чтения логов.
### Backlog — Sharded mutex
Актуально при >500 concurrent tenant с >1 onboarding/sec. Технически feasible без breaking interface change (см. `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`).
---
## 4. FunctionServiceCache — текущий инвариант
`FunctionServiceCache` (`fsCache` в gpm и newdeploy) **не очищается** при `RemoveNamespace`. Это осознанное решение:
-`idleObjectReaper` периодически вызывает `fsCache.ListOldForPool()` → для каждой записи проверяет `podLister[ns]` → если NS удалён, `podLister[ns]` == nil → pod не найден → запись считается expired → `fsCache.DeleteEntry()`.
- Временной лаг = интервал reaper-а (по умолчанию ~1 мин). При высоком churn возможно накопление stale записей, но они не вызывают функциональных ошибок (только небольшой overhead на reaper iteration).
**Когда станет проблемой:** при отключении/изменении reaper-а или при >10 000 stale записей (O(n) iteration).
---
## 5. Sharded Mutex — вердикт
**Технически реализуемо** без breaking interface change. Полный код — в `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`.
**Вердикт:** не оправдано при текущей нагрузке. Реальный bottleneck — AdoptExistingResources race — закрыт (`2a7d6101`). Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.
# Forensic Architecture Audit: Fission Fork (feature/multitenant, May 2026)
---
## Architectural Decisions (реально принятые)
- **Dynamic Namespace Discovery**: Введён механизм динамического обнаружения и подключения tenant-namespace через label `fission.io/managed=true` (см. `pkg/utils/namespace_manager.go`, `pkg/executor/multitenant/ns_watcher.go`).
- **Namespace Lifecycle Management**: Весь жизненный цикл namespace теперь централизован через интерфейс `NamespaceManager` с подписчиками (executor, router, buildermgr).
- **Decoupled Registration**: Каждый компонент (executor, router, buildermgr) подписывается как subscriber и реализует свою логику инициализации/чистки ресурсов при появлении/удалении namespace.
- **Backward Compatibility**: Сохраняется поддержка статического списка через env (`FISSION_RESOURCE_NAMESPACES`), но теперь он расширяется динамически.
- **No-Restart Onboarding**: Добавление нового tenant не требует рестарта pod-ов — watcher реагирует на label, триггерит регистрацию во всех подсистемах.
- **RBAC/SA Provisioning**: Автоматическое создание service account и RBAC для новых namespace (см. `EnsureNamespaceSA`).
- **Informer Factories Per Namespace**: Для каждого нового namespace создаются отдельные informer factory для CRD и core-ресурсов.
- **Explicit Namespace Removal Strategy**: Поддержка двух стратегий удаления: track-only (по умолчанию) и dispatch-remove (с вызовом OnNamespaceRemove у подписчиков).
## Core Complexity Centers
- **NamespaceManager & Watcher**: Центр всей динамики — сложная координация событий, фаз, подписчиков, race-conditions.
- **ExecutorType Subsystems**: Poolmgr, NewDeploy, Container — каждый хранит собственное состояние, кэш, логику adoption и reaping.
- **Informer Lifecycle**: Динамическое создание/удаление informer-ов на лету для каждого namespace.
- **FunctionServiceCache**: Кэширование и lifecycle function pod-ов, синхронизация с событиями из разных источников.
## Hidden Coupling & Accidental Complexity
- **Implicit Contract**: Все компоненты обязаны корректно реализовать NamespaceSubscriber — нарушение приводит к silent drift.
- **Global vs Local State**: Есть глобальный NamespaceResolver и локальные состояния в каждом executor type — возможны рассинхронизации.
- **Deduplication Responsibility**: Deduplication namespace размазан между глобальным резолвером и локальными структурами.
- **Event Handler Ordering**: Порядок подписчиков влияет на фазу и side-effects, но не гарантируется явно.
- **RBAC Drift**: Provisioning SA/RBAC делается в одном месте, но cleanup — в другом, возможны dangling ресурсы.
## Iterative Growth
- **Layered Refactor**: Ветка развивается через серию малых шагов (см. doc/thinking/2026-04-26-namespace-manager-step*.md), каждый шаг — отдельный инвариант.
- **Hybrid Model**: Некоторое время coexist старый статический и новый динамический pipeline, с явным разделением путей.
- **Feature Flags via Env**: Многое управляется через env-переменные, что позволяет поэтапно включать/выключать новые механики.
## Workaround-Driven Decisions
- **Track-Only Removal**: По умолчанию удаление namespace не вызывает cleanup в подписчиках — workaround против race-condition при массовых удалениях.
- **Manual Adoption**: При старте executor-ы делают adopt orphaned ресурсов (pods, deployments) — workaround для несовершенного lifecycle.
- **Explicit Reaper Loops**: Для чистки orphaned объектов используются отдельные циклы (object reaper), а не event-driven подход.
## Fragile Operational Components
- **Informer Factory Lifecycle**: Ошибки в динамическом создании/удалении informer-ов приводят к memory leak или stale watchers.
- **RBAC/SA Drift**: Неконсистентность между созданием и удалением сервисных аккаунтов и ролей.
- **Cache Invalidation**: FunctionServiceCache может рассинхронизироваться при сбоях в event flow.
- **Adoption Loops**: AdoptExistingResources может не покрыть все edge-case, особенно при race между startup и watcher.
## Poor Scalability Risks
- **Informer Explosion**: На сотнях/тысячах namespace число informer-ов и goroutine растёт линейно, возможен memory/FD exhaustion.
- **Synchronous Dispatch**: Все подписчики вызываются синхронно, при долгой инициализации одного — блокируются остальные.
- **Centralized Locking**: NamespaceManager держит глобальный mutex на все операции — bottleneck при высокой churn rate.
- **No Sharding**: Нет горизонтального масштабирования NamespaceManager — всё в одном процессе.
## Future Maintenance Problems
- **Hidden State Machines**: Фазы namespace и частей (part state) реализованы неявно, без явной state machine — сложно дебажить stuck state.
- **Implicit Error Handling**: Ошибки в подписчиках часто логируются, но не эскалируются — возможна silent failure.
- **Contract Drift**: Любое изменение интерфейса NamespaceSubscriber требует синхронного обновления всех компонентов.
- **Complex Test Surface**: Много интеграционных точек, сложно покрыть тестами все сценарии гонок и отказов.
## Deepest Upstream Divergence
- **Полная замена статической модели discovery на динамическую через watcher и NamespaceManager.**
- **Весь lifecycle tenant-namespace теперь event-driven, а не env-driven.**
- **Введён централизованный интерфейс подписки на события namespace для всех core-компонентов.**
- **Механика adopt orphaned ресурсов и явная поддержка rollback/cleanup.**
- **Maturity**: Архитектурно зрелый, хорошо документированный, с явным reasoning и поэтапным внедрением.
- **Complexity**: Высокая, особенно в динамике и синхронизации между компонентами.
- **Maintainability**: Среднесрочная — без явной state machine и горизонтального масштабирования возможны проблемы при росте нагрузки.
- **Production-Grade**: Ближе к production-grade platform engineering, чем к эксперименту, но требует доработки по масштабированию и явной формализации state transitions.
---
## Architectural Drift / Entropy / Hazards
- **Drift**: Возможен drift между глобальным и локальным состоянием, если подписчики реализованы несимметрично.
- **Entropy**: Много точек входа, implicit contract, нет явной state machine — сложность будет расти.
- **Hazards**: Memory leak, race-condition, orphaned ресурсы, silent failure при ошибках в подписчиках.
---
## Summary
Этот форк — зрелая попытка перевести Fission на event-driven multi-tenant архитектуру с динамическим discovery и централизованным lifecycle management. Основные сложности и риски — в управлении состоянием, синхронизации и масштабируемости. Требует дальнейшей формализации state machine, горизонтального масштабирования и усиления тестового покрытия для production-grade эксплуатации.
---
# Deep Risk Analysis (May 2026)
> Конкретные сценарии отказа, оценка при 50–100 tenant, предложения по исправлению.
---
## 1. Сценарии отказа для каждого "Risky Decision"
### 1.1 Informer Lifecycle Management
**Сценарий: повторная регистрация namespace через relabel**
2. Namespace-watcher вызывает `HandleWatcherNamespaceRemoval()`. Стратегия `TrackOnly`: NamespaceManager помечает запись как `removed` и **не вызывает**`OnNamespaceRemove`у подписчиков.
3. Informer-ы executor (gpm, newdeploy) и router продолжают работать — pool для tenant-42 жив, функции маршрутизируются.
5.`RunManagedNamespaceWatcher` (resync 30 мин) может не вызвать Add снова для уже известного NS.
6.**Router**: `AddNamespace` вызывает `DefaultNSResolver().AddNamespace(ns)`. Глобальный resolver уже содержит tenant-42 (его никто не удалял из-за track-only) → возвращает `false` → router делает **early return без создания новых informer-ов** (строка 460 `httpTriggers.go`). Router считает namespace активным (старые informer-ы ещё работают) — но если они были остановлены контекстом — тихое 404.
7.**Executor**: `gpm.AddNamespace` проверяет `poolPodC.envLister[ns]` — если старый lister жив, возвращает nil сразу (дедупликация). Всё выглядит нормально, но фактически используются **устаревшие informer-ы** с застрявшим кэшем.
**Итог**: relabel-цикл создаёт phantom-состояние: компоненты думают что NS активен, но его lifecycle разорван.
---
### 1.2 Centralized Mutex
**Сценарий: высокая churn + concurrent Snapshot**
`dispatch()` снимает write-lock перед вызовом каждого subscriber-а, затем берёт его снова для следующего. Структура:
```
mu.Lock() → читаем список subs →
mu.Unlock() → вызываем handler(sub1) [k8s API call, может занять сотни мс]
mu.Lock() → читаем следующий sub →
mu.Unlock() → вызываем handler(sub2)
```
Параллельно: router каждые 20 мс делает `syncTriggers()` → `updateRouter()` → итерирует `snapshotFuncInformers()` → берёт `informerMu.RLock`. Это другой mutex, но `DefaultNSResolver().Snapshot()` вызывается из `idleObjectReaper` каждые 5 сек под глобальным `RWMutex` NamespaceManager.
При 100 tenant с churn 10 ns/час: в среднем каждые 6 мин добавляется namespace. Само по себе безвредно. Но при пике (батч-онбординг 10 tenant за 1 минуту): `dispatch()` держит write-lock с паузами на unlock/relock для каждого subscriber × 10 параллельных dispatch → конкуренция за mutex возрастает. `Snapshot()` в `idleObjectReaper` (каждые 5 сек) и в `AdoptExistingResources` (каждый рестарт) будут ждать.
**Итог**: не deadlock, но latency spike на Snapshot на старте и при батч-онбординге — 200–500 мс при 10+ concurrent dispatch.
---
### 1.3 Manual Adoption (AdoptExistingResources)
**Сценарий: гонка adoption vs watcher**
1. Executor стартует. `AdoptExistingResources` запускается, берёт `DefaultNSResolver().Snapshot()` — snapshot содержит только статические NS из `FISSION_RESOURCE_NAMESPACES`.
2. Параллельно запускается `RunManagedNamespaceWatcher`. Watcher вызывает `BootstrapAndDispatch()`, который регистрирует managed NS и вызывает `registerNamespace()` у executor-подписчика.
3.`registerNamespace()` вызывает `DefaultNSResolver().AddNamespace(ns)` (глобальный guard), затем `gpm.AddNamespace()`.
4.**Но `AdoptExistingResources` уже завершила свой loop** — managed NS не попал в snapshot. Orphaned pods в tenant NS не приняты.
5. Функции в этих pod-ах будут вызываться ещё раз через cold start — лишний latency spike и потеря статуса `instanceID` у подов (старый instanceID в annotation не перезаписан → `CleanupOldExecutorObjects` сочтёт их orphaned → удалит).
**Hardcoded 30s timeout**: `AdoptExistingResources` в poolmgr не имеет явного timeout, но `k8sCache.WaitForCacheSync` в `Run()` блокирует до готовности — только после этого запускается `service()`. Если namespace watcher опередил, poolmgr получит env-события до того как `AdoptExistingResources` завершится → гонка на `gpm.pools` map (не защищена mutex вне `service()` goroutine).
2.`registerNamespace()` вызывает `EnsureNamespaceSA()` — Kubernetes API momentarily unavailable (503).
3.`EnsureNamespaceSA()` возвращает ошибку → вызывающий код (предположительно) пишет в лог и помечает часть как `NamespacePartStateFailed`.
4.`deriveNamespacePhase()` выставляет namespace в `NamespacePhaseFailed`.
5.**Нет reconcile-цикла**: нет горутины, которая периодически проверяет failed namespace и пытается повторить. Phase останется `failed` до рестарта процесса.
6. Router был вызван следующим в цепочке dispatch. Т.к. dispatch вызывается подписчики последовательно без barrier, router **уже создал свои informer-ы** до того как executor завершился с ошибкой.
7.**Dirty state**: router видит `tenant-99` как активный (informer-ы есть), executor — нет (SA/RBAC не создан). Любой вызов функции из tenant-99 → executor не может специализировать pod (нет fetcher SA) → 503.
Лог покажет ошибку, но namespace останется в `failed` навсегда (до рестарта). Оператор не получит никакого k8s-статуса — ни condition на Namespace объекте, ни event.
---
### 1.5 Eventual Consistency
**Сценарий: HTTPTrigger создан в окне до ready informer**
1. Tenant создаёт namespace с label → namespace добавляется в NamespaceManager.
2.`dispatch()` вызывает router subscriber → `AddNamespace()`:
Router ждёт sync и перестраивает роутинг. Это занимает несколько секунд.
3. Tenant **немедленно** после создания namespace создаёт HTTPTrigger через API.
4. Если trigger создан **до** завершения `WaitForCacheSync` в router → informer ещё не синхронизирован, но trigger уже в etcd.
5. После sync informer получит это событие через `AddFunc` → `syncTriggers()`. Это нормально.
6. **Проблема в другом**: `dispatch()` вызывает подписчиков **последовательно**. Если executor (первый в списке) занимается `EnsureNamespaceSA` + `registerExecutorTypes` (10–30 сек при медленном API) → router subscriber не вызывается всё это время. HTTPTrigger, созданный в этом окне, попадёт в informer, но router ещё не начал слушать → `AddFunc` для этого trigger не вызовется никогда (resync через 30 мин).
7. Результат: trigger существует в etcd, но **отсутствует в роутере 30 минут**.
---
## 2. Анализ при 50–100 tenant с churn 10 ns/час
### Informer Explosion
При 100 активных tenant:
- **Executor (poolmgr)**: 1 `SharedInformerFactory` (Fission CRD) + 1 `SharedInformerFactory` (k8s pods/RS) на NS = 200 factory. Каждая factory запускает горутины на каждый informer (~3–5 горутин). **~600–1000 goroutine** только от poolmgr.
- **Executor (newdeploy)**: аналогично — ещё 200 factory, ~600 goroutин.
Итого: **~1500–2000 goroutine** только от informer-ов. При пике churn (10 ns/час) — каждые 6 минут добавляется NS, создаётся ~20 новых горутин, они не убираются при track-only removal.
При **100 NS × 30 мин resync**: каждые 30 мин каждый informer делает LIST всех объектов в своём NS. 100 × 5 informer-типов × LIST = **500 concurrent LIST-запросов** к Kubernetes API раз в 30 минут — возможный thundering herd.
### Stuck Failed State
10 ns/час churn с 1% API error rate = ~2.4 failed namespace/сутки. Каждый остаётся в `failed` навсегда. За 30 дней = ~72 "мёртвых" записи в NamespaceManager. `Snapshot()` возвращает их в `idleObjectReaper` → лишние LIST к k8s API для несуществующих/неактивных NS → ошибки, логи, load.
### AdoptExistingResources Race
Каждый рестарт executor-а — race. При rolling update в k8s (новый pod стартует, старый ещё жив): оба executor-а параллельно делают `AdoptExistingResources` → оба патчат `instanceID` на одних и тех же pod-ах → `CleanupOldExecutorObjects` нового экземпляра удаляет pod-ы старого (ожидаемо), но при race может удалить pod, который новый экземпляр уже adoptировал.
### Router Dedup Gap — критический сценарий при рестарте
При рестарте executor + router одновременно:
1. `FISSION_RESOURCE_NAMESPACES` содержит `fission-fn` (статический NS).
2. `namespace.go` `init()` добавляет его в `DefaultNSResolver`.
3. `BootstrapAndDispatch()` в NamespaceManager вызывает dispatch для всех managed NS, включая `fission-fn`.
4. **Router** `AddNamespace("fission-fn")` → `DefaultNSResolver().AddNamespace("fission-fn")` → **false** (уже добавлен в `init()`!) → **early return, informer для fission-fn НЕ создан**.
5. Executor (gpm, newdeploy) — используют own dedup (envLister/deplLister), `fission-fn` там нет → создают informer.
6. Router слеп к HTTPTrigger и Function событиям из `fission-fn` при динамическом пути. Спасает только то, что `GetInformersForNamespaces` вызывается в `MakeHTTPTriggerSet` при старте — но только для NS из env.
**Вывод**: если `fission-fn` включён в `FISSION_RESOURCE_NAMESPACES` И помечен `fission.io/managed=true` — возможна ситуация, когда после рестарта router использует startup-informer, а executor использует watcher-informer с другим lifecycle → рассинхронизация при следующем relabel-цикле.
---
## 3. Минимальное изменение: явная state machine без полного рефакторинга
Текущая проблема: `failed` namespace остаётся в `failed` навсегда — нет retry.
**Изменение**: добавить reconcile-очередь в `inMemoryNamespaceManager` без изменения публичного интерфейса.
```go
// В inMemoryNamespaceManager добавить:
type reconcileRequest struct {
ns string
attempt int
}
reconcileQueue chan reconcileRequest // небуферизованный или с буфером 64
// В MarkPartFailed (или в dispatch при возврате ошибки от subscriber):
// Вызвать только тех подписчиков, у кого часть в FailedState:
m.dispatchRetry(ctx, req.ns, req.attempt+1)
})
}
}
}
```
**Изменения интерфейса**: `NamespaceManager` получает метод `RunReconciler(ctx)` — добавляется в интерфейс, но не breaking change для существующих вызывающих (можно добавить как опциональный метод или вызвать из `BootstrapAndDispatch`).
**Что НЕ меняется**: `NamespaceSubscriber`, `NamespaceRecord`, публичные методы `Upsert`/`Snapshot`/`Subscribe` — всё прежнее.
---
## 4. Track-Only Removal: скрытые допущения и dirty state
### Допущение 1: `DefaultNSResolver` — только append
`pkg/utils/namespace.go`: метод `AddNamespace` добавляет NS в глобальный map, метода `RemoveNamespace` не существует. Последствия:
- Namespace, удалённый через label-снятие, **навсегда остаётся** в глобальном resolver-е.
- `idleObjectReaper` в poolmgr и newdeploy делает `DefaultNSResolver().Snapshot()` → итерирует удалённые NS → делает LIST Environments/Functions в уже несуществующем (или чужом) namespace → получает k8s 403/404 → логирует ошибку → возвращает из reaper-а (!) — `return` на ошибке прерывает весь цикл reaper-а для текущей итерации.
### Допущение 2: Informer-ы продолжают работать
После track-only removal informer-ы executor-а и router-а **не останавливаются**. Для poolmgr: env-events из удалённого namespace продолжают триггерить создание пулов. Пулы создаются в k8s (или пытаются) — для namespace, который более не является managed. RBAC мог быть уже удалён оператором → pod-ы не могут pull fetcher image → CrashLoopBackOff в "удалённом" namespace.
### Допущение 3: FunctionServiceCache не очищается
`fsCache` (в gpm и newdeploy) содержит записи с `Function.Namespace = "tenant-42"`. После track-only removal записи не удаляются. `idleObjectReaper` находит их через `fsCache.ListOldForPool()` → пытается найти pod в `gpm.podLister["tenant-42"]` → lister ещё жив (informer работает) → pod может быть найден → считается "valid" → не reaped → запись в кэше живёт вечно.
### Допущение 4 (критическое): повторное добавление того же NS → router слеп
2. NS удалён (track-only) → resolver не очищен → informer router-а продолжает работать.
3. NS добавлен снова (новый tenant с тем же именем, например после namespace-переименования).
4. `AddNamespace("tenant-42")` на router-е → `DefaultNSResolver().AddNamespace("tenant-42")` → **false** (уже в map!) → **early return**.
5. Router **не создаёт новый informer** — считает что уже обслуживает namespace. Но старый informer работает с **кэшем от предыдущего tenants** — старые Function и HTTPTrigger объекты (с другими UID) видны в `funcInformer.GetStore()`.
6. Executor (gpm): `poolPodC.envLister["tenant-42"]` тоже существует → own dedup → early return → executor тоже не создаёт новый informer.
7. Новые HTTPTrigger-ы нового tenant-42 **никогда не попадут в router** (resync через 30 мин принесёт их, но с кэшем старого tenanta!).
**Результат**: dirty state — оба компонента убеждены что всё нормально, но фактически обслуживают кэш несуществующего tenant с объектами с устаревшими UID. Вызовы функций нового tenant → 404 или выполнение **функций старого tenant** если имена совпадают.
---
## 5. Оценка замены centralized mutex на sharded lock
### Техническая реализация (feasible)
```go
const numShards = 16
type shardedNamespaceManager struct {
shards [numShards]nsShard
subsMu sync.RWMutex
subs map[string]NamespaceSubscriber
// ... остальные поля
}
type nsShard struct {
mu sync.RWMutex
records map[string]NamespaceRecord // только NS принадлежащие этому шарду
}
func shardIndex(ns string) int {
h := fnv.New32a()
h.Write([]byte(ns))
return int(h.Sum32()) % numShards
}
```
`Upsert(ns, ...)` → берёт lock только шарда `shardIndex(ns)`.
`Get(ns)` → RLock только нужного шарда.
`Snapshot()` → **последовательно** берёт RLock каждого шарда, копирует, освобождает, переходит к следующему. N=16 последовательных lock-acquisitions.
### Сохранение интерфейса
Публичный интерфейс `NamespaceManager` (Upsert, Get, Snapshot, Subscribe, Dispatch) не меняется. Подписчики (`NamespaceSubscriber`) не меняются.
### Анализ выгоды
При 10 ns/час churn: **одно upsert каждые 6 минут**. Текущий bottleneck — не mutex, а:
1. Synchronous subscriber dispatch (каждый делает k8s API calls)
2. Informer resync thundering herd
3. AdoptExistingResources race
Sharded lock убирает конкуренцию за mutex при **параллельных per-namespace операциях**. Но `dispatch()` сам снимает/берёт lock несколько раз — sharding не помогает здесь (dispatch по одному NS всегда один шард).
`Snapshot()` становится чуть медленнее (16 lock-acquisitions вместо 1 RLock) при маленьком числе NS, и сопоставима при большом.
### Вердикт
**Технически реализуемо с сохранением интерфейса. Не оправдано при текущей нагрузке.**
Sharded mutex даст реальный выигрыш только если `Upsert` и `Get` вызываются **параллельно для разных NS** с частотой > 100 ops/sec. При 10 ns/час это недостижимо. Реальные bottleneck-и — в subscriber dispatch и informer lifecycle, не в mutex.
Приоритет вместо sharding:
1. Сделать subscriber dispatch **параллельным** (goroutine per subscriber с errgroup) — немедленное ускорение онбординга.
2. Добавить `RemoveNamespace` в `DefaultNSResolver` — закрывает класс dirty-state багов.
3. Добавить reconcile-очередь (см. п. 3) — закрывает stuck-failed.
Sharded lock — в backlog, актуально при > 500 concurrent tenant с > 1 onboarding/sec.
| `env` | Стенд: `prod`, `dev`, `test` (только для JWT; по умолчанию `test`) |
**Ответ 200:**
```json
{
"ok":true,
"env":"test",
"namespace":"fission-a3f9c1b2d4e6f8a1",
"email":"user@example.com"
}
```
**Ошибки:**
| Код | Причина |
|-----|---------|
| 400 | Тело не JSON или `token` пустой |
| 401 | Токен < 6 символов или JWT не прошёл валидацию в Deck API |
| 405 | GET вместо POST |
> **Namespace детерминирован**: `fission-` + hex(SHA256(sub)[:8]) — одинаковый токен → всегда один namespace.
> Namespace и RBAC создаются автоматически при первом обращении.
---
## Мультитенантность ★ КЛЮЧЕВОЕ ОТЛИЧИЕ
- Каждый пользователь работает в **изолированном K8s namespace**: `fission-<hash(token)>`
- Все операции (создание, список, вызов, удаление) **автоматически ограничены своим namespace**
- Указать namespace вручную **невозможно**
- Функции другого пользователя **не видны и не доступны** — любая операция над чужим объектом возвращает **404** (не 403, чтобы не раскрывать факт существования)
- **Routes изолированы**: функции разных пользователей с одинаковым именем получают разные HTTP-маршруты
### Квоты (применяются автоматически, значения по умолчанию)
-`fission-executor` → `create SA/Role/RoleBinding` в user NS (`fission-executor-sa-provisioner`)
Без этого RBAC `EnsureNamespaceSA` будет падать с Forbidden, но **консоль продолжит работать** — она создаёт SA/RoleBindings сама и не зависит от NSWatcher.
---
## Вердикт
| Сценарий | Статус |
|---|---|
| Новый NS создаётся через консоль | ✅ работает как раньше |
| NSWatcher обнаруживает NS по `fission.io/managed=true` | ✅ совместимо |
| `EnsureNamespaceSA` вызывается в уже настроенном NS | ✅ no-op, нет конфликтов |
| Старый NS (без нового fission-bundle) | ✅ консоль не зависит от NSWatcher |
> **Purpose:** This document exists so that any AI agent or engineer can fully
> understand what was changed to add multi-tenancy to this Fission fork, why
> each decision was made, and what needs to be ported when a new upstream
> Fission release arrives.
>
> Fork base: `github.com/fission/fission` tag `v1.22.0` (2025-12-16)
> Our branch: `feature/multitenant`
---
## 1. The Problem We Solved
In stock Fission v1.22.0 all resource namespaces must be listed in the
`FISSION_RESOURCE_NAMESPACES` environment variable **before** the process starts.
Adding a new tenant namespace requires:
1. Patching that env var on executor, router, buildermgr deployments
2. Triggering a rolling restart of all three components (~30 s downtime each)
At scale (hundreds of tenants created continuously) this causes a permanent
rolling-restart loop and cascading failures for existing users.
**Our solution:** hot namespace registration without pod restarts. Any platform
(console, operator, CI/CD) creates a Kubernetes Namespace with label
`fission.io/managed=true` — all three Fission components detect it within
milliseconds via k8s Watch and register it live.
---
## 2. Integration Contract (external platforms)
The entire contract between an external platform and Fission is a single label:
```yaml
apiVersion:v1
kind:Namespace
metadata:
name:tenant-abc123
labels:
fission.io/managed:"true"
```
No other coupling to Fission internals is required.
To remove a tenant namespace: delete the namespace or remove the label.
Current removal strategy is `track-only` (the manager records the event but does
not actively deregister — the executor types stop receiving events for deleted
resources naturally). `dispatch-remove` strategy exists in the model but is not
wired by default (see §8).
---
## 2a. How the Console (`../fission`) Integrates
The Fission Console (`github.com/naeel/fission`, package `console`) creates tenant
namespaces via `SetupFissionNamespace()` in
`console/internal/fission/namespace.go`.
That function does three things:
1. Creates the Namespace with two labels:
-`managed-by=fission-console` — console's own filter
- **`fission.io/managed=true`** — this is the NSWatcher trigger
2. Creates `fission-fetcher` and `fission-builder` ServiceAccounts in the new NS
3. Creates RoleBindings for all Fission system SAs (`fission-executor`,
`fission-router`, `fission-buildermgr`, etc.) using `cluster-admin` scoped to
the namespace
**The coupling is exactly one label.** The console does not call any Fission
internal API to register the namespace — it just sets `fission.io/managed=true`
and the NSWatcher in executor/router/buildermgr picks it up automatically within
~50ms.
**Before `v1.22.0-mt1` (today's deploy):** the console set the label but the
executor was running the official `ghcr.io/fission/fission-bundle:v1.22.0` image
which has no NSWatcher — so the label was silently ignored. Tenant namespaces
still worked because the console also created the SA/RoleBindings manually (step 2
and 3 above), so pool pods could start. But executor/router/buildermgr were not
dynamically aware of new namespaces — they relied on whatever was in
`FISSION_RESOURCE_NAMESPACES` at startup.
**After `v1.22.0-mt1`:** executor/router/buildermgr detect the label
automatically. The SA creation in `EnsureNamespaceSA` (our code in
`pkg/utils/serviceaccount.go`) now runs from the executor side as well — but since
the console already created the SA, `EnsureNamespaceSA` is a no-op (idempotent).
No conflict, no double work.
---
## 3. Backward Compatibility
`FISSION_RESOURCE_NAMESPACES` continues to work exactly as before. Namespaces
listed there are bootstrapped at startup with source `env` and do not require the
label. The NSWatcher layer adds **on top** of the existing mechanism — nothing
was removed.
---
## 4. File Map
### New files (did not exist in v1.22.0)
| File | Purpose | What breaks if removed |
|------|---------|------------------------|
| `pkg/utils/namespace_manager_model.go` | All types: `NamespaceRecord`, `NamespacePhase`, `NamespaceSource`, `NamespaceEvent`, `NamespaceRemovalStrategy`, `ManagedNamespaceWatcherConfig` | Everything — all other files import these types |
| `pkg/utils/namespace_manager.go` | `NamespaceManager` interface + `inMemoryNamespaceManager` implementation. `RunManagedNamespaceWatcher()` — the single entry point used by all three components. `NewNamespaceWatcherEventHandlers()` — k8s informer callbacks. `EnsureNamespaceSA` helper call site. | All NSWatcher functionality |
| `pkg/utils/namespace_manager_test.go` | Unit + integration tests for NamespaceManager | Tests only |
| `pkg/utils/namespace_manager_model_test.go` | Tests for model helpers | Tests only |
| `pkg/utils/serviceaccount.go` (was modified, `EnsureNamespaceSA` added at bottom) | `EnsureNamespaceSA(ctx, client, logger, ns)` — creates fission-fetcher SA/Role/RoleBinding in a new namespace idempotently | Pool pods in new namespaces fail to start (no SA to run fetcher) |
| `pkg/executor/multitenant/ns_watcher.go` | `StartNSWatcher()` — executor entry point. `registerNamespace()` — calls `AddNamespace` on global resolver + `EnsureNamespaceSA` + all executor types. | Executor never learns about new namespaces |
| `pkg/executor/multitenant/namespace_subscriber.go` | `NewNamespaceSubscriber()` — adapter from `NamespaceSubscriber` interface to executor `registerNamespace()` | Same as above |
| `pkg/executor/multitenant/ns_watcher_test.go` | Tests | Tests only |
| `pkg/executor/multitenant/namespace_subscriber_test.go` | Tests | Tests only |
| `pkg/router/ns_watcher.go` | `StartNSWatcher()` — router entry point, 5 lines | Router never learns about new namespaces |
| `pkg/router/namespace_subscriber.go` | `NewNamespaceSubscriber()` — adapter calling `HTTPTriggerSet.AddNamespace` | Same as above |
| `pkg/router/namespace_subscriber_test.go` | Tests | Tests only |
| `pkg/buildermgr/ns_watcher.go` | `StartNSWatcher()` — buildermgr entry point, 5 lines | Buildermgr never learns about new namespaces |
| `pkg/buildermgr/namespace_subscriber.go` | `NewNamespaceSubscriber()` — adapter calling `envWatcher.AddNamespace` + `pkgWatcher.AddNamespace` | Same as above |
| `pkg/buildermgr/namespace_subscriber_test.go` | Tests | Tests only |
| `deploy/multitenant/rbac.yaml` | ClusterRoles + ClusterRoleBindings for all three components (see §6) | Components crash at startup or fail to watch namespaces |
### Modified files (existed in v1.22.0, we changed them)
| File | What we added | What breaks if reverted |
|------|--------------|-------------------------|
| `pkg/utils/namespace.go` | `ManagedNamespaceLabelKey/Value` constants, `ManagedNamespaceLabelSelector()`, `IsManagedNamespace()`, `AddNamespace()` (thread-safe dedup), `Snapshot()` (sorted slice copy under read-lock), `SnapshotWithOptions()` | All callers of `Snapshot()` break — there are many; label constants used by informer filter |
| `pkg/utils/namespace_test.go` | Tests for new methods | Tests only |
| `pkg/utils/informer.go` | `NewSharedInformerFactoryForNamespaces(namespaces []string)` — creates informer factory filtered to a dynamic list of namespaces | Executor types cannot create per-NS informers for new namespaces |
| `pkg/executor/executor.go` | `StartNSWatcher(...)` call added after executor types are initialized | NSWatcher never starts in executor |
| `pkg/executor/executortype/executortype.go` | `AddNamespace(ctx, ns, mgr)` added to the `ExecutorType` interface | All three executor types must implement this; compilation fails |
| `pkg/executor/executortype/poolmgr/gpm.go` | `AddNamespace()` implementation — creates per-NS informer factory, pod lister, event handlers | poolmgr never picks up functions in new namespaces |
| `pkg/executor/executortype/poolmgr/poolpodcontroller.go` | Uses `Snapshot()` in runtime loop instead of static namespace list | Pool pods not created in new namespaces |
| `pkg/executor/executortype/newdeploy/newdeploymgr.go` | `AddNamespace()` implementation | newdeploy never picks up functions in new namespaces |
| `pkg/executor/executortype/container/containermgr.go` | `AddNamespace()` implementation | container executor never picks up functions in new namespaces |
| `pkg/router/router.go` | `StartNSWatcher(...)` call added | NSWatcher never starts in router |
| `pkg/router/httpTriggers.go` | `AddNamespace(ns string)` on `HTTPTriggerSet` — starts per-NS informers for HTTPTriggers and Functions | Router ignores HTTPTriggers in new namespaces |
| `pkg/router/functionReferenceResolver.go` | Uses `Snapshot()` in runtime loop | Router resolves functions only in statically-configured namespaces |
| `pkg/buildermgr/buildermgr.go` | `StartNSWatcher(...)` call added | NSWatcher never starts in buildermgr |
| `pkg/buildermgr/envwatcher.go` | `AddNamespace(ns string)` — starts per-NS Environment informer | Buildermgr ignores Environments in new namespaces |
| `pkg/buildermgr/pkgwatcher.go` | `AddNamespace(ns string)` — starts per-NS Package informer | Buildermgr ignores Packages in new namespaces |
| `pkg/storagesvc/archivePruner.go` | Uses `Snapshot()` instead of static namespace list | Archive pruner only cleans old namespaces |
| `.gitignore` | Added `*.token` | Minor — token files would be committed accidentally |
| `deploy/multitenant/rbac.yaml` | New file (see above) | — |
| `dispatch-remove` full wiring | Not needed for current use case | `ManagedNamespaceWatcherConfig.RemovalStrategy` — just switch the constant |
| Buildermgr ClusterRole in rbac.yaml | Buildermgr uses the same SA as executor in our deployment; check your setup | Add a third ClusterRole/Binding to `deploy/multitenant/rbac.yaml` |
| Helm chart integration | We apply rbac.yaml manually. A proper Helm chart would include these RBAC objects | `charts/fission-all/templates/` |
| Layer 2 (tenant isolation: per-NS network policy, resource quotas) | Out of scope for Layer 1 | Not started |
| Layer 3 (per-tenant auth, billing hooks) | Out of scope | Not started |
| `NamespaceManager.DispatchRemove` subscriber wiring | Each subscriber has a `RemoveFunc` stub returning nil | Implement per subscriber |
---
## 9. Porting to a New Upstream Version
When `github.com/fission/fission` releases v1.23 or later, follow this order:
1.**Check the upstream changelog** for any changes to:
-`pkg/utils/namespace.go` — if they renamed or refactored `NamespaceResolver`, our `AddNamespace`/`Snapshot` additions need to be reapplied
-`pkg/executor/executortype/executortype.go` — if they changed the `ExecutorType` interface, our `AddNamespace` method needs to be reapplied
-`pkg/router/httpTriggers.go` — if `HTTPTriggerSet` changed, our `AddNamespace` method on it needs to be reapplied
-`pkg/buildermgr/envwatcher.go`, `pkgwatcher.go` — same
-`pkg/utils/informer.go` — if the informer factory pattern changed
2.**Apply in this order** (each depends on the previous):
1.`pkg/utils/namespace_manager_model.go` — pure types, no deps on other changed files
# 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:
- все 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`:
Нужен не просто список коммитов, а объяснение инженерной логики:
- что именно было не так в коде;
- почему исправление выбрано именно таким;
- почему изменения разбиты на маленькие шаги;
- какие инварианты я старался сохранить;
- что уже исправлено, а что еще нет.
Этот документ описывает серию маленьких безопасных шагов в ветке
`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, в которую можно смотреть отовсюду».
Это очень плохое свойство для 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.
Это важно, потому что именно в этом файле пользовательский контекст отдельно предупредил о возможных дополнительных изменениях между сообщениями.
Зафиксировать 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 по умолчанию.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.