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, DiskInfo: "78G/200G", LastHeartbeat: &hb}, {Name: "build-host-02", IP: "10.0.8.12", Labels: `["docker","nodejs"]`, MaxConcurrency: 2, Status: "busy", CurrentJobs: 2, DiskInfo: "156G/500G", LastHeartbeat: &hb}, {Name: "build-host-03", IP: "10.0.8.13", Labels: `["python"]`, MaxConcurrency: 2, Status: "offline", DiskInfo: "45G/100G", 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}, }) }