diff --git a/doc/thinking/2026-04-26-namespace-manager-step34.md b/doc/thinking/2026-04-26-namespace-manager-step34.md new file mode 100644 index 00000000..aaf26a02 --- /dev/null +++ b/doc/thinking/2026-04-26-namespace-manager-step34.md @@ -0,0 +1,19 @@ +# 2026-04-26 — NamespaceManager rewrite, step 34 + +## Цель шага + +Подготовить безопасный helper для delete/tombstone событий Namespace informer-а. + +## Что меняем + +1. Добавляем `NamespaceFromObject()`. +2. Helper поддерживает: + - `*corev1.Namespace` + - `cache.DeletedFinalStateUnknown` +3. Добавляем `NamespaceEventFromObject()`. +4. Добавляем unit tests. + +## Что НЕ меняем + +- не подключаем delete handling в watcher-ы на этом шаге; +- не меняем runtime behavior. \ No newline at end of file diff --git a/pkg/utils/namespace_manager_model.go b/pkg/utils/namespace_manager_model.go index 3d504600..fea48ffb 100644 --- a/pkg/utils/namespace_manager_model.go +++ b/pkg/utils/namespace_manager_model.go @@ -4,6 +4,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" + k8sCache "k8s.io/client-go/tools/cache" ) const ( @@ -88,6 +89,32 @@ func NamespaceEventFromNamespace(eventType NamespaceEventType, namespace *corev1 return NewNamespaceEvent(eventType, namespace.Name, namespace.Labels, source, observedAt) } +func NamespaceFromObject(obj interface{}) (*corev1.Namespace, bool) { + switch typed := obj.(type) { + case *corev1.Namespace: + return typed, true + case k8sCache.DeletedFinalStateUnknown: + namespace, ok := typed.Obj.(*corev1.Namespace) + return namespace, ok + case *k8sCache.DeletedFinalStateUnknown: + if typed == nil { + return nil, false + } + namespace, ok := typed.Obj.(*corev1.Namespace) + return namespace, ok + default: + return nil, false + } +} + +func NamespaceEventFromObject(eventType NamespaceEventType, obj interface{}, source NamespaceSource, observedAt time.Time) NamespaceEvent { + namespace, ok := NamespaceFromObject(obj) + if !ok { + return NewNamespaceEvent(eventType, "", nil, source, observedAt) + } + return NamespaceEventFromNamespace(eventType, namespace, source, observedAt) +} + func (nr NamespaceRecord) Clone() NamespaceRecord { clone := nr clone.Labels = cloneStringMap(nr.Labels) diff --git a/pkg/utils/namespace_manager_model_test.go b/pkg/utils/namespace_manager_model_test.go index bf0664d0..a6a599b3 100644 --- a/pkg/utils/namespace_manager_model_test.go +++ b/pkg/utils/namespace_manager_model_test.go @@ -5,6 +5,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" + k8sCache "k8s.io/client-go/tools/cache" ) func TestNamespaceRecordClone(t *testing.T) { @@ -107,3 +108,40 @@ func TestNamespaceEventFromNamespace(t *testing.T) { t.Fatalf("expected event labels to stay detached from original namespace") } } + +func TestNamespaceFromObject(t *testing.T) { + namespace := &corev1.Namespace{} + namespace.Name = "tenant-a" + + extracted, ok := NamespaceFromObject(namespace) + if !ok || extracted.Name != "tenant-a" { + t.Fatalf("expected namespace object to be extracted directly") + } + + extracted, ok = NamespaceFromObject(k8sCache.DeletedFinalStateUnknown{Obj: namespace}) + if !ok || extracted.Name != "tenant-a" { + t.Fatalf("expected namespace tombstone to be extracted") + } +} + +func TestNamespaceEventFromObject(t *testing.T) { + namespace := &corev1.Namespace{} + namespace.Name = "tenant-a" + namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue} + + event := NamespaceEventFromObject(NamespaceEventRemove, k8sCache.DeletedFinalStateUnknown{Obj: namespace}, NamespaceSourceWatcher, time.Now().UTC()) + if event.Type != NamespaceEventRemove { + t.Fatalf("expected remove event type") + } + if event.Name != "tenant-a" { + t.Fatalf("expected tenant-a event name") + } + if event.Labels[ManagedNamespaceLabelKey] != ManagedNamespaceLabelValue { + t.Fatalf("expected labels copied from tombstone namespace") + } + + emptyEvent := NamespaceEventFromObject(NamespaceEventRemove, "not-a-namespace", NamespaceSourceWatcher, time.Now().UTC()) + if emptyEvent.Name != "" { + t.Fatalf("expected empty name for unsupported object") + } +}