diff --git a/console/main.go b/console/main.go index b5614fd..2856d67 100644 --- a/console/main.go +++ b/console/main.go @@ -800,7 +800,18 @@ func namespaceFromJWT(token string) (string, error) { } // ensureUserNamespace создаёт K8s namespace и shared environments если не существуют. -// addNSToFission dynamically adds ns to FISSION_RESOURCE_NAMESPACES on all Fission deployments. +// addNSToFission динамически добавляет namespace в FISSION_RESOURCE_NAMESPACES у всех Fission deployments. +// +// Зачем это нужно: Fission компоненты смотрят только те namespace-ы, что указаны в +// FISSION_RESOURCE_NAMESPACES. Если не добавить новый namespace — executor/router не будут +// создавать пулы и обрабатывать триггеры, invoke вернёт 404. +// +// Алгоритм: +// 1. Читаем текущее значение FISSION_RESOURCE_NAMESPACES из router deployment. +// 2. Если namespace уже в списке — выходим (idempotent). +// 3. Иначе добавляем namespace к списку и патчим все Fission deployments через StrategicMergePatch. +// +// StrategicMergePatch позволяет обновить только одну env переменную не трогая остальные. func (s *server) addNSToFission(ctx context.Context, ns string) error { fissionNS := os.Getenv("FISSION_SYSTEM_NAMESPACE") if fissionNS == "" { @@ -808,20 +819,23 @@ func (s *server) addNSToFission(ctx context.Context, ns string) error { } fissionDeployments := []string{"router", "executor", "buildermgr", "kubewatcher", "timer"} - // Читаем текущее значение из router + // Читаем текущее значение FISSION_RESOURCE_NAMESPACES из router deployment. + // Используем router как источник истины — он первым получает изменения. routerDep, err := s.dyn.Resource(deploymentGVR).Namespace(fissionNS).Get(ctx, "router", metav1.GetOptions{}) if err != nil { return fmt.Errorf("get router deployment: %w", err) } - currentVal := "default" - containerName := "router" + currentVal := "default" // fallback если переменная не найдена + containerName := "router" // имя контейнера нужно для StrategicMergePatch containers, _, _ := unstructured.NestedSlice(routerDep.Object, "spec", "template", "spec", "containers") for _, c := range containers { cont, ok := c.(map[string]any) if !ok { continue } + // Запоминаем реальное имя контейнера — оно используется как merge key в StrategicMergePatch. + // Без точного имени патч создаст дублирующий контейнер вместо обновления существующего. if n, ok := cont["name"].(string); ok { containerName = n } @@ -837,7 +851,7 @@ func (s *server) addNSToFission(ctx context.Context, ns string) error { } } } - break + break // берём только первый контейнер } // Проверяем что namespace ещё не в списке @@ -848,7 +862,9 @@ func (s *server) addNSToFission(ctx context.Context, ns string) error { } newVal := currentVal + "," + ns - // Патчим все Fission deployments + // Патчим все Fission deployments одним и тем же значением. + // StrategicMergePatch обновляет только указанные поля (env var), не затрагивая остальные. + // Обычный MergePatch заменил бы весь массив containers — нельзя использовать. patch := map[string]any{ "spec": map[string]any{ "template": map[string]any{ @@ -873,7 +889,8 @@ func (s *server) addNSToFission(ctx context.Context, ns string) error { return fmt.Errorf("marshal patch: %w", err) } for _, dep := range fissionDeployments { - // container name совпадает с deployment name в fission + // В Fission каждый deployment имеет один контейнер с тем же именем что и deployment. + // Подставляем имя контейнера под конкретный deployment для корректного merge key. patch["spec"].(map[string]any)["template"].(map[string]any)["spec"].(map[string]any)["containers"].([]any)[0].(map[string]any)["name"] = dep patchBytes, _ = json.Marshal(patch) _, patchErr := s.dyn.Resource(deploymentGVR).Namespace(fissionNS).Patch( @@ -906,43 +923,60 @@ func (s *server) ensureUserNamespace(ctx context.Context, ns string) error { return fmt.Errorf("create namespace %s: %w", ns, err) } - // 1b. Создаём RoleBindings для Fission SA (всегда — idempotent через IsAlreadyExists) - fissionSAs := []string{"fission-executor", "fission-router", "fission-buildermgr", "fission-kubewatcher", "fission-timer", "fission-fetcher", "fission-builder"} - fissionSysNS := os.Getenv("FISSION_SYSTEM_NAMESPACE") - if fissionSysNS == "" { - fissionSysNS = "fission" - } - for _, sa := range fissionSAs { - rbObj := &unstructured.Unstructured{ - Object: map[string]any{ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "RoleBinding", - "metadata": map[string]any{ - "name": "fission-" + sa + "-user-ns", - "namespace": ns, - }, - "roleRef": map[string]any{ - "apiGroup": "rbac.authorization.k8s.io", - "kind": "ClusterRole", - "name": "cluster-admin", - }, - "subjects": []any{ - map[string]any{ - "kind": "ServiceAccount", - "name": sa, - "namespace": fissionSysNS, - }, - }, - }, - } - rbGVR := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"} - _, rbErr := s.dyn.Resource(rbGVR).Namespace(ns).Create(ctx, rbObj, metav1.CreateOptions{}) - if rbErr != nil && !apierrors.IsAlreadyExists(rbErr) { - log.Printf("ensureUserNamespace: create rolebinding %s/%s: %v", ns, sa, rbErr) - } - } +// 1b. RoleBindings для Fission SA в user namespace. + // + // Проблема: Fission компоненты (executor, router, buildermgr и др.) работают в namespace + // "fission", но при добавлении нового namespace в FISSION_RESOURCE_NAMESPACES они начинают + // туда смотреть (list/watch). По умолчанию у их SA нет прав в чужих namespace-ах → + // "forbidden: cannot list environments.fission.io in namespace X". + // + // Почему cluster-admin, а не admin: + // ClusterRole "admin" не включает custom resource группы (fission.io/*). + // Fission executor при старте пытается создать Role с правами на fission.io/packages, + // и получает "attempting to grant RBAC permissions not currently held" — RBAC escalation + // prevention. ClusterRole "cluster-admin" в контексте RoleBinding (не ClusterRoleBinding) + // даёт полный доступ ТОЛЬКО внутри конкретного namespace — это безопасно. + // + // Операция idempotent: если RoleBinding уже существует — IsAlreadyExists игнорируется. + fissionSAs := []string{"fission-executor", "fission-router", "fission-buildermgr", "fission-kubewatcher", "fission-timer", "fission-fetcher", "fission-builder"} + fissionSysNS := os.Getenv("FISSION_SYSTEM_NAMESPACE") + if fissionSysNS == "" { + fissionSysNS = "fission" + } + rbGVR := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"} + for _, sa := range fissionSAs { + rbObj := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": map[string]any{ + "name": "fission-" + sa + "-user-ns", + "namespace": ns, + }, + "roleRef": map[string]any{ + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "cluster-admin", // namespace-scoped через RoleBinding, не ClusterRoleBinding + }, + "subjects": []any{ + map[string]any{ + "kind": "ServiceAccount", + "name": sa, + "namespace": fissionSysNS, + }, + }, + }, + } + _, rbErr := s.dyn.Resource(rbGVR).Namespace(ns).Create(ctx, rbObj, metav1.CreateOptions{}) + if rbErr != nil && !apierrors.IsAlreadyExists(rbErr) { + log.Printf("ensureUserNamespace: create rolebinding %s/%s: %v", ns, sa, rbErr) + } + } - // 1c. Патчим Fission если namespace новый + // 1c. Регистрируем новый namespace в Fission (FISSION_RESOURCE_NAMESPACES). + // Только при первом создании — повторный патч не нужен, Fission уже знает о namespace. + // addNSToFission читает текущее значение переменной у router-а, добавляет ns и патчит + // все Fission deployments (router, executor, buildermgr, kubewatcher, timer). if newlyCreated { if patchErr := s.addNSToFission(ctx, ns); patchErr != nil { log.Printf("ensureUserNamespace: addNSToFission: %v", patchErr)