package api import ( "context" "net/http" "os" "time" "fission-console/internal/fission" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) var requiredBootstrapServiceAccounts = []string{"fission-fetcher", "fission-builder"} var requiredBootstrapRoleBindings = []string{ "fission-executor-user-ns", "fission-router-user-ns", "fission-buildermgr-user-ns", "fission-kubewatcher-user-ns", "fission-timer-user-ns", "fission-fetcher-system-user-ns", "fission-builder-system-user-ns", "fission-fetcher-local-user-ns", "fission-builder-local-user-ns", } type namespacePreparationStatus struct { ManagedByConsole bool `json:"managedByConsole"` WatcherLabel bool `json:"watcherLabel"` ServiceAccounts map[string]bool `json:"serviceAccounts"` RoleBindings map[string]bool `json:"roleBindings"` } func (s namespacePreparationStatus) Ready() bool { if !s.ManagedByConsole || !s.WatcherLabel { return false } for _, ok := range s.ServiceAccounts { if !ok { return false } } for _, ok := range s.RoleBindings { if !ok { return false } } return true } type controlPlaneServiceStatus struct { Ready bool `json:"ready"` PodCount int `json:"podCount"` ReadyPods []string `json:"readyPods"` } type controlPlaneStatus struct { Namespace string `json:"namespace"` Ready bool `json:"ready"` Executor controlPlaneServiceStatus `json:"executor"` Router controlPlaneServiceStatus `json:"router"` } // handleNSStatus возвращает статус инициализации пользовательского namespace. // Используется UI для отображения прогресса при первом входе нового пользователя. // // Три стадии: // 1. Namespace существует и Active // 2. Namespace подготовлен для Layer 1 NSWatcher: label + bootstrap RBAC/SA // 3. Fission control-plane готов: executor и router Running + Ready func (s *Server) handleNSStatus(w http.ResponseWriter, r *http.Request) { ns := s.userNS(r) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() type stageInfo struct { Name string `json:"name"` Done bool `json:"done"` } stages := []stageInfo{ {Name: "Создание пространства имён"}, {Name: "Подготовка namespace для Fission"}, {Name: "Готовность control-plane"}, } nsObj, err := s.dyn.Resource(fission.NamespaceGVR).Get(ctx, ns, metav1.GetOptions{}) var preparation namespacePreparationStatus control := controlPlaneStatus{Namespace: fissionSystemNamespace()} if err == nil { phase, _, _ := unstructured.NestedString(nsObj.Object, "status", "phase") stages[0].Done = phase == "Active" } if stages[0].Done { preparation = s.namespacePreparationStatus(ctx, ns, nsObj) stages[1].Done = preparation.Ready() } if stages[1].Done { control = s.controlPlaneStatus(ctx) stages[2].Done = control.Ready } ready := stages[0].Done && stages[1].Done && stages[2].Done writeAnyJSON(w, http.StatusOK, map[string]any{ "ready": ready, "stages": stages, }) } func (s *Server) handleNSDebug(w http.ResponseWriter, r *http.Request) { ns := s.userNS(r) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() nsObj, err := s.dyn.Resource(fission.NamespaceGVR).Get(ctx, ns, metav1.GetOptions{}) labels := map[string]string{} phase := "" exists := err == nil active := false preparation := namespacePreparationStatus{} if exists { labels, _, _ = unstructured.NestedStringMap(nsObj.Object, "metadata", "labels") phase, _, _ = unstructured.NestedString(nsObj.Object, "status", "phase") active = phase == "Active" preparation = s.namespacePreparationStatus(ctx, ns, nsObj) } control := s.controlPlaneStatus(ctx) writeAnyJSON(w, http.StatusOK, map[string]any{ "namespace": ns, "exists": exists, "active": active, "phase": phase, "labels": labels, "prepared": preparation.Ready(), "preparation": preparation, "controlPlane": control, "namespaceReady": active && preparation.Ready() && control.Ready, }) } func (s *Server) namespacePreparationStatus(ctx context.Context, ns string, nsObj *unstructured.Unstructured) namespacePreparationStatus { status := namespacePreparationStatus{ ServiceAccounts: make(map[string]bool, len(requiredBootstrapServiceAccounts)), RoleBindings: make(map[string]bool, len(requiredBootstrapRoleBindings)), } if nsObj != nil { labels, _, _ := unstructured.NestedStringMap(nsObj.Object, "metadata", "labels") status.ManagedByConsole = labels["managed-by"] == "fission-console" status.WatcherLabel = labels["fission.io/managed"] == "true" } saGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "serviceaccounts"} for _, name := range requiredBootstrapServiceAccounts { _, err := s.dyn.Resource(saGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) status.ServiceAccounts[name] = err == nil } rbGVR := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"} for _, name := range requiredBootstrapRoleBindings { _, err := s.dyn.Resource(rbGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) status.RoleBindings[name] = err == nil } return status } func (s *Server) controlPlaneStatus(ctx context.Context) controlPlaneStatus { status := controlPlaneStatus{Namespace: fissionSystemNamespace()} status.Executor = s.serviceStatus(ctx, status.Namespace, "executor") status.Router = s.serviceStatus(ctx, status.Namespace, "router") status.Ready = status.Executor.Ready && status.Router.Ready return status } func (s *Server) serviceStatus(ctx context.Context, namespace, service string) controlPlaneServiceStatus { podGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} podList, err := s.dyn.Resource(podGVR).Namespace(namespace).List(ctx, metav1.ListOptions{ LabelSelector: "svc=" + service, }) if err != nil { return controlPlaneServiceStatus{} } status := controlPlaneServiceStatus{PodCount: len(podList.Items)} for _, pod := range podList.Items { if podReady(&pod) { status.Ready = true status.ReadyPods = append(status.ReadyPods, pod.GetName()) } } return status } func fissionSystemNamespace() string { ns := os.Getenv("FISSION_SYSTEM_NAMESPACE") if ns == "" { return "fission" } return ns } func podReady(pod *unstructured.Unstructured) bool { if pod == nil { return false } phase, _, _ := unstructured.NestedString(pod.Object, "status", "phase") if phase != "Running" { return false } conditions, _, _ := unstructured.NestedSlice(pod.Object, "status", "conditions") for _, c := range conditions { cond, ok := c.(map[string]any) if !ok { continue } if cond["type"] == "Ready" && cond["status"] == "True" { return true } } return false }