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)) }