feat: LokiDeploy 初始提交 - 轻量级 K8s Loki 部署平台
Go(Gin) + React(Vite + shadcn/ui + Tailwind) 前后端分离,全中文 UI - 集群管理:Kubeconfig / Token 双接入,kubectl + helm 部署 - 主机管理:SSH 密钥 / 密码双认证,CPU/内存/磁盘使用率监控,SSH 错误中文友好提示 - 存储配置、部署管理、系统设置 - Docker 镜像内置 kubectl + helm,开箱即用 - run.sh 一键启停(start/stop/restart/status) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
2a93de600d
134 files changed
+19595
No files matched your search
@@ -0,0 +1 @@
|
||||
46832
|
||||
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
instance *Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerPort string
|
||||
DatabasePath string
|
||||
JWTSecret string
|
||||
DataDir string
|
||||
KubeDir string
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
once.Do(func() {
|
||||
dataDir := getEnv("DATA_DIR", "./data")
|
||||
instance = &Config{
|
||||
ServerPort: getEnv("SERVER_PORT", "8080"),
|
||||
DatabasePath: filepath.Join(dataDir, "lokideploy.json"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "lokideploy-secret-key-change-in-production"),
|
||||
DataDir: dataDir,
|
||||
KubeDir: filepath.Join(dataDir, "kubeconfigs"),
|
||||
}
|
||||
_ = os.MkdirAll(instance.KubeDir, 0755)
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/services"
|
||||
"lokideploy/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthController struct {
|
||||
service *services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthController() *AuthController {
|
||||
return &AuthController{service: services.NewAuthService()}
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Nickname string `json:"nickname"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"oldPassword" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
user, token, err := ctrl.service.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
utils.Unauthorized(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"token": token, "user": models.SanitizedJSON(user)})
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
if req.Role == "" {
|
||||
req.Role = "user"
|
||||
}
|
||||
user, err := ctrl.service.Register(req.Username, req.Password, req.Nickname, req.Role)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(user))
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) GetUser(c *gin.Context) {
|
||||
claims, ok := utils.GetUserFromContext(c)
|
||||
if !ok {
|
||||
utils.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
user, err := ctrl.service.GetUserByID(claims.UserID)
|
||||
if err != nil {
|
||||
utils.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(user))
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) ChangePassword(c *gin.Context) {
|
||||
var req ChangePasswordRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
claims, ok := utils.GetUserFromContext(c)
|
||||
if !ok {
|
||||
utils.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.ChangePassword(claims.UserID, req.OldPassword, req.NewPassword); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "password changed"})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/services"
|
||||
"lokideploy/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ClusterController struct {
|
||||
service *services.ClusterService
|
||||
}
|
||||
|
||||
func NewClusterController() *ClusterController {
|
||||
return &ClusterController{service: services.NewClusterService()}
|
||||
}
|
||||
|
||||
type CreateClusterRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
ConfigType string `json:"configType"` // kubeconfig | token
|
||||
APIServer string `json:"apiServer"` // token 模式必填
|
||||
KubeConfig string `json:"kubeconfig"` // kubeconfig 模式必填
|
||||
Token string `json:"token"` // token 模式必填
|
||||
Context string `json:"context"`
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) Create(c *gin.Context) {
|
||||
var req CreateClusterRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
if req.ConfigType == "" {
|
||||
req.ConfigType = "kubeconfig"
|
||||
}
|
||||
if req.ConfigType == "kubeconfig" && req.KubeConfig == "" {
|
||||
utils.BadRequest(c, "请填写 kubeconfig 内容")
|
||||
return
|
||||
}
|
||||
if req.ConfigType == "token" && (req.APIServer == "" || req.Token == "") {
|
||||
utils.BadRequest(c, "Token 接入方式需要填写 API Server 和 Token")
|
||||
return
|
||||
}
|
||||
|
||||
cluster, err := ctrl.service.Create(req.Name, req.Description, req.ConfigType, req.APIServer, req.KubeConfig, req.Token, req.Context)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(cluster))
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) Get(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
cluster, err := ctrl.service.Get(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "集群不存在")
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(cluster))
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) List(c *gin.Context) {
|
||||
page, pageSize := utils.Pagination(c)
|
||||
clusters, total, err := ctrl.service.List(page, pageSize)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, utils.PageData{
|
||||
List: models.SanitizedJSON(clusters), Total: total, Page: page, PageSize: pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) Update(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
var updates map[string]interface{}
|
||||
if !utils.BindAndValidate(c, &updates) {
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Update(uint(id), updates); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) Delete(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Delete(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) Validate(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
go ctrl.service.Validate(uint(id))
|
||||
utils.Success(c, gin.H{"message": "已开始验证连接"})
|
||||
}
|
||||
|
||||
func (ctrl *ClusterController) GetInfo(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
cluster, err := ctrl.service.Get(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "集群不存在")
|
||||
return
|
||||
}
|
||||
info, err := services.GetClusterInfo(c.Request.Context(), cluster)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, info)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"lokideploy/services"
|
||||
"lokideploy/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DeploymentController struct {
|
||||
service *services.DeploymentService
|
||||
}
|
||||
|
||||
func NewDeploymentController() *DeploymentController {
|
||||
return &DeploymentController{service: services.NewDeploymentService()}
|
||||
}
|
||||
|
||||
type CreateDeploymentRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Mode string `json:"mode" binding:"required,oneof=k8s-helm k8s-yaml host-binary"`
|
||||
ClusterID *uint `json:"clusterID"`
|
||||
HostID *uint `json:"hostID"`
|
||||
Namespace string `json:"namespace"`
|
||||
StorageConfigID uint `json:"storageConfigID"`
|
||||
AuthEnabled bool `json:"authEnabled"`
|
||||
RetentionDays int `json:"retentionDays"`
|
||||
Replicas int `json:"replicas"`
|
||||
ResourcesCPU string `json:"resourcesCPU"`
|
||||
ResourcesMem string `json:"resourcesMem"`
|
||||
StorageSize string `json:"storageSize"`
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Create(c *gin.Context) {
|
||||
var req CreateDeploymentRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
deployment, err := ctrl.service.Create(&services.CreateDeploymentRequest{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
Mode: req.Mode,
|
||||
ClusterID: req.ClusterID,
|
||||
HostID: req.HostID,
|
||||
Namespace: req.Namespace,
|
||||
StorageConfigID: req.StorageConfigID,
|
||||
AuthEnabled: req.AuthEnabled,
|
||||
RetentionDays: req.RetentionDays,
|
||||
Replicas: req.Replicas,
|
||||
ResourcesCPU: req.ResourcesCPU,
|
||||
ResourcesMem: req.ResourcesMem,
|
||||
StorageSize: req.StorageSize,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, deployment)
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Get(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
deployment, err := ctrl.service.Get(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "部署不存在")
|
||||
return
|
||||
}
|
||||
utils.Success(c, deployment)
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) List(c *gin.Context) {
|
||||
page, pageSize := utils.Pagination(c)
|
||||
deployments, total, err := ctrl.service.List(page, pageSize)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, utils.PageData{
|
||||
List: deployments, Total: total, Page: page, PageSize: pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Update(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
var updates map[string]interface{}
|
||||
if !utils.BindAndValidate(c, &updates) {
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Update(uint(id), updates); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "updated"})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Delete(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Delete(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Deploy(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.StartDeploy(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "deployment started"})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Stop(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.StopDeployment(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "deployment stopped"})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) Restart(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.RestartDeployment(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "deployment restarted"})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) GetLogs(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
page, pageSize := utils.Pagination(c)
|
||||
logs, total, err := ctrl.service.GetLogs(uint(id), page, pageSize)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, utils.PageData{
|
||||
List: logs, Total: total, Page: page, PageSize: pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *DeploymentController) GetStats(c *gin.Context) {
|
||||
stats, err := ctrl.service.GetStats()
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, stats)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/services"
|
||||
"lokideploy/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type HostController struct {
|
||||
service *services.HostService
|
||||
}
|
||||
|
||||
func NewHostController() *HostController {
|
||||
return &HostController{service: services.NewHostService()}
|
||||
}
|
||||
|
||||
type CreateHostRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
HostIP string `json:"hostIP" binding:"required"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user" binding:"required"`
|
||||
AuthType string `json:"authType" binding:"required,oneof=password key"`
|
||||
Password string `json:"password"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
KeyPhrase string `json:"keyPhrase"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Labels string `json:"labels"`
|
||||
}
|
||||
|
||||
func (ctrl *HostController) Create(c *gin.Context) {
|
||||
var req CreateHostRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
host, err := ctrl.service.Create(req.Name, req.HostIP, req.Port, req.User, req.AuthType,
|
||||
req.Password, req.PrivateKey, req.KeyPhrase, req.OS, req.Arch, req.Labels)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(host))
|
||||
}
|
||||
|
||||
func (ctrl *HostController) Get(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
host, err := ctrl.service.Get(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "主机不存在")
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(host))
|
||||
}
|
||||
|
||||
func (ctrl *HostController) List(c *gin.Context) {
|
||||
page, pageSize := utils.Pagination(c)
|
||||
hosts, total, err := ctrl.service.List(page, pageSize)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, utils.PageData{
|
||||
List: models.SanitizedJSON(hosts),
|
||||
Total: total, Page: page, PageSize: pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *HostController) Update(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
var updates map[string]interface{}
|
||||
if !utils.BindAndValidate(c, &updates) {
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Update(uint(id), updates); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "updated"})
|
||||
}
|
||||
|
||||
func (ctrl *HostController) Delete(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Delete(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
func (ctrl *HostController) Validate(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
go ctrl.service.Validate(uint(id))
|
||||
utils.Success(c, gin.H{"message": "validation started"})
|
||||
}
|
||||
|
||||
func (ctrl *HostController) GetInfo(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
host, err := ctrl.service.Get(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "主机不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := host.Password
|
||||
if len(password) > 4 && password[:4] == "ENC:" {
|
||||
password, _ = utils.Decrypt(password[4:])
|
||||
}
|
||||
|
||||
client, err := services.NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
info, err := client.GetHostInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, info)
|
||||
}
|
||||
|
||||
func (ctrl *HostController) Execute(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Command string `json:"command" binding:"required"`
|
||||
}
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
stdout, stderr, err := ctrl.service.ExecuteCommand(uint(id), req.Command)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"stdout": stdout, "stderr": stderr})
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/services"
|
||||
"lokideploy/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type StorageController struct {
|
||||
service *services.StorageService
|
||||
}
|
||||
|
||||
func NewStorageController() *StorageController {
|
||||
return &StorageController{service: services.NewStorageService()}
|
||||
}
|
||||
|
||||
type CreateStorageRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
StorageType string `json:"storageType" binding:"required,oneof=s3 oss minio rustfs"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Bucket string `json:"bucket" binding:"required"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
PathPrefix string `json:"pathPrefix"`
|
||||
Insecure bool `json:"insecure"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
func (ctrl *StorageController) Create(c *gin.Context) {
|
||||
var req CreateStorageRequest
|
||||
if !utils.BindAndValidate(c, &req) {
|
||||
return
|
||||
}
|
||||
storage, err := ctrl.service.Create(req.Name, req.StorageType, req.Endpoint, req.Bucket,
|
||||
req.Region, req.AccessKey, req.SecretKey, req.PathPrefix, req.Insecure, req.IsDefault)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(storage))
|
||||
}
|
||||
|
||||
func (ctrl *StorageController) Get(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
storage, err := ctrl.service.Get(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "存储配置不存在")
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(storage))
|
||||
}
|
||||
|
||||
func (ctrl *StorageController) List(c *gin.Context) {
|
||||
page, pageSize := utils.Pagination(c)
|
||||
storages, total, err := ctrl.service.List(page, pageSize)
|
||||
if err != nil {
|
||||
utils.Error(c, 500, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, utils.PageData{
|
||||
List: models.SanitizedJSON(storages),
|
||||
Total: total, Page: page, PageSize: pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *StorageController) Update(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
var updates map[string]interface{}
|
||||
if !utils.BindAndValidate(c, &updates) {
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Update(uint(id), updates); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "updated"})
|
||||
}
|
||||
|
||||
func (ctrl *StorageController) Delete(c *gin.Context) {
|
||||
id, err := utils.IntParam(c, "id")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "ID 无效")
|
||||
return
|
||||
}
|
||||
if err := ctrl.service.Delete(uint(id)); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
func (ctrl *StorageController) GetDefault(c *gin.Context) {
|
||||
storage, err := ctrl.service.GetDefault()
|
||||
if err != nil {
|
||||
utils.NotFound(c, "未设置默认存储配置")
|
||||
return
|
||||
}
|
||||
utils.Success(c, models.SanitizedJSON(storage))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
module lokideploy
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.3
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
||||
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
|
||||
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"lokideploy/config"
|
||||
"lokideploy/models"
|
||||
"lokideploy/routers"
|
||||
"lokideploy/seed"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.GetConfig()
|
||||
|
||||
log.Println("Initializing database...")
|
||||
if err := models.InitDB(); err != nil {
|
||||
log.Fatalf("Failed to init DB: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Seeding default data...")
|
||||
seed.Seed()
|
||||
|
||||
r := routers.SetupRouter()
|
||||
|
||||
log.Printf("LokiDeploy starting on port %s", cfg.ServerPort)
|
||||
log.Printf("API: http://localhost:%s/api/v1", cfg.ServerPort)
|
||||
log.Printf("Default login: admin / admin123")
|
||||
|
||||
if err := r.Run(":" + cfg.ServerPort); err != nil {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"lokideploy/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func JWTAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
utils.Unauthorized(c, "missing authorization header")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
utils.Unauthorized(c, "invalid authorization header format")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := utils.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
utils.Unauthorized(c, "invalid or expired token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user", claims)
|
||||
c.Set("userId", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists || role != "admin" {
|
||||
utils.Forbidden(c, "admin access required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
}
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization, Accept, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Cluster struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ConfigType string `json:"configType"` // kubeconfig | token(空值视为 kubeconfig,向后兼容)
|
||||
APIServer string `json:"apiServer"` // 展示用
|
||||
KubeConfig string `json:"kubeconfig"` // 统一存储(token 接入时存构造出的等价 kubeconfig)
|
||||
KubeConfigPath string `json:"kubeconfigPath"`
|
||||
Context string `json:"context"`
|
||||
Version string `json:"version"`
|
||||
NodesCount int `json:"nodesCount"`
|
||||
Status string `json:"status"`
|
||||
LastConnected time.Time `json:"lastConnected"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (c *Cluster) Sanitized() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": c.ID,
|
||||
"name": c.Name,
|
||||
"description": c.Description,
|
||||
"configType": c.ConfigType,
|
||||
"apiServer": c.APIServer,
|
||||
"context": c.Context,
|
||||
"version": c.Version,
|
||||
"nodesCount": c.NodesCount,
|
||||
"status": c.Status,
|
||||
"hasKubeconfig": c.KubeConfig != "",
|
||||
"lastConnected": c.LastConnected,
|
||||
"createdAt": c.CreatedAt,
|
||||
"updatedAt": c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"lokideploy/config"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type JsonDB struct {
|
||||
mu sync.RWMutex
|
||||
dataFile string
|
||||
Users []User `json:"users"`
|
||||
Clusters []Cluster `json:"clusters"`
|
||||
Hosts []Host `json:"hosts"`
|
||||
Storages []StorageConfig `json:"storages"`
|
||||
Deployments []LokiDeployment `json:"deployments"`
|
||||
Logs []DeploymentLog `json:"logs"`
|
||||
Settings []SystemSetting `json:"settings"`
|
||||
sequences map[string]uint
|
||||
}
|
||||
|
||||
var db *JsonDB
|
||||
|
||||
func GetDB() *JsonDB {
|
||||
return db
|
||||
}
|
||||
|
||||
func InitDB() error {
|
||||
cfg := config.GetConfig()
|
||||
_ = os.MkdirAll(cfg.DataDir, 0755)
|
||||
|
||||
d := &JsonDB{
|
||||
dataFile: cfg.DatabasePath,
|
||||
Users: []User{},
|
||||
Clusters: []Cluster{},
|
||||
Hosts: []Host{},
|
||||
Storages: []StorageConfig{},
|
||||
Deployments: []LokiDeployment{},
|
||||
Logs: []DeploymentLog{},
|
||||
Settings: []SystemSetting{},
|
||||
sequences: map[string]uint{
|
||||
"users": 0,
|
||||
"clusters": 0,
|
||||
"hosts": 0,
|
||||
"storages": 0,
|
||||
"deployments": 0,
|
||||
"logs": 0,
|
||||
},
|
||||
}
|
||||
|
||||
if data, err := os.ReadFile(d.dataFile); err == nil {
|
||||
if err := json.Unmarshal(data, d); err == nil {
|
||||
d.sequences = map[string]uint{}
|
||||
for _, u := range d.Users { if u.ID > d.sequences["users"] { d.sequences["users"] = u.ID } }
|
||||
for _, c := range d.Clusters { if c.ID > d.sequences["clusters"] { d.sequences["clusters"] = c.ID } }
|
||||
for _, h := range d.Hosts { if h.ID > d.sequences["hosts"] { d.sequences["hosts"] = h.ID } }
|
||||
for _, s := range d.Storages { if s.ID > d.sequences["storages"] { d.sequences["storages"] = s.ID } }
|
||||
for _, de := range d.Deployments { if de.ID > d.sequences["deployments"] { d.sequences["deployments"] = de.ID } }
|
||||
for _, l := range d.Logs { if l.ID > d.sequences["logs"] { d.sequences["logs"] = l.ID } }
|
||||
}
|
||||
}
|
||||
|
||||
db = d
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *JsonDB) Save() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
data, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(d.dataFile, data, 0644)
|
||||
}
|
||||
|
||||
func (d *JsonDB) NextID(table string) uint {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.sequences[table]++
|
||||
return d.sequences[table]
|
||||
}
|
||||
|
||||
func (d *JsonDB) Lock() {
|
||||
d.mu.Lock()
|
||||
}
|
||||
|
||||
func (d *JsonDB) Unlock() {
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func (d *JsonDB) RLock() {
|
||||
d.mu.RLock()
|
||||
}
|
||||
|
||||
func (d *JsonDB) RUnlock() {
|
||||
d.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (d *JsonDB) CreateUser(user *User) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if user.ID == 0 {
|
||||
user.ID = d.sequences["users"] + 1
|
||||
d.sequences["users"] = user.ID
|
||||
}
|
||||
d.Users = append(d.Users, *user)
|
||||
go d.Save()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LokiDeployment struct {
|
||||
ID uint `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Mode string `json:"mode"`
|
||||
ClusterID *uint `json:"clusterID,omitempty"`
|
||||
Cluster *Cluster `json:"cluster,omitempty"`
|
||||
HostID *uint `json:"hostID,omitempty"`
|
||||
Host *Host `json:"host,omitempty"`
|
||||
Namespace string `json:"namespace"`
|
||||
StorageConfigID uint `json:"storageConfigID"`
|
||||
StorageConfig *StorageConfig `json:"storageConfig,omitempty"`
|
||||
AuthEnabled bool `json:"authEnabled"`
|
||||
RetentionDays int `json:"retentionDays"`
|
||||
Replicas int `json:"replicas"`
|
||||
ResourcesCPU string `json:"resourcesCPU"`
|
||||
ResourcesMem string `json:"resourcesMem"`
|
||||
StorageSize string `json:"storageSize"`
|
||||
Status string `json:"status"`
|
||||
StatusMessage string `json:"statusMessage"`
|
||||
HelmValues string `json:"helmValues"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
AdminUser string `json:"adminUser"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (d *LokiDeployment) BeforeCreate() {
|
||||
if d.UUID == "" {
|
||||
d.UUID = uuid.New().String()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Host struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
HostIP string `json:"hostIP"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
AuthType string `json:"authType"`
|
||||
Password string `json:"password"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
KeyPhrase string `json:"keyPhrase"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Status string `json:"status"`
|
||||
Labels string `json:"labels"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (h *Host) Sanitized() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": h.ID,
|
||||
"name": h.Name,
|
||||
"hostIP": h.HostIP,
|
||||
"port": h.Port,
|
||||
"user": h.User,
|
||||
"authType": h.AuthType,
|
||||
"os": h.OS,
|
||||
"arch": h.Arch,
|
||||
"status": h.Status,
|
||||
"labels": h.Labels,
|
||||
"createdAt": h.CreatedAt,
|
||||
"updatedAt": h.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type DeploymentLog struct {
|
||||
ID uint `json:"id"`
|
||||
DeploymentID uint `json:"deploymentID"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package models
|
||||
|
||||
// Sanitizer interface for models that can sanitize themselves
|
||||
type Sanitizer interface {
|
||||
Sanitized() map[string]interface{}
|
||||
}
|
||||
|
||||
// SanitizedJSON sanitizes a single model or a slice of models
|
||||
func SanitizedJSON(v any) any {
|
||||
switch val := v.(type) {
|
||||
// Single objects
|
||||
case User:
|
||||
return val.Sanitized()
|
||||
case *User:
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.Sanitized()
|
||||
case Cluster:
|
||||
return val.Sanitized()
|
||||
case *Cluster:
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.Sanitized()
|
||||
case Host:
|
||||
return val.Sanitized()
|
||||
case *Host:
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.Sanitized()
|
||||
case StorageConfig:
|
||||
return val.Sanitized()
|
||||
case *StorageConfig:
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.Sanitized()
|
||||
// Slices
|
||||
case []User:
|
||||
result := make([]map[string]interface{}, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = item.Sanitized()
|
||||
}
|
||||
return result
|
||||
case []Cluster:
|
||||
result := make([]map[string]interface{}, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = item.Sanitized()
|
||||
}
|
||||
return result
|
||||
case []Host:
|
||||
result := make([]map[string]interface{}, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = item.Sanitized()
|
||||
}
|
||||
return result
|
||||
case []StorageConfig:
|
||||
result := make([]map[string]interface{}, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = item.Sanitized()
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
type SystemSetting struct {
|
||||
ID uint `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type StorageConfig struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StorageType string `json:"storageType"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
PathPrefix string `json:"pathPrefix"`
|
||||
Insecure bool `json:"insecure"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (s *StorageConfig) Sanitized() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"name": s.Name,
|
||||
"storageType": s.StorageType,
|
||||
"endpoint": s.Endpoint,
|
||||
"bucket": s.Bucket,
|
||||
"region": s.Region,
|
||||
"pathPrefix": s.PathPrefix,
|
||||
"insecure": s.Insecure,
|
||||
"isDefault": s.IsDefault,
|
||||
"createdAt": s.CreatedAt,
|
||||
"updatedAt": s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (u *User) Sanitized() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": u.ID,
|
||||
"username": u.Username,
|
||||
"nickname": u.Nickname,
|
||||
"role": u.Role,
|
||||
"createdAt": u.CreatedAt,
|
||||
"updatedAt": u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"lokideploy/controllers"
|
||||
"lokideploy/middleware"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// CORS
|
||||
cfg := cors.DefaultConfig()
|
||||
cfg.AllowAllOrigins = true
|
||||
cfg.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"}
|
||||
cfg.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
|
||||
r.Use(cors.New(cfg))
|
||||
|
||||
// Controllers
|
||||
authCtrl := controllers.NewAuthController()
|
||||
clusterCtrl := controllers.NewClusterController()
|
||||
hostCtrl := controllers.NewHostController()
|
||||
storageCtrl := controllers.NewStorageController()
|
||||
deploymentCtrl := controllers.NewDeploymentController()
|
||||
|
||||
// API v1
|
||||
api := r.Group("/api/v1")
|
||||
|
||||
// Public
|
||||
api.POST("/auth/login", authCtrl.Login)
|
||||
|
||||
// Auth required
|
||||
authorized := api.Group("")
|
||||
authorized.Use(middleware.JWTAuth())
|
||||
|
||||
// Auth routes
|
||||
authorized.GET("/auth/user", authCtrl.GetUser)
|
||||
authorized.POST("/auth/password", authCtrl.ChangePassword)
|
||||
authorized.POST("/auth/register", middleware.AdminAuth(), authCtrl.Register)
|
||||
|
||||
// Clusters
|
||||
authorized.GET("/clusters", clusterCtrl.List)
|
||||
authorized.GET("/clusters/:id", clusterCtrl.Get)
|
||||
authorized.POST("/clusters", clusterCtrl.Create)
|
||||
authorized.PUT("/clusters/:id", clusterCtrl.Update)
|
||||
authorized.DELETE("/clusters/:id", clusterCtrl.Delete)
|
||||
authorized.POST("/clusters/:id/validate", clusterCtrl.Validate)
|
||||
authorized.GET("/clusters/:id/info", clusterCtrl.GetInfo)
|
||||
|
||||
// Hosts
|
||||
authorized.GET("/hosts", hostCtrl.List)
|
||||
authorized.GET("/hosts/:id", hostCtrl.Get)
|
||||
authorized.POST("/hosts", hostCtrl.Create)
|
||||
authorized.PUT("/hosts/:id", hostCtrl.Update)
|
||||
authorized.DELETE("/hosts/:id", hostCtrl.Delete)
|
||||
authorized.POST("/hosts/:id/validate", hostCtrl.Validate)
|
||||
authorized.GET("/hosts/:id/info", hostCtrl.GetInfo)
|
||||
authorized.POST("/hosts/:id/execute", hostCtrl.Execute)
|
||||
|
||||
// Storage
|
||||
authorized.GET("/storages", storageCtrl.List)
|
||||
authorized.GET("/storages/:id", storageCtrl.Get)
|
||||
authorized.POST("/storages", storageCtrl.Create)
|
||||
authorized.PUT("/storages/:id", storageCtrl.Update)
|
||||
authorized.DELETE("/storages/:id", storageCtrl.Delete)
|
||||
authorized.GET("/storages/default", storageCtrl.GetDefault)
|
||||
|
||||
// Deployments
|
||||
authorized.GET("/deployments", deploymentCtrl.List)
|
||||
authorized.GET("/deployments/:id", deploymentCtrl.Get)
|
||||
authorized.POST("/deployments", deploymentCtrl.Create)
|
||||
authorized.PUT("/deployments/:id", deploymentCtrl.Update)
|
||||
authorized.DELETE("/deployments/:id", deploymentCtrl.Delete)
|
||||
authorized.POST("/deployments/:id/deploy", deploymentCtrl.Deploy)
|
||||
authorized.POST("/deployments/:id/stop", deploymentCtrl.Stop)
|
||||
authorized.POST("/deployments/:id/restart", deploymentCtrl.Restart)
|
||||
authorized.GET("/deployments/:id/logs", deploymentCtrl.GetLogs)
|
||||
authorized.GET("/deployments/stats", deploymentCtrl.GetStats)
|
||||
|
||||
// Static files - use executable directory as base
|
||||
distPath := findDistPath()
|
||||
|
||||
if distPath != "" {
|
||||
r.Static("/assets", filepath.Join(distPath, "assets"))
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
|
||||
c.JSON(404, gin.H{"code": 404, "message": "API endpoint not found"})
|
||||
return
|
||||
}
|
||||
c.File(filepath.Join(distPath, "index.html"))
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// findDistPath finds the frontend dist directory
|
||||
func findDistPath() string {
|
||||
// Try relative to executable
|
||||
if ex, err := os.Executable(); err == nil {
|
||||
execDir := filepath.Dir(ex)
|
||||
// If running via "go run", executable is in temp dir - fall through
|
||||
if !strings.Contains(execDir, "/tmp/go-build") {
|
||||
for _, rel := range []string{"dist", "../dist", "../../dist"} {
|
||||
p := filepath.Join(execDir, rel)
|
||||
if _, err := os.Stat(filepath.Join(p, "index.html")); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try working directory relative paths
|
||||
for _, p := range []string{"./dist", "../dist", "/app/dist"} {
|
||||
if abs, err := filepath.Abs(p); err == nil {
|
||||
if _, err := os.Stat(filepath.Join(abs, "index.html")); err == nil {
|
||||
return abs
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
func Seed() {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
hasUsers := len(db.Users) > 0
|
||||
db.RUnlock()
|
||||
|
||||
if !hasUsers {
|
||||
hash, _ := utils.HashPassword("admin123")
|
||||
models.GetDB().CreateUser(&models.User{
|
||||
Username: "admin",
|
||||
Password: hash,
|
||||
Nickname: "Administrator",
|
||||
Role: "admin",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
type AuthService struct{}
|
||||
|
||||
func NewAuthService() *AuthService {
|
||||
return &AuthService{}
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(username, password string) (*models.User, string, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Users {
|
||||
if db.Users[i].Username == username {
|
||||
if !utils.VerifyPassword(password, db.Users[i].Password) {
|
||||
return nil, "", utils.ErrInvalidCredentials
|
||||
}
|
||||
token, err := utils.GenerateToken(db.Users[i].ID, db.Users[i].Username, db.Users[i].Role)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return &db.Users[i], token, nil
|
||||
}
|
||||
}
|
||||
return nil, "", utils.ErrInvalidCredentials
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(username, password, nickname, role string) (*models.User, error) {
|
||||
hash, err := utils.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.User{
|
||||
ID: models.GetDB().NextID("users"),
|
||||
Username: username,
|
||||
Password: hash,
|
||||
Nickname: nickname,
|
||||
Role: role,
|
||||
}
|
||||
if err := models.GetDB().CreateUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) GetUserByID(id uint) (*models.User, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Users {
|
||||
if db.Users[i].ID == id {
|
||||
u := db.Users[i]
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Users {
|
||||
if db.Users[i].ID == userID {
|
||||
if !utils.VerifyPassword(oldPassword, db.Users[i].Password) {
|
||||
return utils.ErrInvalidCredentials
|
||||
}
|
||||
hash, err := utils.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Users[i].Password = hash
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
type ClusterService struct{}
|
||||
|
||||
func NewClusterService() *ClusterService {
|
||||
return &ClusterService{}
|
||||
}
|
||||
|
||||
func SaveKubeConfig(name, content string) (string, error) {
|
||||
dir := filepath.Join("data", "kubeconfigs")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := filepath.Join(dir, fmt.Sprintf("%s.yaml", name))
|
||||
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Create 创建集群。token 接入时自动构造等价 kubeconfig,统一存储,使部署模块同样可用。
|
||||
func (s *ClusterService) Create(name, description, configType, apiServer, kubeconfig, token, contextName string) (*models.Cluster, error) {
|
||||
if configType == "" {
|
||||
configType = "kubeconfig"
|
||||
}
|
||||
|
||||
finalKubeconfig := kubeconfig
|
||||
if configType == "token" {
|
||||
finalKubeconfig = BuildKubeconfigFromToken(apiServer, token)
|
||||
}
|
||||
// 展示用 APIServer:token 模式取入参,kubeconfig 模式从内容提取
|
||||
if apiServer == "" {
|
||||
apiServer = ExtractAPIServer(finalKubeconfig)
|
||||
}
|
||||
|
||||
path, err := SaveKubeConfig(name, finalKubeconfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kubeconfig 保存失败: %w", err)
|
||||
}
|
||||
|
||||
cluster := models.Cluster{
|
||||
ID: models.GetDB().NextID("clusters"),
|
||||
Name: name,
|
||||
Description: description,
|
||||
ConfigType: configType,
|
||||
APIServer: apiServer,
|
||||
KubeConfig: finalKubeconfig,
|
||||
KubeConfigPath: path,
|
||||
Context: contextName,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
db.Clusters = append(db.Clusters, cluster)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
|
||||
go s.Validate(cluster.ID)
|
||||
return &cluster, nil
|
||||
}
|
||||
|
||||
func (s *ClusterService) Get(id uint) (*models.Cluster, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Clusters {
|
||||
if db.Clusters[i].ID == id {
|
||||
c := db.Clusters[i]
|
||||
return &c, nil
|
||||
}
|
||||
}
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *ClusterService) List(page, pageSize int) ([]models.Cluster, int64, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
total := int64(len(db.Clusters))
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 || start >= len(db.Clusters) {
|
||||
return []models.Cluster{}, total, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(db.Clusters) {
|
||||
end = len(db.Clusters)
|
||||
}
|
||||
result := make([]models.Cluster, end-start)
|
||||
copy(result, db.Clusters[start:end])
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s *ClusterService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Clusters {
|
||||
if db.Clusters[i].ID == id {
|
||||
if v, ok := updates["name"].(string); ok && v != "" {
|
||||
db.Clusters[i].Name = v
|
||||
}
|
||||
if v, ok := updates["description"].(string); ok {
|
||||
db.Clusters[i].Description = v
|
||||
}
|
||||
if v, ok := updates["status"].(string); ok && v != "" {
|
||||
db.Clusters[i].Status = v
|
||||
}
|
||||
if v, ok := updates["version"].(string); ok {
|
||||
db.Clusters[i].Version = v
|
||||
}
|
||||
if v, ok := updates["nodesCount"].(int); ok {
|
||||
db.Clusters[i].NodesCount = v
|
||||
}
|
||||
if v, ok := updates["context"].(string); ok {
|
||||
db.Clusters[i].Context = v
|
||||
}
|
||||
if v, ok := updates["lastConnected"].(time.Time); ok {
|
||||
db.Clusters[i].LastConnected = v
|
||||
}
|
||||
// 接入方式与展示字段
|
||||
if v, ok := updates["configType"].(string); ok && v != "" {
|
||||
db.Clusters[i].ConfigType = v
|
||||
}
|
||||
if v, ok := updates["apiServer"].(string); ok {
|
||||
db.Clusters[i].APIServer = v
|
||||
}
|
||||
// 凭据字段:空值不覆盖,避免编辑其它项时清空
|
||||
if v, ok := updates["kubeconfig"].(string); ok && v != "" {
|
||||
db.Clusters[i].KubeConfig = v
|
||||
if db.Clusters[i].KubeConfigPath != "" {
|
||||
_ = os.WriteFile(db.Clusters[i].KubeConfigPath, []byte(v), 0600)
|
||||
}
|
||||
}
|
||||
db.Clusters[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *ClusterService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, c := range db.Clusters {
|
||||
if c.ID == id {
|
||||
if c.KubeConfigPath != "" {
|
||||
_ = os.Remove(c.KubeConfigPath)
|
||||
}
|
||||
db.Clusters = append(db.Clusters[:i], db.Clusters[i+1:]...)
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *ClusterService) Validate(id uint) {
|
||||
cluster, err := s.Get(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
info, err := GetClusterInfo(ctx, cluster)
|
||||
if err != nil {
|
||||
s.Update(id, map[string]interface{}{"status": "error"})
|
||||
return
|
||||
}
|
||||
|
||||
s.Update(id, map[string]interface{}{
|
||||
"status": "active",
|
||||
"version": info.Version,
|
||||
"nodesCount": len(info.Nodes),
|
||||
"lastConnected": time.Now(),
|
||||
"context": info.CurrentCtx,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"lokideploy/models"
|
||||
)
|
||||
|
||||
type ClusterInfo struct {
|
||||
Version string `json:"version"`
|
||||
Nodes []NodeInfo `json:"nodes"`
|
||||
Namespaces []string `json:"namespaces"`
|
||||
Contexts []string `json:"contexts"`
|
||||
CurrentCtx string `json:"currentContext"`
|
||||
}
|
||||
|
||||
type NodeInfo struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Roles []string `json:"roles"`
|
||||
Version string `json:"version"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
InternalIP string `json:"internalIP"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// GetClusterInfo 通过 kubectl 查询集群信息。
|
||||
// Token 接入的集群在创建时已构造等价 kubeconfig,因此 kubectl 同样适用。
|
||||
func GetClusterInfo(ctx context.Context, cluster *models.Cluster) (*ClusterInfo, error) {
|
||||
kubeconfig, err := resolveKubeconfig(cluster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubeFile, err := os.CreateTemp("", "kube-*.yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(kubeFile.Name())
|
||||
kubeFile.WriteString(kubeconfig)
|
||||
kubeFile.Close()
|
||||
|
||||
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name()))
|
||||
|
||||
info := &ClusterInfo{
|
||||
Nodes: []NodeInfo{},
|
||||
Namespaces: []string{},
|
||||
Contexts: []string{},
|
||||
}
|
||||
|
||||
// Version(best-effort)
|
||||
if versionOut, err := runCmd(ctx, env, "kubectl", "version", "-o", "json"); err == nil {
|
||||
var versionData struct {
|
||||
ServerVersion struct {
|
||||
GitVersion string `json:"gitVersion"`
|
||||
} `json:"serverVersion"`
|
||||
}
|
||||
json.Unmarshal(versionOut, &versionData)
|
||||
info.Version = versionData.ServerVersion.GitVersion
|
||||
}
|
||||
|
||||
// Nodes
|
||||
nodesOut, err := runCmd(ctx, env, "kubectl", "get", "nodes", "-o", "json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取节点列表失败: %v", err)
|
||||
}
|
||||
var nodesData struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
} `json:"metadata"`
|
||||
Status struct {
|
||||
Conditions []struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
} `json:"conditions"`
|
||||
NodeInfo struct {
|
||||
KubeletVersion string `json:"kubeletVersion"`
|
||||
OperatingSystem string `json:"operatingSystem"`
|
||||
Architecture string `json:"architecture"`
|
||||
} `json:"nodeInfo"`
|
||||
Addresses []struct {
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
} `json:"addresses"`
|
||||
} `json:"status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
json.Unmarshal(nodesOut, &nodesData)
|
||||
|
||||
for _, item := range nodesData.Items {
|
||||
status := "Unknown"
|
||||
for _, cond := range item.Status.Conditions {
|
||||
if cond.Type == "Ready" {
|
||||
if cond.Status == "True" {
|
||||
status = "Ready"
|
||||
} else {
|
||||
status = "NotReady"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
var roles []string
|
||||
for label := range item.Metadata.Labels {
|
||||
if strings.HasPrefix(label, "node-role.kubernetes.io/") {
|
||||
roles = append(roles, label[len("node-role.kubernetes.io/"):])
|
||||
}
|
||||
}
|
||||
if len(roles) == 0 {
|
||||
roles = []string{"worker"}
|
||||
}
|
||||
internalIP := ""
|
||||
for _, addr := range item.Status.Addresses {
|
||||
if addr.Type == "InternalIP" {
|
||||
internalIP = addr.Address
|
||||
break
|
||||
}
|
||||
}
|
||||
info.Nodes = append(info.Nodes, NodeInfo{
|
||||
Name: item.Metadata.Name,
|
||||
Status: status,
|
||||
Roles: roles,
|
||||
Version: item.Status.NodeInfo.KubeletVersion,
|
||||
OS: item.Status.NodeInfo.OperatingSystem,
|
||||
Arch: item.Status.NodeInfo.Architecture,
|
||||
InternalIP: internalIP,
|
||||
Labels: item.Metadata.Labels,
|
||||
})
|
||||
}
|
||||
|
||||
// Namespaces(best-effort)
|
||||
if nsOut, err := runCmd(ctx, env, "kubectl", "get", "ns", "-o", "jsonpath={.items[*].metadata.name}"); err == nil {
|
||||
info.Namespaces = strings.Fields(string(nsOut))
|
||||
}
|
||||
|
||||
// Contexts(best-effort)
|
||||
if ctxOut, err := runCmd(ctx, env, "kubectl", "config", "get-contexts", "-o", "name"); err == nil {
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(ctxOut)), "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
info.Contexts = append(info.Contexts, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current context(best-effort)
|
||||
if currentCtxOut, err := runCmd(ctx, env, "kubectl", "config", "current-context"); err == nil {
|
||||
info.CurrentCtx = strings.TrimSpace(string(currentCtxOut))
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func runCmd(ctx context.Context, env []string, name string, args ...string) ([]byte, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Env = env
|
||||
return cmd.CombinedOutput()
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
type DeploymentService struct{}
|
||||
|
||||
func NewDeploymentService() *DeploymentService {
|
||||
return &DeploymentService{}
|
||||
}
|
||||
|
||||
type CreateDeploymentRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Mode string `json:"mode" binding:"required"`
|
||||
ClusterID *uint `json:"clusterID"`
|
||||
HostID *uint `json:"hostID"`
|
||||
Namespace string `json:"namespace"`
|
||||
StorageConfigID uint `json:"storageConfigID"`
|
||||
AuthEnabled bool `json:"authEnabled"`
|
||||
RetentionDays int `json:"retentionDays"`
|
||||
Replicas int `json:"replicas"`
|
||||
ResourcesCPU string `json:"resourcesCPU"`
|
||||
ResourcesMem string `json:"resourcesMem"`
|
||||
StorageSize string `json:"storageSize"`
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDeployment, error) {
|
||||
db := models.GetDB()
|
||||
d := models.LokiDeployment{
|
||||
ID: db.NextID("deployments"),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
Mode: req.Mode,
|
||||
ClusterID: req.ClusterID,
|
||||
HostID: req.HostID,
|
||||
Namespace: req.Namespace,
|
||||
StorageConfigID: req.StorageConfigID,
|
||||
AuthEnabled: req.AuthEnabled,
|
||||
RetentionDays: req.RetentionDays,
|
||||
Replicas: req.Replicas,
|
||||
ResourcesCPU: req.ResourcesCPU,
|
||||
ResourcesMem: req.ResourcesMem,
|
||||
StorageSize: req.StorageSize,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
d.BeforeCreate()
|
||||
|
||||
// Load relations
|
||||
if d.ClusterID != nil && *d.ClusterID > 0 {
|
||||
for i := range db.Clusters {
|
||||
if db.Clusters[i].ID == *d.ClusterID {
|
||||
c := db.Clusters[i]
|
||||
d.Cluster = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.HostID != nil && *d.HostID > 0 {
|
||||
for i := range db.Hosts {
|
||||
if db.Hosts[i].ID == *d.HostID {
|
||||
h := db.Hosts[i]
|
||||
d.Host = &h
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.StorageConfigID > 0 {
|
||||
for i := range db.Storages {
|
||||
if db.Storages[i].ID == d.StorageConfigID {
|
||||
st := db.Storages[i]
|
||||
d.StorageConfig = &st
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.Lock()
|
||||
db.Deployments = append(db.Deployments, d)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Deployments {
|
||||
if db.Deployments[i].ID == id {
|
||||
d := db.Deployments[i]
|
||||
s.loadRelations(&d)
|
||||
return &d, nil
|
||||
}
|
||||
}
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, int64, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
total := int64(len(db.Deployments))
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 || start >= len(db.Deployments) {
|
||||
return []models.LokiDeployment{}, total, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(db.Deployments) {
|
||||
end = len(db.Deployments)
|
||||
}
|
||||
result := make([]models.LokiDeployment, end-start)
|
||||
copy(result, db.Deployments[start:end])
|
||||
for i := range result {
|
||||
s.loadRelations(&result[i])
|
||||
}
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Deployments {
|
||||
if db.Deployments[i].ID == id {
|
||||
if v, ok := updates["status"].(string); ok {
|
||||
db.Deployments[i].Status = v
|
||||
}
|
||||
if v, ok := updates["statusMessage"].(string); ok {
|
||||
db.Deployments[i].StatusMessage = v
|
||||
}
|
||||
if v, ok := updates["endpoint"].(string); ok {
|
||||
db.Deployments[i].Endpoint = v
|
||||
}
|
||||
db.Deployments[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, d := range db.Deployments {
|
||||
if d.ID == id {
|
||||
db.Deployments = append(db.Deployments[:i], db.Deployments[i+1:]...)
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *DeploymentService) StartDeploy(id uint) error {
|
||||
deployment, err := s.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if deployment.Status == "deploying" {
|
||||
return fmt.Errorf("部署正在进行中,请勿重复操作")
|
||||
}
|
||||
s.Update(id, map[string]interface{}{"status": "deploying"})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
logFunc := func(level, message, detail string) {
|
||||
s.AddLog(id, level, message, detail)
|
||||
}
|
||||
|
||||
deployer := NewLokiDeployer(deployment, logFunc)
|
||||
go func() {
|
||||
err := deployer.Deploy(ctx)
|
||||
if err != nil {
|
||||
s.Update(id, map[string]interface{}{
|
||||
"status": "failed",
|
||||
"statusMessage": fmt.Sprintf("Deployment failed: %v", err),
|
||||
})
|
||||
s.AddLog(id, "error", fmt.Sprintf("Deployment failed: %v", err), "")
|
||||
} else {
|
||||
s.Update(id, map[string]interface{}{
|
||||
"status": "running",
|
||||
"statusMessage": "Deployment completed successfully",
|
||||
"endpoint": s.getEndpoint(deployment),
|
||||
})
|
||||
s.AddLog(id, "success", "Deployment completed successfully", "")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) StopDeployment(id uint) error {
|
||||
deployment, err := s.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if deployment.Status != "running" && deployment.Status != "deploying" {
|
||||
return fmt.Errorf("部署未在运行,无法停止")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
CleanupLoki(ctx, deployment)
|
||||
return s.Update(id, map[string]interface{}{
|
||||
"status": "stopped",
|
||||
"statusMessage": "Deployment stopped by user",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeploymentService) RestartDeployment(id uint) error {
|
||||
if err := s.StopDeployment(id); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
return s.StartDeploy(id)
|
||||
}
|
||||
|
||||
func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail string) {
|
||||
db := models.GetDB()
|
||||
log := models.DeploymentLog{
|
||||
ID: db.NextID("logs"),
|
||||
DeploymentID: deploymentID,
|
||||
Level: level,
|
||||
Message: message,
|
||||
Detail: detail,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Lock()
|
||||
db.Logs = append(db.Logs, log)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
}
|
||||
|
||||
func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]models.DeploymentLog, int64, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
var filtered []models.DeploymentLog
|
||||
for i := len(db.Logs) - 1; i >= 0; i-- {
|
||||
if db.Logs[i].DeploymentID == deploymentID {
|
||||
filtered = append(filtered, db.Logs[i])
|
||||
}
|
||||
}
|
||||
|
||||
total := int64(len(filtered))
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(filtered) {
|
||||
return []models.DeploymentLog{}, total, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(filtered) {
|
||||
end = len(filtered)
|
||||
}
|
||||
return filtered[start:end], total, nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) GetStats() (map[string]interface{}, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
var running, failed, pending, stopped int64
|
||||
for _, d := range db.Deployments {
|
||||
switch d.Status {
|
||||
case "running":
|
||||
running++
|
||||
case "failed":
|
||||
failed++
|
||||
case "pending":
|
||||
pending++
|
||||
case "stopped":
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"totalDeployments": int64(len(db.Deployments)),
|
||||
"running": running,
|
||||
"failed": failed,
|
||||
"pending": pending,
|
||||
"stopped": stopped,
|
||||
"clusterCount": int64(len(db.Clusters)),
|
||||
"hostCount": int64(len(db.Hosts)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) loadRelations(d *models.LokiDeployment) {
|
||||
db := models.GetDB()
|
||||
if d.ClusterID != nil && *d.ClusterID > 0 {
|
||||
for i := range db.Clusters {
|
||||
if db.Clusters[i].ID == *d.ClusterID {
|
||||
c := db.Clusters[i]
|
||||
d.Cluster = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.HostID != nil && *d.HostID > 0 {
|
||||
for i := range db.Hosts {
|
||||
if db.Hosts[i].ID == *d.HostID {
|
||||
h := db.Hosts[i]
|
||||
d.Host = &h
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.StorageConfigID > 0 {
|
||||
for i := range db.Storages {
|
||||
if db.Storages[i].ID == d.StorageConfigID {
|
||||
st := db.Storages[i]
|
||||
d.StorageConfig = &st
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
|
||||
switch d.Mode {
|
||||
case "k8s-helm", "k8s-yaml":
|
||||
ns := d.Namespace
|
||||
if ns == "" {
|
||||
ns = "loki"
|
||||
}
|
||||
return fmt.Sprintf("http://loki.%s.svc.cluster.local:3100", ns)
|
||||
case "host-binary":
|
||||
if d.Host != nil {
|
||||
return fmt.Sprintf("http://%s:3100", d.Host.HostIP)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
type HostService struct{}
|
||||
|
||||
func NewHostService() *HostService {
|
||||
return &HostService{}
|
||||
}
|
||||
|
||||
func (s *HostService) Create(name, hostIP string, port int, user, authType, password, privateKey, keyPhrase, os, arch, labels string) (*models.Host, error) {
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
host := models.Host{
|
||||
ID: models.GetDB().NextID("hosts"),
|
||||
Name: name,
|
||||
HostIP: hostIP,
|
||||
Port: port,
|
||||
User: user,
|
||||
AuthType: authType,
|
||||
OS: os,
|
||||
Arch: arch,
|
||||
Labels: labels,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if password != "" {
|
||||
enc, _ := utils.Encrypt(password)
|
||||
host.Password = "ENC:" + enc
|
||||
}
|
||||
if privateKey != "" {
|
||||
enc, _ := utils.Encrypt(privateKey)
|
||||
host.PrivateKey = "ENC:" + enc
|
||||
}
|
||||
if keyPhrase != "" {
|
||||
enc, _ := utils.Encrypt(keyPhrase)
|
||||
host.KeyPhrase = "ENC:" + enc
|
||||
}
|
||||
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
db.Hosts = append(db.Hosts, host)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
|
||||
go s.Validate(host.ID)
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (s *HostService) Get(id uint) (*models.Host, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Hosts {
|
||||
if db.Hosts[i].ID == id {
|
||||
h := db.Hosts[i]
|
||||
return &h, nil
|
||||
}
|
||||
}
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *HostService) List(page, pageSize int) ([]models.Host, int64, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
total := int64(len(db.Hosts))
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 || start >= len(db.Hosts) {
|
||||
return []models.Host{}, total, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(db.Hosts) {
|
||||
end = len(db.Hosts)
|
||||
}
|
||||
result := make([]models.Host, end-start)
|
||||
copy(result, db.Hosts[start:end])
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s *HostService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Hosts {
|
||||
if db.Hosts[i].ID == id {
|
||||
if v, ok := updates["name"].(string); ok {
|
||||
db.Hosts[i].Name = v
|
||||
}
|
||||
if v, ok := updates["status"].(string); ok {
|
||||
db.Hosts[i].Status = v
|
||||
}
|
||||
if v, ok := updates["os"].(string); ok {
|
||||
db.Hosts[i].OS = v
|
||||
}
|
||||
if v, ok := updates["arch"].(string); ok {
|
||||
db.Hosts[i].Arch = v
|
||||
}
|
||||
db.Hosts[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *HostService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, h := range db.Hosts {
|
||||
if h.ID == id {
|
||||
db.Hosts = append(db.Hosts[:i], db.Hosts[i+1:]...)
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *HostService) Validate(id uint) {
|
||||
host, err := s.Get(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
password := host.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password, _ = utils.Decrypt(password[4:])
|
||||
}
|
||||
|
||||
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
if err != nil {
|
||||
s.Update(id, map[string]interface{}{"status": "error"})
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if info, err := client.GetHostInfo(ctx); err == nil {
|
||||
s.Update(id, map[string]interface{}{"status": "active", "os": info.OS, "arch": info.Arch})
|
||||
} else {
|
||||
s.Update(id, map[string]interface{}{"status": "active"})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HostService) ExecuteCommand(id uint, command string) (string, string, error) {
|
||||
host, err := s.Get(id)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
password := host.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password, _ = utils.Decrypt(password[4:])
|
||||
}
|
||||
|
||||
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer client.Close()
|
||||
return client.Execute(ctx, command)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"lokideploy/models"
|
||||
)
|
||||
|
||||
// resolveKubeconfig 取集群最终的 kubeconfig 文本(优先内存字段,其次磁盘文件)
|
||||
func resolveKubeconfig(cluster *models.Cluster) (string, error) {
|
||||
if cluster.KubeConfig != "" {
|
||||
return cluster.KubeConfig, nil
|
||||
}
|
||||
if cluster.KubeConfigPath != "" {
|
||||
data, err := os.ReadFile(cluster.KubeConfigPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取 kubeconfig 文件失败: %v", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
return "", fmt.Errorf("集群未配置 kubeconfig")
|
||||
}
|
||||
|
||||
// BuildKubeconfigFromToken 用 API Server + Token 构造最小 kubeconfig
|
||||
// 采用 insecure-skip-tls-verify,便于内网/自签证书集群接入
|
||||
func BuildKubeconfigFromToken(apiServer, token string) string {
|
||||
return fmt.Sprintf(`apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: %s
|
||||
name: loki-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: loki-cluster
|
||||
user: loki-user
|
||||
name: loki-context
|
||||
current-context: loki-context
|
||||
users:
|
||||
- name: loki-user
|
||||
user:
|
||||
token: %s
|
||||
`, apiServer, token)
|
||||
}
|
||||
|
||||
var apiServerRe = regexp.MustCompile(`(?m)^\s*server:\s*(\S+)`)
|
||||
|
||||
// ExtractAPIServer 从 kubeconfig 文本提取 server 地址(展示用)
|
||||
func ExtractAPIServer(kubeconfig string) string {
|
||||
m := apiServerRe.FindStringSubmatch(kubeconfig)
|
||||
if len(m) >= 2 {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
type LokiDeployer struct {
|
||||
deployment *models.LokiDeployment
|
||||
logFunc func(level, message, detail string)
|
||||
}
|
||||
|
||||
func NewLokiDeployer(deployment *models.LokiDeployment, logFunc func(level, message, detail string)) *LokiDeployer {
|
||||
return &LokiDeployer{deployment: deployment, logFunc: logFunc}
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) Deploy(ctx context.Context) error {
|
||||
d.log("info", fmt.Sprintf("Starting Loki deployment: %s (mode: %s)", d.deployment.Name, d.deployment.Mode), "")
|
||||
switch d.deployment.Mode {
|
||||
case "k8s-helm":
|
||||
return d.deployHelm(ctx)
|
||||
case "k8s-yaml":
|
||||
return d.deployYaml(ctx)
|
||||
case "host-binary":
|
||||
return d.deployBinary(ctx)
|
||||
default:
|
||||
return fmt.Errorf("不支持的部署模式: %s", d.deployment.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) deployHelm(ctx context.Context) error {
|
||||
if d.deployment.Cluster == nil {
|
||||
return fmt.Errorf("请选择目标集群")
|
||||
}
|
||||
kubeconfig := d.deployment.Cluster.KubeConfig
|
||||
if kubeconfig == "" && d.deployment.Cluster.KubeConfigPath != "" {
|
||||
data, _ := os.ReadFile(d.deployment.Cluster.KubeConfigPath)
|
||||
kubeconfig = string(data)
|
||||
}
|
||||
|
||||
kubeFile, _ := os.CreateTemp("", "kube-*.yaml")
|
||||
defer os.Remove(kubeFile.Name())
|
||||
kubeFile.WriteString(kubeconfig)
|
||||
kubeFile.Close()
|
||||
|
||||
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name()))
|
||||
ns := d.deployment.Namespace
|
||||
if ns == "" {
|
||||
ns = "loki"
|
||||
}
|
||||
|
||||
d.log("info", fmt.Sprintf("Creating namespace %s...", ns), "")
|
||||
exec.CommandContext(ctx, "kubectl", "create", "namespace", ns, "--dry-run=client", "-o", "yaml").CombinedOutput()
|
||||
|
||||
d.log("info", "Adding Grafana Helm repo...", "")
|
||||
exec.CommandContext(ctx, "helm", "repo", "add", "grafana", "https://grafana.github.io/helm-charts").CombinedOutput()
|
||||
exec.CommandContext(ctx, "helm", "repo", "update").CombinedOutput()
|
||||
|
||||
d.log("info", fmt.Sprintf("Installing Loki in %s...", ns), "")
|
||||
releaseName := d.deployment.Name
|
||||
if releaseName == "" {
|
||||
releaseName = "loki"
|
||||
}
|
||||
|
||||
chart := "grafana/loki"
|
||||
if d.deployment.Replicas > 1 {
|
||||
chart = "grafana/loki-distributed"
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "helm", "upgrade", "--install", releaseName, chart,
|
||||
"--namespace", ns, "--create-namespace", "--wait", "--timeout", "10m")
|
||||
cmd.Env = env
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.log("error", fmt.Sprintf("Helm install failed: %v", err), string(out))
|
||||
return err
|
||||
}
|
||||
d.log("success", "Loki installed via Helm", string(out))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) deployYaml(ctx context.Context) error {
|
||||
if d.deployment.Cluster == nil {
|
||||
return fmt.Errorf("请选择目标集群")
|
||||
}
|
||||
kubeconfig := d.deployment.Cluster.KubeConfig
|
||||
if kubeconfig == "" && d.deployment.Cluster.KubeConfigPath != "" {
|
||||
data, _ := os.ReadFile(d.deployment.Cluster.KubeConfigPath)
|
||||
kubeconfig = string(data)
|
||||
}
|
||||
|
||||
kubeFile, _ := os.CreateTemp("", "kube-*.yaml")
|
||||
defer os.Remove(kubeFile.Name())
|
||||
kubeFile.WriteString(kubeconfig)
|
||||
kubeFile.Close()
|
||||
|
||||
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name()))
|
||||
ns := d.deployment.Namespace
|
||||
if ns == "" {
|
||||
ns = "loki"
|
||||
}
|
||||
|
||||
manifest := d.generateManifest(ns)
|
||||
manifestFile, _ := os.CreateTemp("", "loki-*.yaml")
|
||||
defer os.Remove(manifestFile.Name())
|
||||
manifestFile.WriteString(manifest)
|
||||
manifestFile.Close()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "kubectl", "apply", "-f", manifestFile.Name(), "-n", ns)
|
||||
cmd.Env = env
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.log("error", fmt.Sprintf("kubectl apply failed: %v", err), string(out))
|
||||
return err
|
||||
}
|
||||
d.log("success", "Loki deployed via YAML", string(out))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) deployBinary(ctx context.Context) error {
|
||||
if d.deployment.Host == nil {
|
||||
return fmt.Errorf("请选择目标主机")
|
||||
}
|
||||
host := d.deployment.Host
|
||||
password := host.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password, _ = DecryptCred(password)
|
||||
}
|
||||
|
||||
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
d.log("info", "SSH connected, downloading Loki...", "")
|
||||
arch, _, _ := client.Execute(ctx, "uname -m")
|
||||
lokiArch := strings.TrimSpace(arch)
|
||||
switch lokiArch {
|
||||
case "x86_64":
|
||||
lokiArch = "amd64"
|
||||
case "aarch64":
|
||||
lokiArch = "arm64"
|
||||
}
|
||||
|
||||
version := d.deployment.Version
|
||||
if version == "" {
|
||||
version = "3.0.0"
|
||||
}
|
||||
|
||||
client.Execute(ctx, "mkdir -p /opt/loki /var/lib/loki /etc/loki")
|
||||
|
||||
downloadURL := fmt.Sprintf("https://github.com/grafana/loki/releases/download/v%s/loki-linux-%s.zip", version, lokiArch)
|
||||
cmd := fmt.Sprintf("cd /tmp && curl -sL -o loki.zip '%s' && unzip -o loki.zip -d /opt/loki/ && chmod +x /opt/loki/loki-linux-%s", downloadURL, lokiArch)
|
||||
client.Execute(ctx, cmd)
|
||||
client.Execute(ctx, "ln -sf /opt/loki/loki-linux-* /usr/local/bin/loki 2>/dev/null || true")
|
||||
|
||||
// Upload config
|
||||
config := d.generateLokiConfig()
|
||||
client.Execute(ctx, fmt.Sprintf("cat > /etc/loki/loki.yaml << 'EOF'\n%s\nEOF", config))
|
||||
|
||||
// Systemd service
|
||||
service := `[Unit]
|
||||
Description=Grafana Loki
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/loki -config.file=/etc/loki/loki.yaml
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
client.Execute(ctx, fmt.Sprintf("cat > /etc/systemd/system/loki.service << 'EOF'\n%s\nEOF", service))
|
||||
client.Execute(ctx, "systemctl daemon-reload && systemctl enable loki && systemctl restart loki")
|
||||
|
||||
d.log("success", fmt.Sprintf("Loki deployed on %s", host.HostIP), "")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) generateManifest(ns string) string {
|
||||
return fmt.Sprintf(`apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: %s
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: loki
|
||||
namespace: %s
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: loki
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: loki
|
||||
spec:
|
||||
containers:
|
||||
- name: loki
|
||||
image: grafana/loki:%s
|
||||
ports:
|
||||
- containerPort: 3100
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: loki
|
||||
namespace: %s
|
||||
spec:
|
||||
ports:
|
||||
- port: 3100
|
||||
selector:
|
||||
app: loki
|
||||
`, ns, ns, d.deployment.Version, ns)
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) generateLokiConfig() string {
|
||||
return fmt.Sprintf(`auth_enabled: false
|
||||
server:
|
||||
http_listen_port: 3100
|
||||
limits_config:
|
||||
retention_period: %dh
|
||||
schema_config:
|
||||
configs:
|
||||
- from: 2020-10-24
|
||||
store: tsdb
|
||||
object_store: filesystem
|
||||
schema: v13
|
||||
index:
|
||||
prefix: index_
|
||||
period: 24h
|
||||
`, d.deployment.RetentionDays*24)
|
||||
}
|
||||
|
||||
func (d *LokiDeployer) log(level, message, detail string) {
|
||||
if d.logFunc != nil {
|
||||
d.logFunc(level, message, detail)
|
||||
}
|
||||
}
|
||||
|
||||
func CleanupLoki(ctx context.Context, deployment *models.LokiDeployment) {
|
||||
if deployment.Mode == "k8s-helm" && deployment.Cluster != nil {
|
||||
kubeconfig := deployment.Cluster.KubeConfig
|
||||
if kubeconfig == "" && deployment.Cluster.KubeConfigPath != "" {
|
||||
data, _ := os.ReadFile(deployment.Cluster.KubeConfigPath)
|
||||
kubeconfig = string(data)
|
||||
}
|
||||
kubeFile, _ := os.CreateTemp("", "kube-*.yaml")
|
||||
defer os.Remove(kubeFile.Name())
|
||||
os.WriteFile(kubeFile.Name(), []byte(kubeconfig), 0600)
|
||||
env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name()))
|
||||
cmd := exec.CommandContext(ctx, "helm", "uninstall", deployment.Name, "--namespace", deployment.Namespace)
|
||||
cmd.Env = env
|
||||
cmd.CombinedOutput()
|
||||
}
|
||||
if deployment.Mode == "host-binary" && deployment.Host != nil {
|
||||
host := deployment.Host
|
||||
password := host.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password, _ = DecryptCred(password)
|
||||
}
|
||||
client, _ := NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
if client != nil {
|
||||
defer client.Close()
|
||||
client.Execute(ctx, "systemctl stop loki && systemctl disable loki && rm -f /etc/systemd/system/loki.service /usr/local/bin/loki")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DecryptCred(encrypted string) (string, error) {
|
||||
if strings.HasPrefix(encrypted, "ENC:") {
|
||||
return utils.Decrypt(encrypted[4:])
|
||||
}
|
||||
return encrypted, nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type SSHClient struct {
|
||||
client *ssh.Client
|
||||
}
|
||||
|
||||
type HostInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
Kernel string `json:"kernel"`
|
||||
CPU string `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
Disk string `json:"disk"`
|
||||
Uptime string `json:"uptime"`
|
||||
|
||||
// 资源使用率(百分比)
|
||||
CPUUsage float64 `json:"cpuUsage"`
|
||||
MemUsage float64 `json:"memUsage"`
|
||||
DiskUsage float64 `json:"diskUsage"`
|
||||
// 资源已用/总量
|
||||
MemTotal string `json:"memTotal"`
|
||||
MemUsed string `json:"memUsed"`
|
||||
DiskTotal string `json:"diskTotal"`
|
||||
DiskUsed string `json:"diskUsed"`
|
||||
}
|
||||
|
||||
func NewSSHClient(host string, port int, user, password string) (*SSHClient, error) {
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 10 * time.Second,
|
||||
Auth: []ssh.AuthMethod{ssh.Password(password)},
|
||||
}
|
||||
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), config)
|
||||
if err != nil {
|
||||
return nil, errors.New(FormatSSHError(err))
|
||||
}
|
||||
return &SSHClient{client: client}, nil
|
||||
}
|
||||
|
||||
func (c *SSHClient) Execute(ctx context.Context, command string) (string, string, error) {
|
||||
session, err := c.client.NewSession()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
session.Stdout = &stdout
|
||||
session.Stderr = &stderr
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- session.Run(command) }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
return stdout.String(), stderr.String(), err
|
||||
case <-ctx.Done():
|
||||
session.Signal(ssh.SIGKILL)
|
||||
return "", "", ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// GetHostInfo 采集主机静态信息与资源使用率
|
||||
func (c *SSHClient) GetHostInfo(ctx context.Context) (*HostInfo, error) {
|
||||
info := &HostInfo{}
|
||||
|
||||
// 静态信息(逐条,best-effort)
|
||||
if out, _, err := c.Execute(ctx, "hostname"); err == nil {
|
||||
info.Hostname = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, `cat /etc/os-release | grep '^ID=' | cut -d= -f2 | tr -d '"'`); err == nil {
|
||||
info.OS = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, `cat /etc/os-release | grep 'VERSION_ID=' | cut -d= -f2 | tr -d '"'`); err == nil {
|
||||
info.Version = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, "uname -m"); err == nil {
|
||||
info.Arch = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, "uname -r"); err == nil {
|
||||
info.Kernel = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, "nproc"); err == nil {
|
||||
info.CPU = strings.TrimSpace(out) + " 核"
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, "free -h | awk '/^Mem:/ {print $2}'"); err == nil {
|
||||
info.Memory = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, "df -h / | awk 'NR==2 {print $2}'"); err == nil {
|
||||
info.Disk = strings.TrimSpace(out)
|
||||
}
|
||||
if out, _, err := c.Execute(ctx, "uptime -p 2>/dev/null || uptime"); err == nil {
|
||||
info.Uptime = strings.TrimSpace(out)
|
||||
}
|
||||
|
||||
// 资源使用率:一次合并命令(减少 SSH 往返)
|
||||
// 输出形如:CPUUSAGE:12.3 / MEM:16384:8192 / DISK:51200:25600:50%
|
||||
metricScript := `top -bn1 2>/dev/null | awk '/Cpu\(s\)/{print "CPUUSAGE:"100-$8; exit}'
|
||||
free -m | awk '/^Mem:/{print "MEM:"$2":"$3}'
|
||||
df -m / | awk 'NR==2{print "DISK:"$2":"$3":"$5}'`
|
||||
if out, _, err := c.Execute(ctx, metricScript); err == nil {
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "CPUUSAGE:"):
|
||||
info.CPUUsage, _ = strconv.ParseFloat(strings.TrimPrefix(line, "CPUUSAGE:"), 64)
|
||||
case strings.HasPrefix(line, "MEM:"):
|
||||
parts := strings.Split(strings.TrimPrefix(line, "MEM:"), ":")
|
||||
if len(parts) == 2 {
|
||||
total, _ := strconv.Atoi(parts[0])
|
||||
used, _ := strconv.Atoi(parts[1])
|
||||
info.MemTotal = fmt.Sprintf("%d MB", total)
|
||||
info.MemUsed = fmt.Sprintf("%d MB", used)
|
||||
if total > 0 {
|
||||
info.MemUsage = float64(used) / float64(total) * 100
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(line, "DISK:"):
|
||||
parts := strings.Split(strings.TrimPrefix(line, "DISK:"), ":")
|
||||
if len(parts) >= 3 {
|
||||
info.DiskTotal = parts[0] + " MB"
|
||||
info.DiskUsed = parts[1] + " MB"
|
||||
pct := strings.TrimSuffix(strings.TrimSpace(parts[2]), "%")
|
||||
info.DiskUsage, _ = strconv.ParseFloat(pct, 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *SSHClient) Close() error {
|
||||
if c.client != nil {
|
||||
return c.client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSSHConnection(host string, port int, user, password string) error {
|
||||
client, err := NewSSHClient(host, port, user, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, _, err = client.Execute(ctx, "echo 'connected'")
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package services
|
||||
|
||||
import "strings"
|
||||
|
||||
// FormatSSHError 将底层 SSH 错误翻译为中文友好提示,便于用户排查
|
||||
func FormatSSHError(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
msg := err.Error()
|
||||
lower := strings.ToLower(msg)
|
||||
|
||||
switch {
|
||||
case strings.Contains(lower, "i/o timeout") ||
|
||||
strings.Contains(lower, "deadline exceeded") ||
|
||||
strings.Contains(lower, "connection timed out"):
|
||||
return "连接超时,请检查主机网络或 SSH 端口是否可达"
|
||||
case strings.Contains(lower, "connection refused"):
|
||||
return "连接被拒绝,目标端口未开放或 SSH 服务未运行"
|
||||
case strings.Contains(lower, "no such host") ||
|
||||
strings.Contains(lower, "no route to host"):
|
||||
return "无法解析或到达目标主机,请检查 IP 地址"
|
||||
case strings.Contains(lower, "permission denied") ||
|
||||
strings.Contains(lower, "unable to authenticate") ||
|
||||
strings.Contains(lower, "handshake") ||
|
||||
strings.Contains(lower, "password"):
|
||||
return "认证失败,请检查用户名、密码或 SSH 密钥"
|
||||
case strings.Contains(lower, "no supported methods"):
|
||||
return "SSH 协商失败,服务端未支持当前认证方式"
|
||||
default:
|
||||
return msg
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
type StorageService struct{}
|
||||
|
||||
func NewStorageService() *StorageService {
|
||||
return &StorageService{}
|
||||
}
|
||||
|
||||
func (s *StorageService) Create(name, storageType, endpoint, bucket, region, accessKey, secretKey, pathPrefix string, insecure, isDefault bool) (*models.StorageConfig, error) {
|
||||
encAccessKey, _ := utils.Encrypt(accessKey)
|
||||
encSecretKey, _ := utils.Encrypt(secretKey)
|
||||
|
||||
storage := models.StorageConfig{
|
||||
ID: models.GetDB().NextID("storages"),
|
||||
Name: name,
|
||||
StorageType: storageType,
|
||||
Endpoint: endpoint,
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
AccessKey: "ENC:" + encAccessKey,
|
||||
SecretKey: "ENC:" + encSecretKey,
|
||||
PathPrefix: pathPrefix,
|
||||
Insecure: insecure,
|
||||
IsDefault: isDefault,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
db := models.GetDB()
|
||||
if isDefault {
|
||||
db.Lock()
|
||||
for i := range db.Storages {
|
||||
db.Storages[i].IsDefault = false
|
||||
}
|
||||
db.Unlock()
|
||||
}
|
||||
db.Lock()
|
||||
db.Storages = append(db.Storages, storage)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
func (s *StorageService) Get(id uint) (*models.StorageConfig, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Storages {
|
||||
if db.Storages[i].ID == id {
|
||||
st := db.Storages[i]
|
||||
return &st, nil
|
||||
}
|
||||
}
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *StorageService) List(page, pageSize int) ([]models.StorageConfig, int64, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
total := int64(len(db.Storages))
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 || start >= len(db.Storages) {
|
||||
return []models.StorageConfig{}, total, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(db.Storages) {
|
||||
end = len(db.Storages)
|
||||
}
|
||||
result := make([]models.StorageConfig, end-start)
|
||||
copy(result, db.Storages[start:end])
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s *StorageService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Storages {
|
||||
if db.Storages[i].ID == id {
|
||||
if v, ok := updates["name"].(string); ok {
|
||||
db.Storages[i].Name = v
|
||||
}
|
||||
if v, ok := updates["endpoint"].(string); ok {
|
||||
db.Storages[i].Endpoint = v
|
||||
}
|
||||
if v, ok := updates["bucket"].(string); ok {
|
||||
db.Storages[i].Bucket = v
|
||||
}
|
||||
if v, ok := updates["region"].(string); ok {
|
||||
db.Storages[i].Region = v
|
||||
}
|
||||
if v, ok := updates["isDefault"].(bool); ok {
|
||||
if v {
|
||||
for j := range db.Storages {
|
||||
db.Storages[j].IsDefault = false
|
||||
}
|
||||
}
|
||||
db.Storages[i].IsDefault = v
|
||||
}
|
||||
db.Storages[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *StorageService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, st := range db.Storages {
|
||||
if st.ID == id {
|
||||
db.Storages = append(db.Storages[:i], db.Storages[i+1:]...)
|
||||
go db.Save()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *StorageService) GetDefault() (*models.StorageConfig, error) {
|
||||
db := models.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Storages {
|
||||
if db.Storages[i].IsDefault {
|
||||
st := db.Storages[i]
|
||||
return &st, nil
|
||||
}
|
||||
}
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"lokideploy/config"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)
|
||||
combined := append(salt, hash...)
|
||||
return base64.StdEncoding.EncodeToString(combined), nil
|
||||
}
|
||||
|
||||
func VerifyPassword(password, hashStr string) bool {
|
||||
combined, err := base64.StdEncoding.DecodeString(hashStr)
|
||||
if err != nil || len(combined) < 16 {
|
||||
return false
|
||||
}
|
||||
salt := combined[:16]
|
||||
hash := combined[16:]
|
||||
newHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)
|
||||
return subtle.ConstantTimeCompare(hash, newHash) == 1
|
||||
}
|
||||
|
||||
func Encrypt(plaintext string) (string, error) {
|
||||
key := []byte(config.GetConfig().JWTSecret)[:32]
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func Decrypt(ciphertext string) (string, error) {
|
||||
key := []byte(config.GetConfig().JWTSecret)[:32]
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(data) < gcm.NonceSize() {
|
||||
return "", errors.New("ciphertext too short")
|
||||
}
|
||||
nonce, ciphertextBytes := data[:gcm.NonceSize()], data[gcm.NonceSize():]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("用户名或密码错误")
|
||||
ErrNotFound = errors.New("资源不存在")
|
||||
ErrAlreadyExists = errors.New("资源已存在")
|
||||
ErrUnauthorized = errors.New("未登录或登录已过期")
|
||||
ErrForbidden = errors.New("没有操作权限")
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"lokideploy/config"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, username, role string) (string, error) {
|
||||
cfg := config.GetConfig()
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Subject: username,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(cfg.JWTSecret))
|
||||
}
|
||||
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
cfg := config.GetConfig()
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(cfg.JWTSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
func GetUserFromContext(c *gin.Context) (*Claims, bool) {
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
claims, ok := user.(*Claims)
|
||||
return claims, ok
|
||||
}
|
||||
|
||||
func IsAdmin(c *gin.Context) bool {
|
||||
claims, ok := GetUserFromContext(c)
|
||||
return ok && claims.Role == "admin"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type PageData struct {
|
||||
List interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{Code: 0, Message: "success", Data: data})
|
||||
}
|
||||
|
||||
func Error(c *gin.Context, code int, message string) {
|
||||
c.JSON(http.StatusOK, Response{Code: code, Message: message})
|
||||
}
|
||||
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
Error(c, 400, message)
|
||||
}
|
||||
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "未登录或登录已过期"
|
||||
}
|
||||
c.JSON(http.StatusUnauthorized, Response{Code: 401, Message: message})
|
||||
}
|
||||
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "没有操作权限"
|
||||
}
|
||||
c.JSON(http.StatusForbidden, Response{Code: 403, Message: message})
|
||||
}
|
||||
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "资源不存在"
|
||||
}
|
||||
Error(c, 404, message)
|
||||
}
|
||||
|
||||
func Pagination(c *gin.Context) (page, pageSize int) {
|
||||
page, pageSize = 1, 10
|
||||
if p := c.Query("page"); p != "" {
|
||||
if v, err := strconv.Atoi(p); err == nil && v > 0 {
|
||||
page = v
|
||||
}
|
||||
}
|
||||
if ps := c.Query("pageSize"); ps != "" {
|
||||
if v, err := strconv.Atoi(ps); err == nil && v > 0 && v <= 100 {
|
||||
pageSize = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func IntParam(c *gin.Context, key string) (int, error) {
|
||||
return strconv.Atoi(c.Param(key))
|
||||
}
|
||||
|
||||
func BindAndValidate(c *gin.Context, obj interface{}) bool {
|
||||
if err := c.ShouldBindJSON(obj); err != nil {
|
||||
BadRequest(c, "请求参数无效: "+err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user