restructure: console→client-console, add admin-console skeleton, move docs to doc/
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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/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})
|
||||
}
|
||||
|
||||
// handleUsage возвращает агрегированные метрики использования для бухгалтерии.
|
||||
func (s *Server) handleUsage(w http.ResponseWriter, r *http.Request) {
|
||||
if s.k8s == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, model.ErrResp("k8s unavailable"))
|
||||
return
|
||||
}
|
||||
usage, err := s.k8s.CollectUsage(r.Context())
|
||||
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
|
||||
}
|
||||
users, err := s.k8s.ListUsers(r.Context())
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"admin-console/internal/model"
|
||||
)
|
||||
|
||||
// K8sClient — обёртка над kubernetes.Clientset для нужд admin-console.
|
||||
type K8sClient struct {
|
||||
cs *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// NewK8sClient создаёт клиента из in-cluster конфига, с fallback на ~/.kube/config.
|
||||
func NewK8sClient() (*K8sClient, error) {
|
||||
cfg, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
cfg, err = clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no k8s config: %w", err)
|
||||
}
|
||||
}
|
||||
cs, err := kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &K8sClient{cs: cs}, nil
|
||||
}
|
||||
|
||||
// ListClientNamespaces возвращает namespace-ы с лейблом managed-by=fission-console.
|
||||
// Для каждого считает количество объектов Fission CRD через Dynamic клиент
|
||||
// (пока упрощённо — только через secrets и labels).
|
||||
func (k *K8sClient) ListClientNamespaces(ctx context.Context) ([]model.NamespaceInfo, error) {
|
||||
nsList, err := k.cs.CoreV1().Namespaces().List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "managed-by=fission-console",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]model.NamespaceInfo, 0, len(nsList.Items))
|
||||
for _, ns := range nsList.Items {
|
||||
info := model.NamespaceInfo{
|
||||
Name: ns.Name,
|
||||
UserEmail: ns.Labels["user"],
|
||||
CreatedAt: ns.CreationTimestamp.Format(time.RFC3339),
|
||||
}
|
||||
// TODO: добавить подсчёт Fission CRD через dynamic client
|
||||
result = append(result, info)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CollectUsage возвращает агрегированную статистику по всем клиентским namespace-ам.
|
||||
func (k *K8sClient) CollectUsage(ctx context.Context) ([]model.UsageEntry, error) {
|
||||
nsList, err := k.cs.CoreV1().Namespaces().List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "managed-by=fission-console",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
result := make([]model.UsageEntry, 0, len(nsList.Items))
|
||||
for _, ns := range nsList.Items {
|
||||
entry := model.UsageEntry{
|
||||
Namespace: ns.Name,
|
||||
UserEmail: ns.Labels["user"],
|
||||
PeriodStart: startOfMonth.Format("2006-01-02"),
|
||||
PeriodEnd: now.Format("2006-01-02"),
|
||||
}
|
||||
// TODO: заполнить реальными данными из Prometheus / metrics-server / S3
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListUsers собирает пользователей из secret fission-console-users в каждом
|
||||
// клиентском namespace.
|
||||
func (k *K8sClient) ListUsers(ctx context.Context) ([]model.UserInfo, error) {
|
||||
nsList, err := k.cs.CoreV1().Namespaces().List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "managed-by=fission-console",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []model.UserInfo
|
||||
for _, ns := range nsList.Items {
|
||||
// label user содержит email владельца namespace
|
||||
email := ns.Labels["user"]
|
||||
if email == "" {
|
||||
// fallback: попробовать прочитать из secret
|
||||
secret, err := k.cs.CoreV1().Secrets(ns.Name).Get(ctx, "fission-console-users", metav1.GetOptions{})
|
||||
if err == nil {
|
||||
for key := range secret.Data {
|
||||
if strings.Contains(key, "@") {
|
||||
email = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
users = append(users, model.UserInfo{
|
||||
Email: email,
|
||||
Namespace: ns.Name,
|
||||
CreatedAt: ns.CreationTimestamp.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package model
|
||||
|
||||
// ErrResp — стандартный ответ с ошибкой.
|
||||
func ErrResp(msg string) map[string]string {
|
||||
return map[string]string{"error": msg}
|
||||
}
|
||||
|
||||
// NamespaceInfo — информация об одном клиентском namespace.
|
||||
type NamespaceInfo struct {
|
||||
Name string `json:"name"`
|
||||
UserEmail string `json:"user_email,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
|
||||
// счётчики ресурсов Fission
|
||||
Functions int `json:"functions"`
|
||||
Packages int `json:"packages"`
|
||||
Environments int `json:"environments"`
|
||||
HTTPTriggers int `json:"http_triggers"`
|
||||
TimeTriggers int `json:"time_triggers"`
|
||||
}
|
||||
|
||||
// UsageEntry — строка использования для одного namespace (для бухгалтерии).
|
||||
type UsageEntry struct {
|
||||
Namespace string `json:"namespace"`
|
||||
UserEmail string `json:"user_email,omitempty"`
|
||||
Functions int `json:"functions"`
|
||||
Invocations int64 `json:"invocations_total"` // TODO: из Prometheus/logs
|
||||
StorageGB float64 `json:"storage_gb"` // TODO: из storagesvc/S3
|
||||
CPUMilliCores int64 `json:"cpu_millicores"` // TODO: из metrics-server
|
||||
MemoryMB int64 `json:"memory_mb"` // TODO: из metrics-server
|
||||
PeriodStart string `json:"period_start"`
|
||||
PeriodEnd string `json:"period_end"`
|
||||
}
|
||||
|
||||
// UserInfo — пользователь из secret fission-console-users.
|
||||
type UserInfo struct {
|
||||
Email string `json:"email"`
|
||||
Namespace string `json:"namespace"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user