├── VERSION ├── .env ├── docs ├── nav.png ├── pod.png ├── pv.png ├── pvc.png ├── run.png ├── svc.png ├── docs.png ├── docs1.png ├── first.png ├── login.png ├── logo.png ├── shell.png ├── cluster.png ├── ingress.png ├── monitor1.png ├── monitor2.png ├── node-ant.png ├── old-logo.png ├── secret.png ├── cilikube12.png ├── configmap.png ├── dashboard.png ├── deployment.png ├── minikube1.png ├── minikube2.png ├── namespace.png ├── techstack.png ├── wechat400x400.png ├── cluster-overview.png ├── cluster-overview1.png └── cluster-overview2.png ├── data └── cilikube.db ├── uploads └── avatars │ └── avatar_1_d79b7ebc-045e-44f9-a78a-f461a0e41401.png ├── internal ├── repository │ ├── dao │ │ └── auth_dao.go │ └── auth_repo.go ├── logger │ └── logger.go ├── routes │ ├── kubernetes_proxy_routes.go │ ├── event_routes.go │ ├── installer_routes.go │ ├── cluster_routes.go │ ├── crd_routes.go │ ├── summary_routes.go │ ├── user_management_routes.go │ ├── system_settings_routes.go │ ├── profile_routes.go │ ├── role_management_routes.go │ └── auth_routes.go ├── service │ ├── kubernetes_proxy_service.go │ ├── encoding.go │ ├── resource_client.go │ ├── pod_exec_service.go │ ├── app_services.go │ ├── pod_logs_service.go │ ├── resource_service_factory.go │ ├── base_resource_service.go │ ├── event_service.go │ ├── cluster_service.go │ └── security_service_test.go ├── models │ ├── summary_models.go │ ├── cluster_model.go │ ├── event_models.go │ ├── crd_models.go │ ├── oauth.go │ ├── audit.go │ └── role.go ├── handlers │ ├── api_common.go │ ├── version.go │ ├── kubernetes_proxy_handlers.go │ ├── health_test.go │ ├── health.go │ ├── summary_handler.go │ ├── event_handler.go │ ├── node_metrics_handler.go │ ├── pod_logs_handlers.go │ ├── installer_handler.go │ ├── oauth_handler.go │ ├── cluster_handler.go │ ├── pod_exec_handlers.go │ └── resource_handler.go ├── store │ ├── encryption.go │ ├── gorm_store.go │ └── interface.go ├── initialization │ └── start.go └── app │ └── app.go ├── pkg ├── auth │ ├── model.conf │ ├── rate_limit_middleware.go │ └── casbin_middleware.go ├── utils │ ├── response_helper.go │ ├── validators.go │ └── cors.go ├── log │ └── logger.go ├── k8s │ └── context.go ├── metrics │ └── prometheus.go └── database │ └── database.go ├── cmd └── server │ └── main.go ├── .gitignore ├── configs ├── config.yaml └── database-examples.yaml ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── ui └── README.md ├── deployments ├── docker │ ├── .env.example │ └── build.sh ├── monitoring │ └── prometheus.yml └── README.md ├── Dockerfile ├── api ├── README.md └── v1 │ └── README.md ├── .golangci.yml ├── Makefile ├── docker-compose.yaml └── go.mod /VERSION: -------------------------------------------------------------------------------- 1 | v0.5.0 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | CILIKUBE_JWT_SECRET="3cX5JnVcClWE6N1tHoydONvQHdCiolsP" -------------------------------------------------------------------------------- /docs/nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/nav.png -------------------------------------------------------------------------------- /docs/pod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/pod.png -------------------------------------------------------------------------------- /docs/pv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/pv.png -------------------------------------------------------------------------------- /docs/pvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/pvc.png -------------------------------------------------------------------------------- /docs/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/run.png -------------------------------------------------------------------------------- /docs/svc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/svc.png -------------------------------------------------------------------------------- /docs/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/docs.png -------------------------------------------------------------------------------- /docs/docs1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/docs1.png -------------------------------------------------------------------------------- /docs/first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/first.png -------------------------------------------------------------------------------- /docs/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/login.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/shell.png -------------------------------------------------------------------------------- /data/cilikube.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/data/cilikube.db -------------------------------------------------------------------------------- /docs/cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/cluster.png -------------------------------------------------------------------------------- /docs/ingress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/ingress.png -------------------------------------------------------------------------------- /docs/monitor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/monitor1.png -------------------------------------------------------------------------------- /docs/monitor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/monitor2.png -------------------------------------------------------------------------------- /docs/node-ant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/node-ant.png -------------------------------------------------------------------------------- /docs/old-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/old-logo.png -------------------------------------------------------------------------------- /docs/secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/secret.png -------------------------------------------------------------------------------- /docs/cilikube12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/cilikube12.png -------------------------------------------------------------------------------- /docs/configmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/configmap.png -------------------------------------------------------------------------------- /docs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/dashboard.png -------------------------------------------------------------------------------- /docs/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/deployment.png -------------------------------------------------------------------------------- /docs/minikube1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/minikube1.png -------------------------------------------------------------------------------- /docs/minikube2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/minikube2.png -------------------------------------------------------------------------------- /docs/namespace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/namespace.png -------------------------------------------------------------------------------- /docs/techstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/techstack.png -------------------------------------------------------------------------------- /docs/wechat400x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/wechat400x400.png -------------------------------------------------------------------------------- /docs/cluster-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/cluster-overview.png -------------------------------------------------------------------------------- /docs/cluster-overview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/cluster-overview1.png -------------------------------------------------------------------------------- /docs/cluster-overview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/docs/cluster-overview2.png -------------------------------------------------------------------------------- /uploads/avatars/avatar_1_d79b7ebc-045e-44f9-a78a-f461a0e41401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciliverse/cilikube/HEAD/uploads/avatars/avatar_1_d79b7ebc-045e-44f9-a78a-f461a0e41401.png -------------------------------------------------------------------------------- /internal/repository/dao/auth_dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import "gorm.io/gorm" 4 | 5 | type AuthDao struct { 6 | db *gorm.DB 7 | } 8 | 9 | func NewAuthDao(db *gorm.DB) *AuthDao { 10 | return &AuthDao{ 11 | db: db, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | ) 7 | 8 | // New initializes and returns a configured slog.Logger 9 | func New(level slog.Level) *slog.Logger { 10 | handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 11 | Level: level, 12 | }) 13 | logger := slog.New(handler) 14 | return logger 15 | } 16 | -------------------------------------------------------------------------------- /internal/routes/kubernetes_proxy_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/ciliverse/cilikube/internal/handlers" 7 | ) 8 | 9 | func KubernetesProxyRoutes(router *gin.RouterGroup, handler *handlers.ProxyHandler) { 10 | proxyGroup := router.Group("/proxy") 11 | { 12 | proxyGroup.Any("/*act", handler.Proxy) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/service/kubernetes_proxy_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // ProxyService struct no longer holds restConfig field 4 | type ProxyService struct { 5 | // No longer need restConfig *rest.Config field 6 | } 7 | 8 | func NewProxyService() *ProxyService { 9 | return &ProxyService{} 10 | } 11 | 12 | //func (s *ProxyService) GetConfig() *rest.Config { 13 | // return s.restConfig 14 | //} 15 | -------------------------------------------------------------------------------- /pkg/auth/model.conf: -------------------------------------------------------------------------------- 1 | # Request definition 2 | [request_definition] 3 | r = sub, obj, act 4 | 5 | # Policy definition 6 | [policy_definition] 7 | p = sub, obj, act 8 | 9 | # Role definition 10 | [role_definition] 11 | g = _, _ 12 | 13 | # Policy effect (determines if policy takes effect) 14 | [policy_effect] 15 | e = some(where (p.eft == allow)) 16 | 17 | # Matching rules 18 | [matchers] 19 | m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/ciliverse/cilikube/internal/app" 8 | ) 9 | 10 | // just do it ! go!go!go! 11 | func main() { 12 | configPath := app.GetConfigPath() 13 | application, err := app.New(configPath) 14 | if err != nil { 15 | slog.Error("failed to initialize app", "error", err) 16 | os.Exit(1) 17 | } 18 | 19 | slog.Info("starting application", "config", configPath) 20 | application.Run() 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/ 3 | minikube-darwin-arm64 4 | output/ 5 | # Ignore build artifacts 6 | dist/ 7 | build/ 8 | out/ 9 | .nuxt/ 10 | .next/ 11 | .output/ 12 | .cache/ 13 | .parcel-cache/ 14 | .vite/ 15 | .rollup.cache/ 16 | # Ignore IDE specific files 17 | .idea/ 18 | # Ignore Minikube binary for macOS ARM64 19 | minikube-darwin-arm64 20 | # Ignore output directory 21 | output/ 22 | # Ignore Cilikube binary and related files 23 | cilikube 24 | cilikube.exe 25 | cilikube-* -------------------------------------------------------------------------------- /internal/routes/event_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func RegisterEventRoutes(router *gin.RouterGroup, handler *handlers.EventHandler) { 9 | eventRoutes := router.Group("/events") 10 | { 11 | // List all events with optional filters 12 | eventRoutes.GET("", handler.ListEvents) 13 | 14 | // Get recent events (for dashboard) 15 | eventRoutes.GET("/recent", handler.GetRecentEvents) 16 | 17 | // Get events related to a specific object 18 | eventRoutes.GET("/object/:kind/:name", handler.GetEventsByObject) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/routes/installer_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // RegisterInstallerRoutes registers routes related to the Minikube installer. 9 | func RegisterInstallerRoutes(router *gin.RouterGroup, installerHandler *handlers.InstallerHandler) { 10 | // Health check endpoint 11 | router.GET("/healthz", installerHandler.HealthCheck) 12 | 13 | installerRoutes := router.Group("/system") // Group under /system or choose another name 14 | { 15 | installerRoutes.GET("/install-minikube", installerHandler.StreamMinikubeInstallation) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/models/summary_models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ResourceSummary represents the count of various cluster resources. 4 | // Use pointers to distinguish between a count of 0 and a failure to retrieve count. 5 | type ResourceSummary struct { 6 | Nodes *int `json:"nodes"` 7 | Namespaces *int `json:"namespaces"` 8 | Pods *int `json:"pods"` 9 | Deployments *int `json:"deployments"` 10 | Services *int `json:"services"` 11 | PersistentVolumes *int `json:"persistentVolumes"` 12 | Pvcs *int `json:"pvcs"` // PersistentVolumeClaims 13 | StatefulSets *int `json:"statefulSets"` 14 | DaemonSets *int `json:"daemonSets"` 15 | ConfigMaps *int `json:"configMaps"` 16 | Secrets *int `json:"secrets"` 17 | Ingresses *int `json:"ingresses"` 18 | // Add more resource types as needed 19 | } 20 | -------------------------------------------------------------------------------- /internal/routes/cluster_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func RegisterClusterRoutes(router *gin.RouterGroup, handler *handlers.ClusterHandler) { 9 | // This route group is now only responsible for cluster metadata management 10 | clusterRoutes := router.Group("/clusters") 11 | { 12 | clusterRoutes.GET("", handler.ListClusters) 13 | clusterRoutes.POST("", handler.CreateCluster) 14 | clusterRoutes.GET("/:id", handler.GetCluster) 15 | clusterRoutes.PUT("/:id", handler.UpdateCluster) 16 | clusterRoutes.DELETE("/:id", handler.DeleteCluster) 17 | 18 | // Active cluster API 19 | activeRoutes := clusterRoutes.Group("/active") 20 | { 21 | activeRoutes.GET("", handler.GetActiveCluster) 22 | activeRoutes.POST("", handler.SetActiveCluster) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/response_helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // Standard API response helper 11 | func ApiSuccess(c *gin.Context, data interface{}, message string) { 12 | if message == "" { 13 | message = "success" 14 | } 15 | c.JSON(http.StatusOK, gin.H{ 16 | "code": http.StatusOK, 17 | "data": data, 18 | "message": message, 19 | }) 20 | } 21 | 22 | func ApiError(c *gin.Context, statusCode int, message string, details ...string) { 23 | detailStr := "" 24 | if len(details) > 0 { 25 | detailStr = details[0] 26 | } 27 | log.Printf("API Error: Status %d, Message: %s, Details: %s, Path: %s", statusCode, message, detailStr, c.Request.URL.Path) 28 | c.JSON(statusCode, gin.H{ 29 | "code": statusCode, 30 | "data": nil, 31 | "message": message, 32 | "details": detailStr, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | // why use slog? because you guess... 12 | var Logger *zap.Logger 13 | 14 | func Init(production bool) { 15 | config := zap.NewProductionConfig() 16 | if !production { 17 | config = zap.NewDevelopmentConfig() 18 | } 19 | config.OutputPaths = []string{"stdout"} 20 | config.ErrorOutputPaths = []string{"stderr"} 21 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 22 | Logger, _ = config.Build() 23 | } 24 | 25 | func GinLogger() gin.HandlerFunc { 26 | return func(c *gin.Context) { 27 | start := time.Now() 28 | 29 | c.Next() 30 | 31 | Logger.Info("request", 32 | zap.String("method", c.Request.Method), 33 | zap.String("path", c.Request.URL.Path), 34 | zap.Int("status", c.Writer.Status()), 35 | zap.Duration("duration", time.Since(start)), 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/handlers/api_common.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // ErrorResponse defines the standard format for API error responses 8 | type ErrorResponse struct { 9 | Code int `json:"code"` 10 | Message string `json:"message"` 11 | } 12 | 13 | // SuccessResponse defines the structure for successful responses 14 | type SuccessResponse struct { 15 | Code int `json:"code"` 16 | Data interface{} `json:"data"` 17 | Message string `json:"message"` 18 | } 19 | 20 | // respondError returns an error response 21 | func respondError(c *gin.Context, code int, message string) { 22 | c.JSON(code, ErrorResponse{ 23 | Code: code, 24 | Message: message, 25 | }) 26 | } 27 | 28 | // respondSuccess returns a successful response 29 | func respondSuccess(c *gin.Context, code int, data interface{}) { 30 | c.JSON(code, SuccessResponse{ 31 | Code: code, 32 | Data: data, 33 | Message: "success", 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /configs/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: "8080" 3 | read_timeout: 30 4 | write_timeout: 30 5 | mode: debug 6 | activeCluster: "907cab34-53f0-4c31-8b32-e238e5bf5769" 7 | encryptionKey: mobSIziSWMBZLMSDIIbuB9kMqc9QebV3 8 | kubernetes: 9 | kubeconfig: /root/.kube/config 10 | installer: 11 | minikubePath: /usr/local/bin/minikube 12 | minikubeDriver: docker 13 | downloadDir: /tmp/cilikube_downloads 14 | database: 15 | enabled: true 16 | type: "sqlite" 17 | database: "./data/cilikube.db" 18 | host: "" 19 | port: 0 20 | username: "" 21 | password: "" 22 | charset: "" 23 | jwt: 24 | secret_key: cilikube-secret-key-change-in-production 25 | expire_duration: 24h0m0s 26 | issuer: cilikube 27 | clusters: 28 | - id: 907cab34-53f0-4c31-8b32-e238e5bf5769 29 | name: Test 30 | config_path: default 31 | description: test cluster 32 | provider: minikube 33 | environment: test 34 | is_active: true 35 | -------------------------------------------------------------------------------- /internal/repository/auth_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ciliverse/cilikube/internal/models" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type AuthRepository struct { 11 | DB *gorm.DB 12 | } 13 | 14 | func NewAuthRepository(db *gorm.DB) *AuthRepository { 15 | return &AuthRepository{ 16 | DB: db, 17 | } 18 | } 19 | 20 | func (r *AuthRepository) FindUserByUsername(username string) (*models.User, error) { 21 | var user models.User 22 | if err := r.DB.Where("username = ?", username).First(&user).Error; err != nil { 23 | if errors.Is(err, gorm.ErrRecordNotFound) { 24 | return nil, errors.New("user does not exist") 25 | } 26 | return nil, err 27 | } 28 | return &user, nil 29 | } 30 | 31 | func (r *AuthRepository) CreateUser(username, password, roles, email string) error { 32 | user := models.User{ 33 | Username: username, 34 | Password: password, 35 | Role: roles, 36 | Email: email, 37 | } 38 | if err := r.DB.Create(&user).Error; err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/handlers/version.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "runtime" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // These variables will be set at compile time via ldflags 11 | var ( 12 | Version = "dev" 13 | BuildTime = "unknown" 14 | GitCommit = "unknown" 15 | ) 16 | 17 | // VersionInfo version information structure 18 | type VersionInfo struct { 19 | Version string `json:"version"` 20 | BuildTime string `json:"build_time"` 21 | GitCommit string `json:"git_commit"` 22 | GoVersion string `json:"go_version"` 23 | Platform string `json:"platform"` 24 | Arch string `json:"arch"` 25 | } 26 | 27 | // GetVersion retrieves version information 28 | func GetVersion(c *gin.Context) { 29 | versionInfo := VersionInfo{ 30 | Version: Version, 31 | BuildTime: BuildTime, 32 | GitCommit: GitCommit, 33 | GoVersion: runtime.Version(), 34 | Platform: runtime.GOOS, 35 | Arch: runtime.GOARCH, 36 | } 37 | 38 | c.JSON(http.StatusOK, gin.H{ 39 | "code": 200, 40 | "data": versionInfo, 41 | "message": "success", 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /internal/routes/crd_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // SetupCRDRoutes sets up CRD routes 9 | func SetupCRDRoutes(router *gin.RouterGroup, crdHandler *handlers.CRDHandler) { 10 | crdGroup := router.Group("/crds") 11 | { 12 | // CRD management 13 | crdGroup.GET("", crdHandler.ListCRDs) // Get CRD list 14 | crdGroup.GET("/definition/:name", crdHandler.GetCRD) // Get CRD details 15 | 16 | // Custom resource management 17 | resourceGroup := crdGroup.Group("/resources/:group/:version/:plural") 18 | { 19 | resourceGroup.GET("", crdHandler.ListCustomResources) // Get custom resource list 20 | resourceGroup.POST("", crdHandler.CreateCustomResource) // Create custom resource 21 | resourceGroup.GET("/:name", crdHandler.GetCustomResource) // Get custom resource details 22 | resourceGroup.PUT("/:name", crdHandler.UpdateCustomResource) // Update custom resource 23 | resourceGroup.DELETE("/:name", crdHandler.DeleteCustomResource) // Delete custom resource 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/routes/summary_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // RegisterSummaryRoutes registers resource summary related routes 9 | func RegisterSummaryRoutes(router *gin.RouterGroup, handler *handlers.SummaryHandler) { 10 | // Resource summary routes 11 | summaryGroup := router.Group("/summary") 12 | { 13 | summaryGroup.GET("/resources", handler.GetResourceSummary) 14 | // *** ADD THIS LINE *** 15 | summaryGroup.GET("/backend-dependencies", handler.GetBackendDependencies) // Register the new handlers 16 | } 17 | } 18 | 19 | // If have an authenticated version, add it there too if needed 20 | /* 21 | func RegisterSummaryRoutesWithAuth(router *gin.RouterGroup, handlers *handlers.SummaryHandler, authMiddleware ...gin.HandlerFunc) { 22 | summaryGroup := router.Group("/summary") 23 | // Apply middleware if needed 24 | // summaryGroup.Use(authMiddleware...) 25 | { 26 | summaryGroup.GET("/resources", handlers.GetResourceSummary) 27 | summaryGroup.GET("/backend-dependencies", handlers.GetBackendDependencies) 28 | } 29 | } 30 | */ 31 | -------------------------------------------------------------------------------- /pkg/k8s/context.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/ciliverse/cilikube/pkg/utils" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // GetClientFromQuery gets clusterId from URL query parameters and returns the corresponding k8s client. 12 | // This is the "gatekeeper" for all resource operation handler functions. 13 | func GetClientFromQuery(c *gin.Context, cm *ClusterManager) (*Client, bool) { 14 | clusterID := c.Query("clusterId") 15 | if clusterID == "" { 16 | // If no clusterId is provided, try to use the currently active cluster as fallback 17 | activeID := cm.GetActiveClusterID() 18 | if activeID == "" { 19 | utils.ApiError(c, http.StatusBadRequest, "missing 'clusterId' query parameter and no active default cluster", "e.g., /api/v1/nodes?clusterId=cls-xxxxx") 20 | return nil, false 21 | } 22 | clusterID = activeID 23 | } 24 | 25 | client, err := cm.GetClientByID(clusterID) 26 | if err != nil { 27 | utils.ApiError(c, http.StatusNotFound, fmt.Sprintf("cluster ID '%s' not found or unavailable", clusterID), err.Error()) 28 | return nil, false 29 | } 30 | 31 | return client, true 32 | } 33 | -------------------------------------------------------------------------------- /internal/routes/user_management_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/ciliverse/cilikube/internal/service" 6 | "github.com/ciliverse/cilikube/pkg/auth" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // RegisterUserManagementRoutes registers user management routes for administrators 11 | func RegisterUserManagementRoutes(router *gin.RouterGroup, authService *service.AuthService, roleService *service.RoleService) { 12 | userHandler := handlers.NewUserManagementHandler(authService, roleService) 13 | 14 | // Apply JWT middleware and admin permission check to all user management routes 15 | userRoutes := router.Group("/users") 16 | userRoutes.Use(auth.JWTAuthMiddleware()) 17 | // TODO: Add admin permission middleware here 18 | { 19 | // User CRUD operations 20 | userRoutes.GET("", userHandler.ListUsers) 21 | userRoutes.POST("", userHandler.CreateUser) 22 | userRoutes.GET("/:id", userHandler.GetUser) 23 | userRoutes.PUT("/:id", userHandler.UpdateUser) 24 | userRoutes.DELETE("/:id", userHandler.DeleteUser) 25 | 26 | // User status management 27 | userRoutes.PUT("/:id/status", userHandler.UpdateUserStatus) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/encoding.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | 8 | "golang.org/x/text/encoding/simplifiedchinese" 9 | "golang.org/x/text/transform" 10 | ) 11 | 12 | // GBKToUTF8Reader wraps an io.Reader and converts GBK bytes to UTF-8 13 | func GBKToUTF8Reader(r io.Reader) io.Reader { 14 | return transform.NewReader(r, simplifiedchinese.GBK.NewDecoder()) 15 | } 16 | 17 | // DetectGBK tries to detect if the first N bytes are GBK encoded (very simple heuristic) 18 | func DetectGBK(data []byte) bool { 19 | // If data contains bytes in 0x81-0xFE range, likely GBK 20 | for _, b := range data { 21 | if b >= 0x81 && b <= 0xFE { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | 28 | // ConvertIfGBK wraps a log stream, if it looks like GBK, convert to UTF-8 29 | func ConvertIfGBK(logStream io.ReadCloser) io.ReadCloser { 30 | peek := make([]byte, 512) 31 | n, _ := logStream.Read(peek) 32 | if n == 0 { 33 | return logStream 34 | } 35 | if DetectGBK(peek[:n]) { 36 | utf8Reader := GBKToUTF8Reader(io.MultiReader(bytes.NewReader(peek[:n]), logStream)) 37 | return ioutil.NopCloser(utf8Reader) 38 | } 39 | return ioutil.NopCloser(io.MultiReader(bytes.NewReader(peek[:n]), logStream)) 40 | } 41 | -------------------------------------------------------------------------------- /internal/routes/system_settings_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/ciliverse/cilikube/pkg/auth" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // RegisterSystemSettingsRoutes registers system settings routes for administrators 10 | func RegisterSystemSettingsRoutes(router *gin.RouterGroup) { 11 | settingsHandler := handlers.NewSystemSettingsHandler() 12 | 13 | // Apply JWT middleware and admin permission check to all system settings routes 14 | settingsRoutes := router.Group("/settings") 15 | settingsRoutes.Use(auth.JWTAuthMiddleware()) 16 | // TODO: Add admin permission middleware here 17 | { 18 | // System information 19 | settingsRoutes.GET("/system", settingsHandler.GetSystemInfo) 20 | 21 | // OAuth settings 22 | settingsRoutes.GET("/oauth", settingsHandler.GetOAuthSettings) 23 | settingsRoutes.PUT("/oauth", settingsHandler.UpdateOAuthSettings) 24 | 25 | // Security settings 26 | settingsRoutes.GET("/security", settingsHandler.GetSecuritySettings) 27 | settingsRoutes.PUT("/security", settingsHandler.UpdateSecuritySettings) 28 | 29 | // System preferences 30 | settingsRoutes.GET("/preferences", settingsHandler.GetSystemPreferences) 31 | settingsRoutes.PUT("/preferences", settingsHandler.UpdateSystemPreferences) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **⚠️ Please check the following before submitting:** 11 | 12 | * [ ] I have searched existing Issues to confirm this feature request hasn't been proposed before. 13 | * [ ] I have considered whether this feature aligns with the project's core goals. 14 | 15 | **Is your feature request related to a problem? Please describe.** 16 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 17 | 18 | **Describe the solution you'd like** 19 | A clear and concise description of what you want to happen. 20 | 21 | **Describe alternatives you've considered (optional)** 22 | A clear and concise description of any alternative solutions or features you've considered. 23 | 24 | **Why would this feature be valuable to other users?** 25 | Explain why you think this feature would be helpful to other users of the project. 26 | 27 | **Additional context (optional)** 28 | Add any other context or screenshots about the feature request here. For example, how does this feature relate to specific aspects of K8s (e.g., deployments, services, storage, network policies, etc.)? How would it simplify K8s management? 29 | 30 | --- -------------------------------------------------------------------------------- /pkg/metrics/prometheus.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | ) 10 | 11 | var ( 12 | RequestCounter = prometheus.NewCounterVec( 13 | prometheus.CounterOpts{ 14 | Name: "http_requests_total", 15 | Help: "Total number of HTTP requests", 16 | }, 17 | []string{"method", "path", "status"}, 18 | ) 19 | 20 | RequestDuration = prometheus.NewHistogramVec( 21 | prometheus.HistogramOpts{ 22 | Name: "http_request_duration_seconds", 23 | Help: "Duration of HTTP requests", 24 | Buckets: []float64{0.1, 0.5, 1, 2, 5}, 25 | }, 26 | []string{"method", "path"}, 27 | ) 28 | ) 29 | 30 | func init() { 31 | prometheus.MustRegister(RequestCounter, RequestDuration) 32 | } 33 | 34 | func PromMiddleware() gin.HandlerFunc { 35 | return func(c *gin.Context) { 36 | timer := prometheus.NewTimer(RequestDuration.WithLabelValues(c.Request.Method, c.FullPath())) 37 | defer timer.ObserveDuration() 38 | 39 | c.Next() 40 | 41 | RequestCounter.WithLabelValues( 42 | c.Request.Method, 43 | c.FullPath(), 44 | fmt.Sprintf("%d", c.Writer.Status()), 45 | ).Inc() 46 | } 47 | } 48 | 49 | func PromHandler() gin.HandlerFunc { 50 | return gin.WrapH(promhttp.Handler()) 51 | } 52 | -------------------------------------------------------------------------------- /internal/routes/profile_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/ciliverse/cilikube/internal/service" 6 | "github.com/ciliverse/cilikube/pkg/auth" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // RegisterProfileRoutes registers profile management routes 11 | func RegisterProfileRoutes(router *gin.RouterGroup, authService *service.AuthService, roleService *service.RoleService) { 12 | profileHandler := handlers.NewProfileHandler(authService, roleService) 13 | 14 | // Apply JWT middleware to all profile routes 15 | profileRoutes := router.Group("/profile") 16 | profileRoutes.Use(auth.JWTAuthMiddleware()) 17 | { 18 | // Profile management 19 | profileRoutes.GET("", profileHandler.GetProfile) 20 | profileRoutes.PUT("", profileHandler.UpdateProfile) 21 | 22 | // Password management 23 | profileRoutes.PUT("/password", profileHandler.ChangePassword) 24 | 25 | // Avatar management 26 | profileRoutes.POST("/avatar", profileHandler.UploadAvatar) 27 | profileRoutes.PUT("/avatar", profileHandler.UpdateAvatar) 28 | profileRoutes.DELETE("/avatar", profileHandler.DeleteAvatar) 29 | 30 | // Role and permission information 31 | profileRoutes.GET("/roles", profileHandler.GetUserRoles) 32 | profileRoutes.GET("/permissions", profileHandler.GetUserPermissions) 33 | 34 | // Activity log 35 | profileRoutes.GET("/activity", profileHandler.GetActivityLog) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # UI 目录已迁移 / UI Directory Has Been Migrated 2 | 3 | ## 中文 4 | 5 | **注意:** 本项目的 `ui` 目录已独立为一个单独的仓库,以更好地管理和维护前端代码。 6 | 7 | ### 新仓库地址 8 | 请访问以下地址获取最新的 UI 代码: 9 | [**ciliverse/cilikube-web**](https://github.com/ciliverse/cilikube-web) 10 | 11 | ### 为什么迁移? 12 | - 将 UI 代码独立为单独仓库,便于前端开发的模块化管理。 13 | - 提高代码维护效率,简化主项目的结构。 14 | - 允许自由选择前端技术栈和 UI 组件,以适应不同的开发需求。 15 | 16 | ### 如何使用? 17 | 请访问新仓库 [ciliverse/cilikube-web](https://github.com/ciliverse/cilikube-web) 获取 UI 部分的源码、文档和贡献指南。 18 | 19 | 如有任何问题,请在新的仓库中提交 Issue 或联系我们! 20 | 21 | --- 22 | 23 | ## English 24 | 25 | **Note:** The `ui` directory of this project has been moved to a separate repository to better manage and maintain the frontend codebase. 26 | 27 | ### New Repository 28 | Please visit the following address to access the latest UI code: 29 | [**ciliverse/cilikube-web**](https://github.com/ciliverse/cilikube-web) 30 | 31 | ### Why the Migration? 32 | - Moving the UI code to a separate repository allows for modular frontend development. 33 | - It improves code maintenance efficiency and simplifies the structure of the main project. 34 | - It enables freedom in choosing frontend technology stacks and UI components to meet diverse development needs. 35 | 36 | ### How to Use? 37 | Visit the new repository [ciliverse/cilikube-web](https://github.com/ciliverse/cilikube-web) for the UI source code, documentation, and contribution guidelines. 38 | 39 | If you have any questions, please open an Issue in the new repository or contact us! 40 | 41 | --- 42 | -------------------------------------------------------------------------------- /deployments/docker/.env.example: -------------------------------------------------------------------------------- 1 | # CiliKube Docker Environment Variables Configuration Example 2 | # Copy this file to .env and modify as needed 3 | 4 | # === Application Configuration === 5 | GIN_MODE=release 6 | CILIKUBE_CONFIG_PATH=/app/configs/config.yaml 7 | 8 | # === Database Configuration === 9 | # MySQL 10 | MYSQL_ROOT_PASSWORD=your_secure_root_password 11 | MYSQL_DATABASE=cilikube_db 12 | MYSQL_USER=cilikube_user 13 | MYSQL_PASSWORD=your_secure_password 14 | 15 | # === Redis Configuration === 16 | REDIS_PASSWORD=your_redis_password 17 | 18 | # === Monitoring Configuration === 19 | # Grafana 20 | GF_SECURITY_ADMIN_PASSWORD=your_grafana_password 21 | 22 | # === Network Configuration === 23 | # Port mappings 24 | BACKEND_PORT=8080 25 | FRONTEND_PORT=80 26 | MYSQL_PORT=3306 27 | REDIS_PORT=6379 28 | PROMETHEUS_PORT=9090 29 | GRAFANA_PORT=3000 30 | 31 | # === Storage Configuration === 32 | # Data volume paths 33 | MYSQL_DATA_PATH=./data/mysql 34 | REDIS_DATA_PATH=./data/redis 35 | PROMETHEUS_DATA_PATH=./data/prometheus 36 | GRAFANA_DATA_PATH=./data/grafana 37 | 38 | # === Kubernetes Configuration === 39 | # Kubeconfig file path 40 | KUBECONFIG_PATH=~/.kube/config 41 | 42 | # === Logging Configuration === 43 | LOG_LEVEL=info 44 | LOG_MAX_SIZE=10m 45 | LOG_MAX_FILES=3 46 | 47 | # === Security Configuration === 48 | # JWT secret key 49 | JWT_SECRET=your_jwt_secret_key_change_in_production 50 | 51 | # === Image Configuration === 52 | # Image registry and versions 53 | REGISTRY=cilliantech 54 | BACKEND_IMAGE_TAG=latest 55 | FRONTEND_IMAGE_TAG=latest -------------------------------------------------------------------------------- /pkg/utils/validators.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | 10 | appsv1 "k8s.io/api/apps/v1" 11 | "k8s.io/apimachinery/pkg/util/yaml" 12 | ) 13 | 14 | var ( 15 | dns1123Regex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) 16 | ) 17 | 18 | // ValidateNamespace validates namespace format 19 | func ValidateNamespace(ns string) bool { 20 | return dns1123Regex.MatchString(ns) && len(ns) <= 63 21 | } 22 | 23 | // ValidateResourceName validates resource name format 24 | func ValidateResourceName(name string) bool { 25 | return dns1123Regex.MatchString(name) && len(name) <= 253 26 | } 27 | 28 | // ParseInt safely converts string to integer 29 | func ParseInt(s string, defaultValue int) int { 30 | if s == "" { 31 | return defaultValue 32 | } 33 | val, err := strconv.Atoi(s) 34 | if err != nil { 35 | return defaultValue 36 | } 37 | return val 38 | } 39 | 40 | // ParseDeploymentFromFile parses YAML/JSON file to Deployment object (using Kubernetes native decoder) 41 | func ParseDeploymentFromFile(data []byte) (*appsv1.Deployment, error) { 42 | // Use YAML/JSON decoder provided by Kubernetes 43 | decoder := yaml.NewYAMLOrJSONDecoder( 44 | io.NopCloser( 45 | io.NewSectionReader( 46 | bytes.NewReader(data), 47 | 0, 48 | int64(len(data)), 49 | ), 50 | ), 51 | 1024, 52 | ) 53 | 54 | var deployment appsv1.Deployment 55 | if err := decoder.Decode(&deployment); err != nil { 56 | return nil, fmt.Errorf("failed to decode YAML/JSON: %v", err.Error()) 57 | } 58 | 59 | return &deployment, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/service/resource_client.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/watch" 9 | "k8s.io/client-go/kubernetes" 10 | ) 11 | 12 | // baseClient base resource client 13 | type baseClient[T runtime.Object] struct{} 14 | 15 | // Get retrieves a resource 16 | func (c *baseClient[T]) Get(ctx context.Context, clientset kubernetes.Interface, name string, opts metav1.GetOptions) (T, error) { 17 | var zero T 18 | return zero, nil 19 | } 20 | 21 | // List retrieves a list of resources 22 | func (c *baseClient[T]) List(ctx context.Context, clientset kubernetes.Interface, opts metav1.ListOptions) ([]T, error) { 23 | return nil, nil 24 | } 25 | 26 | // Create creates a resource 27 | func (c *baseClient[T]) Create(ctx context.Context, clientset kubernetes.Interface, obj T, opts metav1.CreateOptions) (T, error) { 28 | var zero T 29 | return zero, nil 30 | } 31 | 32 | // Update updates a resource 33 | func (c *baseClient[T]) Update(ctx context.Context, clientset kubernetes.Interface, obj T, opts metav1.UpdateOptions) (T, error) { 34 | var zero T 35 | return zero, nil 36 | } 37 | 38 | // Delete deletes a resource 39 | func (c *baseClient[T]) Delete(ctx context.Context, clientset kubernetes.Interface, name string, opts metav1.DeleteOptions) error { 40 | return nil 41 | } 42 | 43 | // Watch watches for resource changes 44 | func (c *baseClient[T]) Watch(ctx context.Context, clientset kubernetes.Interface, opts metav1.ListOptions) (watch.Interface, error) { 45 | return nil, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/handlers/kubernetes_proxy_handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | "github.com/ciliverse/cilikube/pkg/k8s" 8 | 9 | "github.com/gin-gonic/gin" 10 | "k8s.io/apimachinery/pkg/util/proxy" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | type ProxyHandler struct { 15 | clusterManager *k8s.ClusterManager 16 | } 17 | 18 | func NewProxyHandler(cm *k8s.ClusterManager) *ProxyHandler { 19 | return &ProxyHandler{clusterManager: cm} 20 | } 21 | 22 | func (h *ProxyHandler) Proxy(c *gin.Context) { 23 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 24 | if !ok { 25 | return 26 | } 27 | 28 | config := k8sClient.Config 29 | transport, err := rest.TransportFor(config) 30 | if err != nil { 31 | respondError(c, http.StatusInternalServerError, "internal server error: "+err.Error()) 32 | return 33 | } 34 | target, err := h.validateTarget(*c.Request.URL, config.Host) 35 | if err != nil { 36 | respondError(c, http.StatusInternalServerError, "internal server error: "+err.Error()) 37 | return 38 | } 39 | httpProxy := proxy.NewUpgradeAwareHandler(target, transport, false, false, nil) 40 | httpProxy.UpgradeTransport = proxy.NewUpgradeRequestRoundTripper(transport, transport) 41 | httpProxy.ServeHTTP(c.Writer, c.Request) 42 | } 43 | 44 | func (h *ProxyHandler) validateTarget(target url.URL, host string) (*url.URL, error) { 45 | kubeURL, err := url.Parse(host) 46 | if err != nil { 47 | return nil, err 48 | } 49 | target.Path = target.Path[len("/api/v1/proxy/"):] 50 | 51 | target.Host = kubeURL.Host 52 | target.Scheme = kubeURL.Scheme 53 | return &target, nil 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **⚠️ Please check the following before submitting:** 11 | 12 | * [ ] I have searched existing Issues to confirm this problem hasn't been reported before. 13 | * [ ] I am using the latest version of CiliKube. 14 | 15 | **Describe the Bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **Steps to Reproduce** 19 | Detailed steps to reproduce this bug: 20 | 1. Execute '...' 21 | 2. Click on '....' 22 | 3. See error '....' 23 | 24 | **Expected Behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Actual Behavior** 28 | A clear and concise description of what actually happened. 29 | 30 | **Screenshots/Logs (if applicable)** 31 | If applicable, add screenshots or relevant log snippets to help explain your problem. 32 | 33 | **Environment Information:** 34 | * **CiliKube Version:** [e.g. v1.2.3] 35 | * **Kubernetes Version (kubectl version):** `clientVersion` and `serverVersion` 36 | * **Cloud Provider (if applicable):** [e.g. AWS, GCP, Azure, local Kubeadm, Minikube, Kind, etc.] 37 | * **Operating System (node and client):** [e.g. Ubuntu 20.04, macOS Big Sur] 38 | * **Browser (if UI-related bug):** [e.g. Chrome 90, Firefox 88] 39 | * **Other relevant tools/versions:** [e.g. Helm version, kubectl plugins, etc.] 40 | 41 | **Additional Information (optional)** 42 | Any other context about the problem. For example, does this issue occur under specific configurations? Have there been any recent environment changes? 43 | 44 | --- -------------------------------------------------------------------------------- /internal/handlers/health_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestHealthCheck(t *testing.T) { 13 | // Set Gin to test mode 14 | gin.SetMode(gin.TestMode) 15 | 16 | // Create router 17 | router := gin.New() 18 | router.GET("/health", HealthCheck) 19 | 20 | // Create test request 21 | req, _ := http.NewRequest("GET", "/health", nil) 22 | w := httptest.NewRecorder() 23 | 24 | // Execute request 25 | router.ServeHTTP(w, req) 26 | 27 | // Verify response 28 | assert.Equal(t, http.StatusOK, w.Code) 29 | assert.Contains(t, w.Body.String(), "healthy") 30 | assert.Contains(t, w.Body.String(), "timestamp") 31 | assert.Contains(t, w.Body.String(), "uptime") 32 | } 33 | 34 | func TestReadinessCheck(t *testing.T) { 35 | gin.SetMode(gin.TestMode) 36 | 37 | router := gin.New() 38 | router.GET("/ready", ReadinessCheck) 39 | 40 | req, _ := http.NewRequest("GET", "/ready", nil) 41 | w := httptest.NewRecorder() 42 | 43 | router.ServeHTTP(w, req) 44 | 45 | assert.Equal(t, http.StatusOK, w.Code) 46 | assert.Contains(t, w.Body.String(), "ready") 47 | assert.Contains(t, w.Body.String(), "checks") 48 | } 49 | 50 | func TestLivenessCheck(t *testing.T) { 51 | gin.SetMode(gin.TestMode) 52 | 53 | router := gin.New() 54 | router.GET("/live", LivenessCheck) 55 | 56 | req, _ := http.NewRequest("GET", "/live", nil) 57 | w := httptest.NewRecorder() 58 | 59 | router.ServeHTTP(w, req) 60 | 61 | assert.Equal(t, http.StatusOK, w.Code) 62 | assert.Contains(t, w.Body.String(), "alive") 63 | } 64 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ---- Stage 1: BUILDER ---- 2 | FROM golang:1.24-alpine AS builder 3 | 4 | # 安装必要的工具 5 | RUN apk add --no-cache git ca-certificates tzdata 6 | 7 | # 设置 Go 代理以加速依赖下载 8 | ENV GOPROXY=https://goproxy.cn,direct 9 | ENV GO111MODULE=on 10 | ENV CGO_ENABLED=0 11 | ENV GOOS=linux 12 | 13 | # 设置工作目录 14 | WORKDIR /build 15 | 16 | # 复制 go.mod 和 go.sum 文件 17 | COPY go.mod go.sum ./ 18 | 19 | # 下载依赖 20 | RUN go mod download && go mod verify 21 | 22 | # 复制源代码 23 | COPY . . 24 | 25 | # 获取构建信息 26 | ARG VERSION=dev 27 | ARG BUILD_TIME 28 | ARG GIT_COMMIT 29 | 30 | # 编译应用 31 | RUN go build \ 32 | -ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT}" \ 33 | -o cilikube \ 34 | ./cmd/server/main.go 35 | 36 | 37 | # ---- Stage 2: RUNNER ---- 38 | FROM alpine:3.19 AS runner 39 | 40 | # 安装必要的运行时依赖 41 | RUN apk add --no-cache ca-certificates tzdata && \ 42 | update-ca-certificates 43 | 44 | # 创建非 root 用户 45 | RUN addgroup -g 1001 -S appgroup && \ 46 | adduser -u 1001 -S appuser -G appgroup 47 | 48 | # 设置工作目录 49 | WORKDIR /app 50 | 51 | # 从构建阶段复制二进制文件和配置 52 | COPY --from=builder /build/cilikube ./ 53 | COPY --from=builder /build/configs ./configs/ 54 | 55 | # 设置文件权限 56 | RUN chown -R appuser:appgroup /app && \ 57 | chmod +x /app/cilikube 58 | 59 | # 健康检查 60 | HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 61 | CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 62 | 63 | # 暴露端口 64 | EXPOSE 8080 65 | 66 | # 切换到非 root 用户 67 | USER appuser 68 | 69 | # 设置入口点 70 | ENTRYPOINT ["./cilikube"] 71 | CMD ["--config", "configs/config.yaml"] 72 | -------------------------------------------------------------------------------- /internal/service/pod_exec_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "io" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "k8s.io/client-go/kubernetes" 8 | "k8s.io/client-go/kubernetes/scheme" 9 | "k8s.io/client-go/rest" 10 | "k8s.io/client-go/tools/remotecommand" 11 | ) 12 | 13 | // ExecOptions execution options 14 | type ExecOptions struct { 15 | Command []string 16 | Container string 17 | Stdin bool 18 | Stdout bool 19 | Stderr bool 20 | TTY bool 21 | } 22 | 23 | // PodExecService handles Pod execution related operations 24 | type PodExecService struct { 25 | config *rest.Config 26 | } 27 | 28 | // NewPodExecService creates Pod execution service 29 | func NewPodExecService(config *rest.Config) *PodExecService { 30 | return &PodExecService{ 31 | config: config, 32 | } 33 | } 34 | 35 | // Exec executes command in Pod 36 | func (s *PodExecService) Exec(clientset kubernetes.Interface, namespace, podName string, options *ExecOptions, stdout io.Writer, stdin io.Reader) error { 37 | req := clientset.CoreV1().RESTClient().Post(). 38 | Resource("pods"). 39 | Name(podName). 40 | Namespace(namespace). 41 | SubResource("exec") 42 | 43 | req.VersionedParams(&corev1.PodExecOptions{ 44 | Container: options.Container, 45 | Command: options.Command, 46 | Stdin: options.Stdin, 47 | Stdout: options.Stdout, 48 | Stderr: options.Stderr, 49 | TTY: options.TTY, 50 | }, scheme.ParameterCodec) 51 | 52 | exec, err := remotecommand.NewSPDYExecutor(s.config, "POST", req.URL()) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return exec.Stream(remotecommand.StreamOptions{ 58 | Stdin: stdin, 59 | Stdout: stdout, 60 | Stderr: stdout, 61 | Tty: options.TTY, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /internal/service/app_services.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | networkingv1 "k8s.io/api/networking/v1" 7 | ) 8 | 9 | // AppServices serves as a collection of all application services, defined here uniformly 10 | type AppServices struct { 11 | // Cluster and installer services 12 | ClusterService *ClusterService 13 | InstallerService InstallerService 14 | 15 | // [Added] Node metrics service 16 | NodeMetricsService *NodeMetricsService 17 | 18 | // [Added] Summary service 19 | SummaryService *SummaryService 20 | 21 | // [Added] Event service 22 | EventService *EventService 23 | 24 | // [Added] CRD service 25 | CRDService CRDService 26 | 27 | // Authentication and authorization services 28 | AuthService *AuthService 29 | OAuthService *OAuthService 30 | RoleService *RoleService 31 | PermissionService *PermissionService 32 | 33 | // Kubernetes resource services 34 | NodeService ResourceService[*corev1.Node] 35 | NamespaceService ResourceService[*corev1.Namespace] 36 | PVService ResourceService[*corev1.PersistentVolume] 37 | PodService ResourceService[*corev1.Pod] 38 | DeploymentService ResourceService[*appsv1.Deployment] 39 | ServiceService ResourceService[*corev1.Service] 40 | DaemonSetService ResourceService[*appsv1.DaemonSet] 41 | IngressService ResourceService[*networkingv1.Ingress] 42 | ConfigMapService ResourceService[*corev1.ConfigMap] 43 | SecretService ResourceService[*corev1.Secret] 44 | PVCService ResourceService[*corev1.PersistentVolumeClaim] 45 | StatefulSetService ResourceService[*appsv1.StatefulSet] 46 | 47 | // Pod logs and terminal services 48 | PodLogsService *PodLogsService 49 | PodExecService *PodExecService 50 | } 51 | -------------------------------------------------------------------------------- /internal/service/pod_logs_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | v1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/kubernetes" 10 | ) 11 | 12 | // PodLogsService handles Pod logs related operations 13 | type PodLogsService struct{} 14 | 15 | // NewPodLogsService creates Pod logs service 16 | func NewPodLogsService() *PodLogsService { 17 | return &PodLogsService{} 18 | } 19 | 20 | // Get retrieves Pod information 21 | func (s *PodLogsService) Get(clientset kubernetes.Interface, namespace, name string) (*v1.Pod, error) { 22 | return clientset.CoreV1().Pods(namespace).Get(context.Background(), name, metav1.GetOptions{}) 23 | } 24 | 25 | // GetPodLogs retrieves Pod log stream 26 | func (s *PodLogsService) GetPodLogs(clientset kubernetes.Interface, namespace, name string, opts *v1.PodLogOptions) (io.ReadCloser, error) { 27 | req := clientset.CoreV1().Pods(namespace).GetLogs(name, opts) 28 | stream, err := req.Stream(context.Background()) 29 | if err != nil { 30 | return nil, err 31 | } 32 | // Automatically detect and convert GBK -> UTF-8 33 | return ConvertIfGBK(stream), nil 34 | } 35 | 36 | // GetLogs retrieves Pod logs 37 | func (s *PodLogsService) GetLogs(clientset kubernetes.Interface, namespace, podName, container string, follow, previous bool, tailLines int64, writer io.Writer) error { 38 | opts := &v1.PodLogOptions{ 39 | Container: container, 40 | Follow: follow, 41 | Previous: previous, 42 | TailLines: &tailLines, 43 | } 44 | 45 | req := clientset.CoreV1().Pods(namespace).GetLogs(podName, opts) 46 | reader, err := req.Stream(context.Background()) 47 | if err != nil { 48 | return err 49 | } 50 | defer reader.Close() 51 | 52 | _, err = io.Copy(writer, reader) 53 | return err 54 | } 55 | -------------------------------------------------------------------------------- /internal/handlers/health.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // HealthResponse health check response structure 11 | type HealthResponse struct { 12 | Status string `json:"status"` 13 | Timestamp time.Time `json:"timestamp"` 14 | Version string `json:"version,omitempty"` 15 | Uptime string `json:"uptime,omitempty"` 16 | Checks map[string]string `json:"checks,omitempty"` 17 | } 18 | 19 | var startTime = time.Now() 20 | 21 | // HealthCheck basic health check 22 | func HealthCheck(c *gin.Context) { 23 | c.JSON(http.StatusOK, HealthResponse{ 24 | Status: "healthy", 25 | Timestamp: time.Now(), 26 | Uptime: time.Since(startTime).String(), 27 | }) 28 | } 29 | 30 | // ReadinessCheck readiness check 31 | func ReadinessCheck(c *gin.Context) { 32 | checks := make(map[string]string) 33 | 34 | // Check Kubernetes connection 35 | checks["kubernetes"] = "ok" 36 | 37 | // Check database connection (if enabled) 38 | checks["database"] = "ok" 39 | 40 | // All checks passed 41 | allHealthy := true 42 | for _, status := range checks { 43 | if status != "ok" { 44 | allHealthy = false 45 | break 46 | } 47 | } 48 | 49 | status := "ready" 50 | httpStatus := http.StatusOK 51 | if !allHealthy { 52 | status = "not ready" 53 | httpStatus = http.StatusServiceUnavailable 54 | } 55 | 56 | c.JSON(httpStatus, HealthResponse{ 57 | Status: status, 58 | Timestamp: time.Now(), 59 | Uptime: time.Since(startTime).String(), 60 | Checks: checks, 61 | }) 62 | } 63 | 64 | // LivenessCheck liveness check 65 | func LivenessCheck(c *gin.Context) { 66 | c.JSON(http.StatusOK, HealthResponse{ 67 | Status: "alive", 68 | Timestamp: time.Now(), 69 | Uptime: time.Since(startTime).String(), 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /internal/routes/role_management_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/ciliverse/cilikube/internal/service" 6 | "github.com/ciliverse/cilikube/pkg/auth" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // RegisterRoleManagementRoutes registers role management routes for administrators 11 | func RegisterRoleManagementRoutes(router *gin.RouterGroup, roleService *service.RoleService) { 12 | roleHandler := handlers.NewRoleManagementHandler(roleService) 13 | 14 | // Apply JWT middleware and admin permission check to all role management routes 15 | roleRoutes := router.Group("/roles") 16 | roleRoutes.Use(auth.JWTAuthMiddleware()) 17 | // TODO: Add admin permission middleware here 18 | { 19 | // Role CRUD operations 20 | roleRoutes.GET("", roleHandler.ListRoles) 21 | roleRoutes.POST("", roleHandler.CreateRole) 22 | roleRoutes.GET("/:id", roleHandler.GetRole) 23 | roleRoutes.PUT("/:id", roleHandler.UpdateRole) 24 | roleRoutes.DELETE("/:id", roleHandler.DeleteRole) 25 | 26 | // Role permission operations 27 | roleRoutes.GET("/:id/permissions", roleHandler.GetRolePermissions) 28 | roleRoutes.PUT("/:id/permissions", roleHandler.UpdateRolePermissions) 29 | 30 | // Role assignment operations 31 | roleRoutes.POST("/assign", roleHandler.AssignRoleToUser) 32 | roleRoutes.POST("/remove", roleHandler.RemoveRoleFromUser) 33 | 34 | // Role-user relationship queries 35 | roleRoutes.GET("/users/:userId", roleHandler.GetUserRoles) 36 | roleRoutes.GET("/:id/users", roleHandler.GetRoleUsers) 37 | } 38 | 39 | // Permission management routes 40 | permissionRoutes := router.Group("/permissions") 41 | permissionRoutes.Use(auth.JWTAuthMiddleware()) 42 | // TODO: Add admin permission middleware here 43 | { 44 | permissionRoutes.GET("", roleHandler.GetAvailablePermissions) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/models/cluster_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type CreateClusterRequest struct { 6 | Name string `json:"name" binding:"required"` 7 | KubeconfigData string `json:"kubeconfigData" binding:"required"` 8 | Provider string `json:"provider"` 9 | Description string `json:"description"` 10 | Environment string `json:"environment"` 11 | Region string `json:"region"` 12 | } 13 | 14 | type UpdateClusterRequest struct { 15 | Name string `json:"name"` 16 | Provider string `json:"provider"` 17 | Description string `json:"description"` 18 | Environment string `json:"environment"` 19 | Region string `json:"region"` 20 | Status string `json:"status"` 21 | Labels map[string]string `json:"labels"` 22 | KubeconfigData string `json:"kubeconfigData,omitempty"` 23 | } 24 | 25 | type ClusterResponse struct { 26 | ID string `json:"id"` 27 | Name string `json:"name"` 28 | Provider string `json:"provider"` 29 | Description string `json:"description"` 30 | Environment string `json:"environment"` 31 | Region string `json:"region"` 32 | Version string `json:"version"` 33 | Status string `json:"status"` 34 | Source string `json:"source"` 35 | Labels map[string]string `json:"labels"` 36 | CreatedAt time.Time `json:"created_at"` 37 | UpdatedAt time.Time `json:"updated_at"` 38 | } 39 | 40 | type ClusterListResponse struct { 41 | ID string `json:"id"` 42 | Name string `json:"name"` 43 | Server string `json:"server"` 44 | Version string `json:"version"` 45 | Status string `json:"status"` 46 | Source string `json:"source"` 47 | Environment string `json:"environment"` 48 | } 49 | -------------------------------------------------------------------------------- /internal/store/encryption.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | // !!! IMPORTANT SECURITY WARNING !!! 12 | // In production environments, the encryption key (encryptionKey) must never be hardcoded in the code. 13 | // It must be loaded from a secure location, such as environment variables or Secrets Manager. 14 | // This key must be 32 bytes long, corresponding to AES-256. 15 | 16 | func Encrypt(data []byte, key []byte) ([]byte, error) { 17 | if len(key) != 32 { 18 | return nil, fmt.Errorf("encryption key must be 32 bytes long for AES-256") 19 | } 20 | block, err := aes.NewCipher(key) 21 | if err != nil { 22 | return nil, fmt.Errorf("could not create new cipher: %w", err) 23 | } 24 | gcm, err := cipher.NewGCM(block) 25 | if err != nil { 26 | return nil, fmt.Errorf("could not create new GCM: %w", err) 27 | } 28 | nonce := make([]byte, gcm.NonceSize()) 29 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 30 | return nil, fmt.Errorf("failed to generate nonce: %w", err) 31 | } 32 | ciphertext := gcm.Seal(nonce, nonce, data, nil) 33 | return ciphertext, nil 34 | } 35 | 36 | func Decrypt(data []byte, key []byte) ([]byte, error) { 37 | if len(key) != 32 { 38 | return nil, fmt.Errorf("encryption key must be 32 bytes long for AES-256") 39 | } 40 | block, err := aes.NewCipher(key) 41 | if err != nil { 42 | return nil, fmt.Errorf("could not create new cipher: %w", err) 43 | } 44 | gcm, err := cipher.NewGCM(block) 45 | if err != nil { 46 | return nil, fmt.Errorf("could not create new GCM: %w", err) 47 | } 48 | nonceSize := gcm.NonceSize() 49 | if len(data) < nonceSize { 50 | return nil, fmt.Errorf("ciphertext is too short") 51 | } 52 | nonce, ciphertext := data[:nonceSize], data[nonceSize:] 53 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to decrypt data: %w", err) 56 | } 57 | return plaintext, nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/routes/auth_routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/ciliverse/cilikube/internal/handlers" 5 | "github.com/ciliverse/cilikube/internal/service" 6 | "github.com/ciliverse/cilikube/pkg/auth" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // RegisterAuthRoutes registers authentication and OAuth routes 11 | func RegisterAuthRoutes(authGroup *gin.RouterGroup, authService *service.AuthService, oauthService *service.OAuthService) { 12 | authHandler := handlers.NewAuthHandler(authService) 13 | oauthHandler := handlers.NewOAuthHandler(oauthService) 14 | 15 | // Routes are registered directly on the passed authGroup, no longer creating our own 16 | 17 | // Public routes (no authentication required) 18 | authGroup.POST("/login", authHandler.Login) 19 | authGroup.POST("/register", authHandler.Register) 20 | 21 | // OAuth routes (public) 22 | oauth := authGroup.Group("/oauth") 23 | { 24 | oauth.GET("/:provider/auth", oauthHandler.GetAuthURL) 25 | oauth.POST("/callback", oauthHandler.HandleCallback) 26 | } 27 | 28 | // Routes requiring authentication 29 | authenticated := authGroup.Group("") 30 | authenticated.Use(auth.JWTAuthMiddleware()) 31 | { 32 | authenticated.GET("/profile", authHandler.GetProfile) 33 | authenticated.GET("/profile/detailed", authHandler.GetDetailedProfile) 34 | authenticated.PUT("/profile", authHandler.UpdateProfile) 35 | authenticated.POST("/change-password", authHandler.ChangePassword) 36 | authenticated.POST("/refresh", authHandler.RefreshToken) 37 | authenticated.POST("/logout", authHandler.Logout) 38 | 39 | // OAuth account management (authenticated) 40 | authenticated.POST("/oauth/link", oauthHandler.LinkAccount) 41 | authenticated.POST("/oauth/unlink", oauthHandler.UnlinkAccount) 42 | } 43 | 44 | // Admin-only routes 45 | admin := authGroup.Group("/admin") // Grouping admin routes under /admin for clarity 46 | admin.Use(auth.JWTAuthMiddleware(), auth.AdminRequiredMiddleware()) 47 | { 48 | admin.GET("/users", authHandler.GetUserList) 49 | admin.PUT("/users/:id/status", authHandler.UpdateUserStatus) 50 | admin.DELETE("/users/:id", authHandler.DeleteUser) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/service/resource_service_factory.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "sync" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | networkingv1 "k8s.io/api/networking/v1" 9 | ) 10 | 11 | // ResourceServiceFactory resource service factory 12 | type ResourceServiceFactory struct { 13 | services map[string]interface{} 14 | mu sync.RWMutex 15 | } 16 | 17 | // NewResourceServiceFactory creates resource service factory 18 | func NewResourceServiceFactory() *ResourceServiceFactory { 19 | return &ResourceServiceFactory{ 20 | services: make(map[string]interface{}), 21 | } 22 | } 23 | 24 | // RegisterService registers resource service 25 | func (f *ResourceServiceFactory) RegisterService(name string, service interface{}) { 26 | f.mu.Lock() 27 | defer f.mu.Unlock() 28 | f.services[name] = service 29 | } 30 | 31 | // GetService gets resource service 32 | func (f *ResourceServiceFactory) GetService(name string) interface{} { 33 | f.mu.RLock() 34 | defer f.mu.RUnlock() 35 | return f.services[name] 36 | } 37 | 38 | // InitializeDefaultServices initializes all default services 39 | func (f *ResourceServiceFactory) InitializeDefaultServices() { 40 | f.RegisterService("nodes", NewBaseResourceService[*corev1.Node](new(NodeClient))) 41 | f.RegisterService("pods", NewBaseResourceService[*corev1.Pod](new(PodClient))) 42 | f.RegisterService("deployments", NewBaseResourceService[*appsv1.Deployment](new(DeploymentClient))) 43 | f.RegisterService("services", NewBaseResourceService[*corev1.Service](new(ServiceClient))) 44 | f.RegisterService("daemonsets", NewBaseResourceService[*appsv1.DaemonSet](new(DaemonSetClient))) 45 | f.RegisterService("ingresses", NewBaseResourceService[*networkingv1.Ingress](new(IngressClient))) 46 | f.RegisterService("configmaps", NewBaseResourceService[*corev1.ConfigMap](new(ConfigMapClient))) 47 | f.RegisterService("secrets", NewBaseResourceService[*corev1.Secret](new(SecretClient))) 48 | f.RegisterService("persistentvolumeclaims", NewBaseResourceService[*corev1.PersistentVolumeClaim](new(PVCClient))) 49 | f.RegisterService("persistentvolumes", NewBaseResourceService[*corev1.PersistentVolume](new(PVClient))) 50 | f.RegisterService("statefulsets", NewBaseResourceService[*appsv1.StatefulSet](new(StatefulSetClient))) 51 | f.RegisterService("namespaces", NewBaseResourceService[*corev1.Namespace](new(NamespaceClient))) 52 | } 53 | -------------------------------------------------------------------------------- /internal/handlers/summary_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/ciliverse/cilikube/pkg/k8s" 7 | 8 | "github.com/ciliverse/cilikube/internal/service" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // Existing SummaryHandler struct... 13 | type SummaryHandler struct { 14 | service *service.SummaryService 15 | clusterManager *k8s.ClusterManager 16 | } 17 | 18 | func NewSummaryHandler(svc *service.SummaryService, cm *k8s.ClusterManager) *SummaryHandler { 19 | return &SummaryHandler{service: svc, clusterManager: cm} 20 | } 21 | 22 | // GetResourceSummary gets resource summary information 23 | func (h *SummaryHandler) GetResourceSummary(c *gin.Context) { 24 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 25 | if !ok { 26 | return 27 | } 28 | 29 | summary, errs := h.service.GetResourceSummary(k8sClient.Clientset) 30 | if len(errs) > 0 { 31 | // Log errors but still return the summary with available data 32 | for resource, err := range errs { 33 | c.Header("X-Resource-Error-"+resource, err.Error()) 34 | } 35 | } 36 | 37 | c.JSON(http.StatusOK, gin.H{ 38 | "code": http.StatusOK, 39 | "data": summary, 40 | "message": "successfully retrieved resource summary", 41 | }) 42 | } 43 | 44 | // --- New Handler for Backend Dependencies --- 45 | 46 | // GetBackendDependencies godoc 47 | // @Summary Get Backend Dependencies 48 | // @Description Retrieves the list of direct Go module dependencies and their versions from go.mod. 49 | // @Tags Summary 50 | // @Accept json 51 | // @Produce json 52 | // @Success 200 {array} service.BackendDependency "List of backend dependencies" 53 | // @Failure 500 {object} handlers.ErrorResponse "Internal Server Error - Failed to read/parse go.mod" 54 | // @Router /api/v1/summary/backend-dependencies [get] 55 | func (h *SummaryHandler) GetBackendDependencies(c *gin.Context) { 56 | dependencies, err := h.service.GetBackendDependencies() 57 | if err != nil { 58 | c.JSON(http.StatusInternalServerError, gin.H{ 59 | "code": http.StatusInternalServerError, 60 | "data": nil, 61 | "message": "Failed to get backend dependencies: " + err.Error(), 62 | }) 63 | return 64 | } 65 | // Use a different response structure if needed, but returning the slice directly is fine 66 | c.JSON(http.StatusOK, gin.H{ 67 | "code": http.StatusOK, 68 | "data": dependencies, // Return the slice directly 69 | "message": "success", 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /internal/handlers/event_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/ciliverse/cilikube/internal/models" 8 | "github.com/ciliverse/cilikube/internal/service" 9 | "github.com/ciliverse/cilikube/pkg/utils" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type EventHandler struct { 14 | service *service.EventService 15 | } 16 | 17 | func NewEventHandler(svc *service.EventService) *EventHandler { 18 | return &EventHandler{service: svc} 19 | } 20 | 21 | // ListEvents handles GET /api/v1/events 22 | func (h *EventHandler) ListEvents(c *gin.Context) { 23 | var req models.EventListRequest 24 | 25 | // Parse query parameters 26 | req.Namespace = c.Query("namespace") 27 | req.Type = c.Query("type") 28 | req.Since = c.Query("since") 29 | 30 | if limitStr := c.Query("limit"); limitStr != "" { 31 | if limit, err := strconv.Atoi(limitStr); err == nil { 32 | req.Limit = limit 33 | } 34 | } 35 | 36 | response, err := h.service.ListEvents(req) 37 | if err != nil { 38 | utils.ApiError(c, http.StatusInternalServerError, "failed to retrieve cluster events", err.Error()) 39 | return 40 | } 41 | 42 | utils.ApiSuccess(c, response, "successfully retrieved cluster events") 43 | } 44 | 45 | // GetRecentEvents handles GET /api/v1/events/recent 46 | func (h *EventHandler) GetRecentEvents(c *gin.Context) { 47 | limit := 10 // Default limit for recent events 48 | if limitStr := c.Query("limit"); limitStr != "" { 49 | if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { 50 | limit = parsedLimit 51 | } 52 | } 53 | 54 | events, err := h.service.GetRecentEvents(limit) 55 | if err != nil { 56 | utils.ApiError(c, http.StatusInternalServerError, "failed to retrieve recent events", err.Error()) 57 | return 58 | } 59 | 60 | utils.ApiSuccess(c, gin.H{ 61 | "events": events, 62 | "total": len(events), 63 | }, "successfully retrieved recent events") 64 | } 65 | 66 | // GetEventsByObject handles GET /api/v1/events/object/:kind/:name 67 | func (h *EventHandler) GetEventsByObject(c *gin.Context) { 68 | kind := c.Param("kind") 69 | name := c.Param("name") 70 | namespace := c.Query("namespace") 71 | 72 | if kind == "" || name == "" { 73 | utils.ApiError(c, http.StatusBadRequest, "invalid parameters", "kind and name are required") 74 | return 75 | } 76 | 77 | events, err := h.service.GetEventsByObject(namespace, kind, name) 78 | if err != nil { 79 | utils.ApiError(c, http.StatusInternalServerError, "failed to retrieve object events", err.Error()) 80 | return 81 | } 82 | 83 | utils.ApiSuccess(c, gin.H{ 84 | "events": events, 85 | "total": len(events), 86 | "object": gin.H{ 87 | "kind": kind, 88 | "name": name, 89 | "namespace": namespace, 90 | }, 91 | }, "successfully retrieved object events") 92 | } 93 | -------------------------------------------------------------------------------- /internal/initialization/start.go: -------------------------------------------------------------------------------- 1 | package initialization 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | // Version allows injection via build parameters (go build -ldflags "-X 'github.com/ciliverse/cilikube/internal/initialization.Version=v0.3.1'") 15 | var Version = "" 16 | 17 | // DisplayServerInfo prints service startup information, including local/LAN addresses, mode, version, Go version, startup time, etc. 18 | func DisplayServerInfo(serverAddr, mode string) { 19 | version := getVersion() 20 | goVersion := runtime.Version() 21 | buildTime := getBuildTime() 22 | hostname, _ := os.Hostname() 23 | // Set to Beijing time (UTC+8) 24 | loc, err := time.LoadLocation("Asia/Shanghai") 25 | var startTime string 26 | if err == nil { 27 | startTime = time.Now().In(loc).Format("2006-01-02 15:04:05 MST") 28 | } else { 29 | startTime = time.Now().Format("2006-01-02 15:04:05") 30 | } 31 | color.Cyan("🚀 CiliKube Server is running!") 32 | color.Green(" ➜ Local: http://127.0.0.1%s", serverAddr) 33 | color.Green(" ➜ Network: http://%s%s", getLocalIP(), serverAddr) 34 | color.Yellow(" ➜ Mode: %s", mode) 35 | color.Magenta(" ➜ Version: %s", version) 36 | color.Cyan(" ➜ Go Version: %s", goVersion) 37 | color.Cyan(" ➜ Hostname: %s", hostname) 38 | color.Cyan(" ➜ Start Time: %s", startTime) 39 | if buildTime != "" { 40 | color.Cyan(" ➜ Build Time: %s", buildTime) 41 | } 42 | color.White("-------------------------------------------------") 43 | } 44 | 45 | // getVersion gets version number, priority: environment variable > build variable > VERSION file > default value 46 | func getVersion() string { 47 | if v := os.Getenv("CILIKUBE_VERSION"); v != "" { 48 | return v 49 | } 50 | if Version != "" { 51 | return Version 52 | } 53 | data, err := os.ReadFile("VERSION") 54 | if err == nil { 55 | return strings.TrimSpace(string(data)) 56 | } 57 | log.Printf("[WARN] Failed to get version number (environment variable, build variable, VERSION file all invalid), using default version: %v", err) 58 | return "v0.0.1" 59 | } 60 | 61 | // getBuildTime supports injecting build time via build parameters (go build -ldflags "-X 'github.com/ciliverse/cilikube/internal/initialization.BuildTime=2025-06-24T12:00:00Z'") 62 | var BuildTime = "" 63 | 64 | func getBuildTime() string { 65 | return BuildTime 66 | } 67 | 68 | // getLocalIP gets the first non-loopback IPv4 address of the local machine, commonly used for LAN access 69 | func getLocalIP() string { 70 | addrs, err := net.InterfaceAddrs() 71 | if err != nil { 72 | return "unknown" 73 | } 74 | for _, addr := range addrs { 75 | if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil { 76 | return ipNet.IP.String() 77 | } 78 | } 79 | return "unknown" 80 | } 81 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # CiliKube API 2 | 3 | This directory contains API definitions, specifications, and documentation for CiliKube. 4 | 5 | ## API Versions 6 | 7 | - **v1** (`/api/v1/`) - Current stable API version 8 | 9 | ## Directory Structure 10 | 11 | ``` 12 | api/ 13 | ├── README.md # This file 14 | └── v1/ # API v1 definitions 15 | ├── README.md # v1 API documentation 16 | ├── openapi.yaml # OpenAPI 3.0 specification 17 | └── schemas/ # JSON schemas (if needed) 18 | ``` 19 | 20 | ## API Design Principles 21 | 22 | ### RESTful Design 23 | - Follow REST conventions for resource operations 24 | - Use appropriate HTTP methods (GET, POST, PUT, DELETE) 25 | - Use meaningful HTTP status codes 26 | - Consistent URL patterns 27 | 28 | ### Consistent Response Format 29 | All API responses follow a standard format: 30 | ```json 31 | { 32 | "code": 200, 33 | "message": "success", 34 | "data": { ... } 35 | } 36 | ``` 37 | 38 | ### Authentication & Authorization 39 | - JWT-based authentication 40 | - Role-based access control (RBAC) 41 | - Secure token handling 42 | 43 | ### Error Handling 44 | - Consistent error response format 45 | - Meaningful error messages 46 | - Appropriate HTTP status codes 47 | 48 | ### Versioning Strategy 49 | - URL path versioning (`/api/v1/`) 50 | - Backward compatibility within major versions 51 | - Clear deprecation notices for breaking changes 52 | 53 | ## Implementation 54 | 55 | The actual API implementation is located in: 56 | - **Handlers**: `internal/handlers/` - HTTP request handlers 57 | - **Routes**: `internal/routes/` - Route definitions and middleware 58 | - **Models**: `internal/models/` - Data models and DTOs 59 | - **Services**: `internal/service/` - Business logic layer 60 | 61 | ## Documentation 62 | 63 | - **OpenAPI Specification**: Complete API specification in OpenAPI 3.0 format 64 | - **Interactive Documentation**: Can be viewed with Swagger UI or similar tools 65 | - **Code Examples**: Provided in language-specific documentation 66 | 67 | ## Development Workflow 68 | 69 | 1. **Design**: Define API endpoints in OpenAPI specification 70 | 2. **Review**: Review API design with team 71 | 3. **Implement**: Implement handlers, routes, and business logic 72 | 4. **Test**: Write and run API tests 73 | 5. **Document**: Update documentation and examples 74 | 6. **Deploy**: Deploy to staging/production environments 75 | 76 | ## Tools and Resources 77 | 78 | - **OpenAPI Generator**: Generate client SDKs and documentation 79 | - **Swagger UI**: Interactive API documentation 80 | - **Postman**: API testing and development 81 | - **curl**: Command-line API testing 82 | 83 | ## Contributing 84 | 85 | When contributing to the API: 86 | 87 | 1. Follow existing patterns and conventions 88 | 2. Update OpenAPI specification for any changes 89 | 3. Add appropriate tests 90 | 4. Update documentation 91 | 5. Ensure backward compatibility -------------------------------------------------------------------------------- /internal/models/event_models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | // ClusterEvent represents a Kubernetes cluster event 10 | type ClusterEvent struct { 11 | ID string `json:"id"` 12 | Name string `json:"name"` 13 | Namespace string `json:"namespace"` 14 | Reason string `json:"reason"` 15 | Message string `json:"message"` 16 | Type string `json:"type"` // Normal, Warning 17 | Source string `json:"source"` // Component that reported this event 18 | Object string `json:"object"` // Involved object (Pod, Service, etc.) 19 | ObjectKind string `json:"objectKind"` // Kind of the involved object 20 | Count int32 `json:"count"` // Number of times this event has occurred 21 | FirstTime time.Time `json:"firstTime"` // First time this event was observed 22 | LastTime time.Time `json:"lastTime"` // Last time this event was observed 23 | CreatedAt time.Time `json:"createdAt"` // Event creation time 24 | } 25 | 26 | // EventListRequest represents the request parameters for listing events 27 | type EventListRequest struct { 28 | Namespace string `form:"namespace" json:"namespace"` // Filter by namespace, empty means all namespaces 29 | Type string `form:"type" json:"type"` // Filter by event type (Normal, Warning) 30 | Limit int `form:"limit" json:"limit"` // Limit number of events returned 31 | Since string `form:"since" json:"since"` // Filter events since this time (RFC3339 format) 32 | } 33 | 34 | // EventListResponse represents the response for event listing 35 | type EventListResponse struct { 36 | Events []ClusterEvent `json:"events"` 37 | Total int `json:"total"` 38 | } 39 | 40 | // ConvertK8sEventToClusterEvent converts Kubernetes Event to ClusterEvent 41 | func ConvertK8sEventToClusterEvent(event *corev1.Event) ClusterEvent { 42 | eventType := "Normal" 43 | if event.Type == corev1.EventTypeWarning { 44 | eventType = "Warning" 45 | } 46 | 47 | objectName := "" 48 | objectKind := "" 49 | if event.InvolvedObject.Name != "" { 50 | objectName = event.InvolvedObject.Name 51 | objectKind = event.InvolvedObject.Kind 52 | } 53 | 54 | source := "" 55 | if event.Source.Component != "" { 56 | source = event.Source.Component 57 | } else if event.ReportingController != "" { 58 | source = event.ReportingController 59 | } 60 | 61 | return ClusterEvent{ 62 | ID: string(event.UID), 63 | Name: event.Name, 64 | Namespace: event.Namespace, 65 | Reason: event.Reason, 66 | Message: event.Message, 67 | Type: eventType, 68 | Source: source, 69 | Object: objectName, 70 | ObjectKind: objectKind, 71 | Count: event.Count, 72 | FirstTime: event.FirstTimestamp.Time, 73 | LastTime: event.LastTimestamp.Time, 74 | CreatedAt: event.CreationTimestamp.Time, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /configs/database-examples.yaml: -------------------------------------------------------------------------------- 1 | # Database Configuration Examples 2 | # Supports MySQL, PostgreSQL and SQLite database types 3 | 4 | # ==================== SQLite Configuration ==================== 5 | # Lightweight file database, suitable for development and small-scale deployment 6 | # Pros: No need to install database server, zero configuration 7 | # Cons: Does not support concurrent writes, not suitable for high concurrency scenarios 8 | 9 | database_sqlite: 10 | enabled: true 11 | type: "sqlite" 12 | database: "./data/cilikube.db" # SQLite database file path 13 | host: "" # SQLite does not need 14 | port: 0 # SQLite does not need 15 | username: "" # SQLite does not need 16 | password: "" # SQLite does not need 17 | charset: "" # SQLite does not need 18 | 19 | # ==================== MySQL Configuration ==================== 20 | # Most popular relational database, suitable for most scenarios 21 | # Pros: Mature and stable, rich ecosystem, excellent performance 22 | # Cons: Requires separate installation and maintenance 23 | 24 | database_mysql: 25 | enabled: true 26 | type: "mysql" 27 | host: "localhost" # MySQL server address 28 | port: 3306 # MySQL default port 29 | username: "root" # MySQL username 30 | password: "your-password" # MySQL password 31 | database: "cilikube" # Database name 32 | charset: "utf8mb4" # Character set 33 | 34 | # ==================== PostgreSQL Configuration ==================== 35 | # Powerful open-source relational database 36 | # Pros: Feature-rich, good standard compatibility, strong extensibility 37 | # Cons: Relatively complex, higher learning curve 38 | 39 | database_postgresql: 40 | enabled: true 41 | type: "postgresql" # Or use "postgres" 42 | host: "localhost" # PostgreSQL server address 43 | port: 5432 # PostgreSQL default port 44 | username: "postgres" # PostgreSQL username 45 | password: "your-password" # PostgreSQL password 46 | database: "cilikube" # Database name 47 | charset: "" # PostgreSQL does not need charset 48 | 49 | # ==================== Usage Instructions ==================== 50 | # 1. Choose one of the configurations and copy to the database section of config.yaml 51 | # 2. Modify connection parameters according to actual situation 52 | # 3. Ensure the corresponding database service is installed and running (except SQLite) 53 | # 4. Restart the application to make the configuration effective 54 | 55 | # ==================== Database Selection Recommendations ==================== 56 | # Development environment: Recommend SQLite, simple and fast 57 | # Testing environment: Recommend SQLite or MySQL 58 | # Production environment: Recommend MySQL or PostgreSQL 59 | # High concurrency scenarios: Recommend MySQL or PostgreSQL 60 | # Complex query scenarios: Recommend PostgreSQL -------------------------------------------------------------------------------- /internal/store/gorm_store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/uuid" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type gormClusterStore struct { 11 | db *gorm.DB 12 | encryptionKey []byte 13 | } 14 | 15 | // NewGormClusterStore creates a new ClusterStore GORM implementation. 16 | func NewGormClusterStore(db *gorm.DB, encryptionKey []byte) (ClusterStore, error) { 17 | if len(encryptionKey) != 32 { 18 | return nil, fmt.Errorf("encryption key must be 32 bytes long for AES-256") 19 | } 20 | return &gormClusterStore{ 21 | db: db, 22 | encryptionKey: encryptionKey, 23 | }, nil 24 | } 25 | 26 | func (s *gormClusterStore) CreateCluster(cluster *Cluster) error { 27 | cluster.ID = uuid.NewString() 28 | encryptedData, err := Encrypt(cluster.KubeconfigData, s.encryptionKey) 29 | if err != nil { 30 | return fmt.Errorf("failed to encrypt kubeconfig: %w", err) 31 | } 32 | cluster.KubeconfigData = encryptedData 33 | return s.db.Create(cluster).Error 34 | } 35 | 36 | func (s *gormClusterStore) GetClusterByID(id string) (*Cluster, error) { 37 | var cluster Cluster 38 | if err := s.db.First(&cluster, "id = ?", id).Error; err != nil { 39 | return nil, err 40 | } 41 | decryptedData, err := Decrypt(cluster.KubeconfigData, s.encryptionKey) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to decrypt kubeconfig for cluster %s: %w", cluster.Name, err) 44 | } 45 | cluster.KubeconfigData = decryptedData 46 | return &cluster, nil 47 | } 48 | 49 | func (s *gormClusterStore) GetClusterByName(name string) (*Cluster, error) { 50 | var cluster Cluster 51 | if err := s.db.First(&cluster, "name = ?", name).Error; err != nil { 52 | return nil, err 53 | } 54 | decryptedData, err := Decrypt(cluster.KubeconfigData, s.encryptionKey) 55 | if err != nil { 56 | return nil, fmt.Errorf("failed to decrypt kubeconfig for cluster %s: %w", cluster.Name, err) 57 | } 58 | cluster.KubeconfigData = decryptedData 59 | return &cluster, nil 60 | } 61 | 62 | func (s *gormClusterStore) GetAllClusters() ([]Cluster, error) { 63 | var clusters []Cluster 64 | if err := s.db.Find(&clusters).Error; err != nil { 65 | return nil, err 66 | } 67 | for i := range clusters { 68 | decryptedData, err := Decrypt(clusters[i].KubeconfigData, s.encryptionKey) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to decrypt kubeconfig for cluster %s: %w", clusters[i].Name, err) 71 | } 72 | clusters[i].KubeconfigData = decryptedData 73 | } 74 | return clusters, nil 75 | } 76 | 77 | func (s *gormClusterStore) UpdateCluster(cluster *Cluster) error { 78 | if len(cluster.KubeconfigData) > 0 { 79 | encryptedData, err := Encrypt(cluster.KubeconfigData, s.encryptionKey) 80 | if err != nil { 81 | return fmt.Errorf("failed to encrypt kubeconfig for update on cluster %s: %w", cluster.Name, err) 82 | } 83 | cluster.KubeconfigData = encryptedData 84 | } 85 | return s.db.Save(cluster).Error 86 | } 87 | 88 | func (s *gormClusterStore) DeleteClusterByName(name string) error { 89 | return s.db.Delete(&Cluster{}, "name = ?", name).Error 90 | } 91 | 92 | func (s *gormClusterStore) DeleteClusterByID(id string) error { 93 | return s.db.Delete(&Cluster{}, "id = ?", id).Error 94 | } 95 | -------------------------------------------------------------------------------- /internal/models/crd_models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // CRDListResponse represents the response for CRD list 8 | type CRDListResponse struct { 9 | Items []CRDItem `json:"items"` 10 | Total int `json:"total"` 11 | } 12 | 13 | // CRDItem represents CRD item information 14 | type CRDItem struct { 15 | Name string `json:"name"` 16 | Group string `json:"group"` 17 | Version string `json:"version"` 18 | Kind string `json:"kind"` 19 | Plural string `json:"plural"` 20 | Singular string `json:"singular"` 21 | Scope string `json:"scope"` 22 | Categories []string `json:"categories,omitempty"` 23 | ShortNames []string `json:"shortNames,omitempty"` 24 | CreatedAt metav1.Time `json:"createdAt"` 25 | Labels map[string]string `json:"labels,omitempty"` 26 | Annotations map[string]string `json:"annotations,omitempty"` 27 | } 28 | 29 | // CustomResourceListResponse represents the response for custom resource list 30 | type CustomResourceListResponse struct { 31 | Items []CustomResourceItem `json:"items"` 32 | Total int `json:"total"` 33 | } 34 | 35 | // CustomResourceItem represents a custom resource item 36 | type CustomResourceItem struct { 37 | Name string `json:"name"` 38 | Namespace string `json:"namespace,omitempty"` 39 | Kind string `json:"kind"` 40 | APIVersion string `json:"apiVersion"` 41 | CreatedAt metav1.Time `json:"createdAt"` 42 | Labels map[string]string `json:"labels,omitempty"` 43 | Annotations map[string]string `json:"annotations,omitempty"` 44 | Spec map[string]interface{} `json:"spec,omitempty"` 45 | Status map[string]interface{} `json:"status,omitempty"` 46 | } 47 | 48 | // CRDDetailResponse represents the response for CRD details 49 | type CRDDetailResponse struct { 50 | CRDItem 51 | Schema map[string]interface{} `json:"schema,omitempty"` 52 | Versions []CRDVersion `json:"versions,omitempty"` 53 | Conditions []CRDCondition `json:"conditions,omitempty"` 54 | Description string `json:"description,omitempty"` 55 | } 56 | 57 | // CRDVersion represents CRD version information 58 | type CRDVersion struct { 59 | Name string `json:"name"` 60 | Served bool `json:"served"` 61 | Storage bool `json:"storage"` 62 | } 63 | 64 | // CRDCondition represents CRD status condition 65 | type CRDCondition struct { 66 | Type string `json:"type"` 67 | Status string `json:"status"` 68 | LastTransitionTime metav1.Time `json:"lastTransitionTime"` 69 | Reason string `json:"reason,omitempty"` 70 | Message string `json:"message,omitempty"` 71 | } 72 | 73 | // CustomResourceRequest represents the request for creating/updating custom resources 74 | type CustomResourceRequest struct { 75 | APIVersion string `json:"apiVersion"` 76 | Kind string `json:"kind"` 77 | Metadata map[string]interface{} `json:"metadata"` 78 | Spec map[string]interface{} `json:"spec,omitempty"` 79 | } 80 | -------------------------------------------------------------------------------- /deployments/monitoring/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | rule_files: 6 | - "rules/*.yml" 7 | 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | scrape_configs: 15 | # Prometheus self-monitoring 16 | - job_name: 'prometheus' 17 | static_configs: 18 | - targets: ['localhost:9090'] 19 | 20 | # CiliKube backend monitoring 21 | - job_name: 'cilikube-backend' 22 | static_configs: 23 | - targets: ['backend:8080'] 24 | metrics_path: '/metrics' 25 | scrape_interval: 30s 26 | scrape_timeout: 10s 27 | 28 | # Node Exporter (if deployed) 29 | - job_name: 'node-exporter' 30 | static_configs: 31 | - targets: ['node-exporter:9100'] 32 | 33 | # MySQL Exporter (if MySQL is enabled) 34 | - job_name: 'mysql-exporter' 35 | static_configs: 36 | - targets: ['mysql-exporter:9104'] 37 | 38 | # Redis Exporter (if Redis is enabled) 39 | - job_name: 'redis-exporter' 40 | static_configs: 41 | - targets: ['redis-exporter:9121'] 42 | 43 | # Kubernetes API Server (if accessible) 44 | - job_name: 'kubernetes-apiservers' 45 | kubernetes_sd_configs: 46 | - role: endpoints 47 | scheme: https 48 | tls_config: 49 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 50 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 51 | relabel_configs: 52 | - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] 53 | action: keep 54 | regex: default;kubernetes;https 55 | 56 | # Kubernetes Nodes 57 | - job_name: 'kubernetes-nodes' 58 | kubernetes_sd_configs: 59 | - role: node 60 | scheme: https 61 | tls_config: 62 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 63 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 64 | relabel_configs: 65 | - action: labelmap 66 | regex: __meta_kubernetes_node_label_(.+) 67 | - target_label: __address__ 68 | replacement: kubernetes.default.svc:443 69 | - source_labels: [__meta_kubernetes_node_name] 70 | regex: (.+) 71 | target_label: __metrics_path__ 72 | replacement: /api/v1/nodes/${1}/proxy/metrics 73 | 74 | # Kubernetes Pods 75 | - job_name: 'kubernetes-pods' 76 | kubernetes_sd_configs: 77 | - role: pod 78 | relabel_configs: 79 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] 80 | action: keep 81 | regex: true 82 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] 83 | action: replace 84 | target_label: __metrics_path__ 85 | regex: (.+) 86 | - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] 87 | action: replace 88 | regex: ([^:]+)(?::\d+)?;(\d+) 89 | replacement: $1:$2 90 | target_label: __address__ 91 | - action: labelmap 92 | regex: __meta_kubernetes_pod_label_(.+) 93 | - source_labels: [__meta_kubernetes_namespace] 94 | action: replace 95 | target_label: kubernetes_namespace 96 | - source_labels: [__meta_kubernetes_pod_name] 97 | action: replace 98 | target_label: kubernetes_pod_name -------------------------------------------------------------------------------- /internal/handlers/node_metrics_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/ciliverse/cilikube/internal/service" 7 | "github.com/ciliverse/cilikube/pkg/k8s" 8 | "github.com/ciliverse/cilikube/pkg/utils" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // NodeMetricsHandler handles node metrics related requests 13 | type NodeMetricsHandler struct { 14 | service *service.NodeMetricsService 15 | clusterManager *k8s.ClusterManager 16 | } 17 | 18 | // NewNodeMetricsHandler creates a new NodeMetricsHandler instance 19 | func NewNodeMetricsHandler(svc *service.NodeMetricsService, k8sManager *k8s.ClusterManager) *NodeMetricsHandler { 20 | return &NodeMetricsHandler{ 21 | service: svc, 22 | clusterManager: k8sManager, 23 | } 24 | } 25 | 26 | // GetNodeMetrics is the HTTP handler function for getting real-time metrics of a single node 27 | func (h *NodeMetricsHandler) GetNodeMetrics(c *gin.Context) { 28 | // 1. Get clusterId from query parameters and get the corresponding cluster's k8s client 29 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 30 | if !ok { 31 | return // Error already handled in GetClientFromQuery 32 | } 33 | 34 | // 2. Get node name from path parameters 35 | nodeName := c.Param("name") 36 | if nodeName == "" { 37 | utils.ApiError(c, http.StatusBadRequest, "node name cannot be empty", "") 38 | return 39 | } 40 | 41 | // 3. Call service layer to get metrics, note that k8sClient.Config needs to be passed 42 | metrics, err := h.service.GetNodeMetrics(k8sClient.Config, nodeName) 43 | if err != nil { 44 | // Judge the error here, if it's caused by metrics-server not being installed, give a friendly prompt 45 | if clientErr, ok := err.(interface{ IsNotFound() bool }); ok && clientErr.IsNotFound() { 46 | utils.ApiError(c, http.StatusNotFound, "failed to get metrics", "Please confirm that Metrics-Server is properly installed and running in the target cluster.") 47 | return 48 | } 49 | utils.ApiError(c, http.StatusInternalServerError, "failed to get node metrics", err.Error()) 50 | return 51 | } 52 | 53 | // 4. Successfully return data 54 | utils.ApiSuccess(c, metrics, "successfully retrieved node metrics") 55 | } 56 | 57 | // GetAllNodesMetrics is the HTTP handler function for getting real-time metrics of all nodes 58 | func (h *NodeMetricsHandler) GetAllNodesMetrics(c *gin.Context) { 59 | // 1. Get clusterId from query parameters and get the corresponding cluster's k8s client 60 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 61 | if !ok { 62 | return // Error already handled in GetClientFromQuery 63 | } 64 | 65 | // 2. Call service layer to get all nodes metrics 66 | metrics, err := h.service.GetAllNodesMetrics(k8sClient.Config) 67 | if err != nil { 68 | // Judge the error here, if it's caused by metrics-server not being installed, give a friendly prompt 69 | if clientErr, ok := err.(interface{ IsNotFound() bool }); ok && clientErr.IsNotFound() { 70 | utils.ApiError(c, http.StatusNotFound, "failed to get metrics", "Please confirm that Metrics-Server is properly installed and running in the target cluster.") 71 | return 72 | } 73 | utils.ApiError(c, http.StatusInternalServerError, "failed to get nodes metrics", err.Error()) 74 | return 75 | } 76 | 77 | // 3. Successfully return data 78 | utils.ApiSuccess(c, metrics, "successfully retrieved all nodes metrics") 79 | } 80 | -------------------------------------------------------------------------------- /api/v1/README.md: -------------------------------------------------------------------------------- 1 | # CiliKube API v1 2 | 3 | This directory contains the API definitions and documentation for CiliKube v1. 4 | 5 | ## API Documentation 6 | 7 | - **OpenAPI Specification**: `openapi.yaml` - Complete API specification in OpenAPI 3.0 format 8 | - **API Version**: v1 9 | - **Base URL**: `/api/v1` 10 | 11 | ## API Overview 12 | 13 | CiliKube API provides comprehensive Kubernetes multi-cluster management capabilities including: 14 | 15 | ### Authentication 16 | - User login/logout 17 | - JWT token-based authentication 18 | - User profile management 19 | - Role-based access control 20 | 21 | ### Cluster Management 22 | - Multi-cluster configuration 23 | - Cluster switching 24 | - Cluster health monitoring 25 | 26 | ### Kubernetes Resources 27 | - Pod management and monitoring 28 | - Service and Ingress management 29 | - ConfigMap and Secret management 30 | - Deployment, StatefulSet, DaemonSet operations 31 | - Storage management (PV/PVC) 32 | - RBAC management 33 | 34 | ### Real-time Features 35 | - Pod logs streaming (WebSocket) 36 | - Pod shell access (WebSocket) 37 | - Resource monitoring and metrics 38 | 39 | ## Authentication 40 | 41 | All API endpoints (except login) require JWT authentication: 42 | 43 | ``` 44 | Authorization: Bearer 45 | ``` 46 | 47 | ## API Endpoints Structure 48 | 49 | ``` 50 | /api/v1/ 51 | ├── auth/ # Authentication endpoints 52 | ├── clusters/ # Cluster management 53 | ├── proxy/ # Kubernetes API proxy 54 | ├── summary/ # Dashboard and summary data 55 | └── installer/ # Installation utilities 56 | ``` 57 | 58 | ## Usage Examples 59 | 60 | ### Login 61 | ```bash 62 | curl -X POST http://localhost:8080/api/v1/auth/login \ 63 | -H "Content-Type: application/json" \ 64 | -d '{"username": "admin", "password": "password"}' 65 | ``` 66 | 67 | ### List Clusters 68 | ```bash 69 | curl -X GET http://localhost:8080/api/v1/clusters \ 70 | -H "Authorization: Bearer " 71 | ``` 72 | 73 | ### Proxy to Kubernetes API 74 | ```bash 75 | curl -X GET "http://localhost:8080/api/v1/proxy/api/v1/pods?clusterId=" \ 76 | -H "Authorization: Bearer " 77 | ``` 78 | 79 | ## WebSocket Endpoints 80 | 81 | ### Pod Logs 82 | ``` 83 | ws://localhost:8080/api/v1/proxy/api/v1/namespaces/{namespace}/pods/{name}/logs/ws?clusterId=&container= 84 | ``` 85 | 86 | ### Pod Shell 87 | ``` 88 | ws://localhost:8080/api/v1/proxy/api/v1/namespaces/{namespace}/pods/{name}/exec/ws?clusterId=&container= 89 | ``` 90 | 91 | ## Response Format 92 | 93 | All API responses follow a consistent format: 94 | 95 | ### Success Response 96 | ```json 97 | { 98 | "code": 200, 99 | "message": "success", 100 | "data": { ... } 101 | } 102 | ``` 103 | 104 | ### Error Response 105 | ```json 106 | { 107 | "code": 400, 108 | "message": "error description" 109 | } 110 | ``` 111 | 112 | ## Development 113 | 114 | When adding new API endpoints: 115 | 116 | 1. Update the OpenAPI specification in `openapi.yaml` 117 | 2. Implement handlers in `internal/handlers/` 118 | 3. Add routes in `internal/routes/` 119 | 4. Update this documentation 120 | 121 | ## Tools 122 | 123 | - **Swagger UI**: Use tools like Swagger Editor to view and test the API 124 | - **Postman**: Import the OpenAPI spec for API testing 125 | - **curl**: Command-line testing examples provided above -------------------------------------------------------------------------------- /internal/models/oauth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // OAuthProvider represents OAuth provider information for a user 8 | type OAuthProvider struct { 9 | ID uint `json:"id" gorm:"primaryKey"` 10 | UserID uint `json:"user_id" gorm:"not null;index"` 11 | Provider string `json:"provider" gorm:"not null;size:50"` 12 | ProviderUserID string `json:"provider_user_id" gorm:"not null;size:100"` 13 | AccessToken string `json:"-" gorm:"type:text"` 14 | RefreshToken string `json:"-" gorm:"type:text"` 15 | ExpiresAt *time.Time `json:"expires_at"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | } 19 | 20 | // TableName specifies the table name for OAuthProvider model 21 | func (OAuthProvider) TableName() string { 22 | return "oauth_providers" 23 | } 24 | 25 | // OAuthLoginRequest request for OAuth login 26 | type OAuthLoginRequest struct { 27 | Provider string `json:"provider" binding:"required"` 28 | Code string `json:"code" binding:"required"` 29 | State string `json:"state" binding:"required"` 30 | } 31 | 32 | // OAuthTokenResponse response containing OAuth tokens 33 | type OAuthTokenResponse struct { 34 | AccessToken string `json:"access_token"` 35 | RefreshToken string `json:"refresh_token,omitempty"` 36 | ExpiresAt *time.Time `json:"expires_at,omitempty"` 37 | TokenType string `json:"token_type"` 38 | } 39 | 40 | // OAuthUserInfo user information from OAuth provider 41 | type OAuthUserInfo struct { 42 | ID string `json:"id"` 43 | Username string `json:"username"` 44 | Email string `json:"email"` 45 | DisplayName string `json:"display_name"` 46 | AvatarURL string `json:"avatar_url"` 47 | } 48 | 49 | // OAuthProviderResponse response for OAuth provider operations 50 | type OAuthProviderResponse struct { 51 | ID uint `json:"id"` 52 | Provider string `json:"provider"` 53 | ProviderUserID string `json:"provider_user_id"` 54 | ConnectedAt time.Time `json:"connected_at"` 55 | ExpiresAt *time.Time `json:"expires_at,omitempty"` 56 | } 57 | 58 | // LinkOAuthAccountRequest request for linking OAuth account 59 | type LinkOAuthAccountRequest struct { 60 | Provider string `json:"provider" binding:"required"` 61 | ProviderUserID string `json:"provider_user_id" binding:"required"` 62 | AccessToken string `json:"access_token" binding:"required"` 63 | RefreshToken string `json:"refresh_token"` 64 | ExpiresAt *time.Time `json:"expires_at"` 65 | } 66 | 67 | // UnlinkOAuthAccountRequest request for unlinking OAuth account 68 | type UnlinkOAuthAccountRequest struct { 69 | Provider string `json:"provider" binding:"required"` 70 | } 71 | 72 | // ToResponse converts OAuthProvider to OAuthProviderResponse 73 | func (o *OAuthProvider) ToResponse() OAuthProviderResponse { 74 | return OAuthProviderResponse{ 75 | ID: o.ID, 76 | Provider: o.Provider, 77 | ProviderUserID: o.ProviderUserID, 78 | ConnectedAt: o.CreatedAt, 79 | ExpiresAt: o.ExpiresAt, 80 | } 81 | } 82 | 83 | // ToProviderInfo converts OAuthProvider to OAuthProviderInfo for user profile 84 | func (o *OAuthProvider) ToProviderInfo() OAuthProviderInfo { 85 | return OAuthProviderInfo{ 86 | Provider: o.Provider, 87 | ProviderUserID: o.ProviderUserID, 88 | ConnectedAt: o.CreatedAt, 89 | ExpiresAt: o.ExpiresAt, 90 | } 91 | } 92 | 93 | // SupportedOAuthProviders list of supported OAuth providers 94 | var SupportedOAuthProviders = []string{ 95 | "github", 96 | // Future providers can be added here 97 | // "google", 98 | // "gitlab", 99 | } 100 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | issues-exit-code: 1 4 | tests: true 5 | skip-dirs: 6 | - vendor 7 | - node_modules 8 | skip-files: 9 | - ".*\\.pb\\.go$" 10 | - ".*_generated\\.go$" 11 | 12 | output: 13 | format: colored-line-number 14 | print-issued-lines: true 15 | print-linter-name: true 16 | 17 | linters-settings: 18 | errcheck: 19 | check-type-assertions: true 20 | check-blank: true 21 | 22 | govet: 23 | check-shadowing: true 24 | enable-all: true 25 | 26 | gocyclo: 27 | min-complexity: 15 28 | 29 | dupl: 30 | threshold: 100 31 | 32 | goconst: 33 | min-len: 3 34 | min-occurrences: 3 35 | 36 | misspell: 37 | locale: US 38 | 39 | lll: 40 | line-length: 120 41 | 42 | goimports: 43 | local-prefixes: github.com/ciliverse/cilikube 44 | 45 | gocritic: 46 | enabled-tags: 47 | - performance 48 | - style 49 | - experimental 50 | disabled-checks: 51 | - wrapperFunc 52 | - dupImport 53 | 54 | funlen: 55 | lines: 100 56 | statements: 50 57 | 58 | gocognit: 59 | min-complexity: 20 60 | 61 | nestif: 62 | min-complexity: 4 63 | 64 | gomnd: 65 | settings: 66 | mnd: 67 | checks: argument,case,condition,operation,return,assign 68 | 69 | godox: 70 | keywords: 71 | - NOTE 72 | - OPTIMIZE 73 | - HACK 74 | 75 | revive: 76 | min-confidence: 0 77 | rules: 78 | - name: var-naming 79 | - name: package-comments 80 | - name: exported 81 | - name: var-declaration 82 | - name: blank-imports 83 | - name: context-as-argument 84 | - name: dot-imports 85 | - name: error-return 86 | - name: error-strings 87 | - name: error-naming 88 | - name: increment-decrement 89 | - name: range 90 | - name: receiver-naming 91 | - name: time-naming 92 | - name: unexported-return 93 | - name: indent-error-flow 94 | - name: errorf 95 | - name: empty-block 96 | - name: superfluous-else 97 | - name: unused-parameter 98 | - name: unreachable-code 99 | - name: redefines-builtin-id 100 | 101 | linters: 102 | enable: 103 | - bodyclose 104 | - deadcode 105 | - depguard 106 | - dogsled 107 | - dupl 108 | - errcheck 109 | - exportloopref 110 | - exhaustive 111 | - funlen 112 | - gochecknoinits 113 | - goconst 114 | - gocritic 115 | - gocyclo 116 | - gofmt 117 | - goimports 118 | - gomnd 119 | - goprintffuncname 120 | - gosec 121 | - gosimple 122 | - govet 123 | - ineffassign 124 | - lll 125 | - misspell 126 | - nakedret 127 | - noctx 128 | - nolintlint 129 | - rowserrcheck 130 | - staticcheck 131 | - structcheck 132 | - stylecheck 133 | - typecheck 134 | - unconvert 135 | - unparam 136 | - unused 137 | - varcheck 138 | - whitespace 139 | - revive 140 | 141 | disable: 142 | - maligned 143 | - prealloc 144 | 145 | issues: 146 | exclude-rules: 147 | - path: _test\.go 148 | linters: 149 | - gomnd 150 | - funlen 151 | - gocyclo 152 | 153 | - path: cmd/ 154 | linters: 155 | - gochecknoinits 156 | 157 | - linters: 158 | - lll 159 | source: "^//go:generate " 160 | 161 | exclude: 162 | - "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked" 163 | - "exported (type|method|function) (.+) should have comment or be unexported" 164 | - "ST1000: at least one file in a package should have a package comment" 165 | 166 | max-issues-per-linter: 0 167 | max-same-issues: 0 168 | new: false -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUT_DIR := output 2 | BINARY_NAME := cilikube 3 | VERSION := $(shell git describe --tags --always --dirty) 4 | BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S') 5 | LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -w -s" 6 | 7 | .PHONY: build run build-linux build-mac build-windows build-all test lint clean dev docker help 8 | 9 | # 默认目标 10 | all: build 11 | 12 | # 更新依赖 13 | update-dependencies: 14 | @echo "Updating Go dependencies..." 15 | go mod tidy 16 | go mod download 17 | 18 | # 开发环境构建 19 | build: clean update-dependencies 20 | @echo "Building $(BINARY_NAME)..." 21 | go build $(LDFLAGS) -o $(OUT_DIR)/$(BINARY_NAME) cmd/server/main.go 22 | 23 | # 开发环境运行 24 | dev: build 25 | @echo "Starting development server..." 26 | ./$(OUT_DIR)/$(BINARY_NAME) --config configs/config.yaml 27 | 28 | # 生产环境运行 29 | run: build 30 | @echo "Starting production server..." 31 | ./$(OUT_DIR)/$(BINARY_NAME) 32 | 33 | # 清理构建文件 34 | clean: 35 | @echo "Cleaning build artifacts..." 36 | rm -rf $(OUT_DIR) 37 | go clean -cache 38 | 39 | # 运行测试 40 | test: 41 | @echo "Running tests..." 42 | go test -v -race -coverprofile=coverage.out ./... 43 | go tool cover -html=coverage.out -o coverage.html 44 | 45 | # 运行基准测试 46 | bench: 47 | @echo "Running benchmarks..." 48 | go test -bench=. -benchmem ./... 49 | 50 | # 代码检查 51 | lint: 52 | @echo "Running linters..." 53 | golangci-lint run ./... 54 | 55 | # 格式化代码 56 | fmt: 57 | @echo "Formatting code..." 58 | go fmt ./... 59 | goimports -w . 60 | 61 | # 安全检查 62 | security: 63 | @echo "Running security checks..." 64 | gosec ./... 65 | 66 | # Linux 构建 67 | build-linux: 68 | @echo "Building for Linux..." 69 | GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(OUT_DIR)/$(BINARY_NAME)-linux-amd64 cmd/server/main.go 70 | 71 | # macOS 构建 72 | build-mac: 73 | @echo "Building for macOS..." 74 | GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(OUT_DIR)/$(BINARY_NAME)-darwin-amd64 cmd/server/main.go 75 | GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(OUT_DIR)/$(BINARY_NAME)-darwin-arm64 cmd/server/main.go 76 | 77 | # Windows 构建 78 | build-windows: 79 | @echo "Building for Windows..." 80 | GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(OUT_DIR)/$(BINARY_NAME)-windows-amd64.exe cmd/server/main.go 81 | 82 | # 全平台构建 83 | build-all: build-linux build-mac build-windows 84 | @echo "All builds completed!" 85 | 86 | # Docker 构建 87 | docker: 88 | @echo "Building Docker image..." 89 | docker build -t cilikube:$(VERSION) . 90 | docker build -t cilikube:latest . 91 | 92 | # Docker 运行 93 | docker-run: 94 | @echo "Running Docker container..." 95 | docker run -d --name cilikube -p 8080:8080 -v ~/.kube:/root/.kube:ro cilikube:latest 96 | 97 | # 安装开发工具 98 | install-tools: 99 | @echo "Installing development tools..." 100 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 101 | go install golang.org/x/tools/cmd/goimports@latest 102 | go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest 103 | 104 | # 生成 API 文档 105 | docs: 106 | @echo "Generating API documentation..." 107 | swag init -g cmd/server/main.go -o docs/swagger 108 | 109 | # 帮助信息 110 | help: 111 | @echo "Available targets:" 112 | @echo " build - Build the application" 113 | @echo " dev - Build and run in development mode" 114 | @echo " run - Build and run in production mode" 115 | @echo " test - Run tests with coverage" 116 | @echo " bench - Run benchmarks" 117 | @echo " lint - Run code linters" 118 | @echo " fmt - Format code" 119 | @echo " security - Run security checks" 120 | @echo " clean - Clean build artifacts" 121 | @echo " build-all - Build for all platforms" 122 | @echo " docker - Build Docker image" 123 | @echo " docker-run - Run Docker container" 124 | @echo " install-tools - Install development tools" 125 | @echo " docs - Generate API documentation" 126 | @echo " help - Show this help message" -------------------------------------------------------------------------------- /internal/store/interface.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "time" 4 | 5 | // ClusterStore defines all methods required for interacting with cluster data persistent storage. 6 | type ClusterStore interface { 7 | CreateCluster(cluster *Cluster) error 8 | GetClusterByID(id string) (*Cluster, error) 9 | GetClusterByName(name string) (*Cluster, error) 10 | GetAllClusters() ([]Cluster, error) 11 | UpdateCluster(cluster *Cluster) error 12 | DeleteClusterByName(name string) error 13 | DeleteClusterByID(id string) error 14 | } 15 | 16 | // UserStore defines all methods required for interacting with user data persistent storage. 17 | type UserStore interface { 18 | CreateUser(user *User) error 19 | GetUserByID(id uint) (*User, error) 20 | GetUserByUsername(username string) (*User, error) 21 | GetUserByEmail(email string) (*User, error) 22 | UpdateUser(user *User) error 23 | DeleteUser(id uint) error 24 | ListUsers(offset, limit int) ([]*User, int64, error) 25 | } 26 | 27 | // RoleStore defines all methods required for interacting with role data persistent storage. 28 | type RoleStore interface { 29 | CreateRole(role *Role) error 30 | GetRoleByID(id uint) (*Role, error) 31 | GetRoleByName(name string) (*Role, error) 32 | UpdateRole(role *Role) error 33 | DeleteRole(id uint) error 34 | ListRoles() ([]*Role, error) 35 | } 36 | 37 | // UserRoleStore defines all methods required for managing user-role associations. 38 | type UserRoleStore interface { 39 | AssignRole(userID, roleID uint) error 40 | RemoveRole(userID, roleID uint) error 41 | GetUserRoles(userID uint) ([]*Role, error) 42 | GetRoleUsers(roleID uint) ([]*User, error) 43 | HasRole(userID, roleID uint) (bool, error) 44 | } 45 | 46 | // OAuthStore defines all methods required for managing OAuth provider data. 47 | type OAuthStore interface { 48 | CreateOAuthProvider(provider *OAuthProvider) error 49 | GetOAuthProvider(userID uint, provider string) (*OAuthProvider, error) 50 | GetOAuthProviderByProviderUserID(provider, providerUserID string) (*OAuthProvider, error) 51 | UpdateOAuthProvider(provider *OAuthProvider) error 52 | DeleteOAuthProvider(userID uint, provider string) error 53 | ListUserOAuthProviders(userID uint) ([]*OAuthProvider, error) 54 | } 55 | 56 | // AuditLogStore defines all methods required for managing audit logs. 57 | type AuditLogStore interface { 58 | CreateAuditLog(log *AuditLog) error 59 | GetAuditLogsByUserID(userID uint, offset, limit int) ([]*AuditLog, int64, error) 60 | GetAuditLogsByAction(action string, offset, limit int) ([]*AuditLog, int64, error) 61 | ListAuditLogs(offset, limit int) ([]*AuditLog, int64, error) 62 | } 63 | 64 | // LoginAttemptStore defines all methods required for managing login attempts. 65 | type LoginAttemptStore interface { 66 | CreateLoginAttempt(attempt *LoginAttempt) error 67 | GetLoginAttemptsByUserID(userID uint, since time.Time) ([]*LoginAttempt, error) 68 | GetLoginAttemptsByUsername(username string, since time.Time) ([]*LoginAttempt, error) 69 | GetLoginAttemptsByIP(ipAddress string, since time.Time) ([]*LoginAttempt, error) 70 | CleanupOldLoginAttempts(before time.Time) error 71 | } 72 | 73 | // UserSessionStore defines all methods required for managing user sessions. 74 | type UserSessionStore interface { 75 | CreateUserSession(session *UserSession) error 76 | GetUserSession(sessionID string) (*UserSession, error) 77 | UpdateUserSession(session *UserSession) error 78 | DeleteUserSession(sessionID string) error 79 | GetUserSessions(userID uint) ([]*UserSession, error) 80 | DeleteUserSessions(userID uint) error 81 | CleanupExpiredSessions(before time.Time) error 82 | } 83 | 84 | // Store is the main interface that combines all storage interfaces 85 | type Store interface { 86 | ClusterStore 87 | UserStore 88 | RoleStore 89 | UserRoleStore 90 | OAuthStore 91 | AuditLogStore 92 | LoginAttemptStore 93 | UserSessionStore 94 | 95 | // Initialize initializes the storage (creates tables, default data, etc.) 96 | Initialize() error 97 | // Close closes the storage connection 98 | Close() error 99 | } 100 | -------------------------------------------------------------------------------- /pkg/auth/rate_limit_middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | "github.com/ciliverse/cilikube/configs" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // RateLimiter represents a rate limiter for API requests 13 | type RateLimiter struct { 14 | requests map[string][]time.Time 15 | mutex sync.RWMutex 16 | config *configs.RateLimitConfig 17 | } 18 | 19 | // NewRateLimiter creates a new rate limiter 20 | func NewRateLimiter(config *configs.RateLimitConfig) *RateLimiter { 21 | limiter := &RateLimiter{ 22 | requests: make(map[string][]time.Time), 23 | config: config, 24 | } 25 | 26 | // Start cleanup goroutine 27 | go limiter.cleanup() 28 | 29 | return limiter 30 | } 31 | 32 | // IsAllowed checks if a request from the given IP is allowed 33 | func (rl *RateLimiter) IsAllowed(ip string, requestType string) bool { 34 | if !rl.config.Enabled { 35 | return true 36 | } 37 | 38 | rl.mutex.Lock() 39 | defer rl.mutex.Unlock() 40 | 41 | now := time.Now() 42 | key := ip + ":" + requestType 43 | 44 | // Get request history for this IP and request type 45 | requests := rl.requests[key] 46 | 47 | // Determine limits based on request type 48 | var limit int 49 | var window time.Duration 50 | 51 | switch requestType { 52 | case "login": 53 | limit = rl.config.LoginAttempts 54 | window = rl.config.LoginWindow 55 | case "api": 56 | limit = rl.config.APIRequests 57 | window = rl.config.APIWindow 58 | default: 59 | limit = rl.config.APIRequests 60 | window = rl.config.APIWindow 61 | } 62 | 63 | // Remove old requests outside the window 64 | cutoff := now.Add(-window) 65 | validRequests := make([]time.Time, 0) 66 | for _, reqTime := range requests { 67 | if reqTime.After(cutoff) { 68 | validRequests = append(validRequests, reqTime) 69 | } 70 | } 71 | 72 | // Check if we're within the limit 73 | if len(validRequests) >= limit { 74 | return false 75 | } 76 | 77 | // Add current request 78 | validRequests = append(validRequests, now) 79 | rl.requests[key] = validRequests 80 | 81 | return true 82 | } 83 | 84 | // cleanup removes old entries from the rate limiter 85 | func (rl *RateLimiter) cleanup() { 86 | ticker := time.NewTicker(5 * time.Minute) 87 | defer ticker.Stop() 88 | 89 | for range ticker.C { 90 | rl.mutex.Lock() 91 | now := time.Now() 92 | 93 | for key, requests := range rl.requests { 94 | // Keep only requests from the last hour 95 | cutoff := now.Add(-time.Hour) 96 | validRequests := make([]time.Time, 0) 97 | 98 | for _, reqTime := range requests { 99 | if reqTime.After(cutoff) { 100 | validRequests = append(validRequests, reqTime) 101 | } 102 | } 103 | 104 | if len(validRequests) == 0 { 105 | delete(rl.requests, key) 106 | } else { 107 | rl.requests[key] = validRequests 108 | } 109 | } 110 | 111 | rl.mutex.Unlock() 112 | } 113 | } 114 | 115 | // Global rate limiter instance 116 | var globalRateLimiter *RateLimiter 117 | 118 | // InitializeRateLimiter initializes the global rate limiter 119 | func InitializeRateLimiter(config *configs.RateLimitConfig) { 120 | globalRateLimiter = NewRateLimiter(config) 121 | } 122 | 123 | // RateLimitMiddleware creates a rate limiting middleware 124 | func RateLimitMiddleware(requestType string) gin.HandlerFunc { 125 | return func(c *gin.Context) { 126 | if globalRateLimiter == nil { 127 | c.Next() 128 | return 129 | } 130 | 131 | ip := c.ClientIP() 132 | if !globalRateLimiter.IsAllowed(ip, requestType) { 133 | c.JSON(http.StatusTooManyRequests, gin.H{ 134 | "code": 429, 135 | "message": "Too many requests. Please try again later.", 136 | }) 137 | c.Abort() 138 | return 139 | } 140 | 141 | c.Next() 142 | } 143 | } 144 | 145 | // LoginRateLimitMiddleware rate limiting middleware specifically for login attempts 146 | func LoginRateLimitMiddleware() gin.HandlerFunc { 147 | return RateLimitMiddleware("login") 148 | } 149 | 150 | // APIRateLimitMiddleware rate limiting middleware for general API requests 151 | func APIRateLimitMiddleware() gin.HandlerFunc { 152 | return RateLimitMiddleware("api") 153 | } 154 | -------------------------------------------------------------------------------- /pkg/utils/cors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // Cors handles cross-origin requests, supports preflight requests 11 | // Suggestion: pass allowedOrigins as parameter or load from configuration 12 | func Cors(allowedOrigins []string) gin.HandlerFunc { 13 | // If allowedOrigins is empty, provide a default value or print warning 14 | if len(allowedOrigins) == 0 { 15 | log.Println("CORS Warning: No allowed origins configured!") 16 | // Can choose to completely disable CORS or allow all (if AllowCredentials is false) 17 | // Here we choose to disable CORS and just continue processing 18 | return func(c *gin.Context) { 19 | c.Next() 20 | } 21 | } 22 | 23 | return func(c *gin.Context) { 24 | // Get the request's Origin 25 | origin := c.Request.Header.Get("Origin") 26 | 27 | // If there's no Origin header (e.g., non-browser requests or same-origin requests), no need to handle CORS 28 | if origin == "" { 29 | c.Next() 30 | return 31 | } 32 | 33 | // Check if the request's Origin is in the allowed list 34 | allowedOrigin := "" 35 | for _, o := range allowedOrigins { 36 | if o == origin { 37 | allowedOrigin = origin 38 | break 39 | } 40 | // Optional: handle more complex matching logic like wildcard subdomains 41 | } 42 | 43 | // If Origin matches successfully 44 | if allowedOrigin != "" { 45 | c.Header("Access-Control-Allow-Origin", allowedOrigin) 46 | // Important: Since Allow-Origin is not "*", we need the Vary header 47 | c.Header("Vary", "Origin") 48 | 49 | // Set other CORS headers 50 | c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE, PATCH") // More comprehensive method list 51 | c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-CSRF-Token, Accept") // Add common headers 52 | c.Header("Access-Control-Allow-Credentials", "true") // Support credentials 53 | c.Header("Access-Control-Max-Age", "86400") // Preflight request cache time 54 | 55 | // Properly handle OPTIONS preflight requests: if Origin is allowed and method is OPTIONS, abort and return 204 56 | if c.Request.Method == "OPTIONS" { 57 | log.Printf("CORS: Preflight request for %s from %s allowed.", c.Request.URL.Path, origin) 58 | c.AbortWithStatus(http.StatusNoContent) 59 | return 60 | } 61 | 62 | // For non-OPTIONS requests, continue processing 63 | log.Printf("CORS: Allowed non-preflight request for %s from %s.", c.Request.URL.Path, origin) 64 | c.Next() 65 | 66 | } else { 67 | // If Origin doesn't match 68 | log.Printf("CORS: Origin '%s' not allowed for %s.", origin, c.Request.URL.Path) 69 | 70 | // For OPTIONS preflight requests, if Origin is not allowed, should also abort but may not set CORS headers 71 | // Browser will reject the request due to missing necessary Allow-Origin header 72 | if c.Request.Method == "OPTIONS" { 73 | // Can choose to return 403 Forbidden or simply Abort 74 | // c.AbortWithStatus(http.StatusForbidden) // More explicit rejection 75 | c.Abort() // Or just abort the processing chain 76 | return 77 | } 78 | 79 | // For non-OPTIONS requests, Origin not allowed 80 | // Browser will send the request but will block frontend JS from reading the response. 81 | // Here we can choose: 82 | // 1. Call c.Next(): let request continue, but browser will error (current code logic) 83 | // 2. Call c.AbortWithStatus(http.StatusForbidden): directly reject request (stricter) 84 | // Choosing c.Next() means backend might execute operations, but frontend can't receive results. 85 | // Choosing Abort is safer, preventing unauthorized origins from triggering operations. 86 | // Here we choose the safer Abort: 87 | log.Printf("CORS: Aborting non-preflight request from disallowed origin '%s' for %s.", origin, c.Request.URL.Path) 88 | c.AbortWithStatus(http.StatusForbidden) // Direct rejection 89 | // Or, if you want to keep the original behavior (allow backend processing but browser blocks): 90 | // c.Next() 91 | 92 | return // Ensure return after abort 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/handlers/pod_logs_handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/ciliverse/cilikube/internal/service" 13 | "github.com/ciliverse/cilikube/pkg/k8s" 14 | "github.com/ciliverse/cilikube/pkg/utils" 15 | "github.com/gin-gonic/gin" 16 | "github.com/gorilla/websocket" 17 | corev1 "k8s.io/api/core/v1" 18 | "k8s.io/apimachinery/pkg/api/errors" 19 | ) 20 | 21 | // PodLogsHandler struct 22 | type PodLogsHandler struct { 23 | service *service.PodLogsService 24 | clusterManager *k8s.ClusterManager 25 | upgrader websocket.Upgrader 26 | } 27 | 28 | // NewPodLogsHandler creates a new PodLogsHandler 29 | func NewPodLogsHandler(service *service.PodLogsService, clusterManager *k8s.ClusterManager) *PodLogsHandler { 30 | return &PodLogsHandler{ 31 | service: service, 32 | clusterManager: clusterManager, 33 | upgrader: websocket.Upgrader{ 34 | ReadBufferSize: 1024, 35 | WriteBufferSize: 1024, 36 | CheckOrigin: func(r *http.Request) bool { 37 | return true 38 | }, 39 | }, 40 | } 41 | } 42 | 43 | // GetPodLogs handles WebSocket requests for pod logs 44 | func (h *PodLogsHandler) GetPodLogs(c *gin.Context) { 45 | ws, err := h.upgrader.Upgrade(c.Writer, c.Request, nil) 46 | if err != nil { 47 | log.Printf("Failed to upgrade to websocket: %v", err) 48 | return 49 | } 50 | defer ws.Close() 51 | 52 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 53 | if !ok { 54 | ws.WriteMessage(websocket.TextMessage, []byte("Failed to get Kubernetes client")) 55 | return 56 | } 57 | 58 | namespace := strings.TrimSpace(c.Param("namespace")) 59 | name := strings.TrimSpace(c.Param("name")) 60 | container := c.Query("container") 61 | timestamps := c.Query("timestamps") == "true" 62 | tailLinesStr := c.Query("tailLines") 63 | 64 | if !utils.ValidateNamespace(namespace) || !utils.ValidateResourceName(name) { 65 | ws.WriteMessage(websocket.TextMessage, []byte("Invalid namespace or pod name")) 66 | return 67 | } 68 | if container == "" { 69 | ws.WriteMessage(websocket.TextMessage, []byte("Container name is required")) 70 | return 71 | } 72 | 73 | pod, err := h.service.Get(k8sClient.Clientset, namespace, name) 74 | if err != nil { 75 | if errors.IsNotFound(err) { 76 | ws.WriteMessage(websocket.TextMessage, []byte("Pod not found")) 77 | return 78 | } 79 | ws.WriteMessage(websocket.TextMessage, []byte("Failed to get pod info: "+err.Error())) 80 | return 81 | } 82 | 83 | containerFound := false 84 | for _, cont := range append(pod.Spec.Containers, pod.Spec.InitContainers...) { 85 | if cont.Name == container { 86 | containerFound = true 87 | break 88 | } 89 | } 90 | if !containerFound { 91 | ws.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Container '%s' not found in pod '%s'", container, name))) 92 | return 93 | } 94 | 95 | follow := c.Query("follow") == "true" 96 | 97 | logOptions := buildLogOptions(container, timestamps, tailLinesStr, follow) 98 | logStream, err := h.service.GetPodLogs(k8sClient.Clientset, namespace, name, logOptions) 99 | if err != nil { 100 | ws.WriteMessage(websocket.TextMessage, []byte("Failed to get log stream: "+err.Error())) 101 | return 102 | } 103 | defer logStream.Close() 104 | 105 | ctx, cancel := context.WithCancel(c.Request.Context()) 106 | defer cancel() 107 | 108 | go func() { 109 | for { 110 | _, _, err := ws.ReadMessage() 111 | if err != nil { 112 | cancel() 113 | break 114 | } 115 | } 116 | }() 117 | 118 | scanner := bufio.NewScanner(logStream) 119 | for scanner.Scan() { 120 | select { 121 | case <-ctx.Done(): 122 | return 123 | default: 124 | err := ws.WriteMessage(websocket.TextMessage, scanner.Bytes()) 125 | if err != nil { 126 | return 127 | } 128 | } 129 | } 130 | } 131 | 132 | func buildLogOptions(container string, timestamps bool, tailLinesStr string, follow bool) *corev1.PodLogOptions { 133 | var tailLines int64 = 1000 134 | if val, err := strconv.ParseInt(tailLinesStr, 10, 64); err == nil { 135 | tailLines = val 136 | } 137 | 138 | return &corev1.PodLogOptions{ 139 | Container: container, 140 | Follow: follow, 141 | Timestamps: timestamps, 142 | TailLines: &tailLines, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /internal/handlers/installer_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/ciliverse/cilikube/internal/service" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type InstallerHandler struct { 14 | installerService service.InstallerService 15 | } 16 | 17 | func NewInstallerHandler(is service.InstallerService) *InstallerHandler { 18 | return &InstallerHandler{ 19 | installerService: is, 20 | } 21 | } 22 | 23 | // HealthCheck handles health check requests 24 | func (h *InstallerHandler) HealthCheck(c *gin.Context) { 25 | c.JSON(http.StatusOK, gin.H{ 26 | "status": "ok", 27 | "message": "Backend service is running", 28 | }) 29 | } 30 | 31 | // StreamMinikubeInstallation handles the SSE request. 32 | func (h *InstallerHandler) StreamMinikubeInstallation(c *gin.Context) { 33 | // Set SSE headers 34 | c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") 35 | c.Writer.Header().Set("Cache-Control", "no-cache") 36 | c.Writer.Header().Set("Connection", "keep-alive") 37 | // CORS handled by middleware 38 | 39 | // Flush headers 40 | c.Writer.Flush() 41 | 42 | // Create channel 43 | messageChan := make(chan service.ProgressUpdate) // Use service.ProgressUpdate 44 | 45 | // Get client disconnect notification channel 46 | // Using c.Request.Context().Done() is more modern and recommended 47 | clientGone := c.Request.Context().Done() // Type is <-chan struct{} 48 | 49 | log.Println("SSE: Connection established, starting installation service Goroutine.") 50 | // Start service in new Goroutine 51 | go h.installerService.InstallMinikube(messageChan, clientGone) 52 | // Pass clientGone (<-chan struct{}) to service 53 | 54 | log.Println("SSE: Handler starts listening to service messages and pushing to client...") 55 | // Process stream in current Goroutine until completion or error 56 | err := h.streamUpdatesToClient(c, messageChan, clientGone) // Pass clientGone (<-chan struct{}) to helper function 57 | if err != nil { 58 | log.Printf("SSE: Stream processing error: %v", err) 59 | } 60 | log.Println("SSE: Handler stream processing ended.") 61 | } 62 | 63 | // streamUpdatesToClient helper function that processes messages from service and pushes to client 64 | func (h *InstallerHandler) streamUpdatesToClient(c *gin.Context, messageChan <-chan service.ProgressUpdate, clientGone <-chan struct{}) error { 65 | defer log.Println("SSE: streamUpdatesToClient loop ended.") 66 | for { 67 | select { 68 | case <-clientGone: // Listen to Context.Done() channel 69 | log.Println("SSE: Client disconnected (Context Done).") 70 | return nil // Client disconnected, normal exit 71 | case update, ok := <-messageChan: 72 | if !ok { 73 | log.Println("SSE: Service channel closed.") 74 | return nil // Service completed or error, normal exit from loop 75 | } 76 | 77 | // Received update, prepare to send 78 | log.Printf("SSE: Received update from service: Step=%s, Progress=%d, Done=%t", update.Step, update.Progress, update.Done) 79 | 80 | jsonData, err := json.Marshal(update) 81 | if err != nil { 82 | log.Printf("SSE: Failed to serialize service update: %v", err) 83 | // Try to notify client 84 | _, writeErr := fmt.Fprintf(c.Writer, "event: error\ndata: {\"error\": \"Internal server error marshalling update: %v\"}\n\n", err) 85 | if writeErr != nil { 86 | log.Printf("SSE: Failed to write serialization error to client: %v", writeErr) 87 | return writeErr // Return write error 88 | } 89 | c.Writer.Flush() 90 | continue // Continue listening for next message 91 | } 92 | 93 | // Send data 94 | _, writeErr := fmt.Fprintf(c.Writer, "event: message\ndata: %s\n\n", string(jsonData)) 95 | if writeErr != nil { 96 | log.Printf("SSE: Failed to write data to client: %v", writeErr) 97 | return writeErr // Return write error 98 | } 99 | 100 | // Flush to ensure sending 101 | if f, ok := c.Writer.(http.Flusher); ok { 102 | f.Flush() 103 | } else { 104 | log.Println("SSE: Warning - ResponseWriter does not support Flushing.") 105 | } 106 | 107 | // Exit if this is the last message 108 | if update.Done { 109 | log.Println("SSE: Final update sent, normal exit from stream processing.") 110 | return nil 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/handlers/oauth_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/ciliverse/cilikube/internal/models" 7 | "github.com/ciliverse/cilikube/internal/service" 8 | "github.com/ciliverse/cilikube/pkg/auth" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // OAuthHandler handles OAuth-related requests 13 | type OAuthHandler struct { 14 | oauthService *service.OAuthService 15 | } 16 | 17 | // NewOAuthHandler creates a new OAuth handler 18 | func NewOAuthHandler(oauthService *service.OAuthService) *OAuthHandler { 19 | return &OAuthHandler{ 20 | oauthService: oauthService, 21 | } 22 | } 23 | 24 | // GetAuthURL generates OAuth authorization URL 25 | func (h *OAuthHandler) GetAuthURL(c *gin.Context) { 26 | provider := c.Param("provider") 27 | state := c.Query("state") 28 | 29 | if state == "" { 30 | state = "default_state" // In production, generate a secure random state 31 | } 32 | 33 | authURL, err := h.oauthService.GetAuthURL(provider, state) 34 | if err != nil { 35 | c.JSON(http.StatusBadRequest, gin.H{ 36 | "code": 400, 37 | "message": "Failed to generate auth URL", 38 | "error": err.Error(), 39 | }) 40 | return 41 | } 42 | 43 | c.JSON(http.StatusOK, gin.H{ 44 | "code": 200, 45 | "data": gin.H{ 46 | "auth_url": authURL, 47 | "state": state, 48 | }, 49 | "message": "Auth URL generated successfully", 50 | }) 51 | } 52 | 53 | // HandleCallback handles OAuth callback 54 | func (h *OAuthHandler) HandleCallback(c *gin.Context) { 55 | var req models.OAuthLoginRequest 56 | if err := c.ShouldBindJSON(&req); err != nil { 57 | c.JSON(http.StatusBadRequest, gin.H{ 58 | "code": 400, 59 | "message": "Invalid request format", 60 | "error": err.Error(), 61 | }) 62 | return 63 | } 64 | 65 | // Handle OAuth login 66 | loginResp, err := h.oauthService.LoginWithOAuth(req.Provider, req.Code) 67 | if err != nil { 68 | c.JSON(http.StatusUnauthorized, gin.H{ 69 | "code": 401, 70 | "message": "OAuth login failed", 71 | "error": err.Error(), 72 | }) 73 | return 74 | } 75 | 76 | c.JSON(http.StatusOK, gin.H{ 77 | "code": 200, 78 | "data": loginResp, 79 | "message": "OAuth login successful", 80 | }) 81 | } 82 | 83 | // LinkAccount links OAuth account to current user 84 | func (h *OAuthHandler) LinkAccount(c *gin.Context) { 85 | // Get current user from JWT token 86 | userID, _, _, ok := auth.GetCurrentUser(c) 87 | if !ok { 88 | c.JSON(http.StatusUnauthorized, gin.H{ 89 | "code": 401, 90 | "message": "Authentication required", 91 | }) 92 | return 93 | } 94 | 95 | var req models.OAuthLinkRequest 96 | if err := c.ShouldBindJSON(&req); err != nil { 97 | c.JSON(http.StatusBadRequest, gin.H{ 98 | "code": 400, 99 | "message": "Invalid request format", 100 | "error": err.Error(), 101 | }) 102 | return 103 | } 104 | 105 | // Link OAuth account 106 | if err := h.oauthService.LinkAccount(userID, req.Provider, req.Code); err != nil { 107 | c.JSON(http.StatusBadRequest, gin.H{ 108 | "code": 400, 109 | "message": "Failed to link OAuth account", 110 | "error": err.Error(), 111 | }) 112 | return 113 | } 114 | 115 | c.JSON(http.StatusOK, gin.H{ 116 | "code": 200, 117 | "message": "OAuth account linked successfully", 118 | }) 119 | } 120 | 121 | // UnlinkAccount unlinks OAuth account from current user 122 | func (h *OAuthHandler) UnlinkAccount(c *gin.Context) { 123 | // Get current user from JWT token 124 | userID, _, _, ok := auth.GetCurrentUser(c) 125 | if !ok { 126 | c.JSON(http.StatusUnauthorized, gin.H{ 127 | "code": 401, 128 | "message": "Authentication required", 129 | }) 130 | return 131 | } 132 | 133 | var req models.OAuthUnlinkRequest 134 | if err := c.ShouldBindJSON(&req); err != nil { 135 | c.JSON(http.StatusBadRequest, gin.H{ 136 | "code": 400, 137 | "message": "Invalid request format", 138 | "error": err.Error(), 139 | }) 140 | return 141 | } 142 | 143 | // Unlink OAuth account 144 | if err := h.oauthService.UnlinkAccount(userID, req.Provider); err != nil { 145 | c.JSON(http.StatusBadRequest, gin.H{ 146 | "code": 400, 147 | "message": "Failed to unlink OAuth account", 148 | "error": err.Error(), 149 | }) 150 | return 151 | } 152 | 153 | c.JSON(http.StatusOK, gin.H{ 154 | "code": 200, 155 | "message": "OAuth account unlinked successfully", 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | # Backend service 5 | backend: 6 | image: cilliantech/cilikube:latest 7 | container_name: cilikube-backend 8 | restart: unless-stopped 9 | ports: 10 | - "8080:8080" 11 | environment: 12 | - GIN_MODE=release 13 | - CILIKUBE_CONFIG_PATH=/app/configs/config.yaml 14 | volumes: 15 | - ~/.kube:/root/.kube:ro 16 | - ./configs:/app/configs:ro 17 | networks: 18 | - cilikube-net 19 | healthcheck: 20 | test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] 21 | interval: 30s 22 | timeout: 10s 23 | retries: 3 24 | start_period: 40s 25 | logging: 26 | driver: "json-file" 27 | options: 28 | max-size: "10m" 29 | max-file: "3" 30 | 31 | # Frontend service 32 | frontend: 33 | image: cilliantech/cilikube-web:latest 34 | container_name: cilikube-frontend 35 | restart: unless-stopped 36 | ports: 37 | - "80:80" 38 | depends_on: 39 | backend: 40 | condition: service_healthy 41 | networks: 42 | - cilikube-net 43 | healthcheck: 44 | test: ["CMD", "curl", "-f", "http://localhost:80/health"] 45 | interval: 30s 46 | timeout: 10s 47 | retries: 3 48 | start_period: 10s 49 | logging: 50 | driver: "json-file" 51 | options: 52 | max-size: "10m" 53 | max-file: "3" 54 | 55 | # Redis cache (optional) 56 | redis: 57 | image: redis:7-alpine 58 | container_name: cilikube-redis 59 | restart: unless-stopped 60 | ports: 61 | - "6379:6379" 62 | volumes: 63 | - redis_data:/data 64 | networks: 65 | - cilikube-net 66 | healthcheck: 67 | test: ["CMD", "redis-cli", "ping"] 68 | interval: 30s 69 | timeout: 10s 70 | retries: 3 71 | logging: 72 | driver: "json-file" 73 | options: 74 | max-size: "5m" 75 | max-file: "3" 76 | 77 | # MySQL database (optional) 78 | mysql: 79 | image: mysql:8.0 80 | container_name: cilikube-mysql 81 | restart: unless-stopped 82 | ports: 83 | - "3306:3306" 84 | environment: 85 | MYSQL_ROOT_PASSWORD: cilikube_root_password 86 | MYSQL_DATABASE: cilikube_db 87 | MYSQL_USER: cilikube_user 88 | MYSQL_PASSWORD: cilikube_password 89 | volumes: 90 | - mysql_data:/var/lib/mysql 91 | - ./scripts/mysql:/docker-entrypoint-initdb.d:ro 92 | networks: 93 | - cilikube-net 94 | healthcheck: 95 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 96 | interval: 30s 97 | timeout: 10s 98 | retries: 3 99 | start_period: 60s 100 | logging: 101 | driver: "json-file" 102 | options: 103 | max-size: "10m" 104 | max-file: "3" 105 | 106 | # Prometheus monitoring (optional) 107 | prometheus: 108 | image: prom/prometheus:latest 109 | container_name: cilikube-prometheus 110 | restart: unless-stopped 111 | ports: 112 | - "9090:9090" 113 | volumes: 114 | - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro 115 | - prometheus_data:/prometheus 116 | command: 117 | - '--config.file=/etc/prometheus/prometheus.yml' 118 | - '--storage.tsdb.path=/prometheus' 119 | - '--web.console.libraries=/etc/prometheus/console_libraries' 120 | - '--web.console.templates=/etc/prometheus/consoles' 121 | - '--storage.tsdb.retention.time=200h' 122 | - '--web.enable-lifecycle' 123 | networks: 124 | - cilikube-net 125 | profiles: 126 | - monitoring 127 | 128 | # Grafana dashboard (optional) 129 | grafana: 130 | image: grafana/grafana:latest 131 | container_name: cilikube-grafana 132 | restart: unless-stopped 133 | ports: 134 | - "3000:3000" 135 | environment: 136 | GF_SECURITY_ADMIN_PASSWORD: admin 137 | volumes: 138 | - grafana_data:/var/lib/grafana 139 | - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro 140 | networks: 141 | - cilikube-net 142 | profiles: 143 | - monitoring 144 | 145 | networks: 146 | cilikube-net: 147 | driver: bridge 148 | ipam: 149 | config: 150 | - subnet: 172.20.0.0/16 151 | 152 | volumes: 153 | mysql_data: 154 | driver: local 155 | redis_data: 156 | driver: local 157 | prometheus_data: 158 | driver: local 159 | grafana_data: 160 | driver: local -------------------------------------------------------------------------------- /internal/models/audit.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // AuditLog represents audit log entries for security and compliance 9 | type AuditLog struct { 10 | ID uint `json:"id" gorm:"primaryKey"` 11 | UserID *uint `json:"user_id" gorm:"index"` 12 | Action string `json:"action" gorm:"not null;size:100;index"` 13 | Resource string `json:"resource" gorm:"size:100;index"` 14 | ResourceID string `json:"resource_id" gorm:"size:100"` 15 | IPAddress string `json:"ip_address" gorm:"size:45"` 16 | UserAgent string `json:"user_agent" gorm:"type:text"` 17 | Details string `json:"details" gorm:"type:json"` 18 | CreatedAt time.Time `json:"created_at" gorm:"index"` 19 | } 20 | 21 | // TableName specifies the table name for AuditLog model 22 | func (AuditLog) TableName() string { 23 | return "audit_logs" 24 | } 25 | 26 | // AuditLogResponse response for audit log operations 27 | type AuditLogResponse struct { 28 | ID uint `json:"id"` 29 | UserID *uint `json:"user_id"` 30 | Username string `json:"username,omitempty"` 31 | Action string `json:"action"` 32 | Resource string `json:"resource"` 33 | ResourceID string `json:"resource_id"` 34 | IPAddress string `json:"ip_address"` 35 | UserAgent string `json:"user_agent"` 36 | Details map[string]interface{} `json:"details"` 37 | CreatedAt time.Time `json:"created_at"` 38 | } 39 | 40 | // CreateAuditLogRequest request for creating audit log entry 41 | type CreateAuditLogRequest struct { 42 | UserID *uint `json:"user_id"` 43 | Action string `json:"action" binding:"required"` 44 | Resource string `json:"resource"` 45 | ResourceID string `json:"resource_id"` 46 | IPAddress string `json:"ip_address"` 47 | UserAgent string `json:"user_agent"` 48 | Details map[string]interface{} `json:"details"` 49 | } 50 | 51 | // AuditLogFilter filter for querying audit logs 52 | type AuditLogFilter struct { 53 | UserID *uint `json:"user_id"` 54 | Action string `json:"action"` 55 | Resource string `json:"resource"` 56 | IPAddress string `json:"ip_address"` 57 | StartDate *time.Time `json:"start_date"` 58 | EndDate *time.Time `json:"end_date"` 59 | Page int `json:"page"` 60 | PageSize int `json:"page_size"` 61 | } 62 | 63 | // ToResponse converts AuditLog to AuditLogResponse 64 | func (a *AuditLog) ToResponse() AuditLogResponse { 65 | var details map[string]interface{} 66 | if a.Details != "" { 67 | json.Unmarshal([]byte(a.Details), &details) 68 | } 69 | 70 | return AuditLogResponse{ 71 | ID: a.ID, 72 | UserID: a.UserID, 73 | Action: a.Action, 74 | Resource: a.Resource, 75 | ResourceID: a.ResourceID, 76 | IPAddress: a.IPAddress, 77 | UserAgent: a.UserAgent, 78 | Details: details, 79 | CreatedAt: a.CreatedAt, 80 | } 81 | } 82 | 83 | // SetDetails sets the details field from a map 84 | func (a *AuditLog) SetDetails(details map[string]interface{}) error { 85 | if details == nil { 86 | a.Details = "" 87 | return nil 88 | } 89 | 90 | detailsJSON, err := json.Marshal(details) 91 | if err != nil { 92 | return err 93 | } 94 | a.Details = string(detailsJSON) 95 | return nil 96 | } 97 | 98 | // GetDetails gets the details field as a map 99 | func (a *AuditLog) GetDetails() (map[string]interface{}, error) { 100 | if a.Details == "" { 101 | return nil, nil 102 | } 103 | 104 | var details map[string]interface{} 105 | err := json.Unmarshal([]byte(a.Details), &details) 106 | return details, err 107 | } 108 | 109 | // Common audit actions 110 | const ( 111 | AuditActionLogin = "login" 112 | AuditActionLoginFailed = "login_failed" 113 | AuditActionLogout = "logout" 114 | AuditActionPasswordChange = "password_change" 115 | AuditActionProfileUpdate = "profile_update" 116 | AuditActionUserCreate = "user_create" 117 | AuditActionUserUpdate = "user_update" 118 | AuditActionUserDelete = "user_delete" 119 | AuditActionRoleAssign = "role_assign" 120 | AuditActionRoleRemove = "role_remove" 121 | AuditActionOAuthLink = "oauth_link" 122 | AuditActionOAuthUnlink = "oauth_unlink" 123 | AuditActionResourceCreate = "resource_create" 124 | AuditActionResourceUpdate = "resource_update" 125 | AuditActionResourceDelete = "resource_delete" 126 | AuditActionPermissionDenied = "permission_denied" 127 | ) 128 | -------------------------------------------------------------------------------- /deployments/docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CiliKube Docker Build Script 4 | # Usage: ./build.sh [version] [environment] 5 | 6 | set -e 7 | 8 | # Default parameters 9 | VERSION=${1:-"dev"} 10 | ENVIRONMENT=${2:-"production"} 11 | IMAGE_NAME="cilikube" 12 | REGISTRY=${REGISTRY:-"cilliantech"} 13 | 14 | # Color output 15 | RED='\033[0;31m' 16 | GREEN='\033[0;32m' 17 | YELLOW='\033[1;33m' 18 | NC='\033[0m' # No Color 19 | 20 | # Logging functions 21 | log_info() { 22 | echo -e "${GREEN}[INFO]${NC} $1" 23 | } 24 | 25 | log_warn() { 26 | echo -e "${YELLOW}[WARN]${NC} $1" 27 | } 28 | 29 | log_error() { 30 | echo -e "${RED}[ERROR]${NC} $1" 31 | } 32 | 33 | # Check if Docker is installed 34 | check_docker() { 35 | if ! command -v docker &> /dev/null; then 36 | log_error "Docker is not installed or not in PATH" 37 | exit 1 38 | fi 39 | log_info "Docker version: $(docker --version)" 40 | } 41 | 42 | # Get build information 43 | get_build_info() { 44 | BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") 45 | GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") 46 | 47 | log_info "Build information:" 48 | log_info " Version: $VERSION" 49 | log_info " Environment: $ENVIRONMENT" 50 | log_info " Build time: $BUILD_TIME" 51 | log_info " Git commit: $GIT_COMMIT" 52 | } 53 | 54 | # Build Docker image 55 | build_image() { 56 | local dockerfile_path="../../Dockerfile" 57 | local context_path="../.." 58 | 59 | log_info "Starting Docker image build..." 60 | 61 | # Build arguments 62 | local build_args=( 63 | "--build-arg" "VERSION=$VERSION" 64 | "--build-arg" "BUILD_TIME=$BUILD_TIME" 65 | "--build-arg" "GIT_COMMIT=$GIT_COMMIT" 66 | "--tag" "$REGISTRY/$IMAGE_NAME:$VERSION" 67 | "--tag" "$REGISTRY/$IMAGE_NAME:latest" 68 | "--file" "$dockerfile_path" 69 | "$context_path" 70 | ) 71 | 72 | # Add extra optimizations for production environment 73 | if [[ "$ENVIRONMENT" == "production" ]]; then 74 | build_args+=("--no-cache") 75 | fi 76 | 77 | # Execute build 78 | if docker build "${build_args[@]}"; then 79 | log_info "Image build successful!" 80 | log_info "Image tags:" 81 | log_info " $REGISTRY/$IMAGE_NAME:$VERSION" 82 | log_info " $REGISTRY/$IMAGE_NAME:latest" 83 | else 84 | log_error "Image build failed!" 85 | exit 1 86 | fi 87 | } 88 | 89 | # Show image information 90 | show_image_info() { 91 | log_info "Image information:" 92 | docker images "$REGISTRY/$IMAGE_NAME" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" 93 | } 94 | 95 | # Push image (optional) 96 | push_image() { 97 | if [[ "$PUSH" == "true" ]]; then 98 | log_info "Pushing image to registry..." 99 | docker push "$REGISTRY/$IMAGE_NAME:$VERSION" 100 | docker push "$REGISTRY/$IMAGE_NAME:latest" 101 | log_info "Image push completed!" 102 | fi 103 | } 104 | 105 | # Cleanup old images (optional) 106 | cleanup_old_images() { 107 | if [[ "$CLEANUP" == "true" ]]; then 108 | log_warn "Cleaning up dangling images..." 109 | docker image prune -f 110 | log_info "Cleanup completed!" 111 | fi 112 | } 113 | 114 | # Main function 115 | main() { 116 | log_info "CiliKube Docker Build Script" 117 | log_info "============================" 118 | 119 | # Change to script directory 120 | cd "$(dirname "$0")" 121 | 122 | check_docker 123 | get_build_info 124 | build_image 125 | show_image_info 126 | push_image 127 | cleanup_old_images 128 | 129 | log_info "Build completed! 🎉" 130 | log_info "" 131 | log_info "Run container:" 132 | log_info " docker run -d --name cilikube -p 8080:8080 $REGISTRY/$IMAGE_NAME:$VERSION" 133 | log_info "" 134 | log_info "Use Docker Compose:" 135 | log_info " cd ../../ && docker-compose up -d" 136 | } 137 | 138 | # Help information 139 | show_help() { 140 | echo "CiliKube Docker Build Script" 141 | echo "" 142 | echo "Usage:" 143 | echo " $0 [version] [environment]" 144 | echo "" 145 | echo "Arguments:" 146 | echo " version Image version tag (default: dev)" 147 | echo " environment Build environment (default: production)" 148 | echo "" 149 | echo "Environment variables:" 150 | echo " REGISTRY Image registry address (default: cilliantech)" 151 | echo " PUSH Whether to push image (true/false)" 152 | echo " CLEANUP Whether to cleanup old images (true/false)" 153 | echo "" 154 | echo "Examples:" 155 | echo " $0 v1.0.0 production" 156 | echo " PUSH=true $0 v1.0.0" 157 | echo " CLEANUP=true $0 latest development" 158 | } 159 | 160 | # Handle command line arguments 161 | case "${1:-}" in 162 | -h|--help) 163 | show_help 164 | exit 0 165 | ;; 166 | *) 167 | main "$@" 168 | ;; 169 | esac -------------------------------------------------------------------------------- /internal/handlers/cluster_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/ciliverse/cilikube/internal/models" 8 | "github.com/ciliverse/cilikube/internal/service" 9 | "github.com/ciliverse/cilikube/pkg/utils" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type ClusterHandler struct { 14 | service *service.ClusterService 15 | } 16 | 17 | func NewClusterHandler(svc *service.ClusterService) *ClusterHandler { 18 | return &ClusterHandler{service: svc} 19 | } 20 | 21 | // ListClusters gets cluster list 22 | func (h *ClusterHandler) ListClusters(c *gin.Context) { 23 | clusters := h.service.ListClusters() 24 | utils.ApiSuccess(c, clusters, "successfully retrieved cluster list") 25 | } 26 | 27 | // GetCluster gets single cluster details 28 | func (h *ClusterHandler) GetCluster(c *gin.Context) { 29 | clusterID := c.Param("id") 30 | cluster, err := h.service.GetClusterByID(clusterID) 31 | if err != nil { 32 | utils.ApiError(c, http.StatusNotFound, "failed to get cluster", err.Error()) 33 | return 34 | } 35 | utils.ApiSuccess(c, cluster, "successfully retrieved cluster details") 36 | } 37 | 38 | // CreateCluster creates a new cluster 39 | func (h *ClusterHandler) CreateCluster(c *gin.Context) { 40 | var req models.CreateClusterRequest 41 | if err := c.ShouldBindJSON(&req); err != nil { 42 | utils.ApiError(c, http.StatusBadRequest, "request parameter error", err.Error()) 43 | return 44 | } 45 | if err := h.service.CreateCluster(req); err != nil { 46 | utils.ApiError(c, http.StatusInternalServerError, "failed to create cluster", err.Error()) 47 | return 48 | } 49 | utils.ApiSuccess(c, nil, "cluster created successfully") 50 | } 51 | 52 | // UpdateCluster updates an existing cluster 53 | func (h *ClusterHandler) UpdateCluster(c *gin.Context) { 54 | clusterID := c.Param("id") 55 | var req models.UpdateClusterRequest 56 | if err := c.ShouldBindJSON(&req); err != nil { 57 | utils.ApiError(c, http.StatusBadRequest, "request parameter error", err.Error()) 58 | return 59 | } 60 | if err := h.service.UpdateCluster(clusterID, req); err != nil { 61 | utils.ApiError(c, http.StatusInternalServerError, "failed to update cluster", err.Error()) 62 | return 63 | } 64 | utils.ApiSuccess(c, nil, "cluster updated successfully") 65 | } 66 | 67 | // DeleteCluster deletes a cluster 68 | func (h *ClusterHandler) DeleteCluster(c *gin.Context) { 69 | clusterID := c.Param("id") 70 | if err := h.service.DeleteClusterByID(clusterID); err != nil { 71 | utils.ApiError(c, http.StatusInternalServerError, "failed to delete cluster", err.Error()) 72 | return 73 | } 74 | utils.ApiSuccess(c, nil, "cluster deleted successfully") 75 | } 76 | 77 | // SetActiveCluster sets the current active cluster 78 | func (h *ClusterHandler) SetActiveCluster(c *gin.Context) { 79 | var req struct { 80 | ID string `json:"id"` 81 | Name string `json:"name"` // Maintain backward compatibility 82 | } 83 | if err := c.ShouldBindJSON(&req); err != nil { 84 | utils.ApiError(c, http.StatusBadRequest, "request parameter error", err.Error()) 85 | return 86 | } 87 | 88 | var targetID string 89 | if req.ID != "" { 90 | // Prioritize using ID 91 | targetID = req.ID 92 | } else if req.Name != "" { 93 | // Backward compatibility: find cluster ID by name 94 | clusters := h.service.ListClusters() 95 | for _, cluster := range clusters { 96 | if cluster.Name == req.Name { 97 | targetID = cluster.ID 98 | break 99 | } 100 | } 101 | if targetID == "" { 102 | utils.ApiError(c, http.StatusNotFound, "cluster does not exist", fmt.Sprintf("cluster named '%s' not found", req.Name)) 103 | return 104 | } 105 | } else { 106 | utils.ApiError(c, http.StatusBadRequest, "request parameter error", "must provide id or name parameter") 107 | return 108 | } 109 | 110 | if err := h.service.SetActiveCluster(targetID); err != nil { 111 | utils.ApiError(c, http.StatusInternalServerError, "failed to switch active cluster", err.Error()) 112 | return 113 | } 114 | 115 | // Return detailed cluster information 116 | activeCluster, err := h.service.GetClusterByID(targetID) 117 | if err != nil { 118 | utils.ApiSuccess(c, gin.H{"activeClusterID": targetID}, "active cluster switched successfully") 119 | } else { 120 | utils.ApiSuccess(c, gin.H{ 121 | "activeClusterID": targetID, 122 | "activeClusterName": activeCluster.Name, 123 | "cluster": activeCluster, 124 | }, "active cluster switched successfully") 125 | } 126 | } 127 | 128 | // GetActiveCluster gets the current active cluster 129 | func (h *ClusterHandler) GetActiveCluster(c *gin.Context) { 130 | activeClusterID := h.service.GetActiveClusterID() 131 | if activeClusterID == "" { 132 | utils.ApiError(c, http.StatusNotFound, "no active cluster currently", "please add and activate a cluster first") 133 | return 134 | } 135 | 136 | // Return the active cluster name directly, if cluster details are needed, they can be obtained through other APIs 137 | utils.ApiSuccess(c, activeClusterID, "successfully retrieved active cluster") 138 | } 139 | -------------------------------------------------------------------------------- /pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/ciliverse/cilikube/configs" 11 | "github.com/ciliverse/cilikube/internal/models" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/driver/sqlite" 14 | "gorm.io/gorm" 15 | "gorm.io/gorm/logger" 16 | ) 17 | 18 | var DB *gorm.DB 19 | 20 | // InitDatabase initializes database connection 21 | func InitDatabase() error { 22 | if !configs.GlobalConfig.Database.Enabled { 23 | log.Println("database not enabled, no initialization needed.") 24 | return nil 25 | } 26 | 27 | var err error 28 | dsn := configs.GlobalConfig.GetDSN() 29 | dbType := configs.GlobalConfig.Database.Type 30 | 31 | // Configure GORM 32 | gormConfig := &gorm.Config{ 33 | Logger: logger.Default.LogMode(logger.Info), 34 | } 35 | 36 | // Connect to database based on type 37 | switch dbType { 38 | case "sqlite": 39 | // Ensure directory exists for SQLite 40 | if err := ensureSQLiteDir(dsn); err != nil { 41 | return fmt.Errorf("failed to create SQLite directory: %v", err) 42 | } 43 | DB, err = gorm.Open(sqlite.Open(dsn), gormConfig) 44 | case "mysql", "": 45 | // Default to MySQL for backward compatibility 46 | DB, err = gorm.Open(mysql.Open(dsn), gormConfig) 47 | default: 48 | return fmt.Errorf("unsupported database type: %s", dbType) 49 | } 50 | 51 | if err != nil { 52 | return fmt.Errorf("failed to connect to %s database: %v", dbType, err) 53 | } 54 | 55 | // Configure connection pool (skip for SQLite) 56 | if dbType != "sqlite" { 57 | sqlDB, err := DB.DB() 58 | if err != nil { 59 | return fmt.Errorf("failed to get underlying sql.DB: %v", err) 60 | } 61 | 62 | // Set connection pool parameters 63 | sqlDB.SetMaxIdleConns(10) // Set maximum number of connections in idle connection pool 64 | sqlDB.SetMaxOpenConns(100) // Set maximum number of open database connections 65 | sqlDB.SetConnMaxLifetime(time.Hour) // Set maximum time a connection can be reused 66 | 67 | // Test connection 68 | if err := sqlDB.Ping(); err != nil { 69 | return fmt.Errorf("failed to ping database: %v", err) 70 | } 71 | } 72 | 73 | log.Printf("Database connected successfully (type: %s)", dbType) 74 | return nil 75 | } 76 | 77 | // ensureSQLiteDir ensures the directory exists for SQLite database file 78 | func ensureSQLiteDir(dbPath string) error { 79 | dir := filepath.Dir(dbPath) 80 | if dir != "." && dir != "" { 81 | if err := os.MkdirAll(dir, 0755); err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | // AutoMigrate automatically migrates database tables 89 | func AutoMigrate() error { 90 | // First check if database is enabled and DB instance is successfully created 91 | if !configs.GlobalConfig.Database.Enabled || DB == nil { 92 | log.Println("database not enabled or not initialized, skipping migration.") 93 | return nil // Not enabled or not initialized, not an error, return directly 94 | } 95 | 96 | log.Println("starting database auto migration...") // Add log 97 | err := DB.AutoMigrate( 98 | &models.User{}, 99 | // Note: Cluster migration is now handled by the store layer 100 | ) 101 | if err != nil { 102 | return fmt.Errorf("failed to migrate database: %v", err) 103 | } 104 | 105 | log.Println("Database migration completed") 106 | return nil 107 | } 108 | 109 | // CreateDefaultAdmin creates default admin account 110 | func CreateDefaultAdmin() error { 111 | if DB == nil { 112 | log.Println("database not initialized, skipping default admin creation.") 113 | return nil 114 | } 115 | 116 | var count int64 117 | DB.Model(&models.User{}).Count(&count) 118 | 119 | // If no users exist, create default admin 120 | if count == 0 { 121 | admin := &models.User{ 122 | Username: "admin", 123 | Email: "admin@cilikube.com", 124 | Password: "12345678", // This password will be encrypted in BeforeCreate hook 125 | Role: "admin", 126 | IsActive: true, 127 | } 128 | 129 | if err := DB.Create(admin).Error; err != nil { 130 | return fmt.Errorf("failed to create default admin: %v", err) 131 | } 132 | 133 | log.Println("Default admin user created: username=admin, password=12345678") 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // GetDB returns the database instance 140 | func GetDB() (*gorm.DB, error) { 141 | if DB == nil { 142 | return nil, fmt.Errorf("database not initialized") 143 | } 144 | return DB, nil 145 | } 146 | 147 | // IsDataBaseEnabled returns whether the database is enabled 148 | func IsDataBaseEnabled() bool { 149 | if configs.GlobalConfig == nil { 150 | return false 151 | } 152 | return configs.GlobalConfig.Database.Enabled 153 | } 154 | 155 | // GetDatabaseType returns the current database type 156 | func GetDatabaseType() string { 157 | if configs.GlobalConfig == nil { 158 | return "unknown" 159 | } 160 | return configs.GlobalConfig.Database.Type 161 | } 162 | 163 | // CloseDatabase closes database connection 164 | func CloseDatabase() error { 165 | // Also check if DB is nil 166 | if DB == nil { 167 | log.Println("database not initialized, no need to close.") 168 | return nil 169 | } 170 | sqlDB, err := DB.DB() 171 | if err != nil { 172 | return err 173 | } 174 | return sqlDB.Close() 175 | } 176 | -------------------------------------------------------------------------------- /deployments/README.md: -------------------------------------------------------------------------------- 1 | # CiliKube Deployment 2 | 3 | This directory contains deployment configurations and scripts for CiliKube across different platforms and environments. 4 | 5 | ## Directory Structure 6 | 7 | ``` 8 | deployments/ 9 | ├── docker/ # Docker deployment files 10 | ├── kubernetes/ # Kubernetes manifests 11 | ├── helm/ # Helm charts 12 | └── monitoring/ # Monitoring configurations 13 | ``` 14 | 15 | ## Deployment Options 16 | 17 | ### 1. Docker Deployment 18 | 19 | **Quick Start:** 20 | ```bash 21 | # Using Docker Compose (Recommended) 22 | docker-compose up -d 23 | 24 | # Using Docker directly 25 | docker run -d --name cilikube -p 8080:8080 cilliantech/cilikube:latest 26 | ``` 27 | 28 | **Features:** 29 | - Multi-service orchestration with Docker Compose 30 | - Production-ready configuration 31 | - Built-in monitoring stack (Prometheus + Grafana) 32 | - Persistent data storage 33 | 34 | **Documentation:** [docker/README.md](docker/README.md) 35 | 36 | ### 2. Kubernetes Deployment 37 | 38 | **Quick Start:** 39 | ```bash 40 | # Apply Kubernetes manifests 41 | kubectl apply -f deployments/kubernetes/ 42 | 43 | # Or using Helm 44 | helm install cilikube deployments/helm/cilikube 45 | ``` 46 | 47 | **Features:** 48 | - High availability setup 49 | - Auto-scaling capabilities 50 | - Service mesh integration 51 | - Cloud-native monitoring 52 | 53 | **Documentation:** [kubernetes/README.md](kubernetes/README.md) 54 | 55 | ### 3. Helm Deployment 56 | 57 | **Quick Start:** 58 | ```bash 59 | # Add Helm repository 60 | helm repo add cilikube https://charts.cillian.website 61 | 62 | # Install CiliKube 63 | helm install cilikube cilikube/cilikube 64 | ``` 65 | 66 | **Features:** 67 | - Parameterized deployments 68 | - Easy upgrades and rollbacks 69 | - Multi-environment support 70 | - Custom value configurations 71 | 72 | **Documentation:** [helm/README.md](helm/README.md) 73 | 74 | ## Environment Support 75 | 76 | | Environment | Docker | Kubernetes | Helm | 77 | |-------------|--------|------------|------| 78 | | Development | ✅ | ✅ | ✅ | 79 | | Staging | ✅ | ✅ | ✅ | 80 | | Production | ✅ | ✅ | ✅ | 81 | 82 | ## Prerequisites 83 | 84 | ### Common Requirements 85 | - **Kubernetes Cluster**: v1.20+ (for K8s/Helm deployments) 86 | - **Docker**: v20.10+ (for Docker deployments) 87 | - **kubectl**: v1.20+ (for K8s management) 88 | - **Helm**: v3.0+ (for Helm deployments) 89 | 90 | ### Resource Requirements 91 | 92 | | Component | CPU | Memory | Storage | 93 | |-----------|-----|--------|---------| 94 | | Backend | 500m | 512Mi | - | 95 | | Frontend | 100m | 128Mi | - | 96 | | MySQL | 500m | 1Gi | 10Gi | 97 | | Redis | 100m | 256Mi | 1Gi | 98 | | Prometheus | 500m | 1Gi | 20Gi | 99 | 100 | ## Configuration 101 | 102 | ### Environment Variables 103 | All deployments support configuration through environment variables. See individual deployment documentation for specific variables. 104 | 105 | ### Secrets Management 106 | - **Docker**: Use `.env` files or Docker secrets 107 | - **Kubernetes**: Use Kubernetes secrets and ConfigMaps 108 | - **Helm**: Use Helm values and external secret operators 109 | 110 | ### Persistent Storage 111 | - **Docker**: Docker volumes 112 | - **Kubernetes**: PersistentVolumes with StorageClasses 113 | - **Helm**: Configurable storage options 114 | 115 | ## Monitoring 116 | 117 | All deployment methods include monitoring capabilities: 118 | 119 | - **Metrics**: Prometheus for metrics collection 120 | - **Visualization**: Grafana dashboards 121 | - **Alerting**: AlertManager for notifications 122 | - **Logging**: Structured logging with log aggregation 123 | 124 | Access monitoring: 125 | - **Prometheus**: http://localhost:9090 126 | - **Grafana**: http://localhost:3000 (admin/admin) 127 | 128 | ## Security 129 | 130 | ### Network Security 131 | - TLS/SSL encryption for all communications 132 | - Network policies for Kubernetes deployments 133 | - Firewall rules for Docker deployments 134 | 135 | ### Authentication & Authorization 136 | - JWT-based authentication 137 | - RBAC integration with Kubernetes 138 | - Role-based access control 139 | 140 | ### Secrets 141 | - Encrypted secret storage 142 | - Rotation policies 143 | - External secret management integration 144 | 145 | ## Troubleshooting 146 | 147 | ### Common Issues 148 | 149 | 1. **Port Conflicts** 150 | ```bash 151 | # Check port usage 152 | netstat -tulpn | grep :8080 153 | ``` 154 | 155 | 2. **Resource Constraints** 156 | ```bash 157 | # Check resource usage 158 | docker stats 159 | kubectl top nodes 160 | ``` 161 | 162 | 3. **Network Issues** 163 | ```bash 164 | # Test connectivity 165 | curl -f http://localhost:8080/health 166 | ``` 167 | 168 | ### Logs 169 | 170 | ```bash 171 | # Docker 172 | docker logs cilikube-backend 173 | 174 | # Kubernetes 175 | kubectl logs -f deployment/cilikube-backend 176 | 177 | # Helm 178 | kubectl logs -f -l app.kubernetes.io/name=cilikube 179 | ``` 180 | 181 | ## Contributing 182 | 183 | When adding new deployment configurations: 184 | 185 | 1. Follow the existing directory structure 186 | 2. Include comprehensive documentation 187 | 3. Add example configurations 188 | 4. Test across different environments 189 | 5. Update this main README 190 | 191 | ## Support 192 | 193 | - **Documentation**: [cilikube.cillian.website](https://cilikube.cillian.website) 194 | - **Issues**: [GitHub Issues](https://github.com/ciliverse/cilikube/issues) 195 | - **Discussions**: [GitHub Discussions](https://github.com/ciliverse/cilikube/discussions) -------------------------------------------------------------------------------- /pkg/auth/casbin_middleware.go: -------------------------------------------------------------------------------- 1 | // pkg/auth/casbin.go 2 | package auth 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "path/filepath" // Import path/filepath 9 | 10 | "github.com/casbin/casbin/v2" 11 | gormadapter "github.com/casbin/gorm-adapter/v3" 12 | "github.com/gin-gonic/gin" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type CasbinBuilder struct { 17 | IgnorePaths []string 18 | } 19 | 20 | func NewCasbinBuilder() *CasbinBuilder { 21 | return &CasbinBuilder{} 22 | } 23 | 24 | // IgnorePath allows chained calls to add paths that need to be ignored 25 | func (r *CasbinBuilder) IgnorePath(path string) *CasbinBuilder { 26 | r.IgnorePaths = append(r.IgnorePaths, path) 27 | return r 28 | } 29 | 30 | // CasbinMiddleware returns a Gin middleware handler function 31 | func (r *CasbinBuilder) CasbinMiddleware(e *casbin.Enforcer) gin.HandlerFunc { 32 | return func(c *gin.Context) { 33 | reqPath := c.Request.URL.Path 34 | // Skip ignored routes 35 | for _, path := range r.IgnorePaths { 36 | // Use filepath.Match to support simple * matching (if needed) 37 | // Or directly compare c.Request.URL.Path == path 38 | if matched, _ := filepath.Match(path, reqPath); matched || reqPath == path { 39 | c.Next() 40 | return 41 | } 42 | } 43 | 44 | // Get user ID from context (set by JWT middleware) 45 | userIDVal, exist := c.Get("userID") 46 | if !exist { 47 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unable to get user information, please login first"}) 48 | return 49 | } 50 | 51 | userID, ok := userIDVal.(uint) 52 | if !ok { 53 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "User information format is incorrect"}) 54 | return 55 | } 56 | 57 | obj := reqPath 58 | act := c.Request.Method 59 | 60 | log.Printf("Permission verification - UserID: %v, Path: %v, Method: %v", userID, obj, act) 61 | 62 | // Use user-based permission checking instead of role-based 63 | userSubject := fmt.Sprintf("user:%d", userID) 64 | 65 | // Use Casbin Enforcer to verify permissions 66 | allowed, err := e.Enforce(userSubject, obj, act) 67 | if err != nil { 68 | log.Printf("Casbin Enforce error: %v", err) 69 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal error occurred during permission check"}) 70 | return 71 | } 72 | 73 | if allowed { 74 | log.Printf("Permission verification passed - UserID: %d, Path: %s, Method: %s", userID, obj, act) 75 | c.Next() 76 | } else { 77 | log.Printf("Permission verification failed - UserID: %d has no access to %s %s", userID, act, obj) 78 | c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "You do not have permission to perform this operation"}) // Use 403 Forbidden 79 | } 80 | } 81 | } 82 | 83 | // addPolicyIfNotExists helper function, checks if policy exists, adds if not 84 | func addPolicyIfNotExists(e *casbin.Enforcer, sub, obj, act string) { 85 | has, err := e.HasPolicy(sub, obj, act) 86 | if err != nil { 87 | log.Fatalf("Error checking if policy exists (%s, %s, %s): %v", sub, obj, act, err) 88 | } 89 | if !has { 90 | added, err := e.AddPolicy(sub, obj, act) 91 | if err != nil { 92 | log.Fatalf("Failed to add policy (%s, %s, %s): %v", sub, obj, act, err) 93 | } 94 | if added { 95 | log.Printf("Successfully added default policy: %s, %s, %s", sub, obj, act) 96 | } else { 97 | log.Printf("Policy already exists, not added: %s, %s, %s", sub, obj, act) 98 | } 99 | } else { 100 | log.Printf("Policy already exists, skipping addition: %s, %s, %s", sub, obj, act) 101 | } 102 | } 103 | 104 | // InitCasbin initializes RBAC permission control 105 | func InitCasbin(db *gorm.DB) (*casbin.Enforcer, error) { 106 | if db == nil { 107 | return nil, fmt.Errorf("database connection (gorm.DB) is nil, cannot initialize Casbin Adapter") 108 | } 109 | 110 | log.Println("Initializing Casbin Adapter...") 111 | adapter, err := gormadapter.NewAdapterByDB(db) 112 | if err != nil { 113 | return nil, fmt.Errorf("failed to create Casbin GORM Adapter: %w", err) 114 | } 115 | 116 | log.Println("Initializing Casbin Enforcer...") 117 | // Ensure model.conf path is correct 118 | e, err := casbin.NewEnforcer("./pkg/auth/model.conf", adapter) 119 | if err != nil { 120 | return nil, fmt.Errorf("failed to create Casbin Enforcer: %w", err) 121 | } 122 | 123 | // Enable logging (optional, but useful for debugging) 124 | e.EnableLog(true) 125 | 126 | // Auto-save policy changes to database 127 | e.EnableAutoSave(true) 128 | 129 | log.Println("Loading policies from database...") 130 | if err = e.LoadPolicy(); err != nil { 131 | log.Printf("Failed to load policies (may be first run, no policies): %v", err) 132 | // Should not Fatal here, as having no policies on first run is normal 133 | } 134 | 135 | log.Println("Adding or verifying default policies...") 136 | // Add default permissions (check if exists) 137 | addPolicyIfNotExists(e, "super_admin", "/api/v1/*", "*") // Admin has all permissions for all v1 interfaces 138 | addPolicyIfNotExists(e, "normal_user", "/api/v1/*", "GET") // Normal users only have GET permissions 139 | 140 | // You may also need to add user to role mappings (g rules) 141 | // For example: e.AddGroupingPolicy("admin", "super_admin") 142 | // This is usually handled during user creation or role assignment, but defaults can be added. 143 | 144 | // Save all possible new policies (if AutoSave is not reliable enough or batch addition is needed) 145 | // if err := e.SavePolicy(); err != nil { 146 | // log.Fatalf("Failed to save policies: %v", err) 147 | // } 148 | 149 | log.Printf("RBAC permission control initialization completed!") 150 | return e, nil 151 | } 152 | -------------------------------------------------------------------------------- /internal/handlers/pod_exec_handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/ciliverse/cilikube/internal/service" 12 | "github.com/ciliverse/cilikube/pkg/k8s" 13 | "github.com/gin-gonic/gin" 14 | "github.com/gorilla/websocket" 15 | ) 16 | 17 | // PodExecHandler handles pod execution requests 18 | type PodExecHandler struct { 19 | service *service.PodExecService 20 | clusterManager *k8s.ClusterManager 21 | upgrader websocket.Upgrader 22 | } 23 | 24 | // NewPodExecHandler creates a new PodExecHandler 25 | func NewPodExecHandler(svc *service.PodExecService, cm *k8s.ClusterManager) *PodExecHandler { 26 | return &PodExecHandler{ 27 | service: svc, 28 | clusterManager: cm, 29 | upgrader: websocket.Upgrader{ 30 | ReadBufferSize: 1024, 31 | WriteBufferSize: 1024, 32 | CheckOrigin: func(r *http.Request) bool { 33 | return true 34 | }, 35 | }, 36 | } 37 | } 38 | 39 | // ExecPod handles WebSocket requests for pod execution 40 | func (h *PodExecHandler) ExecPod(c *gin.Context) { 41 | ws, err := h.upgrader.Upgrade(c.Writer, c.Request, nil) 42 | if err != nil { 43 | log.Printf("Failed to upgrade to websocket: %v", err) 44 | return 45 | } 46 | defer ws.Close() 47 | 48 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 49 | if !ok { 50 | ws.WriteMessage(websocket.TextMessage, []byte("Failed to get Kubernetes client")) 51 | return 52 | } 53 | 54 | namespace := c.Param("namespace") 55 | podName := c.Param("name") 56 | container := c.Query("container") 57 | command := c.QueryArray("command") 58 | 59 | shell := c.Query("shell") 60 | if len(command) == 0 { 61 | if shell != "" { 62 | command = []string{shell} 63 | } else { 64 | command = []string{"/bin/sh"} 65 | } 66 | } 67 | 68 | wsStreamHandler := &WebSocketStreamHandler{ 69 | conn: ws, 70 | stdinChan: make(chan []byte, 100), 71 | stdoutChan: make(chan []byte, 100), 72 | closeChan: make(chan struct{}), 73 | stdinClosed: false, 74 | } 75 | defer wsStreamHandler.Close() 76 | 77 | go wsStreamHandler.readMessages() 78 | go wsStreamHandler.writeMessages() 79 | 80 | options := &service.ExecOptions{ 81 | Command: command, 82 | Container: container, 83 | Stdin: true, 84 | Stdout: true, 85 | Stderr: true, 86 | TTY: true, 87 | } 88 | 89 | err = h.service.Exec(k8sClient.Clientset, namespace, podName, options, wsStreamHandler, wsStreamHandler) 90 | if err != nil { 91 | errmsg := []byte(fmt.Sprintf("\r\n--- Command Execution Failed ---\r\nError: %v\r\n", err)) 92 | wsStreamHandler.WriteMessage(websocket.TextMessage, errmsg) 93 | log.Printf("Exec error: %v", err) 94 | return 95 | } 96 | 97 | log.Println("Exec finished without error.") 98 | } 99 | 100 | // WebSocketStreamHandler implements io.Reader and io.Writer for WebSocket data 101 | type WebSocketStreamHandler struct { 102 | conn *websocket.Conn 103 | stdinChan chan []byte 104 | stdoutChan chan []byte 105 | closeChan chan struct{} 106 | mu sync.Mutex 107 | stdinClosed bool 108 | buffer []byte 109 | } 110 | 111 | // readMessages reads messages from WebSocket and sends to stdinChan 112 | func (h *WebSocketStreamHandler) readMessages() { 113 | defer h.closeStdin() 114 | for { 115 | _, message, err := h.conn.ReadMessage() 116 | if err != nil { 117 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 118 | log.Printf("WebSocket read error: %v", err) 119 | } 120 | return 121 | } 122 | if message != nil { 123 | h.stdinChan <- message 124 | } 125 | } 126 | } 127 | 128 | // closeStdin closes stdinChan 129 | func (h *WebSocketStreamHandler) closeStdin() { 130 | h.mu.Lock() 131 | defer h.mu.Unlock() 132 | if !h.stdinClosed { 133 | close(h.stdinChan) 134 | h.stdinClosed = true 135 | } 136 | } 137 | 138 | // writeMessages reads from stdoutChan and writes to WebSocket 139 | func (h *WebSocketStreamHandler) writeMessages() { 140 | for { 141 | select { 142 | case data, ok := <-h.stdoutChan: 143 | if !ok { 144 | return 145 | } 146 | if err := h.WriteMessage(websocket.BinaryMessage, data); err != nil { 147 | log.Printf("WebSocket write error: %v", err) 148 | return 149 | } 150 | case <-h.closeChan: 151 | return 152 | } 153 | } 154 | } 155 | 156 | // Read reads data from stdinChan for container execution 157 | func (h *WebSocketStreamHandler) Read(p []byte) (n int, err error) { 158 | if len(h.buffer) > 0 { 159 | n = copy(p, h.buffer) 160 | h.buffer = h.buffer[n:] 161 | return n, nil 162 | } 163 | data, ok := <-h.stdinChan 164 | if !ok { 165 | return 0, io.EOF 166 | } 167 | n = copy(p, data) 168 | h.buffer = append(h.buffer, data[n:]...) 169 | return n, nil 170 | } 171 | 172 | // Write writes container output to stdoutChan 173 | func (h *WebSocketStreamHandler) Write(p []byte) (n int, err error) { 174 | h.stdoutChan <- p 175 | return len(p), nil 176 | } 177 | 178 | // WriteMessage sends a WebSocket message 179 | func (h *WebSocketStreamHandler) WriteMessage(messageType int, data []byte) error { 180 | return h.conn.WriteMessage(messageType, data) 181 | } 182 | 183 | // Close closes the WebSocket connection 184 | func (h *WebSocketStreamHandler) Close() error { 185 | close(h.closeChan) 186 | return h.conn.Close() 187 | } 188 | 189 | // buildCommand builds the command array 190 | func buildCommand(commandStr, argsStr string) []string { 191 | command := []string{commandStr} 192 | if argsStr != "" { 193 | args := strings.Split(argsStr, " ") 194 | command = append(command, args...) 195 | } 196 | return command 197 | } -------------------------------------------------------------------------------- /internal/service/base_resource_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/watch" 10 | "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | // ResourceClient resource client interface 14 | // For consistency, all methods accept namespace parameter. For non-namespaced resources, implementations can ignore this parameter. 15 | type ResourceClient[T runtime.Object] interface { 16 | Get(ctx context.Context, clientset kubernetes.Interface, namespace, name string, opts metav1.GetOptions) (T, error) 17 | List(ctx context.Context, clientset kubernetes.Interface, namespace string, opts metav1.ListOptions) (runtime.Object, error) 18 | Create(ctx context.Context, clientset kubernetes.Interface, namespace string, obj T, opts metav1.CreateOptions) (T, error) 19 | Update(ctx context.Context, clientset kubernetes.Interface, namespace string, obj T, opts metav1.UpdateOptions) (T, error) 20 | Delete(ctx context.Context, clientset kubernetes.Interface, namespace, name string, opts metav1.DeleteOptions) error 21 | Watch(ctx context.Context, clientset kubernetes.Interface, namespace string, opts metav1.ListOptions) (watch.Interface, error) 22 | } 23 | 24 | // ResourceService resource service interface 25 | type ResourceService[T runtime.Object] interface { 26 | List(clientset kubernetes.Interface, namespace, selector string, limit int64, continueToken string) (runtime.Object, error) 27 | Get(clientset kubernetes.Interface, namespace, name string) (T, error) 28 | Create(clientset kubernetes.Interface, namespace string, obj T) (T, error) 29 | Update(clientset kubernetes.Interface, namespace, name string, obj T) (T, error) 30 | Patch(clientset kubernetes.Interface, namespace, name string, current T, patchData map[string]interface{}) (T, error) 31 | Delete(clientset kubernetes.Interface, namespace, name string) error 32 | Watch(clientset kubernetes.Interface, namespace, selector string, resourceVersion string, timeoutSeconds int64) (watch.Interface, error) 33 | } 34 | 35 | // BaseResourceService basic resource service implementation 36 | type BaseResourceService[T runtime.Object] struct { 37 | client ResourceClient[T] 38 | } 39 | 40 | // NewBaseResourceService creates basic resource service 41 | func NewBaseResourceService[T runtime.Object](client ResourceClient[T]) *BaseResourceService[T] { 42 | return &BaseResourceService[T]{ 43 | client: client, 44 | } 45 | } 46 | 47 | // Get retrieves a single resource 48 | func (s *BaseResourceService[T]) Get(clientset kubernetes.Interface, namespace, name string) (T, error) { 49 | ctx := context.Background() 50 | return s.client.Get(ctx, clientset, namespace, name, metav1.GetOptions{}) 51 | } 52 | 53 | // List retrieves resource list 54 | func (s *BaseResourceService[T]) List(clientset kubernetes.Interface, namespace, selector string, limit int64, continueToken string) (runtime.Object, error) { 55 | ctx := context.Background() 56 | opts := metav1.ListOptions{ 57 | LabelSelector: selector, 58 | Limit: limit, 59 | Continue: continueToken, 60 | } 61 | return s.client.List(ctx, clientset, namespace, opts) 62 | } 63 | 64 | // Create creates resource 65 | func (s *BaseResourceService[T]) Create(clientset kubernetes.Interface, namespace string, obj T) (T, error) { 66 | ctx := context.Background() 67 | return s.client.Create(ctx, clientset, namespace, obj, metav1.CreateOptions{}) 68 | } 69 | 70 | // Update updates resource 71 | func (s *BaseResourceService[T]) Update(clientset kubernetes.Interface, namespace, name string, obj T) (T, error) { 72 | ctx := context.Background() 73 | return s.client.Update(ctx, clientset, namespace, obj, metav1.UpdateOptions{}) 74 | } 75 | 76 | // Patch patches resource (for partial updates like scaling) 77 | func (s *BaseResourceService[T]) Patch(clientset kubernetes.Interface, namespace, name string, current T, patchData map[string]interface{}) (T, error) { 78 | // For now, we'll implement a simple patch by modifying the current object 79 | // In a production environment, you might want to use strategic merge patch or JSON patch 80 | 81 | // This is a simplified implementation - we'll update the current object and then call Update 82 | // For deployment scaling, we expect patchData to contain spec.replicas 83 | if spec, ok := patchData["spec"].(map[string]interface{}); ok { 84 | if _, exists := spec["replicas"]; exists { 85 | // This is a hack for deployment scaling - in a real implementation, 86 | // you'd use proper reflection or type assertions based on the resource type 87 | // For now, we'll just call Update with the modified object 88 | } 89 | } 90 | 91 | // For simplicity, we'll just call Update - this should be improved for production use 92 | return s.Update(clientset, namespace, name, current) 93 | } 94 | 95 | // Delete deletes resource 96 | func (s *BaseResourceService[T]) Delete(clientset kubernetes.Interface, namespace, name string) error { 97 | ctx := context.Background() 98 | return s.client.Delete(ctx, clientset, namespace, name, metav1.DeleteOptions{}) 99 | } 100 | 101 | // Watch watches resource changes 102 | func (s *BaseResourceService[T]) Watch(clientset kubernetes.Interface, namespace, selector string, resourceVersion string, timeoutSeconds int64) (watch.Interface, error) { 103 | ctx := context.Background() 104 | if timeoutSeconds > 0 { 105 | var cancel context.CancelFunc 106 | ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second) 107 | defer cancel() 108 | } 109 | opts := metav1.ListOptions{ 110 | LabelSelector: selector, 111 | ResourceVersion: resourceVersion, 112 | Watch: true, 113 | } 114 | return s.client.Watch(ctx, clientset, namespace, opts) 115 | } 116 | -------------------------------------------------------------------------------- /internal/service/event_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "time" 8 | 9 | "github.com/ciliverse/cilikube/internal/models" 10 | "github.com/ciliverse/cilikube/pkg/k8s" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | // EventService provides business logic for cluster events 16 | type EventService struct { 17 | k8sManager *k8s.ClusterManager 18 | } 19 | 20 | // NewEventService creates a new EventService instance 21 | func NewEventService(k8sManager *k8s.ClusterManager) *EventService { 22 | return &EventService{ 23 | k8sManager: k8sManager, 24 | } 25 | } 26 | 27 | // ListEvents retrieves cluster events based on the provided filters 28 | func (s *EventService) ListEvents(req models.EventListRequest) (*models.EventListResponse, error) { 29 | client, err := s.k8sManager.GetActiveClient() 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to get active cluster client: %w", err) 32 | } 33 | 34 | // Set default limit if not specified 35 | if req.Limit <= 0 { 36 | req.Limit = 50 37 | } 38 | if req.Limit > 200 { 39 | req.Limit = 200 // Maximum limit to prevent performance issues 40 | } 41 | 42 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 43 | defer cancel() 44 | 45 | var events []corev1.Event 46 | 47 | if req.Namespace != "" { 48 | // Get events from specific namespace 49 | eventList, err := client.Clientset.CoreV1().Events(req.Namespace).List(ctx, metav1.ListOptions{}) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to list events in namespace %s: %w", req.Namespace, err) 52 | } 53 | events = eventList.Items 54 | } else { 55 | // Get events from all namespaces 56 | eventList, err := client.Clientset.CoreV1().Events("").List(ctx, metav1.ListOptions{}) 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to list events from all namespaces: %w", err) 59 | } 60 | events = eventList.Items 61 | } 62 | 63 | // Filter events based on request parameters 64 | filteredEvents := s.filterEvents(events, req) 65 | 66 | // Sort events by last timestamp (newest first) 67 | sort.Slice(filteredEvents, func(i, j int) bool { 68 | return filteredEvents[i].LastTimestamp.Time.After(filteredEvents[j].LastTimestamp.Time) 69 | }) 70 | 71 | // Apply limit 72 | total := len(filteredEvents) 73 | if len(filteredEvents) > req.Limit { 74 | filteredEvents = filteredEvents[:req.Limit] 75 | } 76 | 77 | // Convert to response format 78 | clusterEvents := make([]models.ClusterEvent, len(filteredEvents)) 79 | for i, event := range filteredEvents { 80 | clusterEvents[i] = models.ConvertK8sEventToClusterEvent(&event) 81 | } 82 | 83 | return &models.EventListResponse{ 84 | Events: clusterEvents, 85 | Total: total, 86 | }, nil 87 | } 88 | 89 | // GetRecentEvents retrieves the most recent cluster events (for dashboard) 90 | func (s *EventService) GetRecentEvents(limit int) ([]models.ClusterEvent, error) { 91 | if limit <= 0 { 92 | limit = 10 93 | } 94 | 95 | req := models.EventListRequest{ 96 | Limit: limit, 97 | } 98 | 99 | response, err := s.ListEvents(req) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return response.Events, nil 105 | } 106 | 107 | // filterEvents applies filters to the event list 108 | func (s *EventService) filterEvents(events []corev1.Event, req models.EventListRequest) []corev1.Event { 109 | var filtered []corev1.Event 110 | 111 | for _, event := range events { 112 | // Filter by event type 113 | if req.Type != "" { 114 | eventType := "Normal" 115 | if event.Type == corev1.EventTypeWarning { 116 | eventType = "Warning" 117 | } 118 | if eventType != req.Type { 119 | continue 120 | } 121 | } 122 | 123 | // Filter by time (since parameter) 124 | if req.Since != "" { 125 | sinceTime, err := time.Parse(time.RFC3339, req.Since) 126 | if err == nil && event.LastTimestamp.Time.Before(sinceTime) { 127 | continue 128 | } 129 | } 130 | 131 | filtered = append(filtered, event) 132 | } 133 | 134 | return filtered 135 | } 136 | 137 | // GetEventsByObject retrieves events related to a specific Kubernetes object 138 | func (s *EventService) GetEventsByObject(namespace, kind, name string) ([]models.ClusterEvent, error) { 139 | client, err := s.k8sManager.GetActiveClient() 140 | if err != nil { 141 | return nil, fmt.Errorf("failed to get active cluster client: %w", err) 142 | } 143 | 144 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 145 | defer cancel() 146 | 147 | // Get events from the specific namespace or all namespaces 148 | searchNamespace := namespace 149 | if searchNamespace == "" { 150 | searchNamespace = metav1.NamespaceAll 151 | } 152 | 153 | eventList, err := client.Clientset.CoreV1().Events(searchNamespace).List(ctx, metav1.ListOptions{}) 154 | if err != nil { 155 | return nil, fmt.Errorf("failed to list events: %w", err) 156 | } 157 | 158 | var relatedEvents []corev1.Event 159 | for _, event := range eventList.Items { 160 | if event.InvolvedObject.Kind == kind && event.InvolvedObject.Name == name { 161 | if namespace == "" || event.InvolvedObject.Namespace == namespace { 162 | relatedEvents = append(relatedEvents, event) 163 | } 164 | } 165 | } 166 | 167 | // Sort by last timestamp (newest first) 168 | sort.Slice(relatedEvents, func(i, j int) bool { 169 | return relatedEvents[i].LastTimestamp.Time.After(relatedEvents[j].LastTimestamp.Time) 170 | }) 171 | 172 | // Convert to response format 173 | clusterEvents := make([]models.ClusterEvent, len(relatedEvents)) 174 | for i, event := range relatedEvents { 175 | clusterEvents[i] = models.ConvertK8sEventToClusterEvent(&event) 176 | } 177 | 178 | return clusterEvents, nil 179 | } 180 | -------------------------------------------------------------------------------- /internal/handlers/resource_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/ciliverse/cilikube/internal/service" 8 | "github.com/ciliverse/cilikube/pkg/k8s" 9 | "github.com/ciliverse/cilikube/pkg/utils" 10 | "github.com/gin-gonic/gin" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | ) 13 | 14 | // ResourceHandler generic handler 15 | type ResourceHandler[T runtime.Object] struct { 16 | service service.ResourceService[T] 17 | clusterManager *k8s.ClusterManager 18 | resourceType string 19 | } 20 | 21 | // NewResourceHandler creates generic handler 22 | func NewResourceHandler[T runtime.Object](svc service.ResourceService[T], k8sManager *k8s.ClusterManager, resourceType string) *ResourceHandler[T] { 23 | return &ResourceHandler[T]{ 24 | service: svc, 25 | clusterManager: k8sManager, 26 | resourceType: resourceType, 27 | } 28 | } 29 | 30 | // List handles list requests 31 | func (h *ResourceHandler[T]) List(c *gin.Context) { 32 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 33 | if !ok { 34 | return // Error already handled in GetClientFromQuery 35 | } 36 | 37 | // For namespaced resources, get from path; for cluster resources, this parameter is empty 38 | namespace := c.Param("namespace") 39 | selector := c.Query("labelSelector") 40 | limit, _ := strconv.ParseInt(c.DefaultQuery("limit", "0"), 10, 64) 41 | continueToken := c.Query("continue") 42 | 43 | items, err := h.service.List(k8sClient.Clientset, namespace, selector, limit, continueToken) 44 | if err != nil { 45 | utils.ApiError(c, http.StatusInternalServerError, "failed to get resource list", err.Error()) 46 | return 47 | } 48 | 49 | utils.ApiSuccess(c, items, "successfully retrieved resource list") 50 | } 51 | 52 | // Get handles single resource retrieval requests 53 | func (h *ResourceHandler[T]) Get(c *gin.Context) { 54 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 55 | if !ok { 56 | return 57 | } 58 | namespace := c.Param("namespace") 59 | name := c.Param("name") 60 | 61 | item, err := h.service.Get(k8sClient.Clientset, namespace, name) 62 | if err != nil { 63 | utils.ApiError(c, http.StatusInternalServerError, "failed to get resource", err.Error()) 64 | return 65 | } 66 | utils.ApiSuccess(c, item, "successfully retrieved resource") 67 | } 68 | 69 | // Create handles resource creation requests 70 | func (h *ResourceHandler[T]) Create(c *gin.Context) { 71 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 72 | if !ok { 73 | return 74 | } 75 | namespace := c.Param("namespace") 76 | 77 | var obj T 78 | // Kubernetes Create API requires a complete object, so we bind from request body 79 | if err := c.ShouldBindJSON(&obj); err != nil { 80 | utils.ApiError(c, http.StatusBadRequest, "invalid request body format", err.Error()) 81 | return 82 | } 83 | 84 | created, err := h.service.Create(k8sClient.Clientset, namespace, obj) 85 | if err != nil { 86 | utils.ApiError(c, http.StatusInternalServerError, "failed to create resource", err.Error()) 87 | return 88 | } 89 | utils.ApiSuccess(c, created, "resource created successfully") 90 | } 91 | 92 | // Update handles resource update requests 93 | func (h *ResourceHandler[T]) Update(c *gin.Context) { 94 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 95 | if !ok { 96 | return 97 | } 98 | namespace := c.Param("namespace") 99 | name := c.Param("name") 100 | 101 | var obj T 102 | if err := c.ShouldBindJSON(&obj); err != nil { 103 | utils.ApiError(c, http.StatusBadRequest, "invalid request body format", err.Error()) 104 | return 105 | } 106 | 107 | updated, err := h.service.Update(k8sClient.Clientset, namespace, name, obj) 108 | if err != nil { 109 | utils.ApiError(c, http.StatusInternalServerError, "failed to update resource", err.Error()) 110 | return 111 | } 112 | utils.ApiSuccess(c, updated, "resource updated successfully") 113 | } 114 | 115 | // Patch handles resource patch requests (for partial updates like scaling) 116 | func (h *ResourceHandler[T]) Patch(c *gin.Context) { 117 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 118 | if !ok { 119 | return 120 | } 121 | namespace := c.Param("namespace") 122 | name := c.Param("name") 123 | 124 | // For PATCH requests, we expect a partial update object 125 | var patchData map[string]interface{} 126 | if err := c.ShouldBindJSON(&patchData); err != nil { 127 | utils.ApiError(c, http.StatusBadRequest, "invalid patch data format", err.Error()) 128 | return 129 | } 130 | 131 | // Get the current resource first 132 | current, err := h.service.Get(k8sClient.Clientset, namespace, name) 133 | if err != nil { 134 | utils.ApiError(c, http.StatusInternalServerError, "failed to get current resource", err.Error()) 135 | return 136 | } 137 | 138 | // Apply patch to the current resource 139 | // This is a simplified patch implementation - in production you might want to use strategic merge patch 140 | updated, err := h.service.Patch(k8sClient.Clientset, namespace, name, current, patchData) 141 | if err != nil { 142 | utils.ApiError(c, http.StatusInternalServerError, "failed to patch resource", err.Error()) 143 | return 144 | } 145 | utils.ApiSuccess(c, updated, "resource patched successfully") 146 | } 147 | 148 | // Delete handles resource deletion requests 149 | func (h *ResourceHandler[T]) Delete(c *gin.Context) { 150 | k8sClient, ok := k8s.GetClientFromQuery(c, h.clusterManager) 151 | if !ok { 152 | return 153 | } 154 | namespace := c.Param("namespace") 155 | name := c.Param("name") 156 | 157 | err := h.service.Delete(k8sClient.Clientset, namespace, name) 158 | if err != nil { 159 | utils.ApiError(c, http.StatusInternalServerError, "failed to delete resource", err.Error()) 160 | return 161 | } 162 | utils.ApiSuccess(c, nil, "resource deleted successfully") 163 | } 164 | 165 | // Watch handles resource watch requests 166 | func (h *ResourceHandler[T]) Watch(c *gin.Context) { 167 | utils.ApiError(c, http.StatusNotImplemented, "Watch not yet implemented", "") 168 | } 169 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ciliverse/cilikube 2 | 3 | go 1.25.3 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.105.0 7 | github.com/casbin/gorm-adapter/v3 v3.32.0 8 | github.com/fatih/color v1.18.0 9 | github.com/golang-jwt/jwt/v5 v5.2.2 10 | github.com/google/uuid v1.6.0 11 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 12 | github.com/prometheus/client_golang v1.22.0 13 | github.com/spf13/viper v1.20.1 14 | github.com/stretchr/testify v1.10.0 15 | go.uber.org/zap v1.27.0 16 | golang.org/x/crypto v0.39.0 17 | golang.org/x/mod v0.25.0 18 | gopkg.in/yaml.v3 v3.0.1 19 | gorm.io/gorm v1.30.0 20 | k8s.io/api v0.34.2 21 | k8s.io/apiextensions-apiserver v0.34.2 22 | k8s.io/client-go v0.34.2 23 | k8s.io/metrics v0.33.2 24 | ) 25 | 26 | require ( 27 | filippo.io/edwards25519 v1.1.0 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect 30 | github.com/bytedance/sonic v1.13.3 // indirect 31 | github.com/bytedance/sonic/loader v0.2.4 // indirect 32 | github.com/casbin/govaluate v1.4.0 // indirect 33 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 34 | github.com/cloudwego/base64x v0.1.5 // indirect 35 | github.com/dustin/go-humanize v1.0.1 // indirect 36 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 37 | github.com/fsnotify/fsnotify v1.9.0 // indirect 38 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 39 | github.com/gin-contrib/sse v1.1.0 // indirect 40 | github.com/glebarez/go-sqlite v1.22.0 // indirect 41 | github.com/glebarez/sqlite v1.11.0 // indirect 42 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 43 | github.com/go-openapi/jsonreference v0.21.0 // indirect 44 | github.com/go-openapi/swag v0.23.1 // indirect 45 | github.com/go-playground/locales v0.14.1 // indirect 46 | github.com/go-playground/universal-translator v0.18.1 // indirect 47 | github.com/go-playground/validator/v10 v10.26.0 // indirect 48 | github.com/go-sql-driver/mysql v1.9.2 // indirect 49 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 50 | github.com/goccy/go-json v0.10.5 // indirect 51 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 52 | github.com/golang-sql/sqlexp v0.1.0 // indirect 53 | github.com/google/gnostic-models v0.7.0 // indirect 54 | github.com/jackc/pgpassfile v1.0.0 // indirect 55 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 56 | github.com/jackc/pgx/v5 v5.7.4 // indirect 57 | github.com/jackc/puddle/v2 v2.2.2 // indirect 58 | github.com/jinzhu/inflection v1.0.0 // indirect 59 | github.com/jinzhu/now v1.1.5 // indirect 60 | github.com/josharian/intern v1.0.0 // indirect 61 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 62 | github.com/leodido/go-urn v1.4.0 // indirect 63 | github.com/mailru/easyjson v0.9.0 // indirect 64 | github.com/mattn/go-colorable v0.1.13 // indirect 65 | github.com/mattn/go-isatty v0.0.20 // indirect 66 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 67 | github.com/microsoft/go-mssqldb v1.8.1 // indirect 68 | github.com/moby/spdystream v0.5.0 // indirect 69 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 70 | github.com/ncruces/go-strftime v0.1.9 // indirect 71 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 72 | github.com/pkg/errors v0.9.1 // indirect 73 | github.com/pmezard/go-difflib v1.0.0 // indirect 74 | github.com/prometheus/client_model v0.6.2 // indirect 75 | github.com/prometheus/common v0.63.0 // indirect 76 | github.com/prometheus/procfs v0.16.1 // indirect 77 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 78 | github.com/sagikazarmark/locafero v0.7.0 // indirect 79 | github.com/sourcegraph/conc v0.3.0 // indirect 80 | github.com/spf13/afero v1.12.0 // indirect 81 | github.com/spf13/cast v1.7.1 // indirect 82 | github.com/spf13/pflag v1.0.6 // indirect 83 | github.com/subosito/gotenv v1.6.0 // indirect 84 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 85 | github.com/ugorji/go/codec v1.3.0 // indirect 86 | go.uber.org/multierr v1.11.0 // indirect 87 | go.yaml.in/yaml/v2 v2.4.2 // indirect 88 | go.yaml.in/yaml/v3 v3.0.4 // indirect 89 | golang.org/x/arch v0.18.0 // indirect 90 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 91 | golang.org/x/sync v0.15.0 // indirect 92 | google.golang.org/protobuf v1.36.6 // indirect 93 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 94 | gorm.io/driver/mysql v1.5.7 // indirect 95 | gorm.io/driver/postgres v1.5.11 // indirect 96 | gorm.io/driver/sqlite v1.6.0 // indirect 97 | gorm.io/driver/sqlserver v1.5.4 // indirect 98 | gorm.io/plugin/dbresolver v1.6.0 // indirect 99 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect 100 | modernc.org/libc v1.65.6 // indirect 101 | modernc.org/mathutil v1.7.1 // indirect 102 | modernc.org/memory v1.10.0 // indirect 103 | modernc.org/sqlite v1.37.0 // indirect 104 | sigs.k8s.io/randfill v1.0.0 // indirect 105 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 106 | ) 107 | 108 | require ( 109 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 110 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 111 | github.com/gin-gonic/gin v1.10.1 112 | github.com/go-logr/logr v1.4.2 // indirect 113 | github.com/gogo/protobuf v1.3.2 // indirect 114 | github.com/json-iterator/go v1.1.12 // indirect 115 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 116 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 117 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 118 | github.com/x448/float16 v0.8.4 // indirect 119 | golang.org/x/net v0.41.0 // indirect 120 | golang.org/x/oauth2 v0.29.0 // indirect 121 | golang.org/x/sys v0.33.0 // indirect 122 | golang.org/x/term v0.32.0 // indirect 123 | golang.org/x/text v0.26.0 124 | golang.org/x/time v0.11.0 // indirect 125 | gopkg.in/inf.v0 v0.9.1 // indirect 126 | k8s.io/apimachinery v0.34.2 127 | k8s.io/klog/v2 v2.130.1 // indirect 128 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 129 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 130 | sigs.k8s.io/yaml v1.6.0 // indirect 131 | ) 132 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/casbin/casbin/v2" 16 | "github.com/gin-gonic/gin" 17 | 18 | "github.com/ciliverse/cilikube/configs" 19 | "github.com/ciliverse/cilikube/internal/initialization" 20 | "github.com/ciliverse/cilikube/internal/logger" 21 | "github.com/ciliverse/cilikube/internal/service" 22 | "github.com/ciliverse/cilikube/internal/store" 23 | "github.com/ciliverse/cilikube/pkg/auth" 24 | "github.com/ciliverse/cilikube/pkg/database" 25 | "github.com/ciliverse/cilikube/pkg/k8s" 26 | ) 27 | 28 | type Application struct { 29 | Config *configs.Config 30 | Logger *slog.Logger 31 | Router *gin.Engine 32 | Server *http.Server 33 | } 34 | 35 | func New(configPath string) (*Application, error) { 36 | // --- 1. Load configuration --- 37 | cfg, err := configs.Load(configPath) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to load configuration: %w", err) 40 | } 41 | 42 | // --- 2. Initialize logger --- 43 | var logLevel slog.Level 44 | if cfg.Server.Mode == "debug" { 45 | logLevel = slog.LevelDebug 46 | } else { 47 | logLevel = slog.LevelInfo 48 | } 49 | 50 | appLogger := logger.New(logLevel) 51 | slog.SetDefault(appLogger) 52 | 53 | // --- 3. Configuration loaded --- 54 | slog.Info("configuration loaded successfully", "path", configPath) 55 | 56 | // --- 4. Database and Store initialization --- 57 | slog.Info("initializing storage system...") 58 | 59 | // Initialize database if enabled 60 | if cfg.Database.Enabled { 61 | slog.Info("database enabled, initializing...") 62 | if err := database.InitDatabase(); err != nil { 63 | return nil, fmt.Errorf("failed to connect to database: %w", err) 64 | } 65 | if err := database.AutoMigrate(); err != nil { 66 | return nil, fmt.Errorf("failed to auto-migrate database: %w", err) 67 | } 68 | // Default admin user will be created by store.Initialize() using RBAC system 69 | slog.Info("database initialized successfully") 70 | } 71 | 72 | // Create unified store 73 | mainStore, err := store.NewStore(cfg) 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to initialize store: %w", err) 76 | } 77 | 78 | // Initialize store (create default data) 79 | if err := mainStore.Initialize(); err != nil { 80 | return nil, fmt.Errorf("failed to initialize store data: %w", err) 81 | } 82 | 83 | slog.Info("storage system initialized successfully", "type", cfg.GetStorageType()) 84 | 85 | // --- 5. Initialize ClusterManager --- 86 | k8sManager, err := k8s.NewClusterManager(mainStore, cfg) 87 | if err != nil { 88 | return nil, fmt.Errorf("failed to initialize Kubernetes cluster manager: %w", err) 89 | } 90 | slog.Info("Kubernetes cluster manager initialized successfully") 91 | 92 | // --- 6. Initialize application services --- 93 | services := initialization.InitializeServices(k8sManager, mainStore, cfg) 94 | 95 | // Initialize default roles 96 | if err := services.RoleService.InitializeDefaultRoles(); err != nil { 97 | return nil, fmt.Errorf("failed to initialize default roles: %w", err) 98 | } 99 | slog.Info("default roles initialized successfully") 100 | 101 | // --- 7. Casbin initialization --- 102 | var e *casbin.Enforcer 103 | if cfg.Database.Enabled { 104 | var casbinErr error 105 | e, casbinErr = auth.InitCasbin(database.DB) 106 | if casbinErr != nil { 107 | return nil, fmt.Errorf("failed to initialize Casbin: %w", casbinErr) 108 | } 109 | slog.Info("Casbin initialized successfully") 110 | } 111 | 112 | // Initialize permission service 113 | services.PermissionService = service.NewPermissionService(mainStore, e) 114 | 115 | // Set permission service reference in role service for synchronization 116 | services.RoleService.SetPermissionService(services.PermissionService) 117 | 118 | // Initialize default policies 119 | if err := services.PermissionService.InitializeDefaultPolicies(); err != nil { 120 | return nil, fmt.Errorf("failed to initialize default policies: %w", err) 121 | } 122 | slog.Info("default policies initialized successfully") 123 | 124 | // --- 8. Gin router setup --- 125 | router := initialization.SetupRouter(cfg, services, k8sManager, e) 126 | slog.Info("Gin router setup completed") 127 | 128 | return &Application{ 129 | Config: cfg, 130 | Logger: appLogger, 131 | Router: router, 132 | }, nil 133 | } 134 | 135 | func (app *Application) Run() { 136 | serverAddr := ":" + app.Config.Server.Port 137 | initialization.DisplayServerInfo(serverAddr, app.Config.Server.Mode) 138 | app.Server = &http.Server{ 139 | Addr: serverAddr, 140 | Handler: app.Router, 141 | ReadTimeout: time.Duration(app.Config.Server.ReadTimeout) * time.Second, 142 | WriteTimeout: time.Duration(app.Config.Server.WriteTimeout) * time.Second, 143 | } 144 | go func() { 145 | app.Logger.Info("server is listening...", "address", app.Server.Addr) 146 | if err := app.Server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 147 | app.Logger.Error("server closed unexpectedly", "error", err) 148 | os.Exit(1) 149 | } 150 | }() 151 | quit := make(chan os.Signal, 1) 152 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 153 | <-quit 154 | app.Logger.Info("received shutdown signal, shutting down server...") 155 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 156 | defer cancel() 157 | if app.Config.Database.Enabled { 158 | database.CloseDatabase() 159 | app.Logger.Info("database connection closed") 160 | } 161 | if err := app.Server.Shutdown(ctx); err != nil { 162 | app.Logger.Error("failed to shutdown server", "error", err) 163 | os.Exit(1) 164 | } 165 | app.Logger.Info("server shutdown gracefully") 166 | } 167 | 168 | // GetConfigPath returns the configuration file path based on command line flags, 169 | // environment variables, or default value 170 | func GetConfigPath() string { 171 | config := flag.String("config", "", "config file path") 172 | flag.Parse() 173 | 174 | if *config != "" { 175 | return *config 176 | } 177 | 178 | if env := os.Getenv("CILIKUBE_CONFIG_PATH"); env != "" { 179 | return env 180 | } 181 | 182 | return "configs/config.yaml" 183 | } 184 | -------------------------------------------------------------------------------- /internal/service/cluster_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "k8s.io/client-go/kubernetes" 8 | "k8s.io/client-go/rest" 9 | "k8s.io/client-go/tools/clientcmd" 10 | 11 | "github.com/ciliverse/cilikube/internal/models" 12 | "github.com/ciliverse/cilikube/internal/store" 13 | "github.com/ciliverse/cilikube/pkg/k8s" 14 | ) 15 | 16 | // ClusterService provides business logic around cluster management. 17 | type ClusterService struct { 18 | k8sManager *k8s.ClusterManager 19 | } 20 | 21 | // NewClusterService creates a new ClusterService instance. 22 | func NewClusterService(k8sManager *k8s.ClusterManager) *ClusterService { 23 | return &ClusterService{ 24 | k8sManager: k8sManager, 25 | } 26 | } 27 | 28 | // ListClusters returns a list of summary information for all managed clusters. 29 | func (s *ClusterService) ListClusters() []models.ClusterListResponse { 30 | // The information structure returned by k8sManager is already suitable for the list page, we just convert it 31 | managerInfo := s.k8sManager.ListClusterInfo() 32 | response := make([]models.ClusterListResponse, len(managerInfo)) 33 | for i, info := range managerInfo { 34 | response[i] = models.ClusterListResponse{ 35 | ID: info.ID, // Ensure k8s.ClusterInfoResponse has ID field 36 | Name: info.Name, 37 | Server: info.Server, 38 | Version: info.Version, 39 | Status: info.Status, 40 | Source: info.Source, 41 | Environment: info.Environment, 42 | } 43 | } 44 | return response 45 | } 46 | 47 | // GetClusterByID gets detailed information for a single cluster. 48 | func (s *ClusterService) GetClusterByID(id string) (*models.ClusterResponse, error) { 49 | cluster, err := s.k8sManager.GetClusterDetailFromDB(id) 50 | if err != nil { 51 | // If not in database, it might be a file-type cluster, we assemble a simple version from cache 52 | if info, ok := s.k8sManager.GetStatusFromCache(id); ok { 53 | return &models.ClusterResponse{ 54 | ID: info.ID, 55 | Name: info.Name, 56 | Version: info.Version, 57 | Status: info.Status, 58 | Environment: info.Environment, 59 | Source: info.Source, 60 | }, nil 61 | } 62 | return nil, fmt.Errorf("cluster ID '%s' not found: %w", id, err) 63 | } 64 | 65 | return &models.ClusterResponse{ 66 | ID: cluster.ID, 67 | Name: cluster.Name, 68 | Provider: cluster.Provider, 69 | Description: cluster.Description, 70 | Environment: cluster.Environment, 71 | Region: cluster.Region, 72 | Version: cluster.Version, 73 | Status: cluster.Status, 74 | Labels: cluster.Labels, 75 | CreatedAt: cluster.CreatedAt, 76 | UpdatedAt: cluster.UpdatedAt, 77 | }, nil 78 | } 79 | 80 | // CreateCluster handles the logic for creating a new cluster. 81 | func (s *ClusterService) CreateCluster(req models.CreateClusterRequest) error { 82 | // 1. Validate kubeconfig 83 | config, err := s.validateKubeconfig(req.KubeconfigData) 84 | if err != nil { 85 | return fmt.Errorf("invalid kubeconfig: %w", err) 86 | } 87 | 88 | // 2. Test connection 89 | if err := s.testConnection(config); err != nil { 90 | return fmt.Errorf("failed to connect to cluster: %w", err) 91 | } 92 | 93 | // 3. Decode and create cluster 94 | kubeconfigBytes, err := base64.StdEncoding.DecodeString(req.KubeconfigData) 95 | if err != nil { 96 | return fmt.Errorf("kubeconfig data is not valid Base64 encoding: %w", err) 97 | } 98 | cluster := &store.Cluster{ 99 | Name: req.Name, 100 | KubeconfigData: kubeconfigBytes, 101 | Provider: req.Provider, 102 | Description: req.Description, 103 | Environment: req.Environment, 104 | Region: req.Region, 105 | } 106 | return s.k8sManager.AddDBCluster(cluster) 107 | } 108 | 109 | // UpdateCluster updates cluster information. 110 | func (s *ClusterService) UpdateCluster(id string, req models.UpdateClusterRequest) error { 111 | return s.k8sManager.UpdateDBCluster(id, req) 112 | } 113 | 114 | // DeleteClusterByID handles the logic for deleting a cluster. 115 | func (s *ClusterService) DeleteClusterByID(id string) error { 116 | return s.k8sManager.RemoveDBClusterByID(id) 117 | } 118 | 119 | // SetActiveCluster handles the logic for switching the active cluster. 120 | func (s *ClusterService) SetActiveCluster(id string) error { 121 | return s.k8sManager.SetActiveClusterByID(id) 122 | } 123 | 124 | // GetActiveClusterID gets the current active cluster ID 125 | func (s *ClusterService) GetActiveClusterID() string { 126 | return s.k8sManager.GetActiveClusterID() 127 | } 128 | 129 | // validateKubeconfig validates the kubeconfig data 130 | func (s *ClusterService) validateKubeconfig(kubeconfigData string) (*rest.Config, error) { 131 | // Decode base64 132 | decoded, err := base64.StdEncoding.DecodeString(kubeconfigData) 133 | if err != nil { 134 | return nil, fmt.Errorf("failed to decode kubeconfig: %w", err) 135 | } 136 | 137 | // Parse kubeconfig 138 | config, err := clientcmd.RESTConfigFromKubeConfig(decoded) 139 | if err != nil { 140 | return nil, fmt.Errorf("failed to parse kubeconfig: %w", err) 141 | } 142 | 143 | return config, nil 144 | } 145 | 146 | // testConnection tests the connection to the Kubernetes cluster 147 | func (s *ClusterService) testConnection(config *rest.Config) error { 148 | // Create a new configuration to avoid modifying the original configuration 149 | testConfig := &rest.Config{ 150 | Host: config.Host, 151 | APIPath: config.APIPath, 152 | // Skip TLS verification 153 | TLSClientConfig: rest.TLSClientConfig{ 154 | Insecure: true, 155 | }, 156 | // Preserve authentication information (if any) 157 | Username: config.Username, 158 | Password: config.Password, 159 | BearerToken: config.BearerToken, 160 | Timeout: config.Timeout, 161 | } 162 | 163 | // Create a clientset 164 | clientset, err := kubernetes.NewForConfig(testConfig) 165 | if err != nil { 166 | return fmt.Errorf("failed to create clientset: %w", err) 167 | } 168 | 169 | // Test connection by getting server version 170 | _, err = clientset.Discovery().ServerVersion() 171 | if err != nil { 172 | return fmt.Errorf("failed to connect to cluster: %w", err) 173 | } 174 | 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /internal/service/security_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ciliverse/cilikube/configs" 8 | "github.com/ciliverse/cilikube/internal/store" 9 | ) 10 | 11 | func TestPasswordValidation(t *testing.T) { 12 | // Create test config with security settings 13 | config := &configs.Config{ 14 | Security: configs.SecurityConfig{ 15 | Password: configs.PasswordConfig{ 16 | MinLength: 8, 17 | RequireUppercase: true, 18 | RequireLowercase: true, 19 | RequireNumbers: true, 20 | RequireSymbols: false, 21 | }, 22 | }, 23 | } 24 | 25 | // Create memory store and security service 26 | memStore := store.NewMemoryStore() 27 | securityService := NewSecurityService(memStore, config) 28 | 29 | tests := []struct { 30 | name string 31 | password string 32 | valid bool 33 | }{ 34 | { 35 | name: "Valid password", 36 | password: "Password123", 37 | valid: true, 38 | }, 39 | { 40 | name: "Too short", 41 | password: "Pass1", 42 | valid: false, 43 | }, 44 | { 45 | name: "No uppercase", 46 | password: "password123", 47 | valid: false, 48 | }, 49 | { 50 | name: "No lowercase", 51 | password: "PASSWORD123", 52 | valid: false, 53 | }, 54 | { 55 | name: "No numbers", 56 | password: "Password", 57 | valid: false, 58 | }, 59 | { 60 | name: "Common weak password", 61 | password: "password", 62 | valid: false, 63 | }, 64 | } 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | errors := securityService.ValidatePassword(tt.password) 69 | isValid := len(errors) == 0 70 | 71 | if isValid != tt.valid { 72 | t.Errorf("ValidatePassword() = %v, want %v", isValid, tt.valid) 73 | if len(errors) > 0 { 74 | t.Logf("Validation errors: %+v", errors) 75 | } 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestSessionManagement(t *testing.T) { 82 | // Create test config 83 | config := &configs.Config{ 84 | Security: configs.SecurityConfig{ 85 | Session: configs.SessionConfig{ 86 | MaxConcurrentSessions: 2, 87 | IdleTimeout: 30 * time.Minute, 88 | AbsoluteTimeout: 8 * time.Hour, 89 | }, 90 | }, 91 | } 92 | 93 | // Create memory store and security service 94 | memStore := store.NewMemoryStore() 95 | securityService := NewSecurityService(memStore, config) 96 | 97 | userID := uint(1) 98 | ipAddress := "192.168.1.1" 99 | userAgent := "Test Browser" 100 | 101 | // Test session creation 102 | sessionID1, err := securityService.CreateSession(userID, ipAddress, userAgent) 103 | if err != nil { 104 | t.Fatalf("Failed to create session: %v", err) 105 | } 106 | 107 | // Test session validation 108 | session, err := securityService.ValidateSession(sessionID1) 109 | if err != nil { 110 | t.Fatalf("Failed to validate session: %v", err) 111 | } 112 | 113 | if session.UserID != userID { 114 | t.Errorf("Session user ID = %v, want %v", session.UserID, userID) 115 | } 116 | 117 | // Test session invalidation 118 | err = securityService.InvalidateSession(sessionID1) 119 | if err != nil { 120 | t.Fatalf("Failed to invalidate session: %v", err) 121 | } 122 | 123 | // Test that invalidated session is no longer valid 124 | _, err = securityService.ValidateSession(sessionID1) 125 | if err == nil { 126 | t.Error("Expected error when validating invalidated session") 127 | } 128 | } 129 | 130 | func TestAccountLockout(t *testing.T) { 131 | // Create test config with account lockout enabled 132 | config := &configs.Config{ 133 | Security: configs.SecurityConfig{ 134 | AccountLock: configs.AccountLockConfig{ 135 | Enabled: true, 136 | MaxFailedAttempts: 3, 137 | LockoutDuration: 15 * time.Minute, 138 | ResetAfter: 1 * time.Hour, 139 | }, 140 | }, 141 | } 142 | 143 | // Create memory store and security service 144 | memStore := store.NewMemoryStore() 145 | if err := memStore.Initialize(); err != nil { 146 | t.Fatalf("Failed to initialize store: %v", err) 147 | } 148 | 149 | securityService := NewSecurityService(memStore, config) 150 | 151 | userID := uint(1) 152 | username := "testuser" 153 | ipAddress := "192.168.1.1" 154 | userAgent := "Test Browser" 155 | 156 | // Record multiple failed login attempts 157 | for i := 0; i < 3; i++ { 158 | err := securityService.RecordFailedLogin(&userID, username, ipAddress, userAgent) 159 | if err != nil { 160 | t.Fatalf("Failed to record failed login: %v", err) 161 | } 162 | } 163 | 164 | // Check if account is locked 165 | isLocked, lockoutEnd, err := securityService.CheckAccountLockout(userID) 166 | if err != nil { 167 | t.Fatalf("Failed to check account lockout: %v", err) 168 | } 169 | 170 | if !isLocked { 171 | t.Error("Expected account to be locked after 3 failed attempts") 172 | } 173 | 174 | if lockoutEnd.IsZero() { 175 | t.Error("Expected lockout end time to be set") 176 | } 177 | 178 | t.Logf("Account locked until: %v", lockoutEnd) 179 | } 180 | 181 | func TestSuspiciousActivityDetection(t *testing.T) { 182 | // Create test config 183 | config := &configs.Config{} 184 | 185 | // Create memory store and security service 186 | memStore := store.NewMemoryStore() 187 | if err := memStore.Initialize(); err != nil { 188 | t.Fatalf("Failed to initialize store: %v", err) 189 | } 190 | 191 | securityService := NewSecurityService(memStore, config) 192 | 193 | userID := uint(1) 194 | 195 | // Create multiple failed login attempts from different IPs 196 | ips := []string{"192.168.1.1", "10.0.0.1", "172.16.0.1", "203.0.113.1"} 197 | for _, ip := range ips { 198 | for i := 0; i < 2; i++ { 199 | err := securityService.RecordFailedLogin(&userID, "testuser", ip, "Test Browser") 200 | if err != nil { 201 | t.Fatalf("Failed to record failed login: %v", err) 202 | } 203 | } 204 | } 205 | 206 | // Detect suspicious activity 207 | warnings, err := securityService.DetectSuspiciousActivity(userID) 208 | if err != nil { 209 | t.Fatalf("Failed to detect suspicious activity: %v", err) 210 | } 211 | 212 | if len(warnings) == 0 { 213 | t.Error("Expected suspicious activity warnings") 214 | } 215 | 216 | t.Logf("Suspicious activity warnings: %v", warnings) 217 | } 218 | -------------------------------------------------------------------------------- /internal/models/role.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Role represents a role in the RBAC system 8 | type Role struct { 9 | ID uint `json:"id" gorm:"primaryKey"` 10 | Name string `json:"name" gorm:"uniqueIndex;not null;size:50"` 11 | DisplayName string `json:"display_name" gorm:"not null;size:100"` 12 | Description string `json:"description" gorm:"type:text"` 13 | IsSystem bool `json:"is_system" gorm:"default:false"` 14 | CreatedAt time.Time `json:"created_at"` 15 | UpdatedAt time.Time `json:"updated_at"` 16 | } 17 | 18 | // TableName specifies the table name for Role model 19 | func (Role) TableName() string { 20 | return "roles" 21 | } 22 | 23 | // UserRole represents the many-to-many relationship between users and roles 24 | type UserRole struct { 25 | ID uint `json:"id" gorm:"primaryKey"` 26 | UserID uint `json:"user_id" gorm:"not null;index"` 27 | RoleID uint `json:"role_id" gorm:"not null;index"` 28 | AssignedBy uint `json:"assigned_by"` 29 | AssignedAt time.Time `json:"assigned_at" gorm:"default:CURRENT_TIMESTAMP"` 30 | } 31 | 32 | // TableName specifies the table name for UserRole model 33 | func (UserRole) TableName() string { 34 | return "user_roles" 35 | } 36 | 37 | // Permission represents a permission in the system 38 | type Permission struct { 39 | ID uint `json:"id" gorm:"primaryKey"` 40 | Name string `json:"name" gorm:"uniqueIndex;not null;size:100"` 41 | Resource string `json:"resource" gorm:"not null;size:100;index"` 42 | Action string `json:"action" gorm:"not null;size:50;index"` 43 | Description string `json:"description" gorm:"type:text"` 44 | CreatedAt time.Time `json:"created_at"` 45 | } 46 | 47 | // TableName specifies the table name for Permission model 48 | func (Permission) TableName() string { 49 | return "permissions" 50 | } 51 | 52 | // RolePermission represents the many-to-many relationship between roles and permissions 53 | type RolePermission struct { 54 | ID uint `json:"id" gorm:"primaryKey"` 55 | RoleID uint `json:"role_id" gorm:"not null;index"` 56 | PermissionID uint `json:"permission_id" gorm:"not null;index"` 57 | } 58 | 59 | // TableName specifies the table name for RolePermission model 60 | func (RolePermission) TableName() string { 61 | return "role_permissions" 62 | } 63 | 64 | // CreateRoleRequest request for creating a new role 65 | type CreateRoleRequest struct { 66 | Name string `json:"name" binding:"required,min=2,max=50"` 67 | DisplayName string `json:"display_name" binding:"required,min=2,max=100"` 68 | Description string `json:"description" binding:"max=500"` 69 | Permissions []string `json:"permissions"` 70 | } 71 | 72 | // UpdateRoleRequest request for updating a role 73 | type UpdateRoleRequest struct { 74 | DisplayName string `json:"display_name" binding:"required,min=2,max=100"` 75 | Description string `json:"description" binding:"max=500"` 76 | Permissions []string `json:"permissions"` 77 | } 78 | 79 | // RoleResponse response for role operations 80 | type RoleResponse struct { 81 | ID uint `json:"id"` 82 | Name string `json:"name"` 83 | DisplayName string `json:"display_name"` 84 | Description string `json:"description"` 85 | Type string `json:"type"` 86 | IsSystem bool `json:"is_system"` 87 | CreatedAt time.Time `json:"created_at"` 88 | UpdatedAt time.Time `json:"updated_at"` 89 | Permissions []PermissionResponse `json:"permissions,omitempty"` 90 | MainPermissions []string `json:"main_permissions"` 91 | UserCount int `json:"user_count,omitempty"` 92 | PermissionCount int `json:"permission_count,omitempty"` 93 | } 94 | 95 | // PermissionResponse response for permission operations 96 | type PermissionResponse struct { 97 | ID uint `json:"id"` 98 | Name string `json:"name"` 99 | Resource string `json:"resource"` 100 | Action string `json:"action"` 101 | Description string `json:"description"` 102 | } 103 | 104 | // AssignRoleRequest request for assigning a role to a user 105 | type AssignRoleRequest struct { 106 | UserID uint `json:"user_id" binding:"required"` 107 | RoleID uint `json:"role_id" binding:"required"` 108 | } 109 | 110 | // RemoveRoleRequest request for removing a role from a user 111 | type RemoveRoleRequest struct { 112 | UserID uint `json:"user_id" binding:"required"` 113 | RoleID uint `json:"role_id" binding:"required"` 114 | } 115 | 116 | // UserRoleResponse response for user role operations 117 | type UserRoleResponse struct { 118 | UserID uint `json:"user_id"` 119 | Username string `json:"username"` 120 | Roles []RoleResponse `json:"roles"` 121 | AssignedAt time.Time `json:"assigned_at"` 122 | AssignedBy uint `json:"assigned_by,omitempty"` 123 | } 124 | 125 | // ToResponse converts Role to RoleResponse 126 | func (r *Role) ToResponse() RoleResponse { 127 | return RoleResponse{ 128 | ID: r.ID, 129 | Name: r.Name, 130 | DisplayName: r.DisplayName, 131 | Description: r.Description, 132 | IsSystem: r.IsSystem, 133 | CreatedAt: r.CreatedAt, 134 | UpdatedAt: r.UpdatedAt, 135 | } 136 | } 137 | 138 | // ToResponse converts Permission to PermissionResponse 139 | func (p *Permission) ToResponse() PermissionResponse { 140 | return PermissionResponse{ 141 | ID: p.ID, 142 | Name: p.Name, 143 | Resource: p.Resource, 144 | Action: p.Action, 145 | Description: p.Description, 146 | } 147 | } 148 | 149 | // DefaultRoles defines the system default roles 150 | var DefaultRoles = []Role{ 151 | { 152 | Name: "admin", 153 | DisplayName: "Administrator", 154 | Description: "Full system access with all permissions", 155 | IsSystem: true, 156 | }, 157 | { 158 | Name: "editor", 159 | DisplayName: "Editor", 160 | Description: "Can read and write most Kubernetes resources", 161 | IsSystem: true, 162 | }, 163 | { 164 | Name: "viewer", 165 | DisplayName: "Viewer", 166 | Description: "Read-only access to Kubernetes resources", 167 | IsSystem: true, 168 | }, 169 | } 170 | --------------------------------------------------------------------------------