权限模型(借鉴 Jenkins Role-Strategy 与云效,默认拒绝): - 三级权限:查看(流水线+成功/失败日志)/ 发布(+构建、批量发布、重试)/ 管理(+配置、成员);admin 全局豁免 - 权限组(管理员配置):默认手动多选流水线(免正则,操作友好);高级模式仅 * 通配符(cex-*) - 单流水线成员直授权作为例外补充;AI 批量发布逐 job 校验,越权明确拒绝 后端: - 新增 PipelineRole/PipelineMember 表与 /api/pipeline-roles、/api/pipelines/:id/members CRUD - RequirePipelineAccess/RequireRunAccess 中间件挂全流水线与运行路由;列表按可见性过滤并附 accessLevel/canRetry - /api/me 会话接口;logs/stream SSE 支持 ?token= 认证(原先完全免鉴权)并校验查看权限 - 凭证/主机/集群/构建节点/工具/Webhook/Jenkins/GitOps/插件/设置全部 AdminOnly;触发人以会话身份落库 前端: - useSession 会话 hook;Sidebar 菜单分级(资源/集成/系统管理 admin-only);admin 路由守卫 - 按钮级权限:执行/配置/删除/保存/重试按等级显隐;新页「流水线权限」与流水线成员弹窗(纯点选) - 登录页记住密码;GUIDE/API 文档补权限章节
245 lines
13 KiB
Go
245 lines
13 KiB
Go
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.PipelineRole{}, &model.PipelineMember{},
|
||
&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 + Kubernetes) ----
|
||
backend := model.Pipeline{
|
||
Name: "aiops-backend", Description: "AIOps 后端服务(Go + Kubernetes 滚动发布)",
|
||
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 工具清单(Ubuntu 构建环境导向;Node 由 nvm 管理默认版本,构建时可按需 nvm install)
|
||
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-openjdk-amd64", CheckCommand: "java -version", InstallHint: "apt update && apt install -y openjdk-17-jdk", SortOrder: 1},
|
||
{ToolKey: "maven", DisplayName: "Maven", Category: "build", Version: "3.8", InstallMode: "lazy", Path: "/usr/share/maven", CheckCommand: "mvn -version", InstallHint: "apt install -y maven", SortOrder: 2},
|
||
{ToolKey: "golang", DisplayName: "Golang", Category: "build", Version: "1.24", InstallMode: "preinstalled", Path: "/usr/local/go", CheckCommand: "go version", InstallHint: "apt install -y golang-1.24-go 或官方 tar 包解压至 /usr/local/go", SortOrder: 3},
|
||
{ToolKey: "nvm", DisplayName: "nvm(Node 版本管理)", Category: "build", Version: "0.40", InstallMode: "preinstalled", Path: "$HOME/.nvm", CheckCommand: `. "$HOME/.nvm/nvm.sh" && nvm --version`, InstallHint: "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash", SortOrder: 4},
|
||
{ToolKey: "nodejs", DisplayName: "Node.js(nvm 管理)", Category: "build", Version: "20", InstallMode: "lazy", Path: "$HOME/.nvm/versions/node", CheckCommand: "node -v", InstallHint: "nvm install 20 && nvm alias default 20(构建时可用 nvm install 18 按需切版本)", SortOrder: 5},
|
||
{ToolKey: "git", DisplayName: "Git", Category: "build", Version: "2.40", InstallMode: "preinstalled", Path: "/usr/bin/git", CheckCommand: "git --version", InstallHint: "apt install -y git", SortOrder: 6},
|
||
{ToolKey: "docker-cli", DisplayName: "Docker CLI", Category: "deploy", Version: "26", InstallMode: "lazy", Path: "/usr/bin/docker", CheckCommand: "docker --version", InstallHint: "apt install -y docker.io", SortOrder: 7},
|
||
})
|
||
}
|