diff --git a/doc/thinking/2026-04-26-namespace-manager-step40.md b/doc/thinking/2026-04-26-namespace-manager-step40.md new file mode 100644 index 00000000..6b34de6b --- /dev/null +++ b/doc/thinking/2026-04-26-namespace-manager-step40.md @@ -0,0 +1,20 @@ +# 2026-04-26 — NamespaceManager rewrite, step 40 + +## Цель шага + +Зафиксировать lifecycle policy для namespace removal в коде явно, а не только комментариями и log-сообщениями. + +## Что меняем + +1. Добавляем `NamespaceRemovalStrategy`. +2. Поддерживаем два режима: + - `track-only` + - `dispatch-remove` +3. Общие watcher handlers принимают strategy. +4. Текущий production flow использует `track-only`. +5. Добавляем unit tests на оба режима. + +## Что НЕ меняем + +- не включаем реальный remove dispatch в watcher-ах; +- не меняем runtime cleanup policy по умолчанию. \ No newline at end of file diff --git a/pkg/buildermgr/ns_watcher.go b/pkg/buildermgr/ns_watcher.go index 89c1a626..c1b178b2 100644 --- a/pkg/buildermgr/ns_watcher.go +++ b/pkg/buildermgr/ns_watcher.go @@ -60,7 +60,7 @@ func StartNSWatcher( } }, DeleteFunc: func(obj interface{}) { - utils.HandleWatcherNamespaceDelete(logger, "buildermgr.NSWatcher", nsManager, obj) + utils.HandleWatcherNamespaceDelete(ctx, logger, "buildermgr.NSWatcher", nsManager, obj, utils.NamespaceRemovalStrategyTrackOnly) }, }) diff --git a/pkg/executor/multitenant/ns_watcher.go b/pkg/executor/multitenant/ns_watcher.go index 58eba378..57d681c5 100644 --- a/pkg/executor/multitenant/ns_watcher.go +++ b/pkg/executor/multitenant/ns_watcher.go @@ -128,7 +128,7 @@ func StartNSWatcher( } }, DeleteFunc: func(obj interface{}) { - utils.HandleWatcherNamespaceDelete(logger, "multitenant.NSWatcher", nsManager, obj) + utils.HandleWatcherNamespaceDelete(ctx, logger, "multitenant.NSWatcher", nsManager, obj, utils.NamespaceRemovalStrategyTrackOnly) }, }) diff --git a/pkg/router/ns_watcher.go b/pkg/router/ns_watcher.go index 41e59fb7..13210eb2 100644 --- a/pkg/router/ns_watcher.go +++ b/pkg/router/ns_watcher.go @@ -60,7 +60,7 @@ func StartNSWatcher( } }, DeleteFunc: func(obj interface{}) { - utils.HandleWatcherNamespaceDelete(logger, "router.NSWatcher", nsManager, obj) + utils.HandleWatcherNamespaceDelete(ctx, logger, "router.NSWatcher", nsManager, obj, utils.NamespaceRemovalStrategyTrackOnly) }, }) diff --git a/pkg/utils/namespace_manager.go b/pkg/utils/namespace_manager.go index 5ab06095..df023082 100644 --- a/pkg/utils/namespace_manager.go +++ b/pkg/utils/namespace_manager.go @@ -152,9 +152,7 @@ func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, compo return } if NamespaceBecameUnmanaged(oldNamespace, newNamespace) { - _, _ = RecordNamespaceRemoval(manager, newNamespace, NamespaceSourceWatcher, time.Now().UTC()) - logger.Info(component+": namespace removed from manager state; runtime registrations kept", - zap.String("namespace", newNamespace.Name)) + HandleWatcherNamespaceRemoval(ctx, logger, component, manager, newNamespace, NamespaceRemovalStrategyTrackOnly) return } if !IsManagedNamespace(newNamespace.Labels) { @@ -165,12 +163,25 @@ func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, compo } } -func HandleWatcherNamespaceDelete(logger *zap.Logger, component string, manager NamespaceManager, obj interface{}) { +func HandleWatcherNamespaceDelete(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, obj interface{}, strategy NamespaceRemovalStrategy) { + HandleWatcherNamespaceRemoval(ctx, logger, component, manager, obj, strategy) +} + +func HandleWatcherNamespaceRemoval(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, obj interface{}, strategy NamespaceRemovalStrategy) { record, ok := RecordNamespaceRemoval(manager, obj, NamespaceSourceWatcher, time.Now().UTC()) if !ok { return } - logger.Info(component+": namespace deleted from manager state; runtime registrations kept", + if strategy == NamespaceRemovalStrategyDispatchRemove { + if _, _, err := manager.DispatchRemove(ctx, record.Name); err != nil { + logger.Error(component+": DispatchRemove failed", zap.String("namespace", record.Name), zap.Error(err)) + return + } + logger.Info(component+": namespace removal dispatched through subscribers", + zap.String("namespace", record.Name)) + return + } + logger.Info(component+": namespace removed from manager state; runtime registrations kept", zap.String("namespace", record.Name)) } diff --git a/pkg/utils/namespace_manager_model.go b/pkg/utils/namespace_manager_model.go index fea48ffb..fc79c901 100644 --- a/pkg/utils/namespace_manager_model.go +++ b/pkg/utils/namespace_manager_model.go @@ -41,6 +41,13 @@ const ( NamespaceEventResync NamespaceEventType = "resync" ) +type NamespaceRemovalStrategy string + +const ( + NamespaceRemovalStrategyTrackOnly NamespaceRemovalStrategy = "track-only" + NamespaceRemovalStrategyDispatchRemove NamespaceRemovalStrategy = "dispatch-remove" +) + type NamespacePartState struct { State string LastError string diff --git a/pkg/utils/namespace_manager_model_test.go b/pkg/utils/namespace_manager_model_test.go index a6a599b3..71d0c2a0 100644 --- a/pkg/utils/namespace_manager_model_test.go +++ b/pkg/utils/namespace_manager_model_test.go @@ -145,3 +145,12 @@ func TestNamespaceEventFromObject(t *testing.T) { t.Fatalf("expected empty name for unsupported object") } } + +func TestNamespaceRemovalStrategyValues(t *testing.T) { + if NamespaceRemovalStrategyTrackOnly != "track-only" { + t.Fatalf("unexpected track-only strategy value") + } + if NamespaceRemovalStrategyDispatchRemove != "dispatch-remove" { + t.Fatalf("unexpected dispatch-remove strategy value") + } +} diff --git a/pkg/utils/namespace_manager_test.go b/pkg/utils/namespace_manager_test.go index 950cd8eb..5065a2b9 100644 --- a/pkg/utils/namespace_manager_test.go +++ b/pkg/utils/namespace_manager_test.go @@ -357,13 +357,32 @@ func TestHandleWatcherNamespaceAddUpdateDelete(t *testing.T) { t.Fatalf("expected update handler to dispatch resync") } - HandleWatcherNamespaceDelete(logger, "router.NSWatcher", manager, k8sCache.DeletedFinalStateUnknown{Obj: namespace}) + HandleWatcherNamespaceDelete(context.Background(), logger, "router.NSWatcher", manager, k8sCache.DeletedFinalStateUnknown{Obj: namespace}, NamespaceRemovalStrategyTrackOnly) record, ok := manager.Get("tenant-a") if !ok || record.Phase != NamespacePhaseRemoved { t.Fatalf("expected delete handler to mark namespace removed") } } +func TestHandleWatcherNamespaceRemovalDispatch(t *testing.T) { + manager := NewNamespaceManager() + router := &testNamespaceSubscriber{name: "router"} + manager.Subscribe(router) + logger := zap.NewNop() + namespace := &corev1.Namespace{} + namespace.Name = "tenant-a" + manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher}) + + HandleWatcherNamespaceRemoval(context.Background(), logger, "router.NSWatcher", manager, namespace, NamespaceRemovalStrategyDispatchRemove) + record, ok := manager.Get("tenant-a") + if !ok || record.Phase != NamespacePhaseRemoved { + t.Fatalf("expected dispatch removal to keep removed record") + } + if router.removeCalls != 1 { + t.Fatalf("expected remove subscriber to be called once") + } +} + func TestNamespaceManagerDispatchAdd(t *testing.T) { manager := NewNamespaceManager() manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})