diff --git a/doc/thinking/2026-04-26-namespace-manager-step2.md b/doc/thinking/2026-04-26-namespace-manager-step2.md new file mode 100644 index 00000000..c9eb54d5 --- /dev/null +++ b/doc/thinking/2026-04-26-namespace-manager-step2.md @@ -0,0 +1,33 @@ +# 2026-04-26 — NamespaceManager rewrite, step 2 + +## Цель шага + +Убрать еще один прямой проход по `FissionResourceNS` и закрыть конкретный баг в +`pkg/utils/serviceaccount.go`. + +## Проблема + +`runSACheck()` сейчас: + +1. итерируется по `sa.nsResolver.FissionResourceNS` напрямую; +2. переиспользует переменную `ns` внутри внутреннего цикла по permissions. + +Из-за этого код выглядит безобидно, но фактически смешивает два разных namespace path: + +- fetcher path через `GetFunctionNS()`; +- builder path через `GetBuilderNS()`. + +Если `FunctionNamespace` и `BuilderNamespace` различаются, builder SA может начать +резолвиться уже не от исходного namespace, а от результата предыдущего шага цикла. + +## Что меняем + +1. Берем base namespaces через thread-safe `Snapshot()`. +2. Для каждого permission вычисляем `targetNS` из исходного `baseNS`, а не из мутированной переменной. +3. Добавляем unit test на routing function/builder namespace. + +## Что НЕ меняем на этом шаге + +- не трогаем глобальные `fetcherCheck` / `builderCheck` структуры; +- не меняем `LocalSubjectAccessReview` path; +- не делаем большой refactor всего SA provisioning. \ No newline at end of file diff --git a/pkg/utils/namespace_test.go b/pkg/utils/namespace_test.go index 3d97abae..06e88a7b 100644 --- a/pkg/utils/namespace_test.go +++ b/pkg/utils/namespace_test.go @@ -262,6 +262,37 @@ func TestNamespaceResolver(t *testing.T) { }) } +func TestServiceAccountResolveSANamespace(t *testing.T) { + sa := &ServiceAccount{ + nsResolver: &NamespaceResolver{ + FunctionNamespace: "fission-function", + BuilderNamespace: "fission-builder", + DefaultNamespace: "default", + }, + } + + t.Run("fetcher uses function namespace for default ns", func(t *testing.T) { + resolvedNS := sa.resolveSANamespace("default", FetcherSAName) + if resolvedNS != "fission-function" { + t.Fatalf("expected function namespace, got %s", resolvedNS) + } + }) + + t.Run("builder uses builder namespace for default ns", func(t *testing.T) { + resolvedNS := sa.resolveSANamespace("default", BuilderSAName) + if resolvedNS != "fission-builder" { + t.Fatalf("expected builder namespace, got %s", resolvedNS) + } + }) + + t.Run("builder keeps tenant namespace unchanged", func(t *testing.T) { + resolvedNS := sa.resolveSANamespace("tenant-a", BuilderSAName) + if resolvedNS != "tenant-a" { + t.Fatalf("expected tenant namespace, got %s", resolvedNS) + } + }) +} + func getFissionNamespaces(builderNS, functionNS, defaultNS string) *NamespaceResolver { return &NamespaceResolver{ FunctionNamespace: functionNS, diff --git a/pkg/utils/serviceaccount.go b/pkg/utils/serviceaccount.go index 8a727039..e39d0b3d 100644 --- a/pkg/utils/serviceaccount.go +++ b/pkg/utils/serviceaccount.go @@ -118,18 +118,22 @@ func getSAObj(kubernetesClient kubernetes.Interface, logger *zap.Logger) *Servic } func (sa *ServiceAccount) runSACheck(ctx context.Context) { - for _, ns := range sa.nsResolver.FissionResourceNS { + for _, baseNS := range sa.nsResolver.Snapshot() { for _, permission := range sa.permissions { - if permission.saName == BuilderSAName { - ns = sa.nsResolver.GetBuilderNS(ns) - } else { - ns = sa.nsResolver.GetFunctionNS(ns) - } - setupSAAndRoleBindings(ctx, sa.kubernetesClient, sa.logger, ns, permission) + targetNS := sa.resolveSANamespace(baseNS, permission.saName) + setupSAAndRoleBindings(ctx, sa.kubernetesClient, sa.logger, targetNS, permission) } } } +func (sa *ServiceAccount) resolveSANamespace(baseNS, saName string) string { + if saName == BuilderSAName { + return sa.nsResolver.GetBuilderNS(baseNS) + } + + return sa.nsResolver.GetFunctionNS(baseNS) +} + func setupSAAndRoleBindings(ctx context.Context, client kubernetes.Interface, logger *zap.Logger, namespace string, ps *ServiceAccountPermissions) { SAObj, err := createGetSA(ctx, client, ps.saName, namespace) if err != nil {