Files
FlowStack/backend/internal/database/database.go
T
Logan.Li 26341c3e8a feat: 去 mock 化 —— 全模块接入真实数据 (参考 jianmu/opsx-jenkins/aiops)
后端(35+ 新路由):
- AES-256-GCM 字段加密(enc:v1:),密钥自动生成 data/encryption.key
- 新模型:Host/K8sCluster/Credential/BuildNode/DeployTool/WebhookEndpoint/
  JenkinsServer/SystemSetting;Pipeline 扩展触发器/变量/缓存列,
  PipelineRun 扩展 buildNumber/externalUrl
- 资源 CRUD + 真实测试端点:主机/集群/构建节点 TCP 拨号、
  工具真实执行版本检测(保守安装策略)、Webhook HMAC-SHA256 签名发送、
  入站流水线触发 /api/hooks/pipeline/:id
- Jenkins 完整集成(移植 opsx-jenkins 生产客户端):crumb/BasicAuth/
  buildWithParameters/Location 队列/progressiveText 增量控制台,
  服务器 CRUD+测试、Job 树拉取(folder 压平)、导入为流水线、
  jenkins 类型流水线真实执行回传日志
- 流水线列表附带 lastRun、运行重试、summary 统计、系统设置单行表
- 运行结束触发出站 Webhook 通知;GitOps 同步状态真实推进

前端(14 页全部去 mock):
- 流水线列表/发布记录/批量发布/环境/凭证/构建节点/工具/Webhook/
  系统设置/Jenkins 导入/GitOps(资源树+差异) 全部接入真实 API
- 编辑器 4 Tab 持久化(基本信息/触发设置含真实 Webhook URL/变量缓存/发布历史)
- TopBar 真实流水线切换 + 仅保存/保存并运行
- Sidebar 徽标真实统计;删除未使用组件与 mock 数据文件
2026-10-27 03:15:20 +08:00

