Files
fission-src/pkg/utils/namespace_manager_test.go
“Naeel” 5d52c9ba94 test: add integration test for NSWatcher with fake Kubernetes client
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).
2026-05-15 07:11:48 +04:00

923 lines
34 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"context"
"errors"
"reflect"
"testing"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sfake "k8s.io/client-go/kubernetes/fake"
k8sCache "k8s.io/client-go/tools/cache"
managerPkg "github.com/fission/fission/pkg/utils/manager"
)
type testNamespaceSubscriber struct {
name string
addErr error
removeErr error
resyncErr error
addCalls int
removeCalls int
resyncCalls int
}
func (s *testNamespaceSubscriber) Name() string {
return s.name
}
func (s *testNamespaceSubscriber) OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error {
s.addCalls++
return s.addErr
}
func (s *testNamespaceSubscriber) OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error {
s.removeCalls++
return s.removeErr
}
func (s *testNamespaceSubscriber) OnNamespaceResync(ctx context.Context, record NamespaceRecord) error {
s.resyncCalls++
return s.resyncErr
}
func TestNamespaceManagerSnapshotAndGet(t *testing.T) {
manager := NewNamespaceManager()
now := time.Now().UTC()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-b", Source: NamespaceSourceWatcher, ObservedAt: now})
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher, ObservedAt: now})
expected := []string{"tenant-a", "tenant-b"}
if !reflect.DeepEqual(expected, manager.Snapshot()) {
t.Fatalf("expected snapshot %v, got %v", expected, manager.Snapshot())
}
record, ok := manager.Get("tenant-a")
if !ok {
t.Fatalf("expected tenant-a to exist")
}
if record.Name != "tenant-a" || record.Phase != NamespacePhaseDiscovered {
t.Fatalf("unexpected record returned: %+v", record)
}
summary := manager.Summary()
if summary.TotalNamespaces != 2 {
t.Fatalf("expected summary total namespaces to be 2, got %d", summary.TotalNamespaces)
}
if summary.LiveNamespaces != 2 {
t.Fatalf("expected live namespaces to be 2, got %d", summary.LiveNamespaces)
}
if summary.SourceCounts[NamespaceSourceWatcher] != 2 {
t.Fatalf("expected watcher source count to be 2")
}
}
func TestNamespaceManagerUpsertIncrementsGeneration(t *testing.T) {
manager := NewNamespaceManager()
now := time.Now().UTC()
first := manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher, ObservedAt: now})
second := manager.Upsert(NamespaceEvent{Type: NamespaceEventUpdate, Name: "tenant-a", Source: NamespaceSourceWatcher, ObservedAt: now.Add(time.Second)})
if first.Generation != 1 {
t.Fatalf("expected first generation to be 1, got %d", first.Generation)
}
if second.Generation != 2 {
t.Fatalf("expected second generation to be 2, got %d", second.Generation)
}
}
func TestNamespaceManagerMarkPartState(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
record, ok := manager.MarkPartState("tenant-a", "router", NamespacePartState{State: NamespacePartStateActive})
if !ok {
t.Fatalf("expected mark part state to succeed")
}
if record.RegisteredParts["router"].State != NamespacePartStateActive {
t.Fatalf("expected router part state to be stored")
}
if record.Phase != NamespacePhaseActive {
t.Fatalf("expected phase to become active, got %s", record.Phase)
}
}
func TestNamespaceManagerRemove(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
if !manager.Remove("tenant-a") {
t.Fatalf("expected remove to succeed")
}
if _, ok := manager.Get("tenant-a"); ok {
t.Fatalf("expected record to be removed")
}
if manager.Remove("tenant-a") {
t.Fatalf("expected second remove to report false")
}
}
func TestNamespaceManagerSnapshotRecordsReturnsCopies(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{
Type: NamespaceEventAdd,
Name: "tenant-a",
Source: NamespaceSourceWatcher,
Labels: map[string]string{"fission.io/managed": "true"},
})
_, _ = manager.MarkPartState("tenant-a", "router", NamespacePartState{State: NamespacePartStateActive})
records := manager.SnapshotRecords()
records[0].Labels["fission.io/managed"] = "false"
records[0].RegisteredParts["router"] = NamespacePartState{State: "failed"}
record, ok := manager.Get("tenant-a")
if !ok {
t.Fatalf("expected tenant-a to exist")
}
if record.Labels["fission.io/managed"] != "true" {
t.Fatalf("expected snapshot records to be detached copies")
}
if record.RegisteredParts["router"].State != NamespacePartStateActive {
t.Fatalf("expected part states to be detached copies")
}
}
func TestNamespaceManagerSubscribers(t *testing.T) {
manager := NewNamespaceManager()
manager.Subscribe(&testNamespaceSubscriber{name: "router"})
manager.Subscribe(&testNamespaceSubscriber{name: "buildermgr"})
manager.Subscribe(&testNamespaceSubscriber{name: "router"})
expected := []string{"buildermgr", "router"}
if !reflect.DeepEqual(expected, manager.SnapshotSubscribers()) {
t.Fatalf("expected subscribers %v, got %v", expected, manager.SnapshotSubscribers())
}
}
func TestNamespaceManagerMarkPartStateDerivesPhase(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
record, ok := manager.MarkPartState("tenant-a", "router", NamespacePartState{State: NamespacePartStateRegistering})
if !ok {
t.Fatalf("expected registering state update to succeed")
}
if record.Phase != NamespacePhaseRegistering {
t.Fatalf("expected phase registering, got %s", record.Phase)
}
record, ok = manager.MarkPartState("tenant-a", "router", NamespacePartState{State: NamespacePartStateFailed})
if !ok {
t.Fatalf("expected failed state update to succeed")
}
if record.Phase != NamespacePhaseFailed {
t.Fatalf("expected phase failed, got %s", record.Phase)
}
}
func TestNamespaceManagerBootstrap(t *testing.T) {
manager := NewNamespaceManager()
now := time.Now().UTC()
records := manager.Bootstrap([]string{"tenant-b", "tenant-a"}, NamespaceSourceBackfill, now)
if len(records) != 2 {
t.Fatalf("expected 2 bootstrap records, got %d", len(records))
}
if !reflect.DeepEqual([]string{"tenant-a", "tenant-b"}, manager.Snapshot()) {
t.Fatalf("expected manager snapshot to contain bootstrapped namespaces")
}
record, ok := manager.Get("tenant-a")
if !ok {
t.Fatalf("expected tenant-a after bootstrap")
}
if record.Source != NamespaceSourceBackfill {
t.Fatalf("expected bootstrap source backfill, got %s", record.Source)
}
}
func TestNamespaceManagerBootstrapAndDispatch(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager.Subscribe(router)
manager.Subscribe(builder)
records, err := manager.BootstrapAndDispatch(context.Background(), []string{"tenant-b", "tenant-a"}, NamespaceSourceBackfill, time.Now().UTC())
if err != nil {
t.Fatalf("expected bootstrap and dispatch success: %v", err)
}
if len(records) != 2 {
t.Fatalf("expected 2 records, got %d", len(records))
}
if router.addCalls != 2 || builder.addCalls != 2 {
t.Fatalf("expected both subscribers to be called for each namespace")
}
if records[0].Phase != NamespacePhaseActive || records[1].Phase != NamespacePhaseActive {
t.Fatalf("expected active records after bootstrap dispatch")
}
}
func TestNamespaceManagerBootstrapAndDispatchFailure(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router", addErr: errors.New("router failed")}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager.Subscribe(router)
manager.Subscribe(builder)
records, err := manager.BootstrapAndDispatch(context.Background(), []string{"tenant-a"}, NamespaceSourceBackfill, time.Now().UTC())
if err == nil {
t.Fatalf("expected bootstrap dispatch failure")
}
if len(records) != 1 {
t.Fatalf("expected single record result")
}
if records[0].Phase != NamespacePhaseFailed {
t.Fatalf("expected failed phase after subscriber error, got %s", records[0].Phase)
}
if records[0].RegisteredParts["router"].LastError != "router failed" {
t.Fatalf("expected router failure to be stored")
}
}
func TestNamespaceManagerPartStateHelpers(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
record, ok := manager.MarkPartRegistering("tenant-a", "router")
if !ok || record.Phase != NamespacePhaseRegistering {
t.Fatalf("expected registering helper to set registering phase")
}
record, ok = manager.MarkPartActive("tenant-a", "router")
if !ok || record.Phase != NamespacePhaseActive {
t.Fatalf("expected active helper to set active phase")
}
record, ok = manager.MarkPartFailed("tenant-a", "router", errors.New("boom"))
if !ok || record.Phase != NamespacePhaseFailed {
t.Fatalf("expected failed helper to set failed phase")
}
if record.RegisteredParts["router"].LastError != "boom" {
t.Fatalf("expected failed helper to persist error")
}
}
func TestNewBootstrappedNamespaceManager(t *testing.T) {
resolver := &NamespaceResolver{
FissionResourceNS: map[string]string{
"tenant-b": "tenant-b",
"tenant-a": "tenant-a",
},
}
manager := NewBootstrappedNamespaceManager(resolver, NamespaceSourceEnv, time.Now().UTC())
if !reflect.DeepEqual([]string{"tenant-a", "tenant-b"}, manager.Snapshot()) {
t.Fatalf("expected bootstrapped manager snapshot from resolver")
}
record, ok := manager.Get("tenant-a")
if !ok {
t.Fatalf("expected tenant-a in bootstrapped manager")
}
if record.Source != NamespaceSourceEnv {
t.Fatalf("expected env source, got %s", record.Source)
}
}
func TestNewWatcherNamespaceManager(t *testing.T) {
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager, err := NewWatcherNamespaceManager(context.Background(), []string{"tenant-b", "tenant-a"}, NamespaceSourceEnv, time.Now().UTC(), router, builder)
if err != nil {
t.Fatalf("expected watcher manager bootstrap success: %v", err)
}
if !reflect.DeepEqual([]string{"tenant-a", "tenant-b"}, manager.Snapshot()) {
t.Fatalf("expected watcher manager snapshot to match bootstrapped namespaces")
}
if router.addCalls != 2 || builder.addCalls != 2 {
t.Fatalf("expected all subscribers to receive bootstrap dispatch calls")
}
}
func TestNamespaceBecameUnmanaged(t *testing.T) {
oldNamespace := &corev1.Namespace{}
oldNamespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
newNamespace := &corev1.Namespace{}
newNamespace.Labels = map[string]string{}
if !NamespaceBecameUnmanaged(oldNamespace, newNamespace) {
t.Fatalf("expected namespace to become unmanaged")
}
if NamespaceBecameUnmanaged(nil, newNamespace) {
t.Fatalf("expected nil old namespace to report false")
}
}
func TestDispatchNamespaceAddAndResync(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router"}
manager.Subscribe(router)
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
record, ok, err := DispatchNamespaceAdd(context.Background(), manager, namespace, NamespaceSourceWatcher, time.Now().UTC())
if !ok || err != nil || record.Phase != NamespacePhaseActive {
t.Fatalf("expected add dispatch success, ok=%v err=%v phase=%s", ok, err, record.Phase)
}
record, ok, err = DispatchNamespaceResync(context.Background(), manager, namespace, NamespaceSourceWatcher, time.Now().UTC())
if !ok || err != nil || record.Phase != NamespacePhaseActive {
t.Fatalf("expected resync dispatch success, ok=%v err=%v phase=%s", ok, err, record.Phase)
}
if router.addCalls != 1 || router.resyncCalls != 1 {
t.Fatalf("expected add and resync subscriber calls")
}
}
func TestRecordNamespaceRemoval(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
record, ok := RecordNamespaceRemoval(manager, k8sCache.DeletedFinalStateUnknown{Obj: namespace}, NamespaceSourceWatcher, time.Now().UTC())
if !ok {
t.Fatalf("expected removal bookkeeping to succeed")
}
if record.Phase != NamespacePhaseRemoved {
t.Fatalf("expected removed phase after bookkeeping, got %s", record.Phase)
}
}
func TestHandleWatcherNamespaceAddUpdateDelete(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router"}
manager.Subscribe(router)
logger := zap.NewNop()
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
HandleWatcherNamespaceAdd(context.Background(), logger, "router.NSWatcher", manager, namespace)
if router.addCalls != 1 {
t.Fatalf("expected add handler to dispatch add")
}
HandleWatcherNamespaceUpdate(context.Background(), logger, "router.NSWatcher", manager, namespace, namespace, NamespaceRemovalStrategyTrackOnly)
if router.resyncCalls != 1 {
t.Fatalf("expected update handler to dispatch resync")
}
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 TestHandleWatcherNamespaceAddLogsSummary(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router"}
manager.Subscribe(router)
core, logs := observer.New(zap.InfoLevel)
logger := zap.New(core)
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
HandleWatcherNamespaceAdd(context.Background(), logger, "router.NSWatcher", manager, namespace)
entries := logs.AllUntimed()
if len(entries) != 1 {
t.Fatalf("expected one summary log entry, got %d", len(entries))
}
if entries[0].Message != "router.NSWatcher: namespace manager summary" {
t.Fatalf("unexpected log message: %s", entries[0].Message)
}
fields := entries[0].ContextMap()
active, ok := fields["has_active_namespaces"].(bool)
if !ok || !active {
t.Fatalf("expected summary log to report active namespaces")
}
if live, ok := fields["live_namespaces"].(int64); !ok || live != 1 {
t.Fatalf("expected summary log to report one live namespace, got %v", fields["live_namespaces"])
}
}
func TestHandleWatcherNamespaceAddWithNilLogger(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router", addErr: errors.New("add failed")}
manager.Subscribe(router)
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
HandleWatcherNamespaceAdd(context.Background(), nil, "router.NSWatcher", manager, namespace)
record, ok := manager.Get("tenant-a")
if !ok {
t.Fatalf("expected tenant-a to exist after add handling")
}
if record.Phase != NamespacePhaseFailed {
t.Fatalf("expected failed phase after subscriber error, got %s", record.Phase)
}
if router.addCalls != 1 {
t.Fatalf("expected failing subscriber to be called once")
}
}
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 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 TestNewNamespaceWatcherEventHandlers(t *testing.T) {
manager := NewNamespaceManager()
router := &testNamespaceSubscriber{name: "router"}
manager.Subscribe(router)
logger := zap.NewNop()
handlers := NewNamespaceWatcherEventHandlers(context.Background(), logger, "router.NSWatcher", manager, NamespaceRemovalStrategyTrackOnly)
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
handlers.AddFunc(namespace)
handlers.UpdateFunc(namespace, namespace)
handlers.DeleteFunc(k8sCache.DeletedFinalStateUnknown{Obj: namespace})
record, ok := manager.Get("tenant-a")
if !ok || record.Phase != NamespacePhaseRemoved {
t.Fatalf("expected watcher event handlers to drive namespace lifecycle")
}
if router.addCalls != 1 || router.resyncCalls != 1 {
t.Fatalf("expected add and resync calls through event handlers")
}
}
func TestPrepareManagedNamespaceWatcher(t *testing.T) {
router := &testNamespaceSubscriber{name: "router"}
logger := zap.NewNop()
manager, handlers, err := PrepareManagedNamespaceWatcher(context.Background(), logger, ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: []string{"tenant-a"},
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: router,
})
if err != nil {
t.Fatalf("expected watcher preparation success: %v", err)
}
if !reflect.DeepEqual([]string{"tenant-a"}, manager.Snapshot()) {
t.Fatalf("expected watcher manager snapshot to contain bootstrapped namespace")
}
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
handlers.UpdateFunc(namespace, namespace)
if router.addCalls != 1 || router.resyncCalls != 1 {
t.Fatalf("expected bootstrap add and handler resync calls")
}
}
func TestRunManagedNamespaceWatcher(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
router := &testNamespaceSubscriber{name: "router"}
logger := zap.NewNop()
manager, err := RunManagedNamespaceWatcher(ctx, logger, k8sfake.NewSimpleClientset(), managerPkg.New(), ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: []string{"tenant-a"},
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: router,
})
if err != nil {
t.Fatalf("expected managed namespace watcher run success: %v", err)
}
if !reflect.DeepEqual([]string{"tenant-a"}, manager.Snapshot()) {
t.Fatalf("expected watcher manager snapshot to contain bootstrapped namespace")
}
}
func TestRunManagedNamespaceWatcherLogsStartedSummary(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
router := &testNamespaceSubscriber{name: "router"}
core, logs := observer.New(zap.InfoLevel)
logger := zap.New(core)
_, err := RunManagedNamespaceWatcher(ctx, logger, k8sfake.NewSimpleClientset(), managerPkg.New(), ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: []string{"tenant-a"},
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: router,
})
if err != nil {
t.Fatalf("expected managed namespace watcher run success: %v", err)
}
entries := logs.FilterMessage("router.NSWatcher: started namespace watcher").AllUntimed()
if len(entries) != 1 {
t.Fatalf("expected one started-summary log entry, got %d", len(entries))
}
fields := entries[0].ContextMap()
if live, ok := fields["live_namespaces"].(int64); !ok || live != 1 {
t.Fatalf("expected started summary to report one live namespace, got %v", fields["live_namespaces"])
}
if active, ok := fields["has_active_namespaces"].(bool); !ok || !active {
t.Fatalf("expected started summary to report active namespaces")
}
if total, ok := fields["total_namespaces"].(int64); !ok || total != 1 {
t.Fatalf("expected started summary to report one total namespace, got %v", fields["total_namespaces"])
}
if got := fields["subscribers"]; got == nil || !reflect.DeepEqual(got, []interface{}{"router"}) && !reflect.DeepEqual(got, []string{"router"}) {
t.Fatalf("expected started summary to report router subscriber, got %v", got)
}
if got := fields["phase_counts"]; got == nil {
t.Fatalf("expected started summary to report phase counts")
}
}
func TestRunManagedNamespaceWatcherWithNilLogger(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
router := &testNamespaceSubscriber{name: "router"}
manager, err := RunManagedNamespaceWatcher(ctx, nil, k8sfake.NewSimpleClientset(), managerPkg.New(), ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: []string{"tenant-a"},
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: router,
})
if err != nil {
t.Fatalf("expected managed namespace watcher run success: %v", err)
}
if !reflect.DeepEqual([]string{"tenant-a"}, manager.Snapshot()) {
t.Fatalf("expected watcher manager snapshot to contain bootstrapped namespace")
}
}
func TestNewDefaultManagedNamespaceWatcherConfig(t *testing.T) {
resolver := DefaultNSResolver()
original := resolver.FissionResourceNS
resolver.FissionResourceNS = map[string]string{"tenant-a": "tenant-a"}
defer func() {
resolver.FissionResourceNS = original
}()
router := &testNamespaceSubscriber{name: "router"}
config := NewDefaultManagedNamespaceWatcherConfig("router.NSWatcher", router)
if config.Component != "router.NSWatcher" {
t.Fatalf("expected component name to be preserved")
}
if config.RemovalStrategy != NamespaceRemovalStrategyTrackOnly {
t.Fatalf("expected default removal strategy track-only")
}
if !reflect.DeepEqual([]string{"tenant-a"}, config.Namespaces) {
t.Fatalf("expected namespaces from resolver snapshot")
}
if config.Subscriber != router {
t.Fatalf("expected subscriber to be preserved")
}
}
func TestPrepareManagedNamespaceWatcherDefaultsToTrackOnly(t *testing.T) {
router := &testNamespaceSubscriber{name: "router"}
logger := zap.NewNop()
manager, handlers, err := PrepareManagedNamespaceWatcher(context.Background(), logger, ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: []string{"tenant-a"},
Subscriber: router,
})
if err != nil {
t.Fatalf("expected watcher preparation success: %v", err)
}
namespace := &corev1.Namespace{}
namespace.Name = "tenant-a"
namespace.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
handlers.DeleteFunc(k8sCache.DeletedFinalStateUnknown{Obj: namespace})
record, ok := manager.Get("tenant-a")
if !ok || record.Phase != NamespacePhaseRemoved {
t.Fatalf("expected default strategy to keep track-only removal semantics")
}
if router.removeCalls != 0 {
t.Fatalf("expected default strategy not to dispatch remove")
}
}
func TestNamespaceManagerSummary(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-b", Source: NamespaceSourceWatcher})
_, _ = manager.MarkPartRegistering("tenant-a", "router")
manager.Upsert(NamespaceEvent{Type: NamespaceEventRemove, Name: "tenant-b", Source: NamespaceSourceWatcher})
manager.Subscribe(&testNamespaceSubscriber{name: "router"})
summary := manager.Summary()
if summary.TotalNamespaces != 2 {
t.Fatalf("expected total namespaces 2, got %d", summary.TotalNamespaces)
}
if summary.LiveNamespaces != 1 {
t.Fatalf("expected one live namespace, got %d", summary.LiveNamespaces)
}
if summary.PhaseCounts[NamespacePhaseRegistering] != 1 {
t.Fatalf("expected one registering namespace")
}
if summary.PhaseCounts[NamespacePhaseRemoved] != 1 {
t.Fatalf("expected one removed namespace")
}
if !reflect.DeepEqual([]string{"router"}, summary.Subscribers) {
t.Fatalf("expected router subscriber in summary")
}
if summary.SourceCounts[NamespaceSourceWatcher] != 2 {
t.Fatalf("expected two watcher-sourced namespaces")
}
}
func TestLogNamespaceManagerSummary(t *testing.T) {
LogNamespaceManagerSummary(nil, "ignored", NamespaceManagerSummary{})
logger := zap.NewNop()
summary := NamespaceManagerSummary{
TotalNamespaces: 2,
LiveNamespaces: 1,
PhaseCounts: map[NamespacePhase]int{
NamespacePhaseActive: 1,
NamespacePhaseRemoved: 1,
},
SourceCounts: map[NamespaceSource]int{
NamespaceSourceWatcher: 1,
NamespaceSourceEnv: 1,
},
Subscribers: []string{"router", "buildermgr"},
}
LogNamespaceManagerSummary(logger, "namespace summary", summary)
}
func TestLogNamespaceManagerSummaryIncludesHasActiveNamespaces(t *testing.T) {
core, logs := observer.New(zap.InfoLevel)
logger := zap.New(core)
LogNamespaceManagerSummary(logger, "namespace summary", NamespaceManagerSummary{
LiveNamespaces: 1,
PhaseCounts: map[NamespacePhase]int{},
SourceCounts: map[NamespaceSource]int{},
Subscribers: []string{},
})
entries := logs.AllUntimed()
if len(entries) != 1 {
t.Fatalf("expected one log entry, got %d", len(entries))
}
fields := entries[0].ContextMap()
active, ok := fields["has_active_namespaces"].(bool)
if !ok {
t.Fatalf("expected has_active_namespaces field in log context")
}
if !active {
t.Fatalf("expected has_active_namespaces=true in log context")
}
}
func TestNamespaceManagerSummaryEmptyContract(t *testing.T) {
manager := NewNamespaceManager()
summary := manager.Summary()
if summary.PhaseCounts == nil {
t.Fatalf("expected phase counts map to be initialized")
}
if summary.SourceCounts == nil {
t.Fatalf("expected source counts map to be initialized")
}
if summary.Subscribers == nil {
t.Fatalf("expected subscribers slice to be initialized")
}
if len(summary.Subscribers) != 0 {
t.Fatalf("expected no subscribers in empty summary")
}
}
func TestLogNamespaceManagerSummaryWithNilLogger(t *testing.T) {
LogNamespaceManagerSummary(nil, "ignored", NamespaceManagerSummary{TotalNamespaces: 1})
}
func TestNamespaceManagerDispatchAdd(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager.Subscribe(router)
manager.Subscribe(builder)
record, ok, err := manager.DispatchAdd(context.Background(), "tenant-a")
if !ok || err != nil {
t.Fatalf("expected dispatch add success, ok=%v err=%v", ok, err)
}
if router.addCalls != 1 || builder.addCalls != 1 {
t.Fatalf("expected both subscribers to receive add call")
}
if record.Phase != NamespacePhaseActive {
t.Fatalf("expected active phase after successful dispatch, got %s", record.Phase)
}
}
func TestNamespaceManagerDispatchResyncFailure(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr", resyncErr: errors.New("resync failed")}
manager.Subscribe(router)
manager.Subscribe(builder)
record, ok, err := manager.DispatchResync(context.Background(), "tenant-a")
if !ok || err == nil {
t.Fatalf("expected dispatch resync failure, ok=%v err=%v", ok, err)
}
if builder.resyncCalls != 1 {
t.Fatalf("expected failing subscriber to receive resync call")
}
if record.Phase != NamespacePhaseFailed {
t.Fatalf("expected failed phase after dispatch error, got %s", record.Phase)
}
if record.RegisteredParts["buildermgr"].LastError != "resync failed" {
t.Fatalf("expected subscriber error to be stored")
}
}
func TestNamespaceManagerDispatchRemove(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager.Subscribe(router)
manager.Subscribe(builder)
record, ok, err := manager.DispatchRemove(context.Background(), "tenant-a")
if !ok || err != nil {
t.Fatalf("expected dispatch remove success, ok=%v err=%v", ok, err)
}
if router.removeCalls != 1 || builder.removeCalls != 1 {
t.Fatalf("expected both subscribers to receive remove call")
}
if record.Phase != NamespacePhaseRemoved {
t.Fatalf("expected removed phase after dispatch remove, got %s", record.Phase)
}
}
func TestNamespaceManagerDispatchRemoveFailure(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
router := &testNamespaceSubscriber{name: "router", removeErr: errors.New("remove failed")}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager.Subscribe(router)
manager.Subscribe(builder)
record, ok, err := manager.DispatchRemove(context.Background(), "tenant-a")
if !ok || err == nil {
t.Fatalf("expected dispatch remove failure, ok=%v err=%v", ok, err)
}
if record.Phase != NamespacePhaseRemoved {
t.Fatalf("expected removed phase even on dispatch remove error, got %s", record.Phase)
}
}
// TestStartManagedNamespaceWatcherIntegration проверяет полный маршрут:
// fake k8s client → информер → AddFunc → subscriber.OnNamespaceAdd.
// Это интеграционный тест без real cluster — использует k8sfake.
func TestStartManagedNamespaceWatcherIntegration(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
fakeClient := k8sfake.NewSimpleClientset()
mgr := managerPkg.New()
router := &testNamespaceSubscriber{name: "router"}
// Запускаем watcher — не Bootstrap, только informer.
// config.Namespaces пусто, чтобы не было pre-loaded namespace-ов.
manager, err := RunManagedNamespaceWatcher(ctx, zap.NewNop(), fakeClient, mgr, ManagedNamespaceWatcherConfig{
Component: "router.NSWatcher",
Namespaces: nil,
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: router,
})
if err != nil {
t.Fatalf("RunManagedNamespaceWatcher failed: %v", err)
}
// Создаём namespace с managed label в fake client.
ns := &corev1.Namespace{}
ns.Name = "tenant-integration-a"
ns.Labels = map[string]string{ManagedNamespaceLabelKey: ManagedNamespaceLabelValue}
if _, err := fakeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil {
t.Fatalf("failed to create test namespace in fake client: %v", err)
}
// Ждём, пока informer обработает event и subscriber получит вызов.
// Timeout намеренно короткий (3 секунды) — fake client синхронный.
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if router.addCalls >= 1 {
break
}
time.Sleep(10 * time.Millisecond)
}
if router.addCalls == 0 {
t.Fatalf("expected subscriber.OnNamespaceAdd to be called after namespace creation, got 0 calls")
}
if router.addCalls != 1 {
t.Fatalf("expected exactly 1 add call (namespace is new), got %d", router.addCalls)
}
// Namespace должен быть в manager snapshot.
snapshot := manager.Snapshot()
found := false
for _, name := range snapshot {
if name == ns.Name {
found = true
break
}
}
if !found {
t.Fatalf("expected %s in manager snapshot after informer event, got %v", ns.Name, snapshot)
}
record, ok := manager.Get(ns.Name)
if !ok {
t.Fatalf("expected record for %s in manager", ns.Name)
}
if record.Phase != NamespacePhaseActive {
t.Fatalf("expected active phase after informer add, got %s", record.Phase)
}
}
func TestNamespaceSubscriberFuncs(t *testing.T) {
addCalls := 0
removeCalls := 0
resyncCalls := 0
subscriber := NamespaceSubscriberFuncs{
SubscriberName: "router",
AddFunc: func(ctx context.Context, record NamespaceRecord) error {
addCalls++
return nil
},
RemoveFunc: func(ctx context.Context, record NamespaceRecord) error {
removeCalls++
return nil
},
ResyncFunc: func(ctx context.Context, record NamespaceRecord) error {
resyncCalls++
return nil
},
}
if subscriber.Name() != "router" {
t.Fatalf("expected subscriber name router")
}
_ = subscriber.OnNamespaceAdd(context.Background(), NamespaceRecord{Name: "tenant-a"})
_ = subscriber.OnNamespaceRemove(context.Background(), NamespaceRecord{Name: "tenant-a"})
_ = subscriber.OnNamespaceResync(context.Background(), NamespaceRecord{Name: "tenant-a"})
if addCalls != 1 || removeCalls != 1 || resyncCalls != 1 {
t.Fatalf("expected all functional callbacks to run once")
}
}