From ac2638f17d782228f88e730e28dbd53007e83772 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 26 Apr 2026 09:54:23 +0300 Subject: [PATCH] layer1: add namespace manager model step 7 --- .../2026-04-26-namespace-manager-step7.md | 36 +++++++ pkg/utils/namespace_manager_model.go | 95 +++++++++++++++++++ pkg/utils/namespace_manager_model_test.go | 67 +++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 doc/thinking/2026-04-26-namespace-manager-step7.md create mode 100644 pkg/utils/namespace_manager_model.go create mode 100644 pkg/utils/namespace_manager_model_test.go diff --git a/doc/thinking/2026-04-26-namespace-manager-step7.md b/doc/thinking/2026-04-26-namespace-manager-step7.md new file mode 100644 index 00000000..4b3d47be --- /dev/null +++ b/doc/thinking/2026-04-26-namespace-manager-step7.md @@ -0,0 +1,36 @@ +# 2026-04-26 — NamespaceManager rewrite, step 7 + +## Цель шага + +Добавить минимальную модель данных для будущего `NamespaceManager`, не меняя пока production wiring. + +## Почему это отдельный шаг + +После шагов 1-6 уже стало ясно, что следующая стадия — не ещё один patch по месту, а переход к явной модели lifecycle. + +Но сразу подключать новый manager к watcher-ам и компонентам рано. Сначала нужна опорная модель: + +- `NamespacePhase` +- `NamespaceSource` +- `NamespaceEventType` +- `NamespaceRecord` +- `NamespacePartState` + +## Что меняем + +1. Добавляем новый файл с типами model layer. +2. Добавляем helper-методы: + - `Clone()` + - `IsActive()` + - `IsTerminal()` +3. Добавляем unit tests на: + - корректный deep copy; + - active semantics; + - terminal semantics. + +## Что НЕ меняем + +- не подключаем manager к production path; +- не меняем watcher-ы; +- не меняем resolver; +- не затрагиваем текущее изменение в `serviceaccount.go`. \ No newline at end of file diff --git a/pkg/utils/namespace_manager_model.go b/pkg/utils/namespace_manager_model.go new file mode 100644 index 00000000..b610a08a --- /dev/null +++ b/pkg/utils/namespace_manager_model.go @@ -0,0 +1,95 @@ +package utils + +import "time" + +type NamespacePhase string + +const ( + NamespacePhaseDiscovered NamespacePhase = "discovered" + NamespacePhaseRegistering NamespacePhase = "registering" + NamespacePhaseActive NamespacePhase = "active" + NamespacePhaseDeregistering NamespacePhase = "deregistering" + NamespacePhaseRemoved NamespacePhase = "removed" + NamespacePhaseFailed NamespacePhase = "failed" +) + +type NamespaceSource string + +const ( + NamespaceSourceEnv NamespaceSource = "env" + NamespaceSourceWatcher NamespaceSource = "watcher" + NamespaceSourceBackfill NamespaceSource = "backfill" +) + +type NamespaceEventType string + +const ( + NamespaceEventAdd NamespaceEventType = "add" + NamespaceEventUpdate NamespaceEventType = "update" + NamespaceEventRemove NamespaceEventType = "remove" + NamespaceEventResync NamespaceEventType = "resync" +) + +type NamespacePartState struct { + State string + LastError string + UpdatedAt time.Time +} + +type NamespaceRecord struct { + Name string + Source NamespaceSource + Labels map[string]string + Phase NamespacePhase + LastError string + Generation int64 + UpdatedAt time.Time + RegisteredParts map[string]NamespacePartState +} + +type NamespaceEvent struct { + Type NamespaceEventType + Name string + Labels map[string]string + Source NamespaceSource + ObservedAt time.Time +} + +func (nr NamespaceRecord) Clone() NamespaceRecord { + clone := nr + clone.Labels = cloneStringMap(nr.Labels) + clone.RegisteredParts = cloneNamespacePartStates(nr.RegisteredParts) + return clone +} + +func (nr NamespaceRecord) IsActive() bool { + return nr.Phase == NamespacePhaseActive +} + +func (nr NamespaceRecord) IsTerminal() bool { + return nr.Phase == NamespacePhaseRemoved || nr.Phase == NamespacePhaseFailed +} + +func cloneStringMap(input map[string]string) map[string]string { + if input == nil { + return nil + } + + clone := make(map[string]string, len(input)) + for key, value := range input { + clone[key] = value + } + return clone +} + +func cloneNamespacePartStates(input map[string]NamespacePartState) map[string]NamespacePartState { + if input == nil { + return nil + } + + clone := make(map[string]NamespacePartState, len(input)) + for key, value := range input { + clone[key] = value + } + return clone +} \ No newline at end of file diff --git a/pkg/utils/namespace_manager_model_test.go b/pkg/utils/namespace_manager_model_test.go new file mode 100644 index 00000000..bfa68b2d --- /dev/null +++ b/pkg/utils/namespace_manager_model_test.go @@ -0,0 +1,67 @@ +package utils + +import ( + "testing" + "time" +) + +func TestNamespaceRecordClone(t *testing.T) { + now := time.Now().UTC() + record := NamespaceRecord{ + Name: "tenant-a", + Source: NamespaceSourceWatcher, + Phase: NamespacePhaseRegistering, + Generation: 7, + UpdatedAt: now, + Labels: map[string]string{ + "fission.io/managed": "true", + }, + RegisteredParts: map[string]NamespacePartState{ + "router": { + State: "active", + UpdatedAt: now, + }, + }, + } + + clone := record.Clone() + clone.Labels["fission.io/managed"] = "false" + clone.RegisteredParts["router"] = NamespacePartState{State: "failed", LastError: "boom"} + + if record.Labels["fission.io/managed"] != "true" { + t.Fatalf("expected labels to be deep copied") + } + + if record.RegisteredParts["router"].State != "active" { + t.Fatalf("expected registered parts to be deep copied") + } +} + +func TestNamespaceRecordIsActive(t *testing.T) { + record := NamespaceRecord{Phase: NamespacePhaseActive} + if !record.IsActive() { + t.Fatalf("expected active phase to be reported as active") + } + + record.Phase = NamespacePhaseRegistering + if record.IsActive() { + t.Fatalf("expected non-active phase to be reported as inactive") + } +} + +func TestNamespaceRecordIsTerminal(t *testing.T) { + for _, test := range []struct { + phase NamespacePhase + expected bool + }{ + {phase: NamespacePhaseRemoved, expected: true}, + {phase: NamespacePhaseFailed, expected: true}, + {phase: NamespacePhaseActive, expected: false}, + {phase: NamespacePhaseRegistering, expected: false}, + } { + record := NamespaceRecord{Phase: test.phase} + if record.IsTerminal() != test.expected { + t.Fatalf("expected terminal=%v for phase %s", test.expected, test.phase) + } + } +} \ No newline at end of file