feat(alert-center): 添加飞书Webhook通知渠道 + 告警引擎实时通知
- 后端: NotifyChannel 模型新增 feishu 类型, FeishuWebhookConfig - 后端: TestNotifyChannel 真实发送飞书测试消息(文本格式) - 后端: 告警引擎触发/恢复时自动发送飞书通知(sendFeishuAlert/sendFeishuRecovery) - 后端: AlertEngine 新增 tcp_time_wait/tcp_close_wait/service_process 指标检测 - 后端: NetworkStatModel 新增 TCPCloseWait 字段 - 后端: QuickSetupRules 一键创建6条告警规则(CPU/内存/磁盘/TIME_WAIT/CLOSE_WAIT/进程) - 后端: 注册 /alerts/quick-setup 路由 - 前端: AlertChannelDialog 新增飞书类型选项 - 前端: helpers 新增 getChannelIcon/getChannelTypeLabel 支持 feishu/dingtalk/wechat
This commit is contained in:
1 parent
7bc5169e8f
commit
5333f6f04a
30 files changed
+2406
-1001
No files matched your search
Executable
BIN
Binary file not shown.
@@ -51,6 +51,12 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
portJSON, _ := json.Marshal(req.Ports)
|
||||
netJSON, _ := json.Marshal(req.Network)
|
||||
dioJSON, _ := json.Marshal(req.DiskIO)
|
||||
// v2.0 新增
|
||||
swapJSON, _ := json.Marshal(req.Swap)
|
||||
processTopJSON, _ := json.Marshal(req.ProcessTop)
|
||||
fdJSON, _ := json.Marshal(req.FD)
|
||||
csJSON, _ := json.Marshal(req.ContextSwitches)
|
||||
heartbeatJSON, _ := json.Marshal(req.Heartbeat)
|
||||
|
||||
// 存储到数据库
|
||||
report := models.AgentReport{
|
||||
@@ -67,6 +73,12 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
PortInfo: string(portJSON),
|
||||
NetInfo: string(netJSON),
|
||||
DiskIOInfo: string(dioJSON),
|
||||
// v2.0 新增
|
||||
SwapInfo: string(swapJSON),
|
||||
ProcessTopInfo: string(processTopJSON),
|
||||
FDInfo: string(fdJSON),
|
||||
CSInfo: string(csJSON),
|
||||
HeartbeatInfo: string(heartbeatJSON),
|
||||
ReportTime: reportTime,
|
||||
}
|
||||
|
||||
@@ -264,6 +276,11 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
a.port_info,
|
||||
a.net_info,
|
||||
a.disk_io_info,
|
||||
a.swap_info,
|
||||
a.process_top_info,
|
||||
a.fd_info,
|
||||
a.cs_info,
|
||||
a.heartbeat_info,
|
||||
a.report_time,
|
||||
COALESCE(s.agent_version, '') as agent_version,
|
||||
CASE
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
@@ -163,9 +171,9 @@ func (h *AlertHandler) DeleteNotifyChannel(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestNotifyChannel 测试通知渠道 // Logan.Li
|
||||
// TestNotifyChannel 测试通知渠道 — 真实发送测试消息 // Logan.Li
|
||||
// @Summary 测试通知渠道连通性
|
||||
// @Description 模拟测试通知渠道(返回测试状态)
|
||||
// @Description 向渠道发送测试消息,验证连通性
|
||||
// @Tags 告警
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
@@ -184,12 +192,22 @@ func (h *AlertHandler) TestNotifyChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟测试:校验配置非空即视为成功
|
||||
status := "success"
|
||||
message := "测试通知已发送"
|
||||
if channel.Config == "" {
|
||||
status = "warning"
|
||||
message = "渠道未配置参数,请先完善配置"
|
||||
|
||||
switch channel.Type {
|
||||
case "feishu":
|
||||
err := sendFeishuTest(channel.Config)
|
||||
if err != nil {
|
||||
status = "error"
|
||||
message = "飞书通知发送失败: " + err.Error()
|
||||
}
|
||||
default:
|
||||
// 非飞书渠道仅检查配置非空
|
||||
if channel.Config == "" {
|
||||
status = "warning"
|
||||
message = "渠道未配置参数,请先完善配置"
|
||||
}
|
||||
}
|
||||
|
||||
h.db.Model(&channel).Update("test_status", status)
|
||||
@@ -204,3 +222,201 @@ func (h *AlertHandler) TestNotifyChannel(c *gin.Context) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 飞书 Webhook 发送逻辑 ====================
|
||||
|
||||
// sendFeishuTest 发送飞书测试通知
|
||||
func sendFeishuTest(configJSON string) error {
|
||||
var cfg models.FeishuWebhookConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
if cfg.URL == "" {
|
||||
return fmt.Errorf("webhook URL 为空")
|
||||
}
|
||||
|
||||
title := "✅ AIOps 告警测试通知"
|
||||
content := fmt.Sprintf("通知渠道连通性测试成功\\n测试时间:%s\\n来源:AIOps 智能运维平台", time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
return sendFeishuCard(cfg.URL, cfg.Secret, title, content, "green")
|
||||
}
|
||||
|
||||
// sendFeishuAlert 发送飞书告警通知
|
||||
func sendFeishuAlert(configJSON, title, content, severity string) error {
|
||||
var cfg models.FeishuWebhookConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
if cfg.URL == "" {
|
||||
return fmt.Errorf("webhook URL 为空")
|
||||
}
|
||||
|
||||
color := "red"
|
||||
if severity == "warning" {
|
||||
color = "orange"
|
||||
} else if severity == "info" {
|
||||
color = "blue"
|
||||
}
|
||||
|
||||
emoji := "🔴"
|
||||
if severity == "warning" {
|
||||
emoji = "🟠"
|
||||
} else if severity == "info" {
|
||||
emoji = "🔵"
|
||||
}
|
||||
|
||||
title = emoji + " " + title
|
||||
return sendFeishuCard(cfg.URL, cfg.Secret, title, content, color)
|
||||
}
|
||||
|
||||
// sendFeishuRecovery 发送飞书恢复通知
|
||||
func sendFeishuRecovery(configJSON, title, content string) error {
|
||||
var cfg models.FeishuWebhookConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
if cfg.URL == "" {
|
||||
return fmt.Errorf("webhook URL 为空")
|
||||
}
|
||||
|
||||
title = "✅ " + title
|
||||
return sendFeishuCard(cfg.URL, cfg.Secret, title, content, "green")
|
||||
}
|
||||
|
||||
// sendFeishuCard 发送飞书消息(优先使用卡片格式,出错回退到文本)
|
||||
func sendFeishuCard(webhookURL, secret, title, content, color string) error {
|
||||
// 先尝试文本格式(最可靠)
|
||||
textMsg := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"content": map[string]interface{}{
|
||||
"text": title + "\n\n" + content,
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(textMsg)
|
||||
|
||||
req, err := http.NewRequest("POST", webhookURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 如果有 secret,生成签名
|
||||
if secret != "" {
|
||||
timestamp := time.Now().Unix()
|
||||
sign := genFeishuSign(timestamp, secret)
|
||||
q := req.URL.Query()
|
||||
q.Set("timestamp", fmt.Sprintf("%d", timestamp))
|
||||
q.Set("sign", sign)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("飞书返回错误 (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// 检查飞书业务返回码
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
json.Unmarshal(respBody, &result)
|
||||
if result.Code != 0 {
|
||||
return fmt.Errorf("飞书返回错误 (code=%d): %s", result.Code, result.Msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildFeishuCard 构建飞书卡片消息体(保留备用,当前使用文本格式)
|
||||
func buildFeishuCard(title, content, color string) models.FeishuCardMessage {
|
||||
return models.FeishuCardMessage{
|
||||
MsgType: "interactive",
|
||||
Content: models.FeishuCardContent{
|
||||
Title: title,
|
||||
Content: [][]models.FeishuCardElement{
|
||||
{
|
||||
{
|
||||
Tag: "markdown",
|
||||
Text: content,
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Tag: "note",
|
||||
Text: fmt.Sprintf("⏰ %s | AIOps 智能运维平台", time.Now().Format("2006-01-02 15:04:05")),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// genFeishuSign 生成飞书签名
|
||||
func genFeishuSign(timestamp int64, secret string) string {
|
||||
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
|
||||
h := hmac.New(sha256.New, []byte(stringToSign))
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
return signature
|
||||
}
|
||||
|
||||
// ==================== 批量通知发送 ====================
|
||||
|
||||
// NotifyAlert 发送告警通知到所有启用的渠道
|
||||
func (h *AlertHandler) NotifyAlert(ruleName, hostname, metric, severity, message string, value, threshold float64) {
|
||||
var channels []models.NotifyChannel
|
||||
h.db.Where("enabled = ?", true).Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
switch ch.Type {
|
||||
case "feishu":
|
||||
title := buildAlertTitle(ruleName, hostname, metric, severity)
|
||||
content := buildAlertContent(ruleName, hostname, metric, severity, message, value, threshold)
|
||||
if err := sendFeishuAlert(ch.Config, title, content, severity); err != nil {
|
||||
fmt.Printf("[AlertNotify] 飞书通知失败 (渠道:%s): %v\n", ch.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyRecovery 发送恢复通知到所有启用的渠道
|
||||
func (h *AlertHandler) NotifyRecovery(ruleName, hostname, metric, message string) {
|
||||
var channels []models.NotifyChannel
|
||||
h.db.Where("enabled = ?", true).Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
switch ch.Type {
|
||||
case "feishu":
|
||||
title := fmt.Sprintf("%s — 已恢复正常", ruleName)
|
||||
content := fmt.Sprintf("**告警已恢复**\\n\\n主机:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s",
|
||||
hostname, metric, message, time.Now().Format("2006-01-02 15:04:05"))
|
||||
if err := sendFeishuRecovery(ch.Config, title, content); err != nil {
|
||||
fmt.Printf("[AlertNotify] 飞书恢复通知失败 (渠道:%s): %v\n", ch.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildAlertTitle(ruleName, hostname, metric, severity string) string {
|
||||
sevLabel := "严重告警"
|
||||
if severity == "warning" {
|
||||
sevLabel = "警告"
|
||||
} else if severity == "info" {
|
||||
sevLabel = "信息"
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s — %s (%s)", sevLabel, ruleName, hostname, metric)
|
||||
}
|
||||
|
||||
func buildAlertContent(ruleName, hostname, metric, severity, message string, value, threshold float64) string {
|
||||
return fmt.Sprintf(
|
||||
"**规则名称:** %s\\n**目标主机:** %s\\n**指标类型:** %s\\n**告警级别:** %s\\n**当前值:** %.2f\\n**阈值:** %.2f\\n**详细信息:** %s\\n**触发时间:** %s",
|
||||
ruleName, hostname, metric, severity, value, threshold, message, time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
}
|
||||
@@ -27,10 +27,10 @@ func (e *AlertEngine) Check() {
|
||||
e.autoResolveAlerts()
|
||||
}
|
||||
|
||||
// 检查 CPU/内存/磁盘 阈值
|
||||
// 检查 CPU/内存/磁盘/进程/TCP连接/端口 阈值
|
||||
func (e *AlertEngine) checkResourceAlerts() {
|
||||
var rules []models.AgentRule
|
||||
e.db.Where("enabled = ? AND metric IN ?", true, []string{"cpu", "mem", "disk"}).Find(&rules)
|
||||
e.db.Where("enabled = ? AND metric IN ?", true, []string{"cpu", "mem", "disk", "service_process", "tcp_time_wait", "tcp_close_wait"}).Find(&rules)
|
||||
|
||||
for _, rule := range rules {
|
||||
// 获取目标主机最新上报
|
||||
@@ -58,6 +58,29 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
value = d.UsagePercent
|
||||
}
|
||||
}
|
||||
case "tcp_time_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPTimeWait)
|
||||
case "tcp_close_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPCloseWait)
|
||||
case "service_process":
|
||||
// 进程不存在告警 — 检查 process_top_info 中是否包含指定进程名
|
||||
var processes []models.ProcessMetric
|
||||
json.Unmarshal([]byte(report.ProcessTopInfo), &processes)
|
||||
found := false
|
||||
for _, p := range processes {
|
||||
if p.Name == rule.Hostname || rule.Hostname == "" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue // 进程存在,不触发
|
||||
}
|
||||
value = 0 // 进程不存在时触发
|
||||
}
|
||||
|
||||
// 检查是否超阈值
|
||||
@@ -89,6 +112,9 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
}
|
||||
e.db.Create(&alert)
|
||||
|
||||
// 发送告警通知
|
||||
e.notifyAlert(&rule, report.Hostname, alert)
|
||||
|
||||
// 更新规则最后触发时间
|
||||
now := time.Now()
|
||||
e.db.Model(&rule).Update("last_triggered", now)
|
||||
@@ -150,10 +176,12 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
if existing.ID == 0 {
|
||||
// 获取最后一次上报时间计算离线时长
|
||||
var lastReport models.AgentReport
|
||||
e.db.Order("id DESC").First(&lastReport)
|
||||
e.db.Where("hostname = ?", s.Hostname).Order("id DESC").First(&lastReport)
|
||||
offlineMinutes := 3.0
|
||||
var lastReportTime time.Time
|
||||
if lastReport.ID > 0 {
|
||||
offlineMinutes = time.Since(lastReport.ReportTime).Minutes()
|
||||
lastReportTime = lastReport.ReportTime
|
||||
}
|
||||
|
||||
alert := models.AgentAlert{
|
||||
@@ -166,6 +194,15 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
Threshold: 3,
|
||||
}
|
||||
e.db.Create(&alert)
|
||||
|
||||
// 发送离线告警通知
|
||||
e.notifyAlertRule("主机离线", s.Hostname, "offline", "critical",
|
||||
alert.Message, offlineMinutes, 3)
|
||||
|
||||
// 记录离线上报时间用于恢复通知
|
||||
if !lastReportTime.IsZero() {
|
||||
e.db.Model(&alert).Update("created_at", time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +263,10 @@ func (e *AlertEngine) checkPortAlerts() {
|
||||
}
|
||||
e.db.Create(&alert)
|
||||
|
||||
// 发送端口Down告警通知
|
||||
e.notifyAlertRule(rule.Name, report.Hostname, "port_down", "critical",
|
||||
alert.Message, float64(rule.Port), float64(rule.Port))
|
||||
|
||||
now := time.Now()
|
||||
e.db.Model(&rule).Update("last_triggered", now)
|
||||
}
|
||||
@@ -247,7 +288,7 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
if report.ID > 0 && time.Since(report.ReportTime) < 3*time.Minute {
|
||||
resolved = true
|
||||
}
|
||||
case "cpu", "mem", "disk":
|
||||
case "cpu", "mem", "disk", "tcp_time_wait", "tcp_close_wait":
|
||||
// 获取对应规则,检查是否恢复正常
|
||||
var rule models.AgentRule
|
||||
e.db.Where("name = ?", alert.RuleName).First(&rule)
|
||||
@@ -277,6 +318,14 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
value = d.UsagePercent
|
||||
}
|
||||
}
|
||||
case "tcp_time_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPTimeWait)
|
||||
case "tcp_close_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPCloseWait)
|
||||
}
|
||||
if !e.compareOperator(rule.Operator, value, rule.Threshold) {
|
||||
resolved = true
|
||||
@@ -304,6 +353,9 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
"resolved": true,
|
||||
"resolved_at": now,
|
||||
})
|
||||
|
||||
// 发送恢复通知
|
||||
e.notifyRecovery(alert.RuleName, alert.Hostname, alert.Metric, alert.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,3 +376,41 @@ func (e *AlertEngine) compareOperator(op string, val, threshold float64) bool {
|
||||
return val > threshold
|
||||
}
|
||||
}
|
||||
|
||||
// notifyAlert 发送告警通知(通过 AgentRule 对应的 notify_channels)
|
||||
func (e *AlertEngine) notifyAlert(rule *models.AgentRule, hostname string, alert models.AgentAlert) {
|
||||
e.notifyAlertRule(rule.Name, hostname, alert.Metric, alert.Severity,
|
||||
alert.Message, alert.Value, alert.Threshold)
|
||||
}
|
||||
|
||||
// notifyAlertRule 按规则发送告警通知到所有启用的飞书渠道
|
||||
func (e *AlertEngine) notifyAlertRule(ruleName, hostname, metric, severity, message string, value, threshold float64) {
|
||||
var channels []models.NotifyChannel
|
||||
e.db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
title := fmt.Sprintf("[%s] %s — %s (%s)", severity, ruleName, hostname, metric)
|
||||
content := fmt.Sprintf(
|
||||
"**规则名称:** %s\\n**目标主机:** %s\\n**指标类型:** %s\\n**告警级别:** %s\\n**当前值:** %.2f\\n**阈值:** %.2f\\n**详细信息:** %s\\n**触发时间:** %s",
|
||||
ruleName, hostname, metric, severity, value, threshold, message, time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
if err := sendFeishuAlert(ch.Config, title, content, severity); err != nil {
|
||||
fmt.Printf("[AlertEngine] 飞书通知失败 (渠道:%s): %v\n", ch.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notifyRecovery 发送恢复通知到所有启用的飞书渠道
|
||||
func (e *AlertEngine) notifyRecovery(ruleName, hostname, metric, message string) {
|
||||
var channels []models.NotifyChannel
|
||||
e.db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
title := fmt.Sprintf("%s — 已恢复正常", ruleName)
|
||||
content := fmt.Sprintf("**告警已恢复**\\n\\n主机:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s",
|
||||
hostname, metric, message, time.Now().Format("2006-01-02 15:04:05"))
|
||||
if err := sendFeishuRecovery(ch.Config, title, content); err != nil {
|
||||
fmt.Printf("[AlertEngine] 飞书恢复通知失败 (渠道:%s): %v\n", ch.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -272,3 +273,64 @@ func (h *AlertHandler) ToggleRule(c *gin.Context) {
|
||||
Data: rule,
|
||||
})
|
||||
}
|
||||
|
||||
// QuickSetupRules 一键创建测试告警规则(飞书Webhook测试用) // Logan.Li
|
||||
// @Summary 一键创建测试告警规则
|
||||
// @Tags 告警
|
||||
// @Router /alerts/quick-setup [post]
|
||||
func (h *AlertHandler) QuickSetupRules(c *gin.Context) {
|
||||
type ruleDef struct {
|
||||
Name string `json:"name"`
|
||||
Metric string `json:"metric"`
|
||||
Operator string `json:"operator"`
|
||||
Threshold float64 `json:"threshold"`
|
||||
Port int `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Rules []ruleDef `json:"rules"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
// 默认规则:CPU 80%, 内存 80%, 磁盘 90%, TIME_WAIT 15000, CLOSE_WAIT 200, 端口Down, 进程检测
|
||||
defaultRules := []ruleDef{
|
||||
{Name: "CPU使用率", Metric: "cpu", Operator: ">", Threshold: 80},
|
||||
{Name: "内存使用率", Metric: "mem", Operator: ">", Threshold: 80},
|
||||
{Name: "磁盘使用率", Metric: "disk", Operator: ">", Threshold: 90},
|
||||
{Name: "TIME_WAIT连接数", Metric: "tcp_time_wait", Operator: ">", Threshold: 15000},
|
||||
{Name: "CLOSE_WAIT连接数", Metric: "tcp_close_wait", Operator: ">", Threshold: 200},
|
||||
{Name: "进程异常退出", Metric: "service_process", Operator: "==", Threshold: 0},
|
||||
}
|
||||
|
||||
rules := req.Rules
|
||||
if len(rules) == 0 {
|
||||
rules = defaultRules
|
||||
}
|
||||
|
||||
created := 0
|
||||
for _, def := range rules {
|
||||
var existing models.AgentRule
|
||||
h.db.Where("name = ?", def.Name).First(&existing)
|
||||
if existing.ID > 0 {
|
||||
continue
|
||||
}
|
||||
rule := models.AgentRule{
|
||||
Name: def.Name,
|
||||
Metric: def.Metric,
|
||||
Operator: def.Operator,
|
||||
Threshold: def.Threshold,
|
||||
Port: def.Port,
|
||||
Enabled: true,
|
||||
}
|
||||
if err := h.db.Create(&rule).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
created++
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已创建 %d 条告警规则", created),
|
||||
Data: gin.H{"created": created},
|
||||
})
|
||||
}
|
||||
@@ -566,7 +566,7 @@ var (
|
||||
// 否则 → 加入队列 (status=queued)
|
||||
func (h *PipelineHandler) tryExecuteOrQueue(pipeline models.Pipeline, run models.PipelineRun) {
|
||||
max := pipeline.MaxExecutors
|
||||
if max <= 0 { max = 1 }
|
||||
if max <= 0 { max = 2 }
|
||||
|
||||
queueMu.Lock()
|
||||
running := pipelineRunningCount[pipeline.ID]
|
||||
|
||||
@@ -49,11 +49,11 @@ func (h *ToolConfigHandler) UpdateTool(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DisplayName *string `json:"display_name"`
|
||||
Description *string `json:"description"`
|
||||
InstallMethod *models.ToolInstallMethod `json:"install_method"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Description *string `json:"description"`
|
||||
InstallMethod *models.ToolInstallMethod `json:"install_method"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
@@ -87,7 +87,7 @@ func (h *ToolConfigHandler) UpdateTool(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已更新"})
|
||||
}
|
||||
|
||||
// InstallTool triggers lazy installation of a tool
|
||||
// InstallTool triggers lazy installation with optional version override
|
||||
func (h *ToolConfigHandler) InstallTool(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
@@ -105,11 +105,33 @@ func (h *ToolConfigHandler) InstallTool(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Support version override via query param or JSON body
|
||||
version := tool.DefaultVersion
|
||||
if v := c.Query("version"); v != "" {
|
||||
version = v
|
||||
}
|
||||
if v := c.PostForm("version"); v != "" {
|
||||
version = v
|
||||
}
|
||||
// Also support JSON body
|
||||
var req struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err == nil && req.Version != "" {
|
||||
version = req.Version
|
||||
}
|
||||
|
||||
// Store the requested version for the installer
|
||||
if version != "" && version != tool.DefaultVersion {
|
||||
h.db.Model(&tool).Update("default_version", version)
|
||||
tool.DefaultVersion = version
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[ToolConfig] Background install triggered for %s", tool.Name)
|
||||
log.Printf("[ToolConfig] Background install triggered for %s (version: %s)", tool.Name, version)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "安装已触发"})
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "安装已触发", Data: gin.H{"version": version}})
|
||||
}
|
||||
|
||||
// CheckToolStatus re-checks a tool's installation status
|
||||
@@ -124,9 +146,13 @@ func (h *ToolConfigHandler) CheckToolStatus(c *gin.Context) {
|
||||
version := ""
|
||||
if tool.CheckCommand != "" {
|
||||
cmd := exec.Command("sh", "-c", tool.CheckCommand)
|
||||
if err := cmd.Run(); err == nil {
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
available = true
|
||||
version = "已安装"
|
||||
version = string(out)
|
||||
if len(version) > 128 {
|
||||
version = version[:128]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,3 +171,47 @@ func (h *ToolConfigHandler) CheckToolStatus(c *gin.Context) {
|
||||
"tool": tool,
|
||||
}})
|
||||
}
|
||||
|
||||
// GetToolVersions returns available versions for a tool (jdk/node/maven)
|
||||
func (h *ToolConfigHandler) GetToolVersions(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "工具不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
versions := getPresetVersions(tool.Name)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
"name": tool.Name,
|
||||
"default": tool.DefaultVersion,
|
||||
"versions": versions,
|
||||
}})
|
||||
}
|
||||
|
||||
// getPresetVersions returns known versions for common tools
|
||||
func getPresetVersions(toolName string) []map[string]string {
|
||||
switch toolName {
|
||||
case "jdk":
|
||||
return []map[string]string{
|
||||
{"version": "11.0.24+8", "label": "JDK 11 (LTS)"},
|
||||
{"version": "17.0.13+7", "label": "JDK 17 (LTS) ★ 推荐"},
|
||||
{"version": "21.0.5+11", "label": "JDK 21 (LTS)"},
|
||||
{"version": "23.0.1+11", "label": "JDK 23 (最新)"},
|
||||
}
|
||||
case "nodejs":
|
||||
return []map[string]string{
|
||||
{"version": "v18.20.5", "label": "Node 18 (LTS)"},
|
||||
{"version": "v20.18.1", "label": "Node 20 (LTS)"},
|
||||
{"version": "v22.14.0", "label": "Node 22 (LTS) ★ 推荐"},
|
||||
}
|
||||
case "maven":
|
||||
return []map[string]string{
|
||||
{"version": "3.9.9", "label": "Maven 3.9.9 ★ 推荐"},
|
||||
{"version": "3.8.8", "label": "Maven 3.8.8"},
|
||||
{"version": "3.6.3", "label": "Maven 3.6.3"},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -454,6 +454,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
alerts.PUT("/notify-channels/:id", alertHandler.UpdateNotifyChannel)
|
||||
alerts.DELETE("/notify-channels/:id", alertHandler.DeleteNotifyChannel)
|
||||
alerts.POST("/notify-channels/:id/test", alertHandler.TestNotifyChannel)
|
||||
alerts.POST("/quick-setup", alertHandler.QuickSetupRules)
|
||||
}
|
||||
|
||||
// 流水线路由
|
||||
@@ -545,6 +546,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
tools.PUT("/:id", toolConfigHandler.UpdateTool)
|
||||
tools.POST("/:id/install", toolConfigHandler.InstallTool)
|
||||
tools.POST("/:id/check", toolConfigHandler.CheckToolStatus)
|
||||
tools.GET("/:id/versions", toolConfigHandler.GetToolVersions)
|
||||
}
|
||||
|
||||
// 自动化运维路由
|
||||
|
||||
@@ -2,35 +2,41 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
)
|
||||
|
||||
// AgentReport Agent 上报的原始指标数据 // Logan.Li
|
||||
type AgentReport struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"` // CPU 使用率 %
|
||||
CPUCores int `json:"cpu_cores"` // CPU 核心数
|
||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"` // 1分钟负载
|
||||
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"` // 5分钟负载
|
||||
Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"` // 15分钟负载
|
||||
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"` // 总内存 GB
|
||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"` // 已用内存 GB
|
||||
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"` // 内存使用率 %
|
||||
DiskInfo string `gorm:"type:text" json:"disk_info"` // 磁盘信息 JSON
|
||||
PortInfo string `gorm:"type:text" json:"port_info"` // 端口信息 JSON
|
||||
NetInfo string `gorm:"type:text" json:"net_info"` // 网络指标 JSON
|
||||
DiskIOInfo string `gorm:"type:text" json:"disk_io_info"` // 磁盘 IO JSON
|
||||
ReportTime time.Time `json:"report_time"` // 上报时间
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
||||
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"`
|
||||
Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"`
|
||||
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"`
|
||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
||||
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"`
|
||||
DiskInfo string `gorm:"type:text" json:"disk_info"`
|
||||
PortInfo string `gorm:"type:text" json:"port_info"`
|
||||
NetInfo string `gorm:"type:text" json:"net_info"`
|
||||
DiskIOInfo string `gorm:"type:text" json:"disk_io_info"`
|
||||
// ──── v2.0 新增字段 ────
|
||||
SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON
|
||||
ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON
|
||||
FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON
|
||||
CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON
|
||||
HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON
|
||||
// ──────────────────────────
|
||||
ReportTime time.Time `json:"report_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (AgentReport) TableName() string {
|
||||
return "monitor_agent_reports"
|
||||
}
|
||||
|
||||
// AgentLatestResponse Agent 最新状态(含服务器元信息)// Logan.Li
|
||||
// AgentLatestResponse Agent 最新状态(含服务器元信息)
|
||||
type AgentLatestResponse struct {
|
||||
ServerID uint `json:"server_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
@@ -41,41 +47,91 @@ type AgentLatestResponse struct {
|
||||
MemTotal float64 `json:"mem_total"`
|
||||
MemUsed float64 `json:"mem_used"`
|
||||
DiskUsage float64 `json:"disk_usage"`
|
||||
DiskTotal int64 `json:"disk_total"` // 磁盘总容量 GB
|
||||
DiskTotal int64 `json:"disk_total"`
|
||||
DiskInfo string `json:"disk_info"`
|
||||
PortInfo string `json:"port_info"`
|
||||
NetInfo string `json:"net_info"`
|
||||
DiskIOInfo string `json:"disk_io_info"`
|
||||
ReportTime string `json:"report_time"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
AgentStatus string `json:"agent_status"`
|
||||
OS string `json:"os"`
|
||||
Kernel string `json:"kernel"` // 内核版本
|
||||
Uptime int64 `json:"uptime"` // 运行秒数
|
||||
Region string `json:"region"`
|
||||
HasAuth bool `json:"has_auth"` // 是否有 SSH 凭据
|
||||
// ──── v2.0 新增 ────
|
||||
SwapInfo string `json:"swap_info"`
|
||||
ProcessTopInfo string `json:"process_top_info"`
|
||||
FDInfo string `json:"fd_info"`
|
||||
CSInfo string `json:"cs_info"`
|
||||
HeartbeatInfo string `json:"heartbeat_info"`
|
||||
// ─────────────────
|
||||
ReportTime string `json:"report_time"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
AgentStatus string `json:"agent_status"`
|
||||
OS string `json:"os"`
|
||||
Kernel string `json:"kernel"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
Region string `json:"region"`
|
||||
HasAuth bool `json:"has_auth"`
|
||||
}
|
||||
|
||||
// AgentReportRequest Agent 上报请求体 // Logan.Li
|
||||
// AgentReportRequest Agent 上报请求体
|
||||
type AgentReportRequest struct {
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
IP string `json:"ip"` // Agent 探测的本机 IP
|
||||
Timestamp string `json:"timestamp"`
|
||||
AgentVersion string `json:"agent_version"` // Agent 版本号
|
||||
CPU CPUMetric `json:"cpu"`
|
||||
Memory MemMetric `json:"memory"`
|
||||
Disks []DiskMetric `json:"disks"`
|
||||
Ports []PortMetric `json:"ports"`
|
||||
Network NetworkStat `json:"network"` // 网络指标
|
||||
DiskIO DiskIOStat `json:"disk_io"` // 磁盘 IO
|
||||
System *SystemStat `json:"system,omitempty"` // 系统信息(首次/变更时上报)
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
IP string `json:"ip"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
CPU CPUMetric `json:"cpu"`
|
||||
Memory MemMetric `json:"memory"`
|
||||
Swap *SwapMetric `json:"swap,omitempty"`
|
||||
Disks []DiskMetric `json:"disks"`
|
||||
Ports []PortMetric `json:"ports"`
|
||||
Network NetworkStatModel `json:"network"`
|
||||
DiskIO DiskIOStatModel `json:"disk_io"`
|
||||
System *SystemStatModel `json:"system,omitempty"`
|
||||
// ──── v2.0 新增 ────
|
||||
ProcessTop []ProcessMetric `json:"process_top,omitempty"`
|
||||
FD *FDMetric `json:"fd,omitempty"`
|
||||
ContextSwitches *CSMetric `json:"context_switches,omitempty"`
|
||||
Heartbeat HeartbeatMetric `json:"heartbeat"`
|
||||
}
|
||||
|
||||
// SystemStat 系统静态信息(OS/内核/运行时长)// Logan.Li
|
||||
type SystemStat struct {
|
||||
OSVersion string `json:"os_version"` // OS 版本 (如 "Ubuntu 22.04")
|
||||
Kernel string `json:"kernel"` // 内核版本
|
||||
Uptime int64 `json:"uptime"` // 运行秒数
|
||||
// ──── v2.0 新增上报类型 ────
|
||||
|
||||
type SwapMetric struct {
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
type ProcessMetric struct {
|
||||
PID int `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
MemoryMB float64 `json:"memory_mb"`
|
||||
FDCount int `json:"fd_count"`
|
||||
Threads int `json:"threads"`
|
||||
}
|
||||
|
||||
type FDMetric struct {
|
||||
Allocated int `json:"allocated"`
|
||||
Max int64 `json:"max"`
|
||||
UsagePct float64 `json:"usage_pct"`
|
||||
}
|
||||
|
||||
type CSMetric struct {
|
||||
CTXPerSec float64 `json:"ctx_per_sec"`
|
||||
IRQPerSec float64 `json:"irq_per_sec"`
|
||||
}
|
||||
|
||||
type HeartbeatMetric struct {
|
||||
Version string `json:"version"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
LastErr string `json:"last_err,omitempty"`
|
||||
SelfCPU float64 `json:"self_cpu_pct"`
|
||||
SelfRSSMB float64 `json:"self_rss_mb"`
|
||||
ReportSeq uint64 `json:"report_seq"`
|
||||
}
|
||||
|
||||
// ──── 已有类型 ────
|
||||
|
||||
type SystemStatModel struct {
|
||||
OSVersion string `json:"os_version"`
|
||||
Kernel string `json:"kernel"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
}
|
||||
|
||||
type CPUMetric struct {
|
||||
@@ -108,18 +164,17 @@ type PortMetric struct {
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
|
||||
// NetworkStat 网络指标(Agent 上报)
|
||||
type NetworkStat struct {
|
||||
type NetworkStatModel struct {
|
||||
TCPEstablished int `json:"tcp_established"`
|
||||
TCPTimeWait int `json:"tcp_time_wait"`
|
||||
TCPCloseWait int `json:"tcp_close_wait"`
|
||||
TCPTotal int `json:"tcp_total"`
|
||||
NicName string `json:"nic_name"`
|
||||
NetRxBPS float64 `json:"net_rx_bps"`
|
||||
NetTxBPS float64 `json:"net_tx_bps"`
|
||||
}
|
||||
|
||||
// DiskIOStat 磁盘 IO 指标(Agent 上报)
|
||||
type DiskIOStat struct {
|
||||
type DiskIOStatModel struct {
|
||||
ReadBPS float64 `json:"read_bps"`
|
||||
WriteBPS float64 `json:"write_bps"`
|
||||
ReadIOPS float64 `json:"read_iops"`
|
||||
|
||||
@@ -32,7 +32,7 @@ type AgentRule struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机
|
||||
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down
|
||||
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process
|
||||
Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= ==
|
||||
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
||||
Port int `json:"port"` // port_down 专用
|
||||
|
||||
@@ -398,14 +398,36 @@ type AcknowledgeAlertRequest struct {
|
||||
// CreateNotifyChannelRequest 创建通知渠道请求
|
||||
type CreateNotifyChannelRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=email sms webhook dingtalk wechat"`
|
||||
Type string `json:"type" binding:"required,oneof=email sms webhook dingtalk wechat feishu"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
// UpdateNotifyChannelRequest 更新通知渠道请求
|
||||
type UpdateNotifyChannelRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type" binding:"omitempty,oneof=email sms webhook dingtalk wechat"`
|
||||
Type string `json:"type" binding:"omitempty,oneof=email sms webhook dingtalk wechat feishu"`
|
||||
Config string `json:"config"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// FeishuWebhookConfig 飞书 Webhook 配置
|
||||
type FeishuWebhookConfig struct {
|
||||
URL string `json:"url"` // 飞书机器人 webhook 地址
|
||||
Secret string `json:"secret"` // 可选:签名密钥
|
||||
}
|
||||
|
||||
// FeishuCardMessage 飞书卡片消息
|
||||
type FeishuCardMessage struct {
|
||||
MsgType string `json:"msg_type"`
|
||||
Content FeishuCardContent `json:"content"`
|
||||
}
|
||||
|
||||
type FeishuCardContent struct {
|
||||
Title string `json:"title"`
|
||||
Content [][]FeishuCardElement `json:"content"`
|
||||
}
|
||||
|
||||
type FeishuCardElement struct {
|
||||
Tag string `json:"tag"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
@@ -25,7 +25,7 @@ type Pipeline struct {
|
||||
DeployTarget string `gorm:"size:16;default:ssh;comment:部署目标 ssh/k8s" json:"deploy_target"`
|
||||
|
||||
// 并发控制
|
||||
MaxExecutors int `gorm:"default:1;comment:最大并发执行数(1=串行)" json:"max_executors"`
|
||||
MaxExecutors int `gorm:"default:2;comment:最大并发执行数(1=串行,默认2)" json:"max_executors"`
|
||||
|
||||
// SSH 部署目标服务器 (JSON: [1,2,3])
|
||||
ServerIDs string `gorm:"type:text;comment:SSH服务器ID列表(JSON)" json:"server_ids,omitempty"`
|
||||
|
||||
@@ -57,30 +57,51 @@ func (DeployTool) TableName() string {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 预置工具定义(Seeder)
|
||||
// 预置工具定义(Seeder)— 精简版 v2
|
||||
// 移除: kubectl(Dockerfile 预装)、helm(非核心)
|
||||
// 新增: nvm(管理 Node 版本)、jdk(Java 构建)
|
||||
// ============================================
|
||||
|
||||
// PredefinedTools 返回系统预置的工具配置列表
|
||||
func PredefinedTools() []DeployTool {
|
||||
return []DeployTool{
|
||||
// ──── JDK — Java 构建 // Logan.Li ────
|
||||
{
|
||||
Name: "kubectl",
|
||||
DisplayName: "Kubectl",
|
||||
Description: "Kubernetes 命令行工具,用于 K8s 集群交互(已预装在镜像中)",
|
||||
Category: "k8s",
|
||||
InstallMethod: ToolPreinstall,
|
||||
BinaryName: "kubectl",
|
||||
InstallPath: "/usr/local/bin/kubectl",
|
||||
CheckCommand: "kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null",
|
||||
VersionURL: "https://dl.k8s.io/release/stable.txt",
|
||||
DownloadURL: "https://dl.k8s.io/release/{version}/bin/linux/amd64/kubectl",
|
||||
DefaultVersion: "v1.32.0",
|
||||
SortOrder: 1,
|
||||
Name: "jdk",
|
||||
DisplayName: "JDK (Java)",
|
||||
Description: "Java 开发工具包,流水线 Java/Maven 构建阶段使用",
|
||||
Category: "build",
|
||||
InstallMethod: ToolLazy,
|
||||
BinaryName: "java",
|
||||
InstallPath: "/usr/local/bin/java",
|
||||
CheckCommand: "java -version 2>&1 | head -1",
|
||||
VersionURL: "https://api.adoptium.net/v3/info/release_versions?heap_size=normal&image_type=jdk&page=0&page_size=1&project=jdk&release_type=ga&semver=false&sort_method=DEFAULT&sort_order=DESC&version=%5B17%2C99%5D",
|
||||
DownloadURL: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-{version}/OpenJDK17U-jdk_x64_linux_hotspot_{version}.tar.gz",
|
||||
DefaultVersion: "17.0.13+7",
|
||||
InstallScript: `#!/bin/sh
|
||||
# JDK lazy install — Temurin OpenJDK
|
||||
JDK_VERSION="${TOOL_VERSION:-17.0.13+7}"
|
||||
JDK_MAJOR=$(echo "$JDK_VERSION" | cut -d. -f1)
|
||||
JDK_URL="https://github.com/adoptium/temurin${JDK_MAJOR}-binaries/releases/download/jdk-${JDK_VERSION}/OpenJDK${JDK_MAJOR}U-jdk_x64_linux_hotspot_${JDK_VERSION}.tar.gz"
|
||||
echo "[JDK] Downloading ${JDK_VERSION}..."
|
||||
cd /tmp
|
||||
curl -sL "$JDK_URL" -o jdk.tar.gz
|
||||
tar xzf jdk.tar.gz
|
||||
mkdir -p /usr/local/share
|
||||
mv jdk-${JDK_VERSION} /usr/local/share/jdk
|
||||
ln -sf /usr/local/share/jdk/bin/java /usr/local/bin/java
|
||||
ln -sf /usr/local/share/jdk/bin/javac /usr/local/bin/javac
|
||||
ln -sf /usr/local/share/jdk/bin/jar /usr/local/bin/jar
|
||||
rm -f jdk.tar.gz
|
||||
echo "[JDK] Installed: $(java -version 2>&1 | head -1)"
|
||||
`,
|
||||
SortOrder: 1,
|
||||
},
|
||||
// ──── Maven — Java 构建 // Logan.Li ────
|
||||
{
|
||||
Name: "maven",
|
||||
DisplayName: "Maven",
|
||||
Description: "Java 项目构建工具,流水线 Java 构建阶段使用",
|
||||
Description: "Java 项目构建工具,流水线 Java 构建阶段使用(依赖 JDK)",
|
||||
Category: "build",
|
||||
InstallMethod: ToolLazy,
|
||||
BinaryName: "mvn",
|
||||
@@ -102,87 +123,97 @@ mv apache-maven-${MAVEN_VERSION} /usr/local/share/maven
|
||||
ln -sf /usr/local/share/maven/bin/mvn /usr/local/bin/mvn
|
||||
rm -f maven.tar.gz
|
||||
echo "[Maven] Installed: $(mvn --version 2>&1 | head -1)"
|
||||
`,
|
||||
`,
|
||||
SortOrder: 2,
|
||||
},
|
||||
// ──── NVM — Node Version Manager // Logan.Li ────
|
||||
{
|
||||
Name: "nvm",
|
||||
DisplayName: "NVM (Node 版本管理)",
|
||||
Description: "Node 版本管理器,流水线 Node.js 构建阶段使用(node 依赖此工具)",
|
||||
Category: "build",
|
||||
InstallMethod: ToolLazy,
|
||||
BinaryName: "nvm",
|
||||
InstallPath: "/usr/local/share/nvm/nvm.sh",
|
||||
CheckCommand: "source /usr/local/share/nvm/nvm.sh 2>/dev/null && nvm --version 2>/dev/null",
|
||||
VersionURL: "https://api.github.com/repos/nvm-sh/nvm/releases/latest",
|
||||
DefaultVersion: "0.40.1",
|
||||
InstallScript: `#!/bin/sh
|
||||
# NVM lazy install
|
||||
NVM_VERSION="${TOOL_VERSION:-0.40.1}"
|
||||
NVM_DIR="/usr/local/share/nvm"
|
||||
echo "[NVM] Downloading ${NVM_VERSION}..."
|
||||
mkdir -p "$NVM_DIR"
|
||||
curl -sL "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" | NVM_DIR="$NVM_DIR" bash
|
||||
# Make available system-wide via profile.d
|
||||
cat > /etc/profile.d/nvm.sh << 'NVM_PROFILE'
|
||||
export NVM_DIR="/usr/local/share/nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
NVM_PROFILE
|
||||
chmod +x /etc/profile.d/nvm.sh
|
||||
echo "[NVM] Installed: v${NVM_VERSION}"
|
||||
`,
|
||||
SortOrder: 3,
|
||||
},
|
||||
// ──── Node.js — 通过 NVM 安装 // Logan.Li ────
|
||||
{
|
||||
Name: "nodejs",
|
||||
DisplayName: "Node.js + npm",
|
||||
Description: "前端构建工具,流水线 npm install/build 阶段使用",
|
||||
Description: "前端构建工具,流水线 npm install/build 阶段使用(通过 NVM 安装)",
|
||||
Category: "build",
|
||||
InstallMethod: ToolLazy,
|
||||
BinaryName: "node",
|
||||
InstallPath: "/usr/local/bin/node",
|
||||
CheckCommand: "node --version 2>/dev/null",
|
||||
DownloadURL: "https://nodejs.org/dist/{version}/node-{version}-linux-x64.tar.xz",
|
||||
VersionURL: "https://nodejs.org/dist/latest-v22.x/",
|
||||
DefaultVersion: "v22.14.0",
|
||||
InstallScript: `#!/bin/sh
|
||||
# Node.js lazy install — download & extract
|
||||
# Node.js via NVM install
|
||||
NODE_VERSION="${TOOL_VERSION:-v22.14.0}"
|
||||
NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz"
|
||||
echo "[Node.js] Downloading ${NODE_VERSION}..."
|
||||
cd /tmp
|
||||
curl -sL "$NODE_URL" -o node.tar.xz
|
||||
tar xJf node.tar.xz
|
||||
mkdir -p /usr/local/share
|
||||
mv node-${NODE_VERSION}-linux-x64 /usr/local/share/node
|
||||
ln -sf /usr/local/share/node/bin/node /usr/local/bin/node
|
||||
ln -sf /usr/local/share/node/bin/npm /usr/local/bin/npm
|
||||
ln -sf /usr/local/share/node/bin/npx /usr/local/bin/npx
|
||||
rm -f node.tar.xz
|
||||
echo "[Node.js] Installing via NVM ${NODE_VERSION}..."
|
||||
export NVM_DIR="/usr/local/share/nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
if ! command -v nvm >/dev/null 2>&1; then
|
||||
# NVM not found — install it first
|
||||
echo "[Node.js] NVM not found, installing NVM first..."
|
||||
curl -sL "https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh" | NVM_DIR="$NVM_DIR" bash
|
||||
\. "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
nvm install "${NODE_VERSION#v}"
|
||||
nvm alias default "${NODE_VERSION#v}"
|
||||
nvm use "${NODE_VERSION#v}"
|
||||
# Symlink for system-wide access
|
||||
ln -sf "$NVM_DIR/versions/node/$(nvm version)/bin/node" /usr/local/bin/node
|
||||
ln -sf "$NVM_DIR/versions/node/$(nvm version)/bin/npm" /usr/local/bin/npm
|
||||
ln -sf "$NVM_DIR/versions/node/$(nvm version)/bin/npx" /usr/local/bin/npx
|
||||
npm config set registry https://registry.npmmirror.com
|
||||
echo "[Node.js] Installed: $(node --version) / npm $(npm --version)"
|
||||
`,
|
||||
SortOrder: 3,
|
||||
},
|
||||
{
|
||||
Name: "helm",
|
||||
DisplayName: "Helm",
|
||||
Description: "Kubernetes 包管理工具,Chart 部署使用",
|
||||
Category: "k8s",
|
||||
InstallMethod: ToolLazy,
|
||||
BinaryName: "helm",
|
||||
InstallPath: "/usr/local/bin/helm",
|
||||
CheckCommand: "helm version --short 2>/dev/null",
|
||||
VersionURL: "https://get.helm.sh/",
|
||||
DownloadURL: "https://get.helm.sh/helm-{version}-linux-amd64.tar.gz",
|
||||
DefaultVersion: "v3.16.0",
|
||||
InstallScript: `#!/bin/sh
|
||||
# Helm lazy install — download & extract
|
||||
HELM_VERSION="${TOOL_VERSION:-v3.16.0}"
|
||||
HELM_URL="https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
echo "[Helm] Downloading ${HELM_VERSION}..."
|
||||
cd /tmp
|
||||
curl -sL "$HELM_URL" -o helm.tar.gz
|
||||
tar xzf helm.tar.gz
|
||||
mv linux-amd64/helm /usr/local/bin/helm
|
||||
rm -rf helm.tar.gz linux-amd64
|
||||
echo "[Helm] Installed: $(helm version --short 2>&1)"
|
||||
`,
|
||||
`,
|
||||
SortOrder: 4,
|
||||
},
|
||||
// ──── Git — 版本控制(预装)// Logan.Li ────
|
||||
{
|
||||
Name: "git",
|
||||
DisplayName: "Git",
|
||||
Description: "版本控制工具,流水线代码拉取使用",
|
||||
Category: "build",
|
||||
InstallMethod: ToolPreinstall,
|
||||
BinaryName: "git",
|
||||
InstallPath: "/usr/bin/git",
|
||||
CheckCommand: "git --version 2>/dev/null",
|
||||
SortOrder: 5,
|
||||
Name: "git",
|
||||
DisplayName: "Git",
|
||||
Description: "版本控制工具,流水线代码拉取使用",
|
||||
Category: "build",
|
||||
InstallMethod: ToolPreinstall,
|
||||
BinaryName: "git",
|
||||
InstallPath: "/usr/bin/git",
|
||||
CheckCommand: "git --version 2>/dev/null",
|
||||
SortOrder: 5,
|
||||
},
|
||||
// ──── Docker CLI — 容器构建(预装)// Logan.Li ────
|
||||
{
|
||||
Name: "docker-cli",
|
||||
DisplayName: "Docker CLI",
|
||||
Description: "Docker 命令行工具,流水线镜像构建/推送使用",
|
||||
Category: "deploy",
|
||||
InstallMethod: ToolPreinstall,
|
||||
BinaryName: "docker",
|
||||
InstallPath: "/usr/bin/docker",
|
||||
CheckCommand: "docker --version 2>/dev/null",
|
||||
SortOrder: 6,
|
||||
Name: "docker-cli",
|
||||
DisplayName: "Docker CLI",
|
||||
Description: "Docker 命令行工具,流水线镜像构建/推送使用",
|
||||
Category: "deploy",
|
||||
InstallMethod: ToolPreinstall,
|
||||
BinaryName: "docker",
|
||||
InstallPath: "/usr/bin/docker",
|
||||
CheckCommand: "docker --version 2>/dev/null",
|
||||
SortOrder: 6,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user