Files
fission-console/admin-console/internal/api/server.go
T

130 lines
4.3 KiB
Go

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)
}