feat: FlowStack 发布系统首个可运行版本 (gin + React)
- Go gin 后端:流水线/节点/连线/运行/日志/GitOps/插件 REST API, SQLite(GORM) 自动建表 + seed,模拟执行引擎 + SSE 实时日志推送, 前端产物 go:embed 嵌入单二进制,一个端口同时服务 API 与页面 - React 19 前端:画布式流水线编辑器(React Flow + Tekton 任务库)、 GitOps 部署、批量发布、发布记录等 14 个模块页面, tRPC 全部替换为 REST(TanStack Query) - 首次启动生成随机密码管理员账号(日志 + data/initial-admin-password.txt) - run.sh 一键构建启动(默认端口 9527,支持 start/stop/restart/build/dev)
This commit is contained in:
commit
04d1342b27
115 files changed
+23268
No files matched your search
@@ -0,0 +1,167 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
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{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
seedAdmin()
|
||||
seedDemoData()
|
||||
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 应用 ----
|
||||
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},
|
||||
{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"},
|
||||
})
|
||||
|
||||
// ---- 已安装插件 ----
|
||||
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},
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user