243 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package database
import (
"log"
"os"
"path/filepath"
"time"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"flowstack/internal/crypto"
"flowstack/internal/model"
)
var DB *gorm.DB
// DataDir 数据目录(密码文件等运行时产物存放处)
var DataDir string
// Init 打开 SQLite、自动建表并写入示例数据(仅首次)
func Init() error {
DataDir = filepath.Join(mustDir(), "data")
if err := os.MkdirAll(DataDir, 0o755); err != nil {
return err
}
if err := crypto.InitEncryptionKey(DataDir); err != nil {
return err
}
db, err := gorm.Open(sqlite.Open(filepath.Join(DataDir, "flowstack.db")), &gorm.Config{})
if err != nil {
return err
}
DB = db
if err := DB.AutoMigrate(
&model.User{},
&model.Pipeline{}, &model.PipelineNode{}, &model.PipelineEdge{},
&model.PipelineRun{}, &model.RunStage{}, &model.LogEntry{},
&model.Application{}, &model.Plugin{},
&model.Host{}, &model.K8sCluster{}, &model.Credential{}, &model.BuildNode{},
&model.DeployTool{}, &model.WebhookEndpoint{}, &model.JenkinsServer{}, &model.SystemSetting{},
); err != nil {
return err
}
seedAdmin()
seedDemoData()
seedResources()
seedTools()
return nil
}
// mustDir 返回二进制所在目录;go run 时回退到当前目录
func mustDir() string {
if exe, err := os.Executable(); err == nil {
return filepath.Dir(exe)
}
return "."
}
// seedAdmin 首次启动生成随机密码的管理员账号:
// 密码打印到日志("已生成管理员账号",grep -A6 可见)并写入 data/initial-admin-password.txt。
func seedAdmin() {
var count int64
DB.Model(&model.User{}).Count(&count)
if count > 0 {
return
}
password, err := crypto.RandomPassword(16)
if err != nil {
log.Printf("[seed] 管理员密码生成失败: %v", err)
return
}
hash, err := crypto.HashPassword(password)
if err != nil {
log.Printf("[seed] 管理员密码哈希失败: %v", err)
return
}
DB.Create(&model.User{
Username: "admin", Password: hash, Nickname: "Administrator", Role: "admin",
})
pwdFile := filepath.Join(DataDir, "initial-admin-password.txt")
if err := os.WriteFile(pwdFile, []byte("admin / "+password+"\n"), 0o600); err != nil {
log.Printf("[seed] 密码文件写入失败: %v", err)
}
log.Println("========================================================")
log.Println("[seed] 已生成管理员账号(首次启动)")
log.Printf("[seed] 用户名: admin")
log.Printf("[seed] 初始密码: %s", password)
log.Printf("[seed] 密码已写入: %s", pwdFile)
log.Println("[seed] 请登录后立即修改密码")
log.Println("========================================================")
}
func seedDemoData() {
var count int64
DB.Model(&model.Pipeline{}).Count(&count)
if count > 0 {
return
}
log.Println("[seed] 首次启动,写入示例数据…")
// ---- 流水线 1:aiops-backend(Go + K8s) ----
backend := model.Pipeline{
Name: "aiops-backend", Description: "AIOps 后端服务(Go + K8s 滚动发布)",
Type: "native", SourceRepo: "https://gitee.com/attacker/aiops.git", SourceBranch: "master",
DeployType: "k8s", Env: "production",
}
DB.Create(&backend)
DB.Create([]model.PipelineNode{
{PipelineID: backend.ID, NodeID: "git-clone-1", Type: "source", Name: "git-clone", Category: "source", PositionX: 60, PositionY: 220, Width: 220, Height: 120,
Config: map[string]interface{}{"url": "https://gitee.com/attacker/aiops.git", "revision": "master"}},
{PipelineID: backend.ID, NodeID: "unit-test-1", Type: "test", Name: "unit-test", Category: "test", PositionX: 400, PositionY: 60, Width: 220, Height: 120,
Config: map[string]interface{}{"test-command": "go test -v -race ./..."}},
{PipelineID: backend.ID, NodeID: "kaniko-build-1", Type: "build", Name: "kaniko-build", Category: "build", PositionX: 400, PositionY: 380, Width: 220, Height: 120,
Config: map[string]interface{}{"dockerfile": "Dockerfile", "image": "registry.example.com/aiops/aiops-backend"}},
{PipelineID: backend.ID, NodeID: "trivy-scan-1", Type: "security", Name: "trivy-scan", Category: "security", PositionX: 740, PositionY: 380, Width: 220, Height: 120,
Config: map[string]interface{}{"image": "registry.example.com/aiops/aiops-backend", "severity": "HIGH,CRITICAL"}},
{PipelineID: backend.ID, NodeID: "kubectl-deploy-1", Type: "deploy", Name: "kubectl-deploy", Category: "deploy", PositionX: 1080, PositionY: 220, Width: 220, Height: 120,
Config: map[string]interface{}{"manifest": "k8s/production/", "namespace": "aiops"}},
})
DB.Create([]model.PipelineEdge{
{PipelineID: backend.ID, EdgeID: "e-clone-test", Source: "git-clone-1", Target: "unit-test-1", Animated: true},
{PipelineID: backend.ID, EdgeID: "e-clone-build", Source: "git-clone-1", Target: "kaniko-build-1", Animated: true},
{PipelineID: backend.ID, EdgeID: "e-build-scan", Source: "kaniko-build-1", Target: "trivy-scan-1", Animated: true},
{PipelineID: backend.ID, EdgeID: "e-test-deploy", Source: "unit-test-1", Target: "kubectl-deploy-1", Animated: true},
{PipelineID: backend.ID, EdgeID: "e-scan-deploy", Source: "trivy-scan-1", Target: "kubectl-deploy-1", Animated: true},
})
// ---- 流水线 2:aiops-web(Vue + 主机部署) ----
web := model.Pipeline{
Name: "aiops-web", Description: "AIOps 前端(Vue + 主机 Nginx 发布)",
Type: "native", SourceRepo: "https://gitee.com/attacker/aiops-web.git", SourceBranch: "master",
DeployType: "host", Env: "production",
}
DB.Create(&web)
DB.Create([]model.PipelineNode{
{PipelineID: web.ID, NodeID: "git-clone-1", Type: "source", Name: "git-clone", Category: "source", PositionX: 60, PositionY: 200, Width: 220, Height: 120,
Config: map[string]interface{}{"url": "https://gitee.com/attacker/aiops-web.git", "revision": "master"}},
{PipelineID: web.ID, NodeID: "npm-build-1", Type: "build", Name: "npm build", Category: "build", PositionX: 400, PositionY: 60, Width: 220, Height: 120,
Config: map[string]interface{}{"script": "npm ci && npm run build"}},
{PipelineID: web.ID, NodeID: "npm-audit-1", Type: "security", Name: "npm audit", Category: "security", PositionX: 400, PositionY: 340, Width: 220, Height: 120,
Config: map[string]interface{}{"severity": "high"}},
{PipelineID: web.ID, NodeID: "host-deploy-1", Type: "deploy", Name: "主机部署", Category: "deploy", PositionX: 760, PositionY: 200, Width: 220, Height: 120,
Config: map[string]interface{}{"主机组": "production-group", "目标路径": "/usr/share/nginx/html", "分批数量": "2"}},
{PipelineID: web.ID, NodeID: "cdn-refresh-1", Type: "custom", Name: "刷新 CDN", Category: "custom", PositionX: 1120, PositionY: 200, Width: 220, Height: 120,
Config: map[string]interface{}{"刷新URL": "https://aiops.example.com/*"}},
})
DB.Create([]model.PipelineEdge{
{PipelineID: web.ID, EdgeID: "e-clone-npm", Source: "git-clone-1", Target: "npm-build-1", Animated: true},
{PipelineID: web.ID, EdgeID: "e-clone-audit", Source: "git-clone-1", Target: "npm-audit-1", Animated: true},
{PipelineID: web.ID, EdgeID: "e-npm-deploy", Source: "npm-build-1", Target: "host-deploy-1", Animated: true},
{PipelineID: web.ID, EdgeID: "e-audit-deploy", Source: "npm-audit-1", Target: "host-deploy-1", Animated: true},
{PipelineID: web.ID, EdgeID: "e-deploy-cdn", Source: "host-deploy-1", Target: "cdn-refresh-1", Animated: true},
})
// ---- GitOps 应用 ----
now := time.Now()
DB.Create([]model.Application{
{Name: "aiops-backend", Repo: "https://gitea.91ai.io/rlwj-lab/aiops-gitops.git", Branch: "main", Path: "apps/aiops-backend", Cluster: "ack-production", Namespace: "aiops", ManifestType: "kustomize", SyncPolicy: "automated", AutoPrune: true, SelfHeal: true, SyncStatus: "Synced", HealthStatus: "Healthy", LastSyncAt: &now},
{Name: "aiops-web", Repo: "https://gitea.91ai.io/rlwj-lab/aiops-gitops.git", Branch: "main", Path: "apps/aiops-web", Cluster: "ack-production", Namespace: "aiops", ManifestType: "kustomize", SyncPolicy: "manual", SyncStatus: "OutOfSync", HealthStatus: "Progressing"},
})
// ---- 已安装插件 ----
DB.Create([]model.Plugin{
{PluginID: "jenkins-import", Name: "Jenkins 导入", Description: "一键导入既有 Jenkins Job 为 FlowStack 流水线", Version: "1.2.0", Author: "FlowStack Team", Category: "部署", Enabled: true},
{PluginID: "webhook-notify", Name: "Webhook 通知", Description: "发布成功/失败后回调企业微信、钉钉", Version: "1.0.3", Author: "FlowStack Team", Category: "通知", Enabled: true},
{PluginID: "k8s-rollout", Name: "K8s 滚动发布", Description: "Deployment 滚动更新与回滚", Version: "2.3.1", Author: "CloudNative", Category: "构建", Enabled: false},
})
}
// seedResources 资源示例数据(主机/集群/凭证/构建节点/Webhook,各表独立判空)
func seedResources() {
var n int64
DB.Model(&model.Host{}).Count(&n)
if n == 0 {
DB.Create([]model.Host{
{Name: "build-host-01", IP: "10.0.8.11", SSHPort: 22, Username: "root", Tags: `["build","docker"]`, Status: "online", OS: "Ubuntu 22.04", Remark: "通用构建机"},
{Name: "prod-web-01", IP: "10.0.1.21", SSHPort: 22, Username: "admin", Tags: `["production","web"]`, Status: "online", OS: "CentOS 7.9", Remark: "Nginx 前端"},
})
}
DB.Model(&model.K8sCluster{}).Count(&n)
if n == 0 {
DB.Create([]model.K8sCluster{
{Name: "ack-production", Context: "ack-production", APIServer: "https://k8s.prod:6443", Version: "v1.30.1", NodeCount: 3, Namespaces: `["aiops","default","kube-system"]`, Status: "disconnected"},
})
}
DB.Model(&model.Credential{}).Count(&n)
if n == 0 {
enc1, _ := crypto.EncryptField(`{"private_key":"CHANGE_ME","ssh_user":"root","ssh_port":22}`)
DB.Create([]model.Credential{
{Name: "prod-ssh-key", Type: "ssh", Scope: "production-group", Content: enc1, Summary: "ssh_key root@22"},
{Name: "registry-example", Type: "docker", Scope: "全局", Summary: "docker registry.example.com aiops-pull"},
})
}
DB.Model(&model.BuildNode{}).Count(&n)
if n == 0 {
hb := time.Now().Add(-10 * time.Second)
DB.Create([]model.BuildNode{
{Name: "build-host-01", IP: "10.0.8.11", Labels: `["docker","go","java"]`, MaxConcurrency: 4, Status: "online", CurrentJobs: 1, LastHeartbeat: &hb},
{Name: "build-host-02", IP: "10.0.8.12", Labels: `["docker","nodejs"]`, MaxConcurrency: 2, Status: "busy", CurrentJobs: 2, LastHeartbeat: &hb},
{Name: "build-host-03", IP: "10.0.8.13", Labels: `["python"]`, MaxConcurrency: 2, Status: "offline", LastHeartbeat: &hb},
})
}
DB.Model(&model.WebhookEndpoint{}).Count(&n)
if n == 0 {
DB.Create([]model.WebhookEndpoint{
{Name: "发布成功通知", URL: "https://open.feishu.cn/open-apis/bot/v2/hook/CHANGE_ME", Events: `["pipeline.success","pipeline.failed"]`, Active: true, LastStatus: "none"},
})
}
DB.Model(&model.SystemSetting{}).Count(&n)
if n == 0 {
DB.Create(&model.SystemSetting{
ID: 1, ToolMode: "local",
Theme: `{"mode":"light","primaryColor":"#0969da","fontSize":"medium","density":"comfortable","sidebarStyle":"full","borderRadius":"soft"}`,
Notification: `{"email":false,"browser":true,"pipelineSuccess":true,"pipelineFailed":true,"securityAlert":false}`,
})
}
}
// seedTools 工具清单(CheckCommand 为真实版本探测命令,安装走保守检测策略)
func seedTools() {
var n int64
DB.Model(&model.DeployTool{}).Count(&n)
if n > 0 {
return
}
DB.Create([]model.DeployTool{
{ToolKey: "jdk", DisplayName: "JDK", Category: "build", Version: "17", InstallMode: "preinstalled", Path: "/usr/lib/jvm/java-17", CheckCommand: "java -version", SortOrder: 1},
{ToolKey: "maven", DisplayName: "Maven", Category: "build", Version: "3.9", InstallMode: "lazy", Path: "/opt/maven", CheckCommand: "mvn -version", SortOrder: 2},
{ToolKey: "golang", DisplayName: "Golang", Category: "build", Version: "1.24", InstallMode: "preinstalled", Path: "/usr/local/go", CheckCommand: "go version", SortOrder: 3},
{ToolKey: "nodejs", DisplayName: "Node.js", Category: "build", Version: "20", InstallMode: "lazy", Path: "/usr/local/nvm", CheckCommand: "node -v", SortOrder: 4},
{ToolKey: "git", DisplayName: "Git", Category: "build", Version: "2.40", InstallMode: "preinstalled", Path: "/usr/bin/git", CheckCommand: "git --version", SortOrder: 5},
{ToolKey: "docker-cli", DisplayName: "Docker CLI", Category: "deploy", Version: "26", InstallMode: "lazy", Path: "/usr/bin/docker", CheckCommand: "docker --version", SortOrder: 6},
})
}