layer1: formalize namespace removal strategy

This commit is contained in:
Naeel
2026-04-26 10:48:42 +03:00
parent 340b9cae84
commit 0755319fac
8 changed files with 75 additions and 9 deletions
@@ -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 по умолчанию.
+1 -1
View File
@@ -60,7 +60,7 @@ func StartNSWatcher(
} }
}, },
DeleteFunc: func(obj interface{}) { DeleteFunc: func(obj interface{}) {
utils.HandleWatcherNamespaceDelete(logger, "buildermgr.NSWatcher", nsManager, obj) utils.HandleWatcherNamespaceDelete(ctx, logger, "buildermgr.NSWatcher", nsManager, obj, utils.NamespaceRemovalStrategyTrackOnly)
}, },
}) })
+1 -1
View File
@@ -128,7 +128,7 @@ func StartNSWatcher(
} }
}, },
DeleteFunc: func(obj interface{}) { DeleteFunc: func(obj interface{}) {
utils.HandleWatcherNamespaceDelete(logger, "multitenant.NSWatcher", nsManager, obj) utils.HandleWatcherNamespaceDelete(ctx, logger, "multitenant.NSWatcher", nsManager, obj, utils.NamespaceRemovalStrategyTrackOnly)
}, },
}) })
+1 -1
View File
@@ -60,7 +60,7 @@ func StartNSWatcher(
} }
}, },
DeleteFunc: func(obj interface{}) { DeleteFunc: func(obj interface{}) {
utils.HandleWatcherNamespaceDelete(logger, "router.NSWatcher", nsManager, obj) utils.HandleWatcherNamespaceDelete(ctx, logger, "router.NSWatcher", nsManager, obj, utils.NamespaceRemovalStrategyTrackOnly)
}, },
}) })
+16 -5
View File
@@ -152,9 +152,7 @@ func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, compo
return return
} }
if NamespaceBecameUnmanaged(oldNamespace, newNamespace) { if NamespaceBecameUnmanaged(oldNamespace, newNamespace) {
_, _ = RecordNamespaceRemoval(manager, newNamespace, NamespaceSourceWatcher, time.Now().UTC()) HandleWatcherNamespaceRemoval(ctx, logger, component, manager, newNamespace, NamespaceRemovalStrategyTrackOnly)
logger.Info(component+": namespace removed from manager state; runtime registrations kept",
zap.String("namespace", newNamespace.Name))
return return
} }
if !IsManagedNamespace(newNamespace.Labels) { 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()) record, ok := RecordNamespaceRemoval(manager, obj, NamespaceSourceWatcher, time.Now().UTC())
if !ok { if !ok {
return 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)) zap.String("namespace", record.Name))
} }
+7
View File
@@ -41,6 +41,13 @@ const (
NamespaceEventResync NamespaceEventType = "resync" NamespaceEventResync NamespaceEventType = "resync"
) )
type NamespaceRemovalStrategy string
const (
NamespaceRemovalStrategyTrackOnly NamespaceRemovalStrategy = "track-only"
NamespaceRemovalStrategyDispatchRemove NamespaceRemovalStrategy = "dispatch-remove"
)
type NamespacePartState struct { type NamespacePartState struct {
State string State string
LastError string LastError string
@@ -145,3 +145,12 @@ func TestNamespaceEventFromObject(t *testing.T) {
t.Fatalf("expected empty name for unsupported object") 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")
}
}
+20 -1
View File
@@ -357,13 +357,32 @@ func TestHandleWatcherNamespaceAddUpdateDelete(t *testing.T) {
t.Fatalf("expected update handler to dispatch resync") 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") record, ok := manager.Get("tenant-a")
if !ok || record.Phase != NamespacePhaseRemoved { if !ok || record.Phase != NamespacePhaseRemoved {
t.Fatalf("expected delete handler to mark namespace removed") 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) { func TestNamespaceManagerDispatchAdd(t *testing.T) {
manager := NewNamespaceManager() manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher}) manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})