feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages
Add new frontend pages for the multi-tenant OCDP platform: - Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories and versions, with deploy modal to launch charts on selected clusters - Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage) and per-node details with resource utilization bars - Chart References page (/chart-references): CRUD for chart metadata references - Values Templates page (/templates): CRUD for Helm values templates with version history and rollback support - Sidebar: Add Charts navigation, update Storage and Templates links - api.ts: Add all API client functions (clusterApi, registryApi, instanceApi, monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi, workspaceApi, userApi) with full TypeScript types Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
@ -27,6 +27,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -104,6 +105,20 @@ func main() {
|
||||
repos.MetricsClient,
|
||||
)
|
||||
|
||||
// Workspace Service
|
||||
workspaceService := service.NewWorkspaceService(
|
||||
repos.WorkspaceRepo,
|
||||
repos.QuotaRepo,
|
||||
repos.UserRepo,
|
||||
)
|
||||
|
||||
// User Management Service
|
||||
userManagementService := service.NewUserManagementService(
|
||||
repos.UserRepo,
|
||||
repos.WorkspaceRepo,
|
||||
passwordHasher,
|
||||
)
|
||||
|
||||
log.Println("✅ Domain Services initialized")
|
||||
|
||||
// ===== 6. 加载并执行 Bootstrap 预注入 =====
|
||||
@ -128,6 +143,27 @@ func main() {
|
||||
monitoringHandler := rest.NewMonitoringHandler(monitoringService)
|
||||
swaggerHandler := rest.NewSwaggerHandler()
|
||||
|
||||
// Workspace Handler
|
||||
workspaceHandler := rest.NewWorkspaceHandler(workspaceService, authService)
|
||||
|
||||
// User Management Handler (Admin only)
|
||||
userManagementHandler := rest.NewUserManagementHandler(userManagementService, authService, workspaceService)
|
||||
|
||||
// User Handler
|
||||
userHandler := rest.NewUserHandler(authService, workspaceService)
|
||||
|
||||
// Storage Handler
|
||||
storageService := service.NewStorageService(repos.StorageRepo)
|
||||
storageHandler := rest.NewStorageHandler(storageService)
|
||||
|
||||
// Chart Reference Handler
|
||||
chartRefService := service.NewChartReferenceService(repos.ChartRefRepo, repos.RegistryRepo)
|
||||
chartRefHandler := rest.NewChartReferenceHandler(chartRefService)
|
||||
|
||||
// Values Template Handler
|
||||
valuesTemplateService := service.NewValuesTemplateService(repos.ValuesTemplateRepo, repos.ChartRefRepo)
|
||||
valuesTemplateHandler := rest.NewValuesTemplateHandler(valuesTemplateService)
|
||||
|
||||
log.Println("✅ Input Adapters (REST handlers) initialized")
|
||||
|
||||
// ===== 8. 设置路由 =====
|
||||
@ -139,6 +175,14 @@ func main() {
|
||||
instanceHandler,
|
||||
monitoringHandler,
|
||||
swaggerHandler,
|
||||
workspaceHandler,
|
||||
userManagementHandler,
|
||||
userHandler,
|
||||
storageHandler,
|
||||
chartRefHandler,
|
||||
valuesTemplateHandler,
|
||||
tokenGenerator,
|
||||
config.AllowedOrigins,
|
||||
)
|
||||
|
||||
// ===== 9. 启动服务器 =====
|
||||
@ -161,21 +205,28 @@ func main() {
|
||||
|
||||
// Config 应用配置
|
||||
type Config struct {
|
||||
AdapterMode string
|
||||
Port string
|
||||
JWTSecret string
|
||||
EncryptionKey string
|
||||
DatabaseURL string
|
||||
AdapterMode string
|
||||
Port string
|
||||
JWTSecret string
|
||||
EncryptionKey string
|
||||
DatabaseURL string
|
||||
AllowedOrigins []string
|
||||
}
|
||||
|
||||
// loadConfig 加载配置
|
||||
func loadConfig() *Config {
|
||||
allowedOrigins := getEnv("ALLOWED_DEV_ORIGINS", "")
|
||||
var origins []string
|
||||
if allowedOrigins != "" {
|
||||
origins = strings.Split(allowedOrigins, ",")
|
||||
}
|
||||
return &Config{
|
||||
AdapterMode: getEnv("ADAPTER_MODE", ""), // 默认为空字符串(真实模式)
|
||||
Port: getEnv("PORT", "8080"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
|
||||
EncryptionKey: getEnv("ENCRYPTION_KEY", "default-encryption-key-change-in-production"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", ""),
|
||||
AdapterMode: getEnv("ADAPTER_MODE", ""), // 默认为空字符串(真实模式)
|
||||
Port: getEnv("PORT", "8080"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
|
||||
EncryptionKey: getEnv("ENCRYPTION_KEY", "default-encryption-key-change-in-production"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", ""),
|
||||
AllowedOrigins: origins,
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,12 +248,66 @@ func setupRouter(
|
||||
instanceHandler *rest.InstanceHandler,
|
||||
monitoringHandler *rest.MonitoringHandler,
|
||||
swaggerHandler *rest.SwaggerHandler,
|
||||
workspaceHandler *rest.WorkspaceHandler,
|
||||
userManagementHandler *rest.UserManagementHandler,
|
||||
userHandler *rest.UserHandler,
|
||||
storageHandler *rest.StorageHandler,
|
||||
chartRefHandler *rest.ChartReferenceHandler,
|
||||
valuesTemplateHandler *rest.ValuesTemplateHandler,
|
||||
tokenGenerator *jwt.JWTManager,
|
||||
allowedOrigins []string,
|
||||
) *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
// 全局中间件
|
||||
router.Use(loggingMiddleware)
|
||||
router.Use(corsMiddleware)
|
||||
router.Use(corsMiddleware(allowedOrigins))
|
||||
|
||||
// 预检请求处理 - 必须放在路由注册之前
|
||||
router.HandleFunc("/{path:.*}", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodOptions {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
// 非 OPTIONS 请求返回 404
|
||||
http.NotFound(w, r)
|
||||
}).Methods(http.MethodOptions)
|
||||
|
||||
// JWT 解析中间件 - 为所有需要认证的请求设置用户信息 header
|
||||
jwtMiddleware := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 跳过认证路由
|
||||
if r.URL.Path == "/api/v1/auth/login" ||
|
||||
r.URL.Path == "/api/v1/auth/register" ||
|
||||
r.URL.Path == "/api/v1/auth/refresh" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
userID, username, role, workspaceID, err := tokenGenerator.Verify(token)
|
||||
if err == nil && userID != "" {
|
||||
// 设置 header 供 handlers 使用
|
||||
r.Header.Set("X-User-ID", userID)
|
||||
r.Header.Set("X-Username", username)
|
||||
r.Header.Set("X-User-Role", role)
|
||||
r.Header.Set("X-Workspace-ID", workspaceID)
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -220,12 +325,39 @@ func setupRouter(
|
||||
|
||||
// API v1
|
||||
api := router.PathPrefix("/api/v1").Subrouter()
|
||||
// 应用 CORS 和 JWT 中间件到所有 API 路由
|
||||
api.Use(corsMiddleware(allowedOrigins))
|
||||
api.Use(jwtMiddleware)
|
||||
|
||||
// ===== 认证路由 =====
|
||||
api.HandleFunc("/auth/register", authHandler.Register)
|
||||
api.HandleFunc("/auth/login", authHandler.Login)
|
||||
api.HandleFunc("/auth/refresh", authHandler.RefreshToken)
|
||||
|
||||
// ===== 用户账户路由 =====
|
||||
api.HandleFunc("/users/me", userHandler.GetCurrentUser).Methods(http.MethodGet)
|
||||
api.HandleFunc("/users/me/password", userHandler.ChangePassword).Methods(http.MethodPut)
|
||||
api.HandleFunc("/users/me/workspace", userHandler.GetCurrentUserWorkspace).Methods(http.MethodGet)
|
||||
|
||||
// ===== 用户管理路由(Admin) =====
|
||||
api.HandleFunc("/admin/users", userManagementHandler.CreateUser).Methods(http.MethodPost)
|
||||
api.HandleFunc("/admin/users", userManagementHandler.ListUsers).Methods(http.MethodGet)
|
||||
api.HandleFunc("/admin/users/{user_id}", userManagementHandler.GetUser).Methods(http.MethodGet)
|
||||
api.HandleFunc("/admin/users/{user_id}", userManagementHandler.UpdateUser).Methods(http.MethodPut)
|
||||
api.HandleFunc("/admin/users/{user_id}/active", userManagementHandler.SetUserActive).Methods(http.MethodPut)
|
||||
api.HandleFunc("/admin/users/{user_id}/workspace", userManagementHandler.ChangeUserWorkspace).Methods(http.MethodPut)
|
||||
api.HandleFunc("/admin/users/{user_id}/password", userManagementHandler.ResetPassword).Methods(http.MethodPut)
|
||||
api.HandleFunc("/admin/users/{user_id}", userManagementHandler.DeleteUser).Methods(http.MethodDelete)
|
||||
|
||||
// ===== Workspace 路由 =====
|
||||
api.HandleFunc("/workspaces", workspaceHandler.CreateWorkspace).Methods(http.MethodPost)
|
||||
api.HandleFunc("/workspaces", workspaceHandler.ListWorkspaces).Methods(http.MethodGet)
|
||||
api.HandleFunc("/workspaces/{workspace_id}", workspaceHandler.GetWorkspace).Methods(http.MethodGet)
|
||||
api.HandleFunc("/workspaces/{workspace_id}", workspaceHandler.UpdateWorkspace).Methods(http.MethodPut)
|
||||
api.HandleFunc("/workspaces/{workspace_id}", workspaceHandler.DeleteWorkspace).Methods(http.MethodDelete)
|
||||
api.HandleFunc("/workspaces/{workspace_id}/quotas", workspaceHandler.GetWorkspaceQuotas).Methods(http.MethodGet)
|
||||
api.HandleFunc("/workspaces/{workspace_id}/quotas", workspaceHandler.SetWorkspaceQuotas).Methods(http.MethodPut)
|
||||
|
||||
// ===== 集群路由 =====
|
||||
api.HandleFunc("/clusters", clusterHandler.CreateCluster).Methods(http.MethodPost)
|
||||
api.HandleFunc("/clusters", clusterHandler.GetAllClusters).Methods(http.MethodGet)
|
||||
@ -242,11 +374,36 @@ func setupRouter(
|
||||
api.HandleFunc("/registries/{registry_id}", registryHandler.DeleteRegistry).Methods(http.MethodDelete)
|
||||
api.HandleFunc("/registries/{registry_id}/health", registryHandler.GetRegistryHealth).Methods(http.MethodGet)
|
||||
|
||||
// ===== Storage Backend 路由 =====
|
||||
api.HandleFunc("/storage-backends", storageHandler.CreateStorage).Methods(http.MethodPost)
|
||||
api.HandleFunc("/storage-backends", storageHandler.GetAllStorage).Methods(http.MethodGet)
|
||||
api.HandleFunc("/storage-backends/{storage_id}", storageHandler.GetStorage).Methods(http.MethodGet)
|
||||
api.HandleFunc("/storage-backends/{storage_id}", storageHandler.UpdateStorage).Methods(http.MethodPut)
|
||||
api.HandleFunc("/storage-backends/{storage_id}", storageHandler.DeleteStorage).Methods(http.MethodDelete)
|
||||
|
||||
// ===== Chart Reference 路由 =====
|
||||
api.HandleFunc("/chart-references", chartRefHandler.CreateChartReference).Methods(http.MethodPost)
|
||||
api.HandleFunc("/chart-references", chartRefHandler.GetAllChartReferences).Methods(http.MethodGet)
|
||||
api.HandleFunc("/chart-references/{chart_reference_id}", chartRefHandler.GetChartReference).Methods(http.MethodGet)
|
||||
api.HandleFunc("/chart-references/{chart_reference_id}", chartRefHandler.UpdateChartReference).Methods(http.MethodPut)
|
||||
api.HandleFunc("/chart-references/{chart_reference_id}", chartRefHandler.DeleteChartReference).Methods(http.MethodDelete)
|
||||
|
||||
// ===== Values Template 路由 =====
|
||||
api.HandleFunc("/values-templates", valuesTemplateHandler.CreateValuesTemplate).Methods(http.MethodPost)
|
||||
api.HandleFunc("/values-templates", valuesTemplateHandler.GetAllValuesTemplates).Methods(http.MethodGet)
|
||||
api.HandleFunc("/values-templates/{template_id}", valuesTemplateHandler.GetValuesTemplate).Methods(http.MethodGet)
|
||||
api.HandleFunc("/values-templates/{template_id}", valuesTemplateHandler.UpdateValuesTemplate).Methods(http.MethodPut)
|
||||
api.HandleFunc("/values-templates/{template_id}", valuesTemplateHandler.DeleteValuesTemplate).Methods(http.MethodDelete)
|
||||
api.HandleFunc("/chart-references/{chart_reference_id}/values-templates", valuesTemplateHandler.GetValuesTemplatesByChartReference).Methods(http.MethodGet)
|
||||
api.HandleFunc("/chart-references/{chart_reference_id}/values-templates/history", valuesTemplateHandler.GetValuesTemplateHistory).Methods(http.MethodGet)
|
||||
api.HandleFunc("/chart-references/{chart_reference_id}/values-templates/rollback", valuesTemplateHandler.RollbackValuesTemplate).Methods(http.MethodPost)
|
||||
|
||||
// ===== Artifact 路由 =====
|
||||
api.HandleFunc("/registries/{registry_id}/repositories", artifactHandler.ListRepositories).Methods(http.MethodGet)
|
||||
api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts", artifactHandler.ListArtifacts).Methods(http.MethodGet)
|
||||
api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}", artifactHandler.GetArtifact).Methods(http.MethodGet)
|
||||
api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values-schema", artifactHandler.GetArtifactValuesSchema).Methods(http.MethodGet)
|
||||
api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values", artifactHandler.GetArtifactValues).Methods(http.MethodGet)
|
||||
|
||||
// ===== Instance 路由 =====
|
||||
api.HandleFunc("/clusters/{cluster_id}/instances", instanceHandler.CreateInstance).Methods(http.MethodPost)
|
||||
@ -285,25 +442,54 @@ func loggingMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// corsMiddleware CORS 中间件
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置 CORS 头
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// 处理 OPTIONS 预检请求
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
// 验证 origin 是否在允许列表中
|
||||
if origin != "" && len(allowedOrigins) > 0 {
|
||||
allowed := false
|
||||
for _, ao := range allowedOrigins {
|
||||
if ao == origin || ao == "*" {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
// Origin 不在允许列表中,拒绝请求
|
||||
w.Header().Set("Access-Control-Allow-Origin", "")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
// 如果没有配置 allowedOrigins,默认允许所有
|
||||
if len(allowedOrigins) == 0 {
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
}
|
||||
}
|
||||
|
||||
// 优先处理 OPTIONS 预检请求
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置 CORS 头
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
11
backend/cmd/gen_hash/main.go
Normal file
11
backend/cmd/gen_hash/main.go
Normal file
@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||
fmt.Println(string(hash))
|
||||
}
|
||||
Reference in New Issue
Block a user