refactor: 前后端目录结构模块化 + v2 优化移植与缺陷修复
前端: - src/srv/api → src/api,按域分文件夹(auth/cluster/host/storage/deployment/collector) - 新增 http.ts 引擎(http.get/post/put/delete),域模块改用语义化调用 - index.html 增加 favicon(匹配应用 logo 的 emerald + Activity 脉冲) 后端分层增强(保留包名): - 新增 repository/:从 models 拆出 JsonDB 持久化层(InitDB/GetDB/Shutdown/RequestSave) - 新增 dto/:从 controllers 抽出各域请求契约(带 binding 校验) - 新增 pkg/crypto、pkg/jwt:从 utils 拆出安全原语(外部 jwt 别名 jwtv5 避免冲突) - models 只留领域实体 + sanitizer;utils 只留 gin/应用相关(response/errors) v2 优化移植与缺陷修复: - 移植密钥分离/SSH TOFU/并发写安全/Loki 生产级配置/Docker 加固 - 修复 deployment.go 并发丢更新、Helm values 重复键、EncryptField 哨兵、loki-gateway 端点等审计缺陷 - 实现真正的优雅关停(main.go http.Server + signal + repository.Shutdown) - 修复登录契约:后端返回 accessToken,前端 auth.ts/Login.tsx 对齐 - 补 crypto/jwt/repository 单元测试(含并发写回归) 验证: go build/vet/test 全绿;npm run build 通过;登录→鉴权→优雅关停冒烟通过 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
fb8d10231a
commit
327e282356
93 files changed
+8433
-3089
No files matched your search
+19
-16
@@ -2,6 +2,9 @@ package services
|
||||
|
||||
import (
|
||||
"lokideploy/models"
|
||||
"lokideploy/pkg/crypto"
|
||||
"lokideploy/pkg/jwt"
|
||||
"lokideploy/repository"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
@@ -11,45 +14,45 @@ func NewAuthService() *AuthService {
|
||||
return &AuthService{}
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(username, password string) (*models.User, string, error) {
|
||||
db := models.GetDB()
|
||||
func (s *AuthService) Login(username, password string) (*models.User, *jwt.TokenPair, error) {
|
||||
db := repository.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
|
||||
if !crypto.VerifyPassword(password, db.Users[i].Password) {
|
||||
return nil, nil, utils.ErrInvalidCredentials
|
||||
}
|
||||
token, err := utils.GenerateToken(db.Users[i].ID, db.Users[i].Username, db.Users[i].Role)
|
||||
tokenPair, err := jwt.GenerateTokenPair(db.Users[i].ID, db.Users[i].Username, db.Users[i].Role)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
return &db.Users[i], token, nil
|
||||
return &db.Users[i], tokenPair, nil
|
||||
}
|
||||
}
|
||||
return nil, "", utils.ErrInvalidCredentials
|
||||
return nil, nil, utils.ErrInvalidCredentials
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(username, password, nickname, role string) (*models.User, error) {
|
||||
hash, err := utils.HashPassword(password)
|
||||
hash, err := crypto.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.User{
|
||||
ID: models.GetDB().NextID("users"),
|
||||
ID: repository.GetDB().NextID("users"),
|
||||
Username: username,
|
||||
Password: hash,
|
||||
Nickname: nickname,
|
||||
Role: role,
|
||||
}
|
||||
if err := models.GetDB().CreateUser(user); err != nil {
|
||||
if err := repository.GetDB().CreateUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) GetUserByID(id uint) (*models.User, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Users {
|
||||
@@ -62,20 +65,20 @@ func (s *AuthService) GetUserByID(id uint) (*models.User, error) {
|
||||
}
|
||||
|
||||
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
db := models.GetDB()
|
||||
db := repository.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) {
|
||||
if !crypto.VerifyPassword(oldPassword, db.Users[i].Password) {
|
||||
return utils.ErrInvalidCredentials
|
||||
}
|
||||
hash, err := utils.HashPassword(newPassword)
|
||||
hash, err := crypto.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Users[i].Password = hash
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+16
-12
@@ -7,7 +7,9 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"lokideploy/config"
|
||||
"lokideploy/models"
|
||||
"lokideploy/repository"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
@@ -18,8 +20,8 @@ func NewClusterService() *ClusterService {
|
||||
}
|
||||
|
||||
func SaveKubeConfig(name, content string) (string, error) {
|
||||
dir := filepath.Join("data", "kubeconfigs")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
dir := config.GetConfig().KubeDir
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := filepath.Join(dir, fmt.Sprintf("%s.yaml", name))
|
||||
@@ -37,7 +39,9 @@ func (s *ClusterService) Create(name, description, configType, apiServer, kubeco
|
||||
|
||||
finalKubeconfig := kubeconfig
|
||||
if configType == "token" {
|
||||
finalKubeconfig = BuildKubeconfigFromToken(apiServer, token)
|
||||
// 安全默认:不嵌入 CA、不跳过 TLS 验证,依赖系统 CA 池。
|
||||
// 自签证书的集群请改用 kubeconfig 模式(其内含 CA 证书)。
|
||||
finalKubeconfig = BuildKubeconfigFromToken(apiServer, token, "", false)
|
||||
}
|
||||
// 展示用 APIServer:token 模式取入参,kubeconfig 模式从内容提取
|
||||
if apiServer == "" {
|
||||
@@ -50,7 +54,7 @@ func (s *ClusterService) Create(name, description, configType, apiServer, kubeco
|
||||
}
|
||||
|
||||
cluster := models.Cluster{
|
||||
ID: models.GetDB().NextID("clusters"),
|
||||
ID: repository.GetDB().NextID("clusters"),
|
||||
Name: name,
|
||||
Description: description,
|
||||
ConfigType: configType,
|
||||
@@ -63,18 +67,18 @@ func (s *ClusterService) Create(name, description, configType, apiServer, kubeco
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
db.Clusters = append(db.Clusters, cluster)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
|
||||
go s.Validate(cluster.ID)
|
||||
return &cluster, nil
|
||||
}
|
||||
|
||||
func (s *ClusterService) Get(id uint) (*models.Cluster, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Clusters {
|
||||
@@ -87,7 +91,7 @@ func (s *ClusterService) Get(id uint) (*models.Cluster, error) {
|
||||
}
|
||||
|
||||
func (s *ClusterService) List(page, pageSize int) ([]models.Cluster, int64, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -106,7 +110,7 @@ func (s *ClusterService) List(page, pageSize int) ([]models.Cluster, int64, erro
|
||||
}
|
||||
|
||||
func (s *ClusterService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Clusters {
|
||||
@@ -147,7 +151,7 @@ func (s *ClusterService) Update(id uint, updates map[string]interface{}) error {
|
||||
}
|
||||
}
|
||||
db.Clusters[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -155,7 +159,7 @@ func (s *ClusterService) Update(id uint, updates map[string]interface{}) error {
|
||||
}
|
||||
|
||||
func (s *ClusterService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, c := range db.Clusters {
|
||||
@@ -164,7 +168,7 @@ func (s *ClusterService) Delete(id uint) error {
|
||||
_ = os.Remove(c.KubeConfigPath)
|
||||
}
|
||||
db.Clusters = append(db.Clusters[:i], db.Clusters[i+1:]...)
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/repository"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
@@ -29,7 +30,7 @@ type CreateCollectorRequest struct {
|
||||
|
||||
// fillAssociations 填充关联的 Loki / Host / Cluster(用于配置生成与展示)
|
||||
func (s *CollectorService) fillAssociations(c *models.Collector) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Deployments {
|
||||
@@ -107,7 +108,7 @@ func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collecto
|
||||
}
|
||||
|
||||
collector := models.Collector{
|
||||
ID: models.GetDB().NextID("collectors"),
|
||||
ID: repository.GetDB().NextID("collectors"),
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
LokiDeploymentID: req.LokiDeploymentID,
|
||||
@@ -129,18 +130,18 @@ func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collecto
|
||||
}
|
||||
collector.Config = s.generateConfigSnapshot(&collector)
|
||||
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
db.Collectors = append(db.Collectors, collector)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
|
||||
go s.Install(collector.ID)
|
||||
return &collector, nil
|
||||
}
|
||||
|
||||
func (s *CollectorService) Get(id uint) (*models.Collector, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
var found *models.Collector
|
||||
for i := range db.Collectors {
|
||||
@@ -159,7 +160,7 @@ func (s *CollectorService) Get(id uint) (*models.Collector, error) {
|
||||
}
|
||||
|
||||
func (s *CollectorService) List(page, pageSize int) ([]models.Collector, int64, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
total := int64(len(db.Collectors))
|
||||
start := (page - 1) * pageSize
|
||||
@@ -181,7 +182,7 @@ func (s *CollectorService) List(page, pageSize int) ([]models.Collector, int64,
|
||||
}
|
||||
|
||||
func (s *CollectorService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Collectors {
|
||||
@@ -202,7 +203,7 @@ func (s *CollectorService) Update(id uint, updates map[string]interface{}) error
|
||||
db.Collectors[i].Labels = v
|
||||
}
|
||||
db.Collectors[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -214,7 +215,7 @@ func (s *CollectorService) Delete(id uint) error {
|
||||
if err != nil {
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
for i, c := range db.Collectors {
|
||||
if c.ID == id {
|
||||
@@ -223,7 +224,7 @@ func (s *CollectorService) Delete(id uint) error {
|
||||
}
|
||||
}
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
// 异步卸载(用拷贝的 collector,避免删除后取不到)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
|
||||
@@ -35,10 +35,15 @@ func kubeconfigEnv(cluster *models.Cluster) (env []string, cleanup func(), err e
|
||||
}
|
||||
kubeFile, err := os.CreateTemp("", "kube-*.yaml")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("create kubeconfig temp file failed: %w", err)
|
||||
}
|
||||
if _, err := kubeFile.WriteString(kubeconfig); err != nil {
|
||||
kubeFile.Close()
|
||||
os.Remove(kubeFile.Name())
|
||||
return nil, nil, fmt.Errorf("write kubeconfig temp file failed: %w", err)
|
||||
}
|
||||
kubeFile.WriteString(kubeconfig)
|
||||
kubeFile.Close()
|
||||
os.Chmod(kubeFile.Name(), 0600)
|
||||
env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeFile.Name()))
|
||||
cleanup = func() { os.Remove(kubeFile.Name()) }
|
||||
return env, cleanup, nil
|
||||
@@ -73,6 +78,7 @@ func (d *CollectorDeployer) Install(ctx context.Context) error {
|
||||
|
||||
// Uninstall 按采集器类型分发卸载
|
||||
func (d *CollectorDeployer) Uninstall(ctx context.Context) error {
|
||||
d.log("info", fmt.Sprintf("开始卸载采集器 %s(类型 %s)", d.collector.Name, d.collector.Type), "")
|
||||
switch d.collector.Type {
|
||||
case "host-promtail":
|
||||
return d.uninstallHost(ctx)
|
||||
@@ -85,6 +91,7 @@ func (d *CollectorDeployer) Uninstall(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// ===== host-promtail:SSH 安装 Promtail 二进制 + systemd =====
|
||||
|
||||
func (d *CollectorDeployer) installHost(ctx context.Context, endpoint string) error {
|
||||
host := d.collector.Host
|
||||
if host == nil {
|
||||
@@ -100,7 +107,7 @@ func (d *CollectorDeployer) installHost(ctx context.Context, endpoint string) er
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 架构
|
||||
// 检测架构
|
||||
archOut, _, _ := client.Execute(ctx, "uname -m")
|
||||
arch := strings.TrimSpace(archOut)
|
||||
switch arch {
|
||||
@@ -112,41 +119,70 @@ func (d *CollectorDeployer) installHost(ctx context.Context, endpoint string) er
|
||||
return fmt.Errorf("不支持的主机架构: %s", arch)
|
||||
}
|
||||
|
||||
// 确定 Promtail 版本(优先使用关联的 Loki 版本)
|
||||
version := "3.0.0"
|
||||
if d.loki != nil && d.loki.Version != "" {
|
||||
version = d.loki.Version
|
||||
}
|
||||
|
||||
// 创建必要目录
|
||||
d.log("info", "准备目录 /opt/promtail /etc/promtail /var/lib/promtail", "")
|
||||
if _, _, err := client.Execute(ctx, "mkdir -p /opt/promtail /etc/promtail /var/lib/promtail"); err != nil {
|
||||
return fmt.Errorf("创建 Promtail 目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 下载 Promtail
|
||||
d.log("info", fmt.Sprintf("下载 Promtail(%s)", arch), "")
|
||||
url := fmt.Sprintf("https://github.com/grafana/loki/releases/download/v3.0.0/promtail-linux-%s.zip", arch)
|
||||
dl := fmt.Sprintf("mkdir -p /opt/promtail /etc/promtail && cd /tmp && curl -fsSL -o p.zip '%s' && unzip -o p.zip -d /opt/promtail/ && chmod +x /opt/promtail/promtail-linux-%s", url, arch)
|
||||
d.log("info", fmt.Sprintf("下载 Promtail %s(%s)", version, arch), "")
|
||||
url := fmt.Sprintf("https://github.com/grafana/loki/releases/download/v%s/promtail-linux-%s.zip", version, arch)
|
||||
dl := fmt.Sprintf("cd /tmp && curl -fsSL -o p.zip '%s' && unzip -o p.zip -d /opt/promtail/ && chmod +x /opt/promtail/promtail-linux-%s", url, arch)
|
||||
if _, _, err := client.Execute(ctx, dl); err != nil {
|
||||
return fmt.Errorf("下载 Promtail 失败: %v", err)
|
||||
return fmt.Errorf("下载 Promtail 失败: %w", err)
|
||||
}
|
||||
client.Execute(ctx, "ln -sf /opt/promtail/promtail-linux-* /usr/local/bin/promtail 2>/dev/null || true")
|
||||
|
||||
// 配置
|
||||
// 生成并上传配置
|
||||
logPath := d.collector.LogPath
|
||||
if logPath == "" {
|
||||
logPath = "/var/log/*.log"
|
||||
}
|
||||
conf := PromtailConfig(endpoint, logPath)
|
||||
client.Execute(ctx, fmt.Sprintf("cat > /etc/promtail/promtail.yaml << 'EOF'\n%s\nEOF", conf))
|
||||
d.log("info", "上传 Promtail 配置", "")
|
||||
if _, _, err := client.Execute(ctx, fmt.Sprintf("cat > /etc/promtail/promtail.yaml << 'EOF'\n%s\nEOF", conf)); err != nil {
|
||||
return fmt.Errorf("上传配置失败: %w", err)
|
||||
}
|
||||
|
||||
// systemd 服务
|
||||
// systemd 服务(生产级)
|
||||
service := `[Unit]
|
||||
Description=Promtail Log Collector
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/promtail -config.file=/etc/promtail/promtail.yaml
|
||||
User=root
|
||||
Group=root
|
||||
ExecStart=/usr/local/bin/promtail -config.file=/etc/promtail/promtail.yaml -positions.enabled=true
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=65536
|
||||
StandardOutput=append:/var/log/promtail.log
|
||||
StandardError=append:/var/log/promtail.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
client.Execute(ctx, fmt.Sprintf("cat > /etc/systemd/system/promtail.service << 'EOF'\n%s\nEOF", service))
|
||||
if _, _, err := client.Execute(ctx, "systemctl daemon-reload && systemctl enable promtail && systemctl restart promtail"); err != nil {
|
||||
return fmt.Errorf("启动 Promtail 失败: %v", err)
|
||||
d.log("info", "安装 systemd 服务", "")
|
||||
if _, _, err := client.Execute(ctx, fmt.Sprintf("cat > /etc/systemd/system/promtail.service << 'EOF'\n%s\nEOF", service)); err != nil {
|
||||
return fmt.Errorf("创建 systemd 服务失败: %w", err)
|
||||
}
|
||||
if _, _, err := client.Execute(ctx, "systemctl daemon-reload && systemctl enable promtail && systemctl restart promtail"); err != nil {
|
||||
return fmt.Errorf("启动 Promtail 失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存配置快照
|
||||
d.collector.Config = conf
|
||||
d.log("success", "Promtail 部署完成", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -161,11 +197,15 @@ func (d *CollectorDeployer) uninstallHost(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
client.Execute(ctx, "systemctl stop promtail 2>/dev/null || true; systemctl disable promtail 2>/dev/null || true; rm -f /etc/systemd/system/promtail.service /usr/local/bin/promtail; systemctl daemon-reload")
|
||||
client.Execute(ctx, "systemctl stop promtail 2>/dev/null || true")
|
||||
client.Execute(ctx, "systemctl disable promtail 2>/dev/null || true")
|
||||
client.Execute(ctx, "rm -rf /etc/systemd/system/promtail.service /usr/local/bin/promtail /etc/promtail /opt/promtail /var/lib/promtail")
|
||||
client.Execute(ctx, "systemctl daemon-reload")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ===== k8s-daemonset:helm 安装 Promtail DaemonSet =====
|
||||
|
||||
func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint string) error {
|
||||
cluster := d.collector.Cluster
|
||||
if cluster == nil {
|
||||
@@ -183,27 +223,27 @@ func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint strin
|
||||
}
|
||||
|
||||
values := PromtailHelmValues(endpoint)
|
||||
valuesFile, err := os.CreateTemp("", "promtail-values-*.yaml")
|
||||
valuesFile, valuesCleanup, err := writeTempFile("promtail-values-*.yaml", values, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(valuesFile.Name())
|
||||
valuesFile.WriteString(values)
|
||||
valuesFile.Close()
|
||||
defer valuesCleanup()
|
||||
|
||||
d.log("info", fmt.Sprintf("helm install promtail(命名空间 %s)", ns), "")
|
||||
// 先添加 grafana repo(幂等)
|
||||
// 添加 grafana repo(幂等)
|
||||
repo := exec.CommandContext(ctx, "helm", "repo", "add", "grafana", "https://grafana.github.io/helm-charts")
|
||||
repo.Env = env
|
||||
repo.CombinedOutput()
|
||||
exec.CommandContext(ctx, "helm", "repo", "update").CombinedOutput()
|
||||
upd := exec.CommandContext(ctx, "helm", "repo", "update")
|
||||
upd.Env = env
|
||||
upd.CombinedOutput()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "helm", "upgrade", "--install", "promtail", "grafana/promtail",
|
||||
"-f", valuesFile.Name(), "--namespace", ns, "--create-namespace", "--wait", "--timeout", "5m")
|
||||
"-f", valuesFile, "--namespace", ns, "--create-namespace", "--wait", "--timeout", "5m")
|
||||
cmd.Env = env
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("helm 安装 Promtail 失败: %v\n%s", err, string(out))
|
||||
return fmt.Errorf("helm 安装 Promtail 失败: %w\n%s", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -229,6 +269,7 @@ func (d *CollectorDeployer) uninstallDaemonset(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// ===== k8s-fluentbit-sidecar:kubectl patch 注入 Fluent Bit sidecar =====
|
||||
|
||||
func (d *CollectorDeployer) installSidecar(ctx context.Context, endpoint string) error {
|
||||
cluster := d.collector.Cluster
|
||||
if cluster == nil {
|
||||
@@ -275,27 +316,31 @@ func (d *CollectorDeployer) installSidecar(ctx context.Context, endpoint string)
|
||||
lokiHost, lokiPort := parseLokiURL(endpoint)
|
||||
conf := FluentbitConfig(lokiHost, lokiPort, logPath, app, ns)
|
||||
cm := FluentbitConfigMapYAML(conf, ns)
|
||||
cmFile, _ := os.CreateTemp("", "fluent-bit-cm-*.yaml")
|
||||
defer os.Remove(cmFile.Name())
|
||||
cmFile.WriteString(cm)
|
||||
cmFile.Close()
|
||||
cmFile, cmCleanup, err := writeTempFile("fluent-bit-cm-*.yaml", cm, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cmCleanup()
|
||||
|
||||
d.log("info", "部署 Fluent Bit ConfigMap", "")
|
||||
applyCm := exec.CommandContext(ctx, "kubectl", "apply", "-f", cmFile.Name())
|
||||
applyCm := exec.CommandContext(ctx, "kubectl", "apply", "-f", cmFile)
|
||||
applyCm.Env = env
|
||||
if out, err := applyCm.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("ConfigMap 部署失败: %v\n%s", err, string(out))
|
||||
return fmt.Errorf("ConfigMap 部署失败: %w\n%s", err, string(out))
|
||||
}
|
||||
|
||||
// 4. patch 注入 sidecar
|
||||
d.log("info", fmt.Sprintf("为 %s 注入 Fluent Bit Sidecar", target), "")
|
||||
patchFile, _ := os.CreateTemp("", "sidecar-patch-*.yaml")
|
||||
defer os.Remove(patchFile.Name())
|
||||
patchFile.WriteString(SidecarPatchYAML)
|
||||
patchFile.Close()
|
||||
patch := exec.CommandContext(ctx, "kubectl", "patch", "deployment", target, "-n", ns, "--type=strategic", "--patch-file", patchFile.Name())
|
||||
patchFile, patchCleanup, err := writeTempFile("sidecar-patch-*.yaml", SidecarPatchYAML, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer patchCleanup()
|
||||
|
||||
patch := exec.CommandContext(ctx, "kubectl", "patch", "deployment", target, "-n", ns, "--type=strategic", "--patch-file", patchFile)
|
||||
patch.Env = env
|
||||
if out, err := patch.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("Sidecar 注入失败: %v\n%s", err, string(out))
|
||||
return fmt.Errorf("Sidecar 注入失败: %w\n%s", err, string(out))
|
||||
}
|
||||
|
||||
// 5. 等待 rollout
|
||||
@@ -320,11 +365,13 @@ func (d *CollectorDeployer) uninstallSidecar(ctx context.Context) error {
|
||||
if ns == "" {
|
||||
ns = "default"
|
||||
}
|
||||
patchFile, _ := os.CreateTemp("", "sidecar-rm-*.yaml")
|
||||
defer os.Remove(patchFile.Name())
|
||||
patchFile.WriteString(SidecarRemovePatchYAML)
|
||||
patchFile.Close()
|
||||
patch := exec.CommandContext(ctx, "kubectl", "patch", "deployment", d.collector.TargetDeployment, "-n", ns, "--type=strategic", "--patch-file", patchFile.Name())
|
||||
patchFile, patchCleanup, err := writeTempFile("sidecar-rm-*.yaml", SidecarRemovePatchYAML, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer patchCleanup()
|
||||
|
||||
patch := exec.CommandContext(ctx, "kubectl", "patch", "deployment", d.collector.TargetDeployment, "-n", ns, "--type=strategic", "--patch-file", patchFile)
|
||||
patch.Env = env
|
||||
patch.CombinedOutput()
|
||||
return nil
|
||||
|
||||
@@ -6,17 +6,35 @@ import (
|
||||
)
|
||||
|
||||
// PromtailConfig 主机 Promtail 配置(推送至 lokiEndpoint,采集 logPath)
|
||||
// 生产级配置:持久化位置文件、批处理、重试、CRI日志格式解析
|
||||
func PromtailConfig(lokiEndpoint, logPath string) string {
|
||||
return fmt.Sprintf(`server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
log_level: info
|
||||
log_format: logfmt
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
filename: /var/lib/promtail/positions.yaml
|
||||
sync_period: 10s
|
||||
|
||||
clients:
|
||||
- url: %s/loki/api/v1/push
|
||||
batchwait: 1s
|
||||
batchsize: 1048576
|
||||
timeout: 10s
|
||||
backoff_config:
|
||||
min_period: 500ms
|
||||
max_period: 5m
|
||||
max_retries: 10
|
||||
external_labels:
|
||||
job: host-logs
|
||||
|
||||
limits_config:
|
||||
readline_rate_enabled: false
|
||||
readline_rate: 1000
|
||||
readline_burst: 10000
|
||||
max_streams: 0
|
||||
|
||||
scrape_configs:
|
||||
- job_name: app
|
||||
@@ -26,28 +44,104 @@ scrape_configs:
|
||||
job: app
|
||||
host: ${HOSTNAME}
|
||||
__path__: %s
|
||||
pipeline_stages:
|
||||
- cri: {}
|
||||
- match:
|
||||
selector: '{job="app"}'
|
||||
stages:
|
||||
- timestamp:
|
||||
source: time
|
||||
format: RFC3339Nano
|
||||
- limit:
|
||||
rate: 1000
|
||||
burst: 10000
|
||||
drop: true
|
||||
`, lokiEndpoint, logPath)
|
||||
}
|
||||
|
||||
// PromtailHelmValues DaemonSet 模式 Promtail Helm values
|
||||
// PromtailHelmValues DaemonSet 模式 Promtail Helm values(生产级)
|
||||
// 包含:cri解析、kubernetes标签、批处理配置、资源限制
|
||||
func PromtailHelmValues(lokiEndpoint string) string {
|
||||
return fmt.Sprintf(`config:
|
||||
clients:
|
||||
- url: %s/loki/api/v1/push
|
||||
batchwait: 1s
|
||||
batchsize: 1048576
|
||||
timeout: 10s
|
||||
backoff_config:
|
||||
min_period: 500ms
|
||||
max_period: 5m
|
||||
max_retries: 10
|
||||
external_labels:
|
||||
cluster: ${CLUSTER_NAME:-unknown}
|
||||
positions:
|
||||
filename: /run/promtail/positions.yaml
|
||||
scrape_configs:
|
||||
- job_name: kubernetes-pods
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
pipeline_stages:
|
||||
- cri: {}
|
||||
- match:
|
||||
selector: '{app!=""}'
|
||||
stages:
|
||||
- docker: {}
|
||||
- match:
|
||||
selector: '{app!=""}'
|
||||
stages:
|
||||
- labels:
|
||||
app:
|
||||
namespace:
|
||||
pod:
|
||||
container:
|
||||
- limit:
|
||||
rate: 1000
|
||||
burst: 10000
|
||||
drop: true
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_pod_controller_name]
|
||||
regex: ([0-9a-z-.]+?)(-[0-9a-f]{8,10})?
|
||||
action: replace
|
||||
target_label: __tmp_controller_name
|
||||
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
|
||||
regex: .+
|
||||
action: replace
|
||||
target_label: app
|
||||
- source_labels: [__meta_kubernetes_pod_label_app]
|
||||
regex: .+
|
||||
action: replace
|
||||
target_label: app
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_kubernetes_io_config_hash]
|
||||
regex: .+
|
||||
action: replace
|
||||
target_label: config_hash
|
||||
- action: replace
|
||||
source_labels: ['__meta_kubernetes__pod_name']
|
||||
target_label: 'pod'
|
||||
- action: replace
|
||||
source_labels: ['__meta_kubernetes_namespace']
|
||||
target_label: 'namespace'
|
||||
- action: replace
|
||||
source_labels: ['__meta_kubernetes_pod_container_name']
|
||||
target_label: 'container'
|
||||
- action: replace
|
||||
source_labels: ['__meta_kubernetes_pod_node_name']
|
||||
target_label: 'node_name'
|
||||
- replacement: /var/log/pods/*$1/*.log
|
||||
separator: /
|
||||
source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
|
||||
target_label: __path__
|
||||
|
||||
daemonset:
|
||||
enabled: true
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
memory: 128Mi
|
||||
|
||||
extraVolumes:
|
||||
- name: varlog
|
||||
@@ -56,6 +150,9 @@ extraVolumes:
|
||||
- name: varlibdockercontainers
|
||||
hostPath:
|
||||
path: /var/lib/docker/containers
|
||||
- name: promtail-run
|
||||
hostPath:
|
||||
path: /run/promtail
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: varlog
|
||||
@@ -64,33 +161,63 @@ extraVolumeMounts:
|
||||
- name: varlibdockercontainers
|
||||
mountPath: /var/lib/docker/containers
|
||||
readOnly: true
|
||||
- name: promtail-run
|
||||
mountPath: /run/promtail
|
||||
`, lokiEndpoint)
|
||||
}
|
||||
|
||||
// FluentbitConfig Sidecar 模式 Fluent Bit 配置(采集 logPath,推送至 lokiHost:lokiPort)
|
||||
// 生产级配置:kubernetes过滤器、内存缓冲、健康检查
|
||||
func FluentbitConfig(lokiHost, lokiPort, logPath, app, namespace string) string {
|
||||
return fmt.Sprintf(`[SERVICE]
|
||||
Flush 5
|
||||
Flush 1
|
||||
Log_Level info
|
||||
HTTP_Server On
|
||||
HTTP_Listen 0.0.0.0
|
||||
HTTP_Port 2020
|
||||
Parsers_File parsers.conf
|
||||
storage.path /tmp/flb-storage/
|
||||
storage.sync normal
|
||||
storage.checksum off
|
||||
storage.max_chunks_pause On
|
||||
storage.backlog.mem_limit 5M
|
||||
Health_Check On
|
||||
HC_Errors_Count 5
|
||||
HC_Retry_Failure_Count 5
|
||||
HC_Period 60
|
||||
|
||||
[INPUT]
|
||||
Name tail
|
||||
Path %s
|
||||
Tag app.%s
|
||||
Refresh_Interval 5
|
||||
Mem_Buf_Limit 5MB
|
||||
Mem_Buf_Limit 10MB
|
||||
Skip_Long_Lines On
|
||||
Read_from_Head True
|
||||
DB /tmp/flb.db
|
||||
DB.Sync Normal
|
||||
Buffer_Max_Size 1MB
|
||||
storage.type filesystem
|
||||
|
||||
[FILTER]
|
||||
Name modify
|
||||
Match app.*
|
||||
Add namespace %s
|
||||
Add app %s
|
||||
Add source fluent-bit
|
||||
|
||||
[FILTER]
|
||||
Name kubernetes
|
||||
Match app.*
|
||||
Kube_URL https://kubernetes.default.svc:443
|
||||
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
Merge_Log On
|
||||
Keep_Log Off
|
||||
K8S-Logging.Parser On
|
||||
K8S-Logging.Exclude On
|
||||
Labels On
|
||||
Annotations Off
|
||||
|
||||
[OUTPUT]
|
||||
Name loki
|
||||
@@ -98,6 +225,10 @@ func FluentbitConfig(lokiHost, lokiPort, logPath, app, namespace string) string
|
||||
Host %s
|
||||
Port %s
|
||||
Labels job=fluent-bit, app=%s, namespace=%s
|
||||
Drop_Records_Without_Labels On
|
||||
Line_Format json
|
||||
Auto_Kubernetes_Labels On
|
||||
Retry_Limit 10
|
||||
`, logPath, app, namespace, app, lokiHost, lokiPort, app, namespace)
|
||||
}
|
||||
|
||||
@@ -115,6 +246,7 @@ data:
|
||||
}
|
||||
|
||||
// SidecarPatchYAML strategic merge patch:为 Deployment 注入 Fluent Bit sidecar 容器
|
||||
// 生产级配置:资源限制、健康检查、安全上下文
|
||||
const SidecarPatchYAML = `spec:
|
||||
template:
|
||||
spec:
|
||||
@@ -128,6 +260,10 @@ const SidecarPatchYAML = `spec:
|
||||
- name: fluent-bit
|
||||
image: cr.fluentbit.io/fluent/fluent-bit:3.1.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 2020
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- name: app-logs
|
||||
mountPath: /var/log/app
|
||||
@@ -141,6 +277,24 @@ const SidecarPatchYAML = `spec:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
`
|
||||
|
||||
// SidecarRemovePatchYAML strategic merge patch:从 Deployment 移除 Fluent Bit sidecar(卸载)
|
||||
|
||||
+154
-27
@@ -3,12 +3,32 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/repository"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
// LokiVersionList 支持的 Loki 版本列表(按推荐程度排序)
|
||||
var LokiVersionList = []string{
|
||||
"3.4.0",
|
||||
"3.3.0",
|
||||
"3.2.0",
|
||||
"3.1.0",
|
||||
"3.0.0",
|
||||
"2.9.8",
|
||||
"2.9.0",
|
||||
"2.8.9",
|
||||
}
|
||||
|
||||
// minSupportedMajorVersion 最低支持的主版本号
|
||||
const minSupportedMajorVersion = 2
|
||||
|
||||
// DeploymentService 部署业务逻辑层
|
||||
type DeploymentService struct{}
|
||||
|
||||
func NewDeploymentService() *DeploymentService {
|
||||
@@ -33,7 +53,14 @@ type CreateDeploymentRequest struct {
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDeployment, error) {
|
||||
db := models.GetDB()
|
||||
// 版本兼容性预检查
|
||||
if req.Version != "" {
|
||||
if err := ValidateLokiVersion(req.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
db := repository.GetDB()
|
||||
d := models.LokiDeployment{
|
||||
ID: db.NextID("deployments"),
|
||||
Name: req.Name,
|
||||
@@ -88,13 +115,13 @@ func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDe
|
||||
db.Lock()
|
||||
db.Deployments = append(db.Deployments, d)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Deployments {
|
||||
@@ -108,7 +135,7 @@ func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) {
|
||||
}
|
||||
|
||||
func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, int64, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -130,7 +157,7 @@ func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, i
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Deployments {
|
||||
@@ -145,7 +172,7 @@ func (s *DeploymentService) Update(id uint, updates map[string]interface{}) erro
|
||||
db.Deployments[i].Endpoint = v
|
||||
}
|
||||
db.Deployments[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -153,19 +180,32 @@ func (s *DeploymentService) Update(id uint, updates map[string]interface{}) erro
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db := repository.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()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return utils.ErrNotFound
|
||||
}
|
||||
|
||||
// ==================== Goroutine 管理改进 ====================
|
||||
|
||||
// deployCtxEntry 保存正在进行的部署上下文
|
||||
type deployCtxEntry struct {
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
var (
|
||||
deployCtxMap = make(map[uint]*deployCtxEntry)
|
||||
deployCtxMu sync.Mutex
|
||||
)
|
||||
|
||||
func (s *DeploymentService) StartDeploy(id uint) error {
|
||||
deployment, err := s.Get(id)
|
||||
if err != nil {
|
||||
@@ -174,19 +214,41 @@ func (s *DeploymentService) StartDeploy(id uint) error {
|
||||
if deployment.Status == "deploying" {
|
||||
return fmt.Errorf("部署正在进行中,请勿重复操作")
|
||||
}
|
||||
s.Update(id, map[string]interface{}{"status": "deploying"})
|
||||
|
||||
// 检查已有 goroutine 状态,防止重复部署
|
||||
deployCtxMu.Lock()
|
||||
if entry, exists := deployCtxMap[id]; exists {
|
||||
select {
|
||||
case <-entry.done:
|
||||
// 已完成,可以重新部署
|
||||
delete(deployCtxMap, id)
|
||||
default:
|
||||
deployCtxMu.Unlock()
|
||||
return fmt.Errorf("部署正在进行中,请勿重复操作")
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
done := make(chan struct{})
|
||||
deployCtxMap[id] = &deployCtxEntry{cancel: cancel, done: done}
|
||||
deployCtxMu.Unlock()
|
||||
|
||||
s.Update(id, map[string]interface{}{"status": "deploying"})
|
||||
|
||||
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 {
|
||||
defer close(done)
|
||||
defer func() {
|
||||
deployCtxMu.Lock()
|
||||
delete(deployCtxMap, id)
|
||||
deployCtxMu.Unlock()
|
||||
}()
|
||||
|
||||
deployer := NewLokiDeployer(deployment, logFunc)
|
||||
if err := deployer.Deploy(ctx); err != nil {
|
||||
s.Update(id, map[string]interface{}{
|
||||
"status": "failed",
|
||||
"statusMessage": fmt.Sprintf("Deployment failed: %v", err),
|
||||
@@ -201,6 +263,7 @@ func (s *DeploymentService) StartDeploy(id uint) error {
|
||||
s.AddLog(id, "success", "Deployment completed successfully", "")
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -212,6 +275,14 @@ func (s *DeploymentService) StopDeployment(id uint) error {
|
||||
if deployment.Status != "running" && deployment.Status != "deploying" {
|
||||
return fmt.Errorf("部署未在运行,无法停止")
|
||||
}
|
||||
|
||||
// 取消正在进行的部署 goroutine
|
||||
deployCtxMu.Lock()
|
||||
if entry, exists := deployCtxMap[id]; exists {
|
||||
entry.cancel()
|
||||
}
|
||||
deployCtxMu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
CleanupLoki(ctx, deployment)
|
||||
@@ -223,14 +294,17 @@ func (s *DeploymentService) StopDeployment(id uint) error {
|
||||
|
||||
func (s *DeploymentService) RestartDeployment(id uint) error {
|
||||
if err := s.StopDeployment(id); err != nil {
|
||||
return err
|
||||
// 如果停止失败但不是因为部署未运行,返回错误
|
||||
if !strings.Contains(err.Error(), "部署未在运行") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
return s.StartDeploy(id)
|
||||
}
|
||||
|
||||
func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail string) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
log := models.DeploymentLog{
|
||||
ID: db.NextID("logs"),
|
||||
DeploymentID: deploymentID,
|
||||
@@ -242,11 +316,11 @@ func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail str
|
||||
db.Lock()
|
||||
db.Logs = append(db.Logs, log)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
}
|
||||
|
||||
func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]models.DeploymentLog, int64, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -270,7 +344,7 @@ func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]mo
|
||||
}
|
||||
|
||||
func (s *DeploymentService) GetStats() (map[string]interface{}, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -289,18 +363,19 @@ func (s *DeploymentService) GetStats() (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
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)),
|
||||
"totalDeployments": int64(len(db.Deployments)),
|
||||
"running": running,
|
||||
"failed": failed,
|
||||
"pending": pending,
|
||||
"stopped": stopped,
|
||||
"clusterCount": int64(len(db.Clusters)),
|
||||
"hostCount": int64(len(db.Hosts)),
|
||||
"supportedVersions": LokiVersionList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeploymentService) loadRelations(d *models.LokiDeployment) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
if d.ClusterID != nil && *d.ClusterID > 0 {
|
||||
for i := range db.Clusters {
|
||||
if db.Clusters[i].ID == *d.ClusterID {
|
||||
@@ -337,7 +412,14 @@ func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
|
||||
if ns == "" {
|
||||
ns = "loki"
|
||||
}
|
||||
return fmt.Sprintf("http://loki.%s.svc.cluster.local:3100", ns)
|
||||
// k8s-yaml manifest 固定创建名为 loki 的 Service;
|
||||
// k8s-helm (grafana/loki) 以 release 名(=部署名,默认 loki)暴露 Service。
|
||||
// 注意:不使用 loki-gateway,gateway 仅在 Distributed+gateway 启用时存在。
|
||||
svc := "loki"
|
||||
if d.Mode == "k8s-helm" && d.Name != "" {
|
||||
svc = d.Name
|
||||
}
|
||||
return fmt.Sprintf("http://%s.%s.svc.cluster.local:3100", svc, ns)
|
||||
case "host-binary":
|
||||
if d.Host != nil {
|
||||
return fmt.Sprintf("http://%s:3100", d.Host.HostIP)
|
||||
@@ -345,3 +427,48 @@ func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ==================== 版本兼容性检查 ====================
|
||||
|
||||
// ValidateLokiVersion 检查 Loki 版本是否在支持列表中且满足最低版本要求
|
||||
func ValidateLokiVersion(version string) error {
|
||||
if version == "" {
|
||||
return nil // 使用默认值,不报错
|
||||
}
|
||||
|
||||
// 基本格式验证
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("版本号格式无效: %s,期望格式如 3.0.0", version)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法解析主版本号: %s", version)
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法解析次版本号: %s", version)
|
||||
}
|
||||
|
||||
// 最低版本检查
|
||||
if major < minSupportedMajorVersion {
|
||||
return fmt.Errorf("Loki %s 版本过低,最低支持 %d.x 版本", version, minSupportedMajorVersion)
|
||||
}
|
||||
if major == 2 && minor < 8 {
|
||||
return fmt.Errorf("Loki %s 版本过低,2.x 最低支持 2.8.x 版本", version)
|
||||
}
|
||||
if major >= 4 {
|
||||
return fmt.Errorf("Loki %s 是未发布的版本,请使用已知稳定版本", version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecommendedVersions 返回推荐的 Loki 版本列表
|
||||
func GetRecommendedVersions() []string {
|
||||
result := make([]string, len(LokiVersionList))
|
||||
copy(result, LokiVersionList)
|
||||
return result
|
||||
}
|
||||
+29
-17
@@ -2,10 +2,13 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/pkg/crypto"
|
||||
"lokideploy/repository"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
@@ -20,7 +23,7 @@ func (s *HostService) Create(name, hostIP string, port int, user, authType, pass
|
||||
port = 22
|
||||
}
|
||||
host := models.Host{
|
||||
ID: models.GetDB().NextID("hosts"),
|
||||
ID: repository.GetDB().NextID("hosts"),
|
||||
Name: name,
|
||||
HostIP: hostIP,
|
||||
Port: port,
|
||||
@@ -34,30 +37,39 @@ func (s *HostService) Create(name, hostIP string, port int, user, authType, pass
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if password != "" {
|
||||
enc, _ := utils.Encrypt(password)
|
||||
host.Password = "ENC:" + enc
|
||||
enc, err := crypto.EncryptField(password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
host.Password = enc
|
||||
}
|
||||
if privateKey != "" {
|
||||
enc, _ := utils.Encrypt(privateKey)
|
||||
host.PrivateKey = "ENC:" + enc
|
||||
enc, err := crypto.EncryptField(privateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("私钥加密失败: %w", err)
|
||||
}
|
||||
host.PrivateKey = enc
|
||||
}
|
||||
if keyPhrase != "" {
|
||||
enc, _ := utils.Encrypt(keyPhrase)
|
||||
host.KeyPhrase = "ENC:" + enc
|
||||
enc, err := crypto.EncryptField(keyPhrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("私钥口令加密失败: %w", err)
|
||||
}
|
||||
host.KeyPhrase = enc
|
||||
}
|
||||
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
db.Hosts = append(db.Hosts, host)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
|
||||
go s.Validate(host.ID)
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func (s *HostService) Get(id uint) (*models.Host, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Hosts {
|
||||
@@ -70,7 +82,7 @@ func (s *HostService) Get(id uint) (*models.Host, error) {
|
||||
}
|
||||
|
||||
func (s *HostService) List(page, pageSize int) ([]models.Host, int64, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -89,7 +101,7 @@ func (s *HostService) List(page, pageSize int) ([]models.Host, int64, error) {
|
||||
}
|
||||
|
||||
func (s *HostService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Hosts {
|
||||
@@ -107,7 +119,7 @@ func (s *HostService) Update(id uint, updates map[string]interface{}) error {
|
||||
db.Hosts[i].Arch = v
|
||||
}
|
||||
db.Hosts[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -115,13 +127,13 @@ func (s *HostService) Update(id uint, updates map[string]interface{}) error {
|
||||
}
|
||||
|
||||
func (s *HostService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db := repository.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()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -138,7 +150,7 @@ func (s *HostService) Validate(id uint) {
|
||||
|
||||
password := host.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password, _ = utils.Decrypt(password[4:])
|
||||
password, _ = crypto.Decrypt(password[4:])
|
||||
}
|
||||
|
||||
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
@@ -165,7 +177,7 @@ func (s *HostService) ExecuteCommand(id uint, command string) (string, string, e
|
||||
|
||||
password := host.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password, _ = utils.Decrypt(password[4:])
|
||||
password, _ = crypto.Decrypt(password[4:])
|
||||
}
|
||||
|
||||
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -16,7 +17,7 @@ func resolveKubeconfig(cluster *models.Cluster) (string, error) {
|
||||
if cluster.KubeConfigPath != "" {
|
||||
data, err := os.ReadFile(cluster.KubeConfigPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取 kubeconfig 文件失败: %v", err)
|
||||
return "", fmt.Errorf("读取 kubeconfig 文件失败: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -24,15 +25,43 @@ func resolveKubeconfig(cluster *models.Cluster) (string, error) {
|
||||
}
|
||||
|
||||
// BuildKubeconfigFromToken 用 API Server + Token 构造最小 kubeconfig
|
||||
// 采用 insecure-skip-tls-verify,便于内网/自签证书集群接入
|
||||
func BuildKubeconfigFromToken(apiServer, token string) string {
|
||||
return fmt.Sprintf(`apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
// 优先使用 CA 证书验证 TLS,仅在用户显式要求时跳过证书验证
|
||||
// Parameters:
|
||||
// - apiServer: Kubernetes API Server 地址,如 https://192.168.1.10:6443
|
||||
// - token: 认证 Token
|
||||
// - caCert: CA 证书 PEM 内容(可选)。若提供则嵌入证书数据进行 TLS 验证;
|
||||
// 若为空且 skipTLSVerify=true 则跳过 TLS 验证;
|
||||
// 若为空且 skipTLSVerify=false 则依赖系统 CA 池
|
||||
// - skipTLSVerify: 是否跳过 TLS 证书验证(不建议在生产环境使用)
|
||||
func BuildKubeconfigFromToken(apiServer, token, caCert string, skipTLSVerify bool) string {
|
||||
var clusterBlock string
|
||||
|
||||
if caCert != "" {
|
||||
// 嵌入 CA 证书进行安全的 TLS 验证
|
||||
encodedCA := base64.StdEncoding.EncodeToString([]byte(caCert))
|
||||
clusterBlock = fmt.Sprintf(`clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: %s
|
||||
server: %s
|
||||
name: loki-cluster`, encodedCA, apiServer)
|
||||
} else if skipTLSVerify {
|
||||
// 用户显式要求跳过 TLS 验证(内网/自签证书场景)
|
||||
clusterBlock = fmt.Sprintf(`clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: %s
|
||||
name: loki-cluster
|
||||
name: loki-cluster`, apiServer)
|
||||
} else {
|
||||
// 依赖系统 CA 池(不嵌入证书也不跳过验证)
|
||||
clusterBlock = fmt.Sprintf(`clusters:
|
||||
- cluster:
|
||||
server: %s
|
||||
name: loki-cluster`, apiServer)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`apiVersion: v1
|
||||
kind: Config
|
||||
%s
|
||||
contexts:
|
||||
- context:
|
||||
cluster: loki-cluster
|
||||
@@ -43,7 +72,7 @@ users:
|
||||
- name: loki-user
|
||||
user:
|
||||
token: %s
|
||||
`, apiServer, token)
|
||||
`, clusterBlock, token)
|
||||
}
|
||||
|
||||
var apiServerRe = regexp.MustCompile(`(?m)^\s*server:\s*(\S+)`)
|
||||
|
||||
+1008
-98
File diff suppressed because it is too large.
Load diff
+194
-5
@@ -3,12 +3,21 @@ package services
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lokideploy/config"
|
||||
"lokideploy/models"
|
||||
"lokideploy/pkg/crypto"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -38,17 +47,169 @@ type HostInfo struct {
|
||||
DiskUsed string `json:"diskUsed"`
|
||||
}
|
||||
|
||||
func NewSSHClient(host string, port int, user, password string) (*SSHClient, error) {
|
||||
if port == 0 {
|
||||
port = 22
|
||||
// SSHAuthConfig 重导出models.SSHAuthConfig以兼容现有调用
|
||||
// Deprecated: 直接使用 models.SSHAuthConfig
|
||||
type SSHAuthConfig = models.SSHAuthConfig
|
||||
|
||||
// validatePort 校验SSH端口范围
|
||||
func validatePort(port int) int {
|
||||
if port <= 0 {
|
||||
return 22
|
||||
}
|
||||
config := &ssh.ClientConfig{
|
||||
if port > 65535 {
|
||||
return 22
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// hostKeyPath 返回主机密钥存储路径
|
||||
func hostKeyPath(host string) string {
|
||||
cfg := config.GetConfig()
|
||||
// 对主机名进行安全编码作为文件名
|
||||
safeHost := base64.URLEncoding.EncodeToString([]byte(host))
|
||||
return filepath.Join(cfg.HostKeyDir, safeHost+".pub")
|
||||
}
|
||||
|
||||
// fingerprint 计算SSH主机密钥的SHA256指纹
|
||||
func fingerprint(key ssh.PublicKey) string {
|
||||
hash := sha256.Sum256(key.Marshal())
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// TrustOnFirstUse 实现TOFU(Trust On First Use)主机密钥验证
|
||||
// 首次连接时记录主机密钥,后续连接时校验
|
||||
func TrustOnFirstUse(host string) ssh.HostKeyCallback {
|
||||
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
fp := fingerprint(key)
|
||||
hostKeyFile := hostKeyPath(host)
|
||||
|
||||
storedKey, err := os.ReadFile(hostKeyFile)
|
||||
if err != nil {
|
||||
// 首次连接:保存主机密钥
|
||||
if os.IsNotExist(err) {
|
||||
if writeErr := os.WriteFile(hostKeyFile, ssh.MarshalAuthorizedKey(key), 0600); writeErr != nil {
|
||||
return fmt.Errorf("failed to save host key: %w", writeErr)
|
||||
}
|
||||
// 记录首次信任的主机密钥指纹(安全审计日志)
|
||||
fmt.Fprintf(os.Stderr, "[SSH-TOFU] First connection to %s, host key fingerprint saved: SHA256:%s\n", host, fp)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read host key file: %w", err)
|
||||
}
|
||||
|
||||
// 解析存储的密钥并与当前密钥比较
|
||||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(storedKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse stored host key: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(parsedKey.Marshal(), key.Marshal()) {
|
||||
return fmt.Errorf("HOST KEY MISMATCH for %s! Expected SHA256:%s, got SHA256:%s. Possible MITM attack!",
|
||||
host, fingerprint(parsedKey), fp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// InsecureIgnoreHostKey 保留但不导出,用于向后兼容的内部测试场景
|
||||
// 生产环境必须使用TrustOnFirstUse
|
||||
func InsecureIgnoreHostKey() ssh.HostKeyCallback {
|
||||
return ssh.InsecureIgnoreHostKey()
|
||||
}
|
||||
|
||||
// buildAuthMethods 构建SSH认证方法列表(优先密钥认证)
|
||||
func buildAuthMethods(auth *models.SSHAuthConfig) []ssh.AuthMethod {
|
||||
methods := []ssh.AuthMethod{}
|
||||
|
||||
// 1. 优先尝试私钥认证
|
||||
if auth.PrivateKey != "" {
|
||||
privateKey := auth.PrivateKey
|
||||
// 如果是加密存储的密钥,先解密
|
||||
if strings.HasPrefix(privateKey, "ENC:") {
|
||||
privateKey = crypto.DecryptField(privateKey)
|
||||
}
|
||||
|
||||
var signer ssh.Signer
|
||||
var err error
|
||||
keyPhrase := auth.KeyPhrase
|
||||
if strings.HasPrefix(keyPhrase, "ENC:") {
|
||||
keyPhrase = crypto.DecryptField(keyPhrase)
|
||||
}
|
||||
|
||||
if keyPhrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(keyPhrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(privateKey))
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
methods = append(methods, ssh.PublicKeys(signer))
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 密码认证作为后备
|
||||
if auth.Password != "" {
|
||||
password := auth.Password
|
||||
if strings.HasPrefix(password, "ENC:") {
|
||||
password = crypto.DecryptField(password)
|
||||
}
|
||||
methods = append(methods, ssh.Password(password))
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
// NewSSHClient 创建SSH客户端(支持密钥+密码认证,TOFU主机密钥验证)
|
||||
func NewSSHClient(host string, port int, user, password string) (*SSHClient, error) {
|
||||
auth := &SSHAuthConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: user,
|
||||
Password: password,
|
||||
}
|
||||
return NewSSHClientWithAuth(auth)
|
||||
}
|
||||
|
||||
// NewSSHClientWithAuth 使用完整认证配置创建SSH客户端
|
||||
func NewSSHClientWithAuth(auth *models.SSHAuthConfig) (*SSHClient, error) {
|
||||
port := validatePort(auth.Port)
|
||||
if auth.User == "" {
|
||||
return nil, errors.New("SSH user is required")
|
||||
}
|
||||
|
||||
authMethods := buildAuthMethods(auth)
|
||||
if len(authMethods) == 0 {
|
||||
return nil, errors.New("no authentication method available")
|
||||
}
|
||||
|
||||
cfg := &ssh.ClientConfig{
|
||||
User: auth.User,
|
||||
HostKeyCallback: TrustOnFirstUse(auth.Host),
|
||||
Timeout: 10 * time.Second,
|
||||
Auth: authMethods,
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", auth.Host, port)
|
||||
client, err := ssh.Dial("tcp", addr, cfg)
|
||||
if err != nil {
|
||||
return nil, errors.New(FormatSSHError(err))
|
||||
}
|
||||
|
||||
return &SSHClient{client: client}, nil
|
||||
}
|
||||
|
||||
// NewSSHClientUnsafe 创建不验证主机密钥的SSH客户端(仅用于内部测试)
|
||||
// Deprecated: 生产环境请使用NewSSHClient或NewSSHClientWithAuth
|
||||
func NewSSHClientUnsafe(host string, port int, user, password string) (*SSHClient, error) {
|
||||
port = validatePort(port)
|
||||
cfg := &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)
|
||||
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), cfg)
|
||||
if err != nil {
|
||||
return nil, errors.New(FormatSSHError(err))
|
||||
}
|
||||
@@ -155,6 +316,7 @@ func (c *SSHClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestSSHConnection 测试SSH连接(使用TOFU模式)
|
||||
func TestSSHConnection(host string, port int, user, password string) error {
|
||||
client, err := NewSSHClient(host, port, user, password)
|
||||
if err != nil {
|
||||
@@ -166,3 +328,30 @@ func TestSSHConnection(host string, port int, user, password string) error {
|
||||
_, _, err = client.Execute(ctx, "echo 'connected'")
|
||||
return err
|
||||
}
|
||||
|
||||
// TestSSHConnectionWithAuth 使用完整认证配置测试SSH连接
|
||||
func TestSSHConnectionWithAuth(auth *models.SSHAuthConfig) error {
|
||||
client, err := NewSSHClientWithAuth(auth)
|
||||
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
|
||||
}
|
||||
|
||||
// GetHostKeyFingerprint 获取已保存的主机密钥指纹
|
||||
func GetHostKeyFingerprint(host string) string {
|
||||
hostKeyFile := hostKeyPath(host)
|
||||
storedKey, err := os.ReadFile(hostKeyFile)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(storedKey)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "SHA256:" + fingerprint(parsedKey)
|
||||
}
|
||||
+23
-14
@@ -1,9 +1,12 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lokideploy/models"
|
||||
"lokideploy/pkg/crypto"
|
||||
"lokideploy/repository"
|
||||
"lokideploy/utils"
|
||||
)
|
||||
|
||||
@@ -14,18 +17,24 @@ func NewStorageService() *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)
|
||||
encAccessKey, err := crypto.EncryptField(accessKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AccessKey 加密失败: %w", err)
|
||||
}
|
||||
encSecretKey, err := crypto.EncryptField(secretKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SecretKey 加密失败: %w", err)
|
||||
}
|
||||
|
||||
storage := models.StorageConfig{
|
||||
ID: models.GetDB().NextID("storages"),
|
||||
ID: repository.GetDB().NextID("storages"),
|
||||
Name: name,
|
||||
StorageType: storageType,
|
||||
Endpoint: endpoint,
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
AccessKey: "ENC:" + encAccessKey,
|
||||
SecretKey: "ENC:" + encSecretKey,
|
||||
AccessKey: encAccessKey,
|
||||
SecretKey: encSecretKey,
|
||||
PathPrefix: pathPrefix,
|
||||
Insecure: insecure,
|
||||
IsDefault: isDefault,
|
||||
@@ -33,7 +42,7 @@ func (s *StorageService) Create(name, storageType, endpoint, bucket, region, acc
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
if isDefault {
|
||||
db.Lock()
|
||||
for i := range db.Storages {
|
||||
@@ -44,13 +53,13 @@ func (s *StorageService) Create(name, storageType, endpoint, bucket, region, acc
|
||||
db.Lock()
|
||||
db.Storages = append(db.Storages, storage)
|
||||
db.Unlock()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
func (s *StorageService) Get(id uint) (*models.StorageConfig, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Storages {
|
||||
@@ -63,7 +72,7 @@ func (s *StorageService) Get(id uint) (*models.StorageConfig, error) {
|
||||
}
|
||||
|
||||
func (s *StorageService) List(page, pageSize int) ([]models.StorageConfig, int64, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -82,7 +91,7 @@ func (s *StorageService) List(page, pageSize int) ([]models.StorageConfig, int64
|
||||
}
|
||||
|
||||
func (s *StorageService) Update(id uint, updates map[string]interface{}) error {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i := range db.Storages {
|
||||
@@ -108,7 +117,7 @@ func (s *StorageService) Update(id uint, updates map[string]interface{}) error {
|
||||
db.Storages[i].IsDefault = v
|
||||
}
|
||||
db.Storages[i].UpdatedAt = time.Now()
|
||||
go db.Save()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -116,13 +125,13 @@ func (s *StorageService) Update(id uint, updates map[string]interface{}) error {
|
||||
}
|
||||
|
||||
func (s *StorageService) Delete(id uint) error {
|
||||
db := models.GetDB()
|
||||
db := repository.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()
|
||||
db.RequestSave()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -130,7 +139,7 @@ func (s *StorageService) Delete(id uint) error {
|
||||
}
|
||||
|
||||
func (s *StorageService) GetDefault() (*models.StorageConfig, error) {
|
||||
db := models.GetDB()
|
||||
db := repository.GetDB()
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for i := range db.Storages {
|
||||
|
||||
Reference in new issue
Block a user