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:
Logan.LiandClaude committed 2026-06-29 17:03:57 +08:00
commit 2a93de600d
134 files changed
+19595

No files matched your search

+38
View File
@@ -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,
}
}
+111
View File
@@ -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
}
+43
View File
@@ -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()
}
}
+38
View File
@@ -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,
}
}
+12
View File
@@ -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"`
}
+68
View File
@@ -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
}
}
+7
View File
@@ -0,0 +1,7 @@
package models
type SystemSetting struct {
ID uint `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
}
+35
View File
@@ -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,
}
}
+24
View File
@@ -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,
}
}