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,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,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user