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:
Logan.Li committed 2026-10-27 02:44:10 +08:00
commit 04d1342b27
115 files changed
+23268

No files matched your search

+167
View File
@@ -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},
})
}