package api import ( "context" "encoding/json" "net/http" "strings" "time" "admin-console/internal/metrics" "admin-console/internal/model" ) // Server — HTTP-сервер admin-console. type Server struct { adminToken string k8s *metrics.K8sClient } func NewServer(adminToken string) *Server { k8s, err := metrics.NewK8sClient() if err != nil { // вне кластера — работаем без k8s (для локальной разработки) k8s = nil } return &Server{adminToken: adminToken, k8s: k8s} } func (s *Server) RegisterRoutes(mux *http.ServeMux) { // статика UI mux.Handle("/admin/", http.StripPrefix("/admin/", http.FileServer(http.Dir("ui")))) // API mux.HandleFunc("/admin/api/health", s.handleHealth) mux.HandleFunc("/admin/api/namespaces", s.authMiddleware(s.handleNamespaces)) mux.HandleFunc("/admin/api/namespaces/", s.authMiddleware(s.handleNamespaceDetail)) mux.HandleFunc("/admin/api/usage", s.authMiddleware(s.handleUsage)) mux.HandleFunc("/admin/api/users", s.authMiddleware(s.handleUsers)) } // authMiddleware проверяет Bearer-токен из заголовка Authorization. func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != s.adminToken { writeJSON(w, http.StatusUnauthorized, model.ErrResp("unauthorized")) return } next(w, r) } } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // handleNamespaces возвращает список клиентских namespace-ов (быстро, без счётчиков). func (s *Server) handleNamespaces(w http.ResponseWriter, r *http.Request) { if s.k8s == nil { writeJSON(w, http.StatusServiceUnavailable, model.ErrResp("k8s unavailable")) return } ns, err := s.k8s.ListClientNamespaces(r.Context()) if err != nil { writeJSON(w, http.StatusInternalServerError, model.ErrResp(err.Error())) return } writeJSON(w, http.StatusOK, map[string]any{"namespaces": ns}) } // handleNamespaceDetail возвращает детальную информацию об одном namespace (с CRD-счётчиками). func (s *Server) handleNamespaceDetail(w http.ResponseWriter, r *http.Request) { if s.k8s == nil { writeJSON(w, http.StatusServiceUnavailable, model.ErrResp("k8s unavailable")) return } // URL: /admin/api/namespaces/{name} name := strings.TrimPrefix(r.URL.Path, "/admin/api/namespaces/") if name == "" { writeJSON(w, http.StatusBadRequest, model.ErrResp("missing namespace name")) return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() info, err := s.k8s.GetNamespaceDetail(ctx, name) if err != nil { writeJSON(w, http.StatusNotFound, model.ErrResp(err.Error())) return } writeJSON(w, http.StatusOK, info) } // handleUsage возвращает агрегированные метрики использования для бухгалтерии. func (s *Server) handleUsage(w http.ResponseWriter, r *http.Request) { if s.k8s == nil { writeJSON(w, http.StatusServiceUnavailable, model.ErrResp("k8s unavailable")) return } ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() usage, err := s.k8s.CollectUsage(ctx) if err != nil { writeJSON(w, http.StatusInternalServerError, model.ErrResp(err.Error())) return } writeJSON(w, http.StatusOK, map[string]any{"usage": usage}) } // handleUsers возвращает список пользователей из secrets в клиентских namespace-ах. func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) { if s.k8s == nil { writeJSON(w, http.StatusServiceUnavailable, model.ErrResp("k8s unavailable")) return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() users, err := s.k8s.ListUsers(ctx) if err != nil { writeJSON(w, http.StatusInternalServerError, model.ErrResp(err.Error())) return } writeJSON(w, http.StatusOK, map[string]any{"users": users}) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) }