diff --git a/doc/thinking/2026-04-26-namespace-manager-step41.md b/doc/thinking/2026-04-26-namespace-manager-step41.md new file mode 100644 index 00000000..52b735be --- /dev/null +++ b/doc/thinking/2026-04-26-namespace-manager-step41.md @@ -0,0 +1,16 @@ +# 2026-04-26 — NamespaceManager rewrite, step 41 + +## Цель шага + +Довести explicit removal strategy до полного покрытия watcher lifecycle paths. + +## Что меняем + +1. `HandleWatcherNamespaceUpdate()` теперь тоже принимает `NamespaceRemovalStrategy`. +2. `managed -> unmanaged` path использует ту же policy, что и `DeleteFunc`. +3. Добавляем unit test на update-path с `dispatch-remove`. + +## Что НЕ меняем + +- текущие watcher-ы остаются на `track-only`; +- runtime cleanup policy по умолчанию не меняется. \ No newline at end of file diff --git a/pkg/buildermgr/ns_watcher.go b/pkg/buildermgr/ns_watcher.go index c1b178b2..ae2acf0e 100644 --- a/pkg/buildermgr/ns_watcher.go +++ b/pkg/buildermgr/ns_watcher.go @@ -56,7 +56,7 @@ func StartNSWatcher( oldNSObj, _ := oldObj.(*corev1.Namespace) nsObj, ok := newObj.(*corev1.Namespace) if ok { - utils.HandleWatcherNamespaceUpdate(ctx, logger, "buildermgr.NSWatcher", nsManager, oldNSObj, nsObj) + utils.HandleWatcherNamespaceUpdate(ctx, logger, "buildermgr.NSWatcher", nsManager, oldNSObj, nsObj, utils.NamespaceRemovalStrategyTrackOnly) } }, DeleteFunc: func(obj interface{}) { diff --git a/pkg/executor/multitenant/ns_watcher.go b/pkg/executor/multitenant/ns_watcher.go index 57d681c5..34daa384 100644 --- a/pkg/executor/multitenant/ns_watcher.go +++ b/pkg/executor/multitenant/ns_watcher.go @@ -124,7 +124,7 @@ func StartNSWatcher( oldNSObj, _ := oldObj.(*corev1.Namespace) nsObj, ok := newObj.(*corev1.Namespace) if ok { - utils.HandleWatcherNamespaceUpdate(ctx, logger, "multitenant.NSWatcher", nsManager, oldNSObj, nsObj) + utils.HandleWatcherNamespaceUpdate(ctx, logger, "multitenant.NSWatcher", nsManager, oldNSObj, nsObj, utils.NamespaceRemovalStrategyTrackOnly) } }, DeleteFunc: func(obj interface{}) { diff --git a/pkg/router/ns_watcher.go b/pkg/router/ns_watcher.go index 13210eb2..f426e566 100644 --- a/pkg/router/ns_watcher.go +++ b/pkg/router/ns_watcher.go @@ -56,7 +56,7 @@ func StartNSWatcher( oldNSObj, _ := oldObj.(*corev1.Namespace) nsObj, ok := newObj.(*corev1.Namespace) if ok { - utils.HandleWatcherNamespaceUpdate(ctx, logger, "router.NSWatcher", nsManager, oldNSObj, nsObj) + utils.HandleWatcherNamespaceUpdate(ctx, logger, "router.NSWatcher", nsManager, oldNSObj, nsObj, utils.NamespaceRemovalStrategyTrackOnly) } }, DeleteFunc: func(obj interface{}) { diff --git a/pkg/utils/namespace_manager.go b/pkg/utils/namespace_manager.go index df023082..67c86334 100644 --- a/pkg/utils/namespace_manager.go +++ b/pkg/utils/namespace_manager.go @@ -147,12 +147,12 @@ func HandleWatcherNamespaceAdd(ctx context.Context, logger *zap.Logger, componen } } -func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, oldNamespace *corev1.Namespace, newNamespace *corev1.Namespace) { +func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, oldNamespace *corev1.Namespace, newNamespace *corev1.Namespace, strategy NamespaceRemovalStrategy) { if newNamespace == nil { return } if NamespaceBecameUnmanaged(oldNamespace, newNamespace) { - HandleWatcherNamespaceRemoval(ctx, logger, component, manager, newNamespace, NamespaceRemovalStrategyTrackOnly) + HandleWatcherNamespaceRemoval(ctx, logger, component, manager, newNamespace, strategy) return } if !IsManagedNamespace(newNamespace.Labels) { diff --git a/pkg/utils/namespace_manager_test.go b/pkg/utils/namespace_manager_test.go index 5065a2b9..0fdf1ca9 100644 --- a/pkg/utils/namespace_manager_test.go +++ b/pkg/utils/namespace_manager_test.go @@ -352,7 +352,7 @@ func TestHandleWatcherNamespaceAddUpdateDelete(t *testing.T) { t.Fatalf("expected add handler to dispatch add") } - HandleWatcherNamespaceUpdate(context.Background(), logger, "router.NSWatcher", manager, namespace, namespace) + HandleWatcherNamespaceUpdate(context.Background(), logger, "router.NSWatcher", manager, namespace, namespace, NamespaceRemovalStrategyTrackOnly) if router.resyncCalls != 1 { t.Fatalf("expected update handler to dispatch resync") } @@ -383,6 +383,29 @@ func TestHandleWatcherNamespaceRemovalDispatch(t *testing.T) { } } +func TestHandleWatcherNamespaceUpdateDispatchRemoval(t *testing.T) { + manager := NewNamespaceManager() + router := &testNamespaceSubscriber{name: "router"} + manager.Subscribe(router) + logger := zap.NewNop() + oldNamespace := &corev1.Namespace{} + oldNamespace.Name = "tenant-a" + oldNamespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue} + newNamespace := &corev1.Namespace{} + newNamespace.Name = "tenant-a" + newNamespace.Labels = map[string]string{} + manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher}) + + HandleWatcherNamespaceUpdate(context.Background(), logger, "router.NSWatcher", manager, oldNamespace, newNamespace, NamespaceRemovalStrategyDispatchRemove) + record, ok := manager.Get("tenant-a") + if !ok || record.Phase != NamespacePhaseRemoved { + t.Fatalf("expected dispatch removal on managed->unmanaged transition") + } + if router.removeCalls != 1 { + t.Fatalf("expected remove subscriber to be called once on label drop") + } +} + func TestNamespaceManagerDispatchAdd(t *testing.T) { manager := NewNamespaceManager() manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})