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 数据文件
This commit is contained in:
1 parent
04d1342b27
commit
26341c3e8a
37 files changed
+5702
-4683
No files matched your search
@@ -0,0 +1,244 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"flowstack/internal/crypto"
|
||||
"flowstack/internal/database"
|
||||
"flowstack/internal/jenkins"
|
||||
"flowstack/internal/model"
|
||||
)
|
||||
|
||||
func clientFromServer(s model.JenkinsServer) *jenkins.Client {
|
||||
token := s.Token
|
||||
if crypto.IsEncrypted(token) {
|
||||
if plain, err := crypto.DecryptField(token); err == nil {
|
||||
token = plain
|
||||
}
|
||||
}
|
||||
return jenkins.NewClient(s.BaseURL, s.Username, token)
|
||||
}
|
||||
|
||||
// ---- Jenkins 服务器 CRUD ----
|
||||
|
||||
func ListJenkinsServers(c *gin.Context) {
|
||||
var list []model.JenkinsServer
|
||||
database.DB.Order("updated_at DESC").Find(&list)
|
||||
for i := range list {
|
||||
list[i].TokenSet = list[i].Token != ""
|
||||
list[i].Token = ""
|
||||
}
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
func CreateJenkinsServer(c *gin.Context) {
|
||||
var raw map[string]any
|
||||
if err := c.ShouldBindJSON(&raw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
name, _ := raw["name"].(string)
|
||||
baseURL, _ := raw["baseUrl"].(string)
|
||||
if name == "" || baseURL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "名称和地址不能为空"})
|
||||
return
|
||||
}
|
||||
s := model.JenkinsServer{Name: name, BaseURL: baseURL, Enabled: true}
|
||||
s.Username, _ = raw["username"].(string)
|
||||
if token, ok := raw["token"].(string); ok && token != "" {
|
||||
enc, err := crypto.EncryptField(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token 加密失败"})
|
||||
return
|
||||
}
|
||||
s.Token = enc
|
||||
}
|
||||
if err := database.DB.Create(&s).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "创建失败:名称可能已存在"})
|
||||
return
|
||||
}
|
||||
s.TokenSet = s.Token != ""
|
||||
s.Token = ""
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
func UpdateJenkinsServer(c *gin.Context) {
|
||||
var s model.JenkinsServer
|
||||
if err := database.DB.First(&s, c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "jenkins server not found"})
|
||||
return
|
||||
}
|
||||
var raw map[string]any
|
||||
_ = c.ShouldBindJSON(&raw)
|
||||
updates := map[string]any{}
|
||||
for _, k := range []string{"name", "baseUrl", "username", "enabled", "remark"} {
|
||||
if v, ok := raw[k]; ok {
|
||||
key := k
|
||||
if k == "baseUrl" {
|
||||
key = "base_url"
|
||||
}
|
||||
updates[key] = v
|
||||
}
|
||||
}
|
||||
if token, ok := raw["token"].(string); ok && token != "" {
|
||||
if enc, err := crypto.EncryptField(token); err == nil {
|
||||
updates["token"] = enc
|
||||
}
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
database.DB.Model(&s).Updates(updates)
|
||||
}
|
||||
database.DB.First(&s, s.ID)
|
||||
s.TokenSet = s.Token != ""
|
||||
s.Token = ""
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
func DeleteJenkinsServer(c *gin.Context) {
|
||||
database.DB.Delete(&model.JenkinsServer{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// TestJenkinsServer POST /api/jenkins-servers/:id/test —— 真实连通测试
|
||||
func TestJenkinsServer(c *gin.Context) {
|
||||
var s model.JenkinsServer
|
||||
if err := database.DB.First(&s, c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "jenkins server not found"})
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
err := clientFromServer(s).Test()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": err == nil, "latencyMs": time.Since(start).Milliseconds(),
|
||||
"message": func() string {
|
||||
if err != nil {
|
||||
return "连接失败: " + err.Error()
|
||||
}
|
||||
return "连接成功,认证通过"
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
// ListJenkinsJobs GET /api/jenkins-servers/:id/jobs —— 真实拉取 Job 树(folder 已压平)
|
||||
func ListJenkinsJobs(c *gin.Context) {
|
||||
var s model.JenkinsServer
|
||||
if err := database.DB.First(&s, c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "jenkins server not found"})
|
||||
return
|
||||
}
|
||||
jobs, err := clientFromServer(s).ListJobs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "拉取 Job 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jobs)
|
||||
}
|
||||
|
||||
// ImportJenkinsJobs POST /api/jenkins-servers/:id/import —— 导入 Job 为 jenkins 类型流水线
|
||||
func ImportJenkinsJobs(c *gin.Context) {
|
||||
var s model.JenkinsServer
|
||||
if err := database.DB.First(&s, c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "jenkins server not found"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Jobs []string `json:"jobs"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || len(body.Jobs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要导入的 Job"})
|
||||
return
|
||||
}
|
||||
|
||||
imported, skipped := 0, 0
|
||||
for _, jobPath := range body.Jobs {
|
||||
var exist model.Pipeline
|
||||
if err := database.DB.Where("jenkins_job_name = ? AND jenkins_server_id = ?", jobPath, s.ID).First(&exist).Error; err == nil {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
name := jobPath
|
||||
if len(name) > 60 {
|
||||
name = name[len(name)-60:]
|
||||
}
|
||||
p := model.Pipeline{
|
||||
Name: name, Description: "从 Jenkins 导入 (" + s.Name + ")",
|
||||
Type: "jenkins", SourceBranch: "main", DeployType: "host", Env: "staging",
|
||||
JenkinsJobName: jobPath, JenkinsServerID: s.ID,
|
||||
}
|
||||
// 名称冲突时追加后缀保证唯一
|
||||
base := name
|
||||
for i := 0; ; i++ {
|
||||
var cnt int64
|
||||
database.DB.Model(&model.Pipeline{}).Where("name = ?", p.Name).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
break
|
||||
}
|
||||
if i >= 5 {
|
||||
p.Name = fmt.Sprintf("%s-%d", base, time.Now().UnixMilli())
|
||||
break
|
||||
}
|
||||
p.Name = fmt.Sprintf("%s-%d", base, i+1)
|
||||
}
|
||||
database.DB.Create(&p)
|
||||
imported++
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "imported": imported, "skipped": skipped})
|
||||
}
|
||||
|
||||
// jenkinsRunEngine jenkins 类型流水线的真实执行引擎:
|
||||
// 触发 → 队列 → 增量控制台 → 结果映射,全程写 LogEntry + SSE 推送
|
||||
func jenkinsRunEngine(hub *Hub, run model.PipelineRun, client *jenkins.Client, jobName string) {
|
||||
start := time.Now()
|
||||
emit := func(msg string) {
|
||||
entry := model.LogEntry{RunID: run.ID, StageID: 0, Level: "info", Message: msg, Source: "jenkins", Timestamp: time.Now()}
|
||||
database.DB.Create(&entry)
|
||||
hub.Publish(run.RunID, "log", entry)
|
||||
}
|
||||
setRun := func(status string) {
|
||||
now := time.Now()
|
||||
database.DB.Model(&model.PipelineRun{}).Where("id = ?", run.ID).Updates(map[string]any{
|
||||
"status": status, "duration_ms": time.Since(start).Milliseconds(), "completed_at": &now,
|
||||
})
|
||||
hub.Publish(run.RunID, "run", gin.H{"runId": run.RunID, "status": status, "durationMs": time.Since(start).Milliseconds()})
|
||||
if status == "success" || status == "failed" {
|
||||
NotifyWebhookEvent("pipeline."+status, map[string]any{
|
||||
"runId": run.RunID, "pipeline": run.PipelineName, "durationMs": time.Since(start).Milliseconds(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
emit("$ jenkins build \"" + jobName + "\"")
|
||||
queueURL, err := client.Trigger(jobName, nil)
|
||||
if err != nil {
|
||||
emit("触发失败: " + err.Error())
|
||||
setRun("failed")
|
||||
return
|
||||
}
|
||||
emit("已进入 Jenkins 构建队列…")
|
||||
|
||||
buildNumber, buildURL, err := client.WaitQueue(queueURL, 10*time.Minute, emit)
|
||||
if err != nil {
|
||||
emit("队列等待失败: " + err.Error())
|
||||
setRun("failed")
|
||||
return
|
||||
}
|
||||
database.DB.Model(&model.PipelineRun{}).Where("id = ?", run.ID).Updates(map[string]any{
|
||||
"build_number": buildNumber, "external_run_id": fmt.Sprintf("#%d", buildNumber), "external_url": buildURL,
|
||||
})
|
||||
emit(fmt.Sprintf("构建 #%d 已开始: %s", buildNumber, buildURL))
|
||||
|
||||
client.StreamConsole(buildURL, emit)
|
||||
result, err := client.WaitResult(buildURL, 30*time.Minute)
|
||||
if err != nil {
|
||||
emit("获取构建结果超时: " + err.Error())
|
||||
setRun("failed")
|
||||
return
|
||||
}
|
||||
emit("Jenkins 结果: " + result)
|
||||
setRun(jenkins.MapResult(result))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user