update
This commit is contained in:
1 parent
0690285147
commit
5618cd2738
154 files changed
+5021
-3161
No files matched your search
+18
-18
@@ -61,29 +61,29 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
|
|
||||||
// 存储到数据库
|
// 存储到数据库
|
||||||
report := model.AgentReport{
|
report := model.AgentReport{
|
||||||
Hostname: req.Hostname,
|
Hostname: req.Hostname,
|
||||||
CPUUsage: req.CPU.UsagePercent,
|
CPUUsage: req.CPU.UsagePercent,
|
||||||
CPUIOwait: req.CPU.IOWait,
|
CPUIOwait: req.CPU.IOWait,
|
||||||
CPUCores: req.CPU.CoreCount,
|
CPUCores: req.CPU.CoreCount,
|
||||||
Load1: req.CPU.Load1,
|
Load1: req.CPU.Load1,
|
||||||
Load5: req.CPU.Load5,
|
Load5: req.CPU.Load5,
|
||||||
Load15: req.CPU.Load15,
|
Load15: req.CPU.Load15,
|
||||||
MemTotal: req.Memory.TotalGB,
|
MemTotal: req.Memory.TotalGB,
|
||||||
MemUsed: req.Memory.UsedGB,
|
MemUsed: req.Memory.UsedGB,
|
||||||
MemUsage: req.Memory.UsagePercent,
|
MemUsage: req.Memory.UsagePercent,
|
||||||
MemAvailable: req.Memory.AvailableGB,
|
MemAvailable: req.Memory.AvailableGB,
|
||||||
MemSwapUsed: req.Memory.SwapUsedGB,
|
MemSwapUsed: req.Memory.SwapUsedGB,
|
||||||
DiskInfo: string(diskJSON),
|
DiskInfo: string(diskJSON),
|
||||||
PortInfo: string(portJSON),
|
PortInfo: string(portJSON),
|
||||||
NetInfo: string(netJSON),
|
NetInfo: string(netJSON),
|
||||||
DiskIOInfo: string(dioJSON),
|
DiskIOInfo: string(dioJSON),
|
||||||
// v2.0 新增
|
// v2.0 新增
|
||||||
SwapInfo: string(swapJSON),
|
SwapInfo: string(swapJSON),
|
||||||
ProcessTopInfo: string(processTopJSON),
|
ProcessTopInfo: string(processTopJSON),
|
||||||
FDInfo: string(fdJSON),
|
FDInfo: string(fdJSON),
|
||||||
CSInfo: string(csJSON),
|
CSInfo: string(csJSON),
|
||||||
HeartbeatInfo: string(heartbeatJSON),
|
HeartbeatInfo: string(heartbeatJSON),
|
||||||
ReportTime: reportTime,
|
ReportTime: reportTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.Create(&report).Error; err != nil {
|
if err := h.db.Create(&report).Error; err != nil {
|
||||||
@@ -99,9 +99,9 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
for _, d := range req.Disks {
|
for _, d := range req.Disks {
|
||||||
totalDiskGB += d.TotalGB
|
totalDiskGB += d.TotalGB
|
||||||
}
|
}
|
||||||
memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整)
|
memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整)
|
||||||
memMB := int64(memGB * 1024) // GB → MB
|
memMB := int64(memGB * 1024) // GB → MB
|
||||||
diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40
|
diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
|
|||||||
+209
-37
@@ -19,6 +19,7 @@ import (
|
|||||||
"ezops/model"
|
"ezops/model"
|
||||||
"ezops/pkg/agentcli"
|
"ezops/pkg/agentcli"
|
||||||
"ezops/pkg/agentcli/core"
|
"ezops/pkg/agentcli/core"
|
||||||
|
"ezops/pkg/agentcli/guardrail"
|
||||||
"ezops/pkg/agentcli/llm"
|
"ezops/pkg/agentcli/llm"
|
||||||
"ezops/pkg/agentcli/safety"
|
"ezops/pkg/agentcli/safety"
|
||||||
"ezops/pkg/agentcli/tools"
|
"ezops/pkg/agentcli/tools"
|
||||||
@@ -86,7 +87,7 @@ type agentConn struct {
|
|||||||
server model.Server
|
server model.Server
|
||||||
sshCfg remoteexec.SSHConfig
|
sshCfg remoteexec.SSHConfig
|
||||||
kubernetesCtx *kubernetesAgentContext
|
kubernetesCtx *kubernetesAgentContext
|
||||||
session *model.AgentCLISession
|
session *model.AIRun
|
||||||
taskCtx context.Context
|
taskCtx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
totalTokens int
|
totalTokens int
|
||||||
@@ -97,6 +98,9 @@ type agentConn struct {
|
|||||||
convHistory []llm.Message
|
convHistory []llm.Message
|
||||||
running bool
|
running bool
|
||||||
runningMu sync.Mutex
|
runningMu sync.Mutex
|
||||||
|
pendingSpans map[string]string
|
||||||
|
seq int
|
||||||
|
seqMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run WebSocket 入口 // Logan.Li
|
// Run WebSocket 入口 // Logan.Li
|
||||||
@@ -144,7 +148,7 @@ func (h *AgentCLIHandler) runHostWS(c *gin.Context) {
|
|||||||
ac := &agentConn{
|
ac := &agentConn{
|
||||||
conn: conn, db: h.db, mode: agentcli.RunModeHost,
|
conn: conn, db: h.db, mode: agentcli.RunModeHost,
|
||||||
userID: middleware.GetUserID(c), server: server, sshCfg: sshCfg,
|
userID: middleware.GetUserID(c), server: server, sshCfg: sshCfg,
|
||||||
taskCtx: context.Background(), confirms: make(map[string]chan bool),
|
taskCtx: context.Background(), confirms: make(map[string]chan bool), pendingSpans: make(map[string]string),
|
||||||
}
|
}
|
||||||
ac.write(wsEvent{Type: "session", SessionID: 0,
|
ac.write(wsEvent{Type: "session", SessionID: 0,
|
||||||
Message: fmt.Sprintf("已连接主机 %s(%s),LLM: %s。请描述要执行的任务。", server.Hostname, server.IP, llmCfg.Model)})
|
Message: fmt.Sprintf("已连接主机 %s(%s),LLM: %s。请描述要执行的任务。", server.Hostname, server.IP, llmCfg.Model)})
|
||||||
@@ -181,7 +185,7 @@ func (h *AgentCLIHandler) runKubernetesWS(c *gin.Context) {
|
|||||||
ac := &agentConn{
|
ac := &agentConn{
|
||||||
conn: conn, db: h.db, mode: agentcli.RunModeKubernetes,
|
conn: conn, db: h.db, mode: agentcli.RunModeKubernetes,
|
||||||
userID: middleware.GetUserID(c), kubernetesCtx: kCtx,
|
userID: middleware.GetUserID(c), kubernetesCtx: kCtx,
|
||||||
taskCtx: context.Background(), confirms: make(map[string]chan bool),
|
taskCtx: context.Background(), confirms: make(map[string]chan bool), pendingSpans: make(map[string]string),
|
||||||
}
|
}
|
||||||
target := buildKubernetesTargetInfo(kCtx)
|
target := buildKubernetesTargetInfo(kCtx)
|
||||||
ac.write(wsEvent{Type: "session", SessionID: 0,
|
ac.write(wsEvent{Type: "session", SessionID: 0,
|
||||||
@@ -194,7 +198,9 @@ func (ac *agentConn) wsReadLoop() {
|
|||||||
for {
|
for {
|
||||||
_, data, err := ac.conn.ReadMessage()
|
_, data, err := ac.conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ac.abort()
|
if ac.isRunning() {
|
||||||
|
ac.abort()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var ctrl wsControl
|
var ctrl wsControl
|
||||||
@@ -256,20 +262,27 @@ func (ac *agentConn) startTask(query, safetyMode string) {
|
|||||||
if r := []rune(title); len(r) > 60 {
|
if r := []rune(title); len(r) > 60 {
|
||||||
title = string(r[:60]) + "..."
|
title = string(r[:60]) + "..."
|
||||||
}
|
}
|
||||||
sess := &model.AgentCLISession{
|
sess := &model.AIRun{
|
||||||
UserID: ac.userID, Mode: ac.mode, Title: title, Status: "running",
|
RunID: guardrail.NewTraceID(), UserID: ac.userID, Feature: "agent_cli",
|
||||||
|
Mode: ac.mode, Title: title, Status: "running",
|
||||||
Provider: llmCfg.Provider, Model: llmCfg.Model,
|
Provider: llmCfg.Provider, Model: llmCfg.Model,
|
||||||
|
SafetyMode: safetyMode, StartedAt: time.Now(),
|
||||||
}
|
}
|
||||||
if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil {
|
if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil {
|
||||||
sess.ClusterID = ac.kubernetesCtx.cluster.ID
|
sess.SubjectType = "k8s_cluster"
|
||||||
sess.ClusterName = ac.kubernetesCtx.cluster.Name
|
if ac.kubernetesCtx.podName != "" {
|
||||||
sess.Namespace = ac.kubernetesCtx.namespace
|
sess.SubjectType = "pod"
|
||||||
sess.PodName = ac.kubernetesCtx.podName
|
} else if ac.kubernetesCtx.namespace != "" {
|
||||||
sess.Container = ac.kubernetesCtx.container
|
sess.SubjectType = "namespace"
|
||||||
|
}
|
||||||
|
sess.SubjectID = fmt.Sprintf("%d", ac.kubernetesCtx.cluster.ID)
|
||||||
|
sess.SubjectName = ac.kubernetesCtx.cluster.Name
|
||||||
|
sess.SubjectScope = ac.kubernetesCtx.namespace
|
||||||
} else {
|
} else {
|
||||||
sess.ServerID = ac.server.ID
|
sess.SubjectType = "host"
|
||||||
sess.ServerName = ac.server.Hostname
|
sess.SubjectID = fmt.Sprintf("%d", ac.server.ID)
|
||||||
sess.ServerHost = ac.server.IP
|
sess.SubjectName = ac.server.Hostname
|
||||||
|
sess.SubjectScope = ac.server.IP
|
||||||
}
|
}
|
||||||
if err := ac.db.Create(sess).Error; err != nil {
|
if err := ac.db.Create(sess).Error; err != nil {
|
||||||
ac.write(wsEvent{Type: "error", Message: "创建会话失败: " + err.Error()})
|
ac.write(wsEvent{Type: "error", Message: "创建会话失败: " + err.Error()})
|
||||||
@@ -287,15 +300,15 @@ func (ac *agentConn) resumeSession(sessionID uint) {
|
|||||||
ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"})
|
ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var sess model.AgentCLISession
|
var sess model.AIRun
|
||||||
q := ac.db.Where("user_id = ?", ac.userID)
|
q := ac.db.Where("user_id = ?", ac.userID)
|
||||||
if ac.mode == agentcli.RunModeKubernetes {
|
if ac.mode == agentcli.RunModeKubernetes {
|
||||||
q = q.Where("mode IN ?", []string{agentcli.RunModeKubernetes, "k8s"})
|
q = q.Where("mode IN ?", []string{agentcli.RunModeKubernetes, "k8s"})
|
||||||
if ac.kubernetesCtx != nil {
|
if ac.kubernetesCtx != nil {
|
||||||
q = q.Where("cluster_id = ? AND namespace = ?", ac.kubernetesCtx.cluster.ID, ac.kubernetesCtx.namespace)
|
q = q.Where("subject_id = ? AND subject_scope = ?", fmt.Sprintf("%d", ac.kubernetesCtx.cluster.ID), ac.kubernetesCtx.namespace)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
q = q.Where("mode = ? AND server_id = ?", ac.mode, ac.server.ID)
|
q = q.Where("mode = ? AND subject_type = ? AND subject_id = ?", ac.mode, "host", fmt.Sprintf("%d", ac.server.ID))
|
||||||
}
|
}
|
||||||
if err := q.First(&sess, sessionID).Error; err != nil {
|
if err := q.First(&sess, sessionID).Error; err != nil {
|
||||||
ac.write(wsEvent{Type: "error", Message: "会话不存在或无权访问"})
|
ac.write(wsEvent{Type: "error", Message: "会话不存在或无权访问"})
|
||||||
@@ -305,8 +318,8 @@ func (ac *agentConn) resumeSession(sessionID uint) {
|
|||||||
ac.write(wsEvent{Type: "error", Message: "该会话仍在执行中,无法恢复"})
|
ac.write(wsEvent{Type: "error", Message: "该会话仍在执行中,无法恢复"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var msgs []model.AgentCLIMessage
|
var msgs []model.AIRunMessage
|
||||||
ac.db.Where("session_id = ?", sessionID).Order("id ASC").Find(&msgs)
|
ac.db.Where("run_id = ?", sess.RunID).Order("seq ASC, id ASC").Find(&msgs)
|
||||||
ac.session = &sess
|
ac.session = &sess
|
||||||
ac.convHistory = buildConvHistoryFromMessages(msgs)
|
ac.convHistory = buildConvHistoryFromMessages(msgs)
|
||||||
ac.setTotalTokens(sess.TotalTokens)
|
ac.setTotalTokens(sess.TotalTokens)
|
||||||
@@ -340,7 +353,7 @@ func (ac *agentConn) continueTask(query, safetyMode string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runTask 执行一轮 ReAct(新建或追问共用) // Logan.Li
|
// runTask 执行一轮 ReAct(新建或追问共用) // Logan.Li
|
||||||
func (ac *agentConn) runTask(sess *model.AgentCLISession, query, safetyMode string, llmCfg model.LLMConfig, isContinue bool) {
|
func (ac *agentConn) runTask(sess *model.AIRun, query, safetyMode string, llmCfg model.LLMConfig, isContinue bool) {
|
||||||
if ac.isRunning() {
|
if ac.isRunning() {
|
||||||
ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"})
|
ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"})
|
||||||
return
|
return
|
||||||
@@ -393,9 +406,9 @@ func (ac *agentConn) runTask(sess *model.AgentCLISession, query, safetyMode stri
|
|||||||
ac.taskCtx = ctx
|
ac.taskCtx = ctx
|
||||||
ac.cancel = cancel
|
ac.cancel = cancel
|
||||||
ac.setRunning(true)
|
ac.setRunning(true)
|
||||||
ac.db.Model(sess).Update("status", "running")
|
ac.db.Model(sess).Updates(map[string]interface{}{"status": "running", "safety_mode": safetyMode})
|
||||||
|
|
||||||
ac.persistMsg(sess.ID, 0, "user", "user", query, "", "", "", "", false, "", 0)
|
ac.persistMsg(sess.RunID, 0, "user", "user", query, "", "", "", "", false, "", 0)
|
||||||
|
|
||||||
targetInfo := ac.buildTargetInfo()
|
targetInfo := ac.buildTargetInfo()
|
||||||
|
|
||||||
@@ -457,6 +470,9 @@ func (ac *agentConn) onEvent(e core.Event) {
|
|||||||
if e.Type == core.EventProgress && e.Tokens > 0 {
|
if e.Type == core.EventProgress && e.Tokens > 0 {
|
||||||
ac.setTotalTokens(e.Tokens)
|
ac.setTotalTokens(e.Tokens)
|
||||||
}
|
}
|
||||||
|
if e.Type == core.EventDone || e.Type == core.EventError {
|
||||||
|
ac.setRunning(false)
|
||||||
|
}
|
||||||
|
|
||||||
ev := wsEvent{
|
ev := wsEvent{
|
||||||
Type: string(e.Type), Phase: e.Phase, Summary: e.Summary,
|
Type: string(e.Type), Phase: e.Phase, Summary: e.Summary,
|
||||||
@@ -474,23 +490,25 @@ func (ac *agentConn) onEvent(e core.Event) {
|
|||||||
if ac.session == nil {
|
if ac.session == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sid := ac.session.ID
|
runID := ac.session.RunID
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
case core.EventStatus:
|
case core.EventStatus:
|
||||||
// status 事件不改 DB schema:duration 字段在 type=status 时复用为 phase。
|
// status 事件不改 DB schema:duration 字段在 type=status 时复用为 phase。
|
||||||
ac.persistMsg(sid, e.Round, "status", "system", e.Summary, e.ToolName, "", "", e.Risk, e.Phase != core.PhaseError && e.Phase != core.PhaseBlocked, e.Phase, e.Tokens)
|
ac.persistMsg(runID, e.Round, "status", "system", e.Summary, e.ToolName, "", "", e.Risk, e.Phase != core.PhaseError && e.Phase != core.PhaseBlocked, e.Phase, e.Tokens)
|
||||||
case core.EventThought:
|
case core.EventThought:
|
||||||
ac.persistMsg(sid, e.Round, "thought", "assistant", e.Thought, "", "", "", "", false, "", 0)
|
ac.persistMsg(runID, e.Round, "thought", "assistant", e.Thought, "", "", "", "", false, "", 0)
|
||||||
case core.EventToolCall:
|
case core.EventToolCall:
|
||||||
argsJSON, _ := json.Marshal(e.ToolArgs)
|
argsJSON, _ := json.Marshal(e.ToolArgs)
|
||||||
ac.persistMsg(sid, e.Round, "tool_call", "tool", "", e.ToolName, string(argsJSON), "", e.Risk, false, "", 0)
|
ac.persistMsg(runID, e.Round, "tool_call", "tool", string(argsJSON), e.ToolName, string(argsJSON), "", e.Risk, false, "", 0)
|
||||||
|
ac.startToolSpan(e, string(argsJSON))
|
||||||
case core.EventToolResult:
|
case core.EventToolResult:
|
||||||
ac.persistMsg(sid, e.Round, "tool_result", "tool", e.Output, e.ToolName, "", "", "", e.Success, e.Duration, 0)
|
ac.persistMsg(runID, e.Round, "tool_result", "tool", e.Output, e.ToolName, "", "", "", e.Success, e.Duration, 0)
|
||||||
|
ac.finishToolSpan(e)
|
||||||
case core.EventDone:
|
case core.EventDone:
|
||||||
ac.setTotalTokens(e.Tokens)
|
ac.setTotalTokens(e.Tokens)
|
||||||
ac.persistMsg(sid, e.Round, "done", "assistant", e.Answer, "", "", "", "", true, "", e.Tokens)
|
ac.persistMsg(runID, e.Round, "done", "assistant", e.Answer, "", "", "", "", true, "", e.Tokens)
|
||||||
case core.EventError:
|
case core.EventError:
|
||||||
ac.persistMsg(sid, e.Round, "error", "assistant", e.Message, "", "", "", "", false, "", 0)
|
ac.persistMsg(runID, e.Round, "error", "assistant", e.Message, "", "", "", "", false, "", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +537,7 @@ func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, ris
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case approved := <-ch:
|
case approved := <-ch:
|
||||||
|
ac.recordConfirmSpan(toolName, args, risk, approved)
|
||||||
return approved, nil
|
return approved, nil
|
||||||
case <-ac.taskCtx.Done():
|
case <-ac.taskCtx.Done():
|
||||||
ac.delConfirm(confirmID)
|
ac.delConfirm(confirmID)
|
||||||
@@ -529,6 +548,25 @@ func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, ris
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) recordConfirmSpan(toolName string, args map[string]interface{}, risk tools.RiskLevel, approved bool) {
|
||||||
|
if ac.session == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decision := "rejected"
|
||||||
|
status := "skipped"
|
||||||
|
if approved {
|
||||||
|
decision = "approved"
|
||||||
|
status = "success"
|
||||||
|
}
|
||||||
|
argsJSON, _ := json.Marshal(args)
|
||||||
|
span := &model.AIRunSpan{
|
||||||
|
RunID: ac.session.RunID, SpanID: guardrail.NewSpanID(), Kind: "confirm", Name: toolName,
|
||||||
|
ToolSource: ac.toolSource(toolName), Input: guardrail.SanitizeJSON(string(argsJSON)),
|
||||||
|
Risk: risk.String(), Decision: decision, Status: status, StartedAt: time.Now(), CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
_ = ac.db.Create(span).Error
|
||||||
|
}
|
||||||
|
|
||||||
// resolveConfirm 前端回传确认结果 // Logan.Li
|
// resolveConfirm 前端回传确认结果 // Logan.Li
|
||||||
func (ac *agentConn) resolveConfirm(confirmID string, approved bool) {
|
func (ac *agentConn) resolveConfirm(confirmID string, approved bool) {
|
||||||
ac.cfMu.Lock()
|
ac.cfMu.Lock()
|
||||||
@@ -571,13 +609,13 @@ func (ac *agentConn) lastRound() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
var maxRound int
|
var maxRound int
|
||||||
ac.db.Model(&model.AgentCLIMessage{}).Where("session_id = ?", ac.session.ID).
|
ac.db.Model(&model.AIRunMessage{}).Where("run_id = ?", ac.session.RunID).
|
||||||
Select("COALESCE(MAX(round),0)").Scan(&maxRound)
|
Select("COALESCE(MAX(round),0)").Scan(&maxRound)
|
||||||
return maxRound
|
return maxRound
|
||||||
}
|
}
|
||||||
|
|
||||||
// finish 结束本轮执行并更新会话 DB // Logan.Li
|
// finish 结束本轮执行并更新会话 DB // Logan.Li
|
||||||
func (ac *agentConn) finish(sess *model.AgentCLISession, status, errMsg string, rounds int, dur time.Duration) {
|
func (ac *agentConn) finish(sess *model.AIRun, status, errMsg string, rounds int, dur time.Duration) {
|
||||||
ac.setRunning(false)
|
ac.setRunning(false)
|
||||||
tokens := ac.getTotalTokens()
|
tokens := ac.getTotalTokens()
|
||||||
// done/error 已由引擎 onEvent 推送;此处仅 aborted 需补推 WS(引擎初始化失败在 runTask 内单独推送) // Logan.Li
|
// done/error 已由引擎 onEvent 推送;此处仅 aborted 需补推 WS(引擎初始化失败在 runTask 内单独推送) // Logan.Li
|
||||||
@@ -588,6 +626,10 @@ func (ac *agentConn) finish(sess *model.AgentCLISession, status, errMsg string,
|
|||||||
"status": status, "rounds": rounds, "duration": dur.Milliseconds(),
|
"status": status, "rounds": rounds, "duration": dur.Milliseconds(),
|
||||||
"total_tokens": tokens,
|
"total_tokens": tokens,
|
||||||
}
|
}
|
||||||
|
if status == "done" || status == "error" || status == "aborted" {
|
||||||
|
now := time.Now()
|
||||||
|
updates["finished_at"] = &now
|
||||||
|
}
|
||||||
if errMsg != "" {
|
if errMsg != "" {
|
||||||
updates["error"] = errMsg
|
updates["error"] = errMsg
|
||||||
}
|
}
|
||||||
@@ -602,15 +644,145 @@ func (ac *agentConn) write(ev wsEvent) {
|
|||||||
_ = ac.conn.WriteMessage(websocket.TextMessage, b)
|
_ = ac.conn.WriteMessage(websocket.TextMessage, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// persistMsg 持久化一条消息 // Logan.Li
|
// persistMsg 持久化一条 AI Run 回放消息。 // Logan.Li
|
||||||
func (ac *agentConn) persistMsg(sid uint, round int, typ, role, content, toolName, args, output, risk string, success bool, dur string, tokens int) {
|
func (ac *agentConn) persistMsg(runID string, round int, typ, role, content, toolName, args, output, risk string, success bool, dur string, tokens int) {
|
||||||
msg := &model.AgentCLIMessage{
|
content = guardrail.SanitizeText(content)
|
||||||
SessionID: sid, Round: round, Type: typ, Role: role, Content: content,
|
if typ == "tool_call" {
|
||||||
ToolName: toolName, ToolArgs: args, ToolOutput: output, Risk: risk,
|
content = guardrail.SanitizeJSON(content)
|
||||||
Success: success, Duration: dur, Tokens: tokens,
|
}
|
||||||
|
msg := &model.AIRunMessage{
|
||||||
|
RunID: runID, Round: round, Seq: ac.nextSeq(),
|
||||||
|
Type: typ, Role: role, Content: content,
|
||||||
|
ToolName: toolName, Tokens: tokens,
|
||||||
}
|
}
|
||||||
if err := ac.db.Create(msg).Error; err != nil {
|
if err := ac.db.Create(msg).Error; err != nil {
|
||||||
// 持久化失败不阻断主流程
|
// 持久化失败不阻断主流程
|
||||||
_ = err
|
_ = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) nextSeq() int {
|
||||||
|
ac.seqMu.Lock()
|
||||||
|
defer ac.seqMu.Unlock()
|
||||||
|
ac.seq++
|
||||||
|
return ac.seq
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) startToolSpan(e core.Event, argsJSON string) {
|
||||||
|
if ac.session == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spanID := guardrail.NewSpanID()
|
||||||
|
key := ac.pendingSpanKey(e.Round, e.ToolName)
|
||||||
|
ac.runningMu.Lock()
|
||||||
|
ac.pendingSpans[key] = spanID
|
||||||
|
ac.runningMu.Unlock()
|
||||||
|
span := &model.AIRunSpan{
|
||||||
|
RunID: ac.session.RunID, SpanID: spanID, Kind: "tool", Name: e.ToolName,
|
||||||
|
ToolSource: ac.toolSource(e.ToolName), Input: guardrail.SanitizeJSON(argsJSON),
|
||||||
|
Risk: e.Risk, Decision: "auto", Status: "running",
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
}
|
||||||
|
_ = ac.db.Create(span).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) finishToolSpan(e core.Event) {
|
||||||
|
if ac.session == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := ac.pendingSpanKey(e.Round, e.ToolName)
|
||||||
|
ac.runningMu.Lock()
|
||||||
|
spanID := ac.pendingSpans[key]
|
||||||
|
delete(ac.pendingSpans, key)
|
||||||
|
ac.runningMu.Unlock()
|
||||||
|
if spanID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status := "success"
|
||||||
|
if !e.Success {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
ac.db.Model(&model.AIRunSpan{}).Where("span_id = ?", spanID).Updates(map[string]interface{}{
|
||||||
|
"output": guardrail.SanitizeText(e.Output), "output_size": e.OutputSize,
|
||||||
|
"duration": parseDurationMillis(e.Duration), "status": status, "ended_at": &now,
|
||||||
|
})
|
||||||
|
ac.recordOpsCommand(e, spanID, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) pendingSpanKey(round int, toolName string) string {
|
||||||
|
return fmt.Sprintf("%d:%s", round, toolName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) toolSource(toolName string) string {
|
||||||
|
switch toolName {
|
||||||
|
case "ssh_execute":
|
||||||
|
return "shell"
|
||||||
|
case "kubernetes_diagnose", "kubernetes_get", "pod_logs", "pod_execute", "kubernetes_action":
|
||||||
|
return "kubernetes"
|
||||||
|
case "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary":
|
||||||
|
return "aiops"
|
||||||
|
default:
|
||||||
|
return "builtin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) recordOpsCommand(e core.Event, spanID, status string) {
|
||||||
|
if ac.session == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command := e.ToolName
|
||||||
|
if e.ToolArgs != nil {
|
||||||
|
if cmd, ok := e.ToolArgs["command"].(string); ok && cmd != "" {
|
||||||
|
command = cmd
|
||||||
|
} else if action, ok := e.ToolArgs["action"]; ok {
|
||||||
|
command = fmt.Sprintf("%v %v", action, e.ToolArgs["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetType, targetID := ac.currentTarget()
|
||||||
|
cmd := &model.OpsCommand{
|
||||||
|
CommandID: guardrail.NewSpanID(), RunID: ac.session.RunID, SpanID: spanID,
|
||||||
|
UserID: ac.userID, ActorType: "ai", TargetType: targetType, TargetID: targetID,
|
||||||
|
CommandType: ac.commandType(e.ToolName), Command: guardrail.SanitizeText(command),
|
||||||
|
Risk: e.Risk, Decision: "auto", Status: status, OutputSize: e.OutputSize,
|
||||||
|
Duration: parseDurationMillis(e.Duration), ExecutedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if !e.Success {
|
||||||
|
cmd.Error = guardrail.SanitizeText(e.Output)
|
||||||
|
}
|
||||||
|
_ = ac.db.Create(cmd).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) currentTarget() (string, string) {
|
||||||
|
if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil {
|
||||||
|
if ac.kubernetesCtx.podName != "" {
|
||||||
|
return "pod", fmt.Sprintf("%d/%s/%s", ac.kubernetesCtx.cluster.ID, ac.kubernetesCtx.namespace, ac.kubernetesCtx.podName)
|
||||||
|
}
|
||||||
|
return "k8s_cluster", fmt.Sprintf("%d", ac.kubernetesCtx.cluster.ID)
|
||||||
|
}
|
||||||
|
return "host", fmt.Sprintf("%d", ac.server.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *agentConn) commandType(toolName string) string {
|
||||||
|
switch toolName {
|
||||||
|
case "ssh_execute", "pod_execute":
|
||||||
|
return "shell"
|
||||||
|
case "kubernetes_diagnose", "kubernetes_action", "kubernetes_get", "pod_logs":
|
||||||
|
return "kubernetes"
|
||||||
|
case "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary":
|
||||||
|
return "api"
|
||||||
|
default:
|
||||||
|
return "tool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDurationMillis(raw string) int64 {
|
||||||
|
if raw == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
d, err := time.ParseDuration(raw)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return d.Milliseconds()
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ func (h *AgentCLIHandler) ListSessions(c *gin.Context) {
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
q := h.db.Model(&model.AgentCLISession{}).Where("user_id = ?", uid)
|
q := h.db.Model(&model.AIRun{}).Where("user_id = ? AND feature = ?", uid, "agent_cli")
|
||||||
if mode := c.Query("mode"); mode != "" {
|
if mode := c.Query("mode"); mode != "" {
|
||||||
mode = agentcli.NormalizeRunMode(mode)
|
mode = agentcli.NormalizeRunMode(mode)
|
||||||
if mode == agentcli.RunModeKubernetes {
|
if mode == agentcli.RunModeKubernetes {
|
||||||
@@ -36,18 +36,18 @@ func (h *AgentCLIHandler) ListSessions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sid := c.Query("server_id"); sid != "" {
|
if sid := c.Query("server_id"); sid != "" {
|
||||||
q = q.Where("server_id = ?", sid)
|
q = q.Where("subject_type = ? AND subject_id = ?", "host", sid)
|
||||||
}
|
}
|
||||||
if cid := c.Query("cluster_id"); cid != "" {
|
if cid := c.Query("cluster_id"); cid != "" {
|
||||||
q = q.Where("cluster_id = ?", cid)
|
q = q.Where("subject_id = ?", cid)
|
||||||
}
|
}
|
||||||
if ns := c.Query("namespace"); ns != "" {
|
if ns := c.Query("namespace"); ns != "" {
|
||||||
q = q.Where("namespace = ?", ns)
|
q = q.Where("subject_scope = ?", ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
q.Count(&total)
|
q.Count(&total)
|
||||||
var sessions []model.AgentCLISession
|
var sessions []model.AIRun
|
||||||
if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions).Error; err != nil {
|
if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
@@ -60,13 +60,13 @@ func (h *AgentCLIHandler) ListSessions(c *gin.Context) {
|
|||||||
func (h *AgentCLIHandler) GetSession(c *gin.Context) {
|
func (h *AgentCLIHandler) GetSession(c *gin.Context) {
|
||||||
uid := middleware.GetUserID(c)
|
uid := middleware.GetUserID(c)
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var sess model.AgentCLISession
|
var sess model.AIRun
|
||||||
if err := h.db.Where("user_id = ?", uid).First(&sess, id).Error; err != nil {
|
if err := h.db.Where("user_id = ?", uid).First(&sess, id).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"})
|
c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var msgs []model.AgentCLIMessage
|
var msgs []model.AIRunMessage
|
||||||
h.db.Where("session_id = ?", id).Order("id ASC").Find(&msgs)
|
h.db.Where("run_id = ?", sess.RunID).Order("seq ASC, id ASC").Find(&msgs)
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"session": sess, "messages": msgs}})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"session": sess, "messages": msgs}})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,16 +75,18 @@ func (h *AgentCLIHandler) GetSession(c *gin.Context) {
|
|||||||
func (h *AgentCLIHandler) DeleteSession(c *gin.Context) {
|
func (h *AgentCLIHandler) DeleteSession(c *gin.Context) {
|
||||||
uid := middleware.GetUserID(c)
|
uid := middleware.GetUserID(c)
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var sess model.AgentCLISession
|
var sess model.AIRun
|
||||||
if err := h.db.Where("user_id = ?", uid).First(&sess, id).Error; err != nil {
|
if err := h.db.Where("user_id = ?", uid).First(&sess, id).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"})
|
c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.db.Where("session_id = ?", id).Delete(&model.AgentCLIMessage{}).Error; err != nil {
|
if err := h.db.Where("run_id = ?", sess.RunID).Delete(&model.AIRunMessage{}).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除消息失败: " + err.Error()})
|
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除消息失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.db.Delete(&model.AgentCLISession{}, id).Error; err != nil {
|
_ = h.db.Where("run_id = ?", sess.RunID).Delete(&model.AIRunSpan{}).Error
|
||||||
|
_ = h.db.Where("run_id = ?", sess.RunID).Delete(&model.OpsCommand{}).Error
|
||||||
|
if err := h.db.Delete(&model.AIRun{}, id).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除会话失败: " + err.Error()})
|
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除会话失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// buildConvHistoryFromMessages 将 DB 消息重建为追问用的 prior 历史(不含 system) // Logan.Li
|
// buildConvHistoryFromMessages 将 DB 消息重建为追问用的 prior 历史(不含 system) // Logan.Li
|
||||||
func buildConvHistoryFromMessages(msgs []model.AgentCLIMessage) []llm.Message {
|
func buildConvHistoryFromMessages(msgs []model.AIRunMessage) []llm.Message {
|
||||||
var out []llm.Message
|
var out []llm.Message
|
||||||
var pendingThought string
|
var pendingThought string
|
||||||
var pendingToolCalls []llm.ToolCall
|
var pendingToolCalls []llm.ToolCall
|
||||||
@@ -40,7 +40,7 @@ func buildConvHistoryFromMessages(msgs []model.AgentCLIMessage) []llm.Message {
|
|||||||
case "thought":
|
case "thought":
|
||||||
pendingThought = m.Content
|
pendingThought = m.Content
|
||||||
case "tool_call":
|
case "tool_call":
|
||||||
args := validateToolArgsJSON(m.ToolArgs)
|
args := validateToolArgsJSON(m.Content)
|
||||||
tcID := fmt.Sprintf("hist-%d", m.ID)
|
tcID := fmt.Sprintf("hist-%d", m.ID)
|
||||||
pendingToolCalls = append(pendingToolCalls, llm.ToolCall{
|
pendingToolCalls = append(pendingToolCalls, llm.ToolCall{
|
||||||
ID: tcID, Type: "function",
|
ID: tcID, Type: "function",
|
||||||
@@ -56,9 +56,6 @@ func buildConvHistoryFromMessages(msgs []model.AgentCLIMessage) []llm.Message {
|
|||||||
pendingToolCallIDs = pendingToolCallIDs[1:]
|
pendingToolCallIDs = pendingToolCallIDs[1:]
|
||||||
}
|
}
|
||||||
content := m.Content
|
content := m.Content
|
||||||
if content == "" {
|
|
||||||
content = m.ToolOutput
|
|
||||||
}
|
|
||||||
out = append(out, llm.Message{
|
out = append(out, llm.Message{
|
||||||
Role: "tool", Name: m.ToolName, Content: content, ToolCallID: toolID,
|
Role: "tool", Name: m.ToolName, Content: content, ToolCallID: toolID,
|
||||||
})
|
})
|
||||||
@@ -86,4 +83,4 @@ func validateToolArgsJSON(raw string) string {
|
|||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
@@ -108,4 +108,4 @@ func kubernetesEvolveHostKey(ctx *kubernetesAgentContext) string {
|
|||||||
return base + "/" + ctx.podName
|
return base + "/" + ctx.podName
|
||||||
}
|
}
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
Message: func() string {
|
Message: func() string {
|
||||||
if installed {
|
if installed {
|
||||||
return "Agent 安装成功,已自动启动并设置开机自启"
|
return "Agent 安装成功,已自动启动并设置开机自启"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func NewAgentTaskHandler(db *gorm.DB) *AgentTaskHandler {
|
|||||||
func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
Type string `json:"type"` // command / script
|
Type string `json:"type"` // command / script
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
}
|
}
|
||||||
@@ -47,12 +47,12 @@ func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
|||||||
username, _ := c.Get("username")
|
username, _ := c.Get("username")
|
||||||
|
|
||||||
task := model.AgentTask{
|
task := model.AgentTask{
|
||||||
TaskID: uuid.New().String(),
|
TaskID: uuid.New().String(),
|
||||||
Hostname: req.Hostname,
|
Hostname: req.Hostname,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
Command: req.Command,
|
Command: req.Command,
|
||||||
Timeout: req.Timeout,
|
Timeout: req.Timeout,
|
||||||
Status: "pending",
|
Status: "pending",
|
||||||
CreatedBy: fmt.Sprintf("%v", username),
|
CreatedBy: fmt.Sprintf("%v", username),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +180,11 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ruleDef struct {
|
type ruleDef struct {
|
||||||
Name string
|
Name string
|
||||||
Metric string
|
Metric string
|
||||||
Operator string
|
Operator string
|
||||||
Threshold float64
|
Threshold float64
|
||||||
Duration int
|
Duration int
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultRules := []ruleDef{
|
defaultRules := []ruleDef{
|
||||||
|
|||||||
@@ -473,5 +473,3 @@ func (h *AlertHandler) NotifyRecovery(ruleName, hostname, metric, message string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -60,11 +60,21 @@ func (h *AlertHandler) UpdateContact(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if req.Name != "" { updates["name"] = req.Name }
|
if req.Name != "" {
|
||||||
if req.Phone != "" { updates["phone"] = req.Phone }
|
updates["name"] = req.Name
|
||||||
if req.Email != "" { updates["email"] = req.Email }
|
}
|
||||||
if req.ChannelType != "" { updates["channel_type"] = req.ChannelType }
|
if req.Phone != "" {
|
||||||
if req.WebhookURL != "" { updates["webhook_url"] = req.WebhookURL }
|
updates["phone"] = req.Phone
|
||||||
|
}
|
||||||
|
if req.Email != "" {
|
||||||
|
updates["email"] = req.Email
|
||||||
|
}
|
||||||
|
if req.ChannelType != "" {
|
||||||
|
updates["channel_type"] = req.ChannelType
|
||||||
|
}
|
||||||
|
if req.WebhookURL != "" {
|
||||||
|
updates["webhook_url"] = req.WebhookURL
|
||||||
|
}
|
||||||
h.db.Model(&contact).Updates(updates)
|
h.db.Model(&contact).Updates(updates)
|
||||||
h.db.First(&contact, id)
|
h.db.First(&contact, id)
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: contact})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: contact})
|
||||||
@@ -127,8 +137,12 @@ func (h *AlertHandler) UpdateContactGroup(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if req.GroupName != "" { updates["group_name"] = req.GroupName }
|
if req.GroupName != "" {
|
||||||
if req.Notes != "" { updates["notes"] = req.Notes }
|
updates["group_name"] = req.GroupName
|
||||||
|
}
|
||||||
|
if req.Notes != "" {
|
||||||
|
updates["notes"] = req.Notes
|
||||||
|
}
|
||||||
if len(req.ContactIDs) > 0 {
|
if len(req.ContactIDs) > 0 {
|
||||||
idsJSON, _ := json.Marshal(req.ContactIDs)
|
idsJSON, _ := json.Marshal(req.ContactIDs)
|
||||||
updates["contact_ids"] = string(idsJSON)
|
updates["contact_ids"] = string(idsJSON)
|
||||||
|
|||||||
@@ -384,4 +384,4 @@ func isMiddlewareMetric(metric string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -181,4 +181,4 @@ func (e *AlertEngine) fireSNMPAlert(rule model.MonitorAlertRule, device model.SN
|
|||||||
// isSNMPMetric 判断 AgentAlert 是否属于 SNMP // Logan.Li
|
// isSNMPMetric 判断 AgentAlert 是否属于 SNMP // Logan.Li
|
||||||
func isSNMPMetric(metric string) bool {
|
func isSNMPMetric(metric string) bool {
|
||||||
return len(metric) >= 5 && metric[:5] == "snmp_"
|
return len(metric) >= 5 && metric[:5] == "snmp_"
|
||||||
}
|
}
|
||||||
@@ -104,4 +104,4 @@ var metricCatalog = []MetricCatalogCategory{
|
|||||||
// GET /api/v1/alerts/metric-catalog
|
// GET /api/v1/alerts/metric-catalog
|
||||||
func (h *AlertHandler) GetMetricCatalog(c *gin.Context) {
|
func (h *AlertHandler) GetMetricCatalog(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"categories": metricCatalog}})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"categories": metricCatalog}})
|
||||||
}
|
}
|
||||||
@@ -178,4 +178,4 @@ func fallbackNotifyRecovery(db *gorm.DB, ruleName, hostname, metric, message str
|
|||||||
fmt.Printf("[AlertNotify] 飞书恢复回退失败 (渠道:%s): %v\n", ch.Name, err)
|
fmt.Printf("[AlertNotify] 飞书恢复回退失败 (渠道:%s): %v\n", ch.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,16 +79,16 @@ func notifyTemplateVars(ruleName, hostname, metric, severity, message string, va
|
|||||||
platform = defaultNotifyPlatformName
|
platform = defaultNotifyPlatformName
|
||||||
}
|
}
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
"platform": platform,
|
"platform": platform,
|
||||||
"rule_name": ruleName,
|
"rule_name": ruleName,
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
"metric": metric,
|
"metric": metric,
|
||||||
"severity": severity,
|
"severity": severity,
|
||||||
"severity_label": sevLabel,
|
"severity_label": sevLabel,
|
||||||
"message": message,
|
"message": message,
|
||||||
"value": fmt.Sprintf("%.2f", value),
|
"value": fmt.Sprintf("%.2f", value),
|
||||||
"threshold": fmt.Sprintf("%.2f", threshold),
|
"threshold": fmt.Sprintf("%.2f", threshold),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,4 +134,4 @@ func buildAlertContent(ruleName, hostname, metric, severity, message string, val
|
|||||||
"**规则名称:** %s\\n**目标主机:** %s\\n**指标类型:** %s\\n**告警级别:** %s\\n**当前值:** %.2f\\n**阈值:** %.2f\\n**详细信息:** %s\\n**触发时间:** %s",
|
"**规则名称:** %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"),
|
ruleName, hostname, metric, severity, value, threshold, message, time.Now().Format("2006-01-02 15:04:05"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -126,4 +126,4 @@ func isValidNotifyCategory(cat model.AlertNotifyCategory) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -15,30 +15,30 @@ import (
|
|||||||
|
|
||||||
// CreateSilenceRequest 创建静默规则请求
|
// CreateSilenceRequest 创建静默规则请求
|
||||||
type CreateSilenceRequest struct {
|
type CreateSilenceRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
RuleIDs []uint `json:"rule_ids"`
|
RuleIDs []uint `json:"rule_ids"`
|
||||||
TargetIDs []uint `json:"target_ids"`
|
TargetIDs []uint `json:"target_ids"`
|
||||||
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
||||||
SilenceType string `json:"silence_type"`
|
SilenceType string `json:"silence_type"`
|
||||||
StartTime string `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
EndTime string `json:"end_time" binding:"required"`
|
EndTime string `json:"end_time" binding:"required"`
|
||||||
Weekdays []int `json:"weekdays"`
|
Weekdays []int `json:"weekdays"`
|
||||||
Status *int `json:"status"`
|
Status *int `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSilenceRequest 更新静默规则请求
|
// UpdateSilenceRequest 更新静默规则请求
|
||||||
type UpdateSilenceRequest struct {
|
type UpdateSilenceRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
RuleIDs []uint `json:"rule_ids"`
|
RuleIDs []uint `json:"rule_ids"`
|
||||||
TargetIDs []uint `json:"target_ids"`
|
TargetIDs []uint `json:"target_ids"`
|
||||||
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
||||||
SilenceType string `json:"silence_type"`
|
SilenceType string `json:"silence_type"`
|
||||||
StartTime string `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
EndTime string `json:"end_time"`
|
EndTime string `json:"end_time"`
|
||||||
Weekdays []int `json:"weekdays"`
|
Weekdays []int `json:"weekdays"`
|
||||||
Status *int `json:"status"`
|
Status *int `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSilences GET /alerts/silences
|
// ListSilences GET /alerts/silences
|
||||||
@@ -60,11 +60,11 @@ func (h *AlertHandler) ListSilences(c *gin.Context) {
|
|||||||
// 解析 JSON 字段为前端友好的格式
|
// 解析 JSON 字段为前端友好的格式
|
||||||
type SilenceWithParsed struct {
|
type SilenceWithParsed struct {
|
||||||
model.AlertSilence
|
model.AlertSilence
|
||||||
RuleIDList []uint `json:"rule_id_list"`
|
RuleIDList []uint `json:"rule_id_list"`
|
||||||
TargetIDList []uint `json:"target_id_list"`
|
TargetIDList []uint `json:"target_id_list"`
|
||||||
MatcherList []model.AlertSilenceMatcher `json:"matcher_list"`
|
MatcherList []model.AlertSilenceMatcher `json:"matcher_list"`
|
||||||
WeekdayList []int `json:"weekday_list"`
|
WeekdayList []int `json:"weekday_list"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []SilenceWithParsed
|
var result []SilenceWithParsed
|
||||||
|
|||||||
@@ -13,19 +13,19 @@ import (
|
|||||||
|
|
||||||
// UnifiedAlertItem 统一告警事件视图 // Logan.Li
|
// UnifiedAlertItem 统一告警事件视图 // Logan.Li
|
||||||
type UnifiedAlertItem struct {
|
type UnifiedAlertItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
SourceType string `json:"source_type"`
|
SourceType string `json:"source_type"`
|
||||||
RuleName string `json:"rule_name"`
|
RuleName string `json:"rule_name"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Severity string `json:"severity"`
|
Severity string `json:"severity"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
Metric string `json:"metric"`
|
Metric string `json:"metric"`
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
Threshold float64 `json:"threshold"`
|
Threshold float64 `json:"threshold"`
|
||||||
StartedAt string `json:"started_at"`
|
StartedAt string `json:"started_at"`
|
||||||
ResolvedAt *string `json:"resolved_at,omitempty"`
|
ResolvedAt *string `json:"resolved_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUnifiedAlerts 统一告警列表(主机 + 中间件 + 站点 + 日志)
|
// ListUnifiedAlerts 统一告警列表(主机 + 中间件 + 站点 + 日志)
|
||||||
@@ -212,4 +212,4 @@ func countSeverityStatus(items []UnifiedAlertItem, sev, status string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
@@ -114,8 +114,8 @@ func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
|
|||||||
var workflowSteps []model.ApprovalWorkflowStep
|
var workflowSteps []model.ApprovalWorkflowStep
|
||||||
stepCount := 1
|
stepCount := 1
|
||||||
var approvalSteps []struct {
|
var approvalSteps []struct {
|
||||||
name string
|
name string
|
||||||
stepType string
|
stepType string
|
||||||
approverIDs []uint
|
approverIDs []uint
|
||||||
}
|
}
|
||||||
if workflow.ID > 0 {
|
if workflow.ID > 0 {
|
||||||
@@ -126,8 +126,8 @@ func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
stepCount++
|
stepCount++
|
||||||
approvalSteps = append(approvalSteps, struct {
|
approvalSteps = append(approvalSteps, struct {
|
||||||
name string
|
name string
|
||||||
stepType string
|
stepType string
|
||||||
approverIDs []uint
|
approverIDs []uint
|
||||||
}{ws.Name, ws.StepType, ParseApproverIDs(ws.ApproverIDs)})
|
}{ws.Name, ws.StepType, ParseApproverIDs(ws.ApproverIDs)})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,14 +111,14 @@ func (h *WorkflowHandler) CreateWorkflow(c *gin.Context) {
|
|||||||
|
|
||||||
for _, s := range req.Steps {
|
for _, s := range req.Steps {
|
||||||
step := model.ApprovalWorkflowStep{
|
step := model.ApprovalWorkflowStep{
|
||||||
WorkflowID: workflow.ID,
|
WorkflowID: workflow.ID,
|
||||||
StepNumber: s.StepNumber,
|
StepNumber: s.StepNumber,
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
StepType: s.StepType,
|
StepType: s.StepType,
|
||||||
ApproverIDs: s.ApproverIDs,
|
ApproverIDs: s.ApproverIDs,
|
||||||
IsRequired: s.IsRequired,
|
IsRequired: s.IsRequired,
|
||||||
TimeoutHours: s.TimeoutHours,
|
TimeoutHours: s.TimeoutHours,
|
||||||
AutoApprove: s.AutoApproveTimeout,
|
AutoApprove: s.AutoApproveTimeout,
|
||||||
}
|
}
|
||||||
if step.TimeoutHours == 0 {
|
if step.TimeoutHours == 0 {
|
||||||
step.TimeoutHours = 24
|
step.TimeoutHours = 24
|
||||||
@@ -195,14 +195,14 @@ func (h *WorkflowHandler) UpdateWorkflow(c *gin.Context) {
|
|||||||
tx.Where("workflow_id = ?", workflow.ID).Delete(&model.ApprovalWorkflowStep{})
|
tx.Where("workflow_id = ?", workflow.ID).Delete(&model.ApprovalWorkflowStep{})
|
||||||
for _, s := range req.Steps {
|
for _, s := range req.Steps {
|
||||||
step := model.ApprovalWorkflowStep{
|
step := model.ApprovalWorkflowStep{
|
||||||
WorkflowID: workflow.ID,
|
WorkflowID: workflow.ID,
|
||||||
StepNumber: s.StepNumber,
|
StepNumber: s.StepNumber,
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
StepType: s.StepType,
|
StepType: s.StepType,
|
||||||
ApproverIDs: s.ApproverIDs,
|
ApproverIDs: s.ApproverIDs,
|
||||||
IsRequired: s.IsRequired,
|
IsRequired: s.IsRequired,
|
||||||
TimeoutHours: s.TimeoutHours,
|
TimeoutHours: s.TimeoutHours,
|
||||||
AutoApprove: s.AutoApproveTimeout,
|
AutoApprove: s.AutoApproveTimeout,
|
||||||
}
|
}
|
||||||
if step.TimeoutHours == 0 {
|
if step.TimeoutHours == 0 {
|
||||||
step.TimeoutHours = 24
|
step.TimeoutHours = 24
|
||||||
|
|||||||
@@ -82,4 +82,4 @@ func (h *BillingHandler) SyncAccountBilling(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: msg, Data: result})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: msg, Data: result})
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func (h *PipelineHandler) ListBuildNodes(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"list": nodes,
|
"list": nodes,
|
||||||
"total": len(nodes),
|
"total": len(nodes),
|
||||||
"master": map[string]interface{}{
|
"master": map[string]interface{}{
|
||||||
"name": "Master",
|
"name": "Master",
|
||||||
@@ -221,4 +221,4 @@ func (h *PipelineHandler) UpdateBuildMaster(c *gin.Context) {
|
|||||||
"name": "Master", "max_executors": cfg.MaxExecutors, "running_count": running,
|
"name": "Master", "max_executors": cfg.MaxExecutors, "running_count": running,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -199,4 +199,3 @@ func (h *CloudAccountHandler) DeleteCloudAccount(c *gin.Context) {
|
|||||||
Message: "删除成功",
|
Message: "删除成功",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,4 +227,3 @@ func (h *DomainHandler) UpdateDomain(c *gin.Context) {
|
|||||||
Data: domain,
|
Data: domain,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,4 +129,3 @@ func (h *PlatformHandler) DeletePlatform(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,10 +227,28 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
|||||||
ip = inst.InstanceID
|
ip = inst.InstanceID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已存在(按 IP 或 hostname)
|
hostname := cloudImportHostname(inst)
|
||||||
var count int64
|
alias := cloudImportAlias(inst)
|
||||||
h.db.Model(&model.Server{}).Where("ip = ? OR hostname = ?", ip, inst.Hostname).Count(&count)
|
|
||||||
if count > 0 {
|
// 检查是否已存在(优先实例 ID,其次 IP/hostname),重复导入只补空别名,避免覆盖人工命名。 // Logan.Li
|
||||||
|
var existing model.Server
|
||||||
|
existingQuery := h.db.Where("ip = ? OR hostname = ?", ip, hostname)
|
||||||
|
if inst.InstanceID != "" {
|
||||||
|
existingQuery = h.db.Where("cloud_instance_id = ? OR ip = ? OR hostname = ?", inst.InstanceID, ip, hostname)
|
||||||
|
}
|
||||||
|
if err := existingQuery.Take(&existing).Error; err == nil {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"cloud_instance_name": inst.InstanceName,
|
||||||
|
"cloud_instance_id": inst.InstanceID,
|
||||||
|
"description": "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")",
|
||||||
|
}
|
||||||
|
if existing.Alias == "" && alias != "" {
|
||||||
|
updates["alias"] = alias
|
||||||
|
}
|
||||||
|
h.db.Model(&existing).Updates(updates)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
skipped++
|
skipped++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -264,33 +282,28 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
|||||||
if ipAddr == "" {
|
if ipAddr == "" {
|
||||||
ipAddr = inst.InstanceID
|
ipAddr = inst.InstanceID
|
||||||
}
|
}
|
||||||
hostname := inst.Hostname
|
|
||||||
if hostname == "" {
|
|
||||||
hostname = inst.InstanceName
|
|
||||||
}
|
|
||||||
if hostname == "" {
|
|
||||||
hostname = inst.InstanceID
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注意:servers 表的 cloud_id 外键引用的是 clouds 表(非 cloud_accounts),
|
// 注意:servers 表的 cloud_id 外键引用的是 clouds 表(非 cloud_accounts),
|
||||||
// region_id 外键引用 regions 表。未找到关联时设为 NULL 避免外键约束失败。
|
// region_id 外键引用 regions 表。未找到关联时设为 NULL 避免外键约束失败。
|
||||||
server := model.Server{
|
server := model.Server{
|
||||||
Hostname: hostname,
|
Hostname: hostname,
|
||||||
IP: ipAddr,
|
Alias: alias,
|
||||||
PublicIP: inst.PublicIP,
|
CloudInstanceName: inst.InstanceName,
|
||||||
Port: defaultPort,
|
CloudInstanceID: inst.InstanceID,
|
||||||
Username: username,
|
IP: ipAddr,
|
||||||
AuthType: "password",
|
PublicIP: inst.PublicIP,
|
||||||
OSType: osType,
|
Port: defaultPort,
|
||||||
Protocol: protocol,
|
Username: username,
|
||||||
RDPPort: 3389,
|
AuthType: "password",
|
||||||
OS: osName,
|
OSType: osType,
|
||||||
CPU: inst.CPU,
|
Protocol: protocol,
|
||||||
Memory: int64(inst.Memory),
|
RDPPort: 3389,
|
||||||
Status: status,
|
OS: osName,
|
||||||
RegionID: regionID, // nil = NULL
|
CPU: inst.CPU,
|
||||||
Description: "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")",
|
Memory: int64(inst.Memory),
|
||||||
AgentStatus: "offline",
|
Status: status,
|
||||||
|
RegionID: regionID, // nil = NULL
|
||||||
|
Description: "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")",
|
||||||
|
AgentStatus: "offline",
|
||||||
}
|
}
|
||||||
if protocol == "rdp" && hasRDPCred {
|
if protocol == "rdp" && hasRDPCred {
|
||||||
applyCredential(rdpCred, rdpPlain, &server)
|
applyCredential(rdpCred, rdpPlain, &server)
|
||||||
@@ -317,3 +330,31 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloudImportHostname(inst cloud.CloudInstance) string {
|
||||||
|
// hostname 保留技术主机名;展示友好名称写入 alias/cloud_instance_name。 // Logan.Li
|
||||||
|
if inst.Hostname != "" && inst.Hostname != inst.InstanceID {
|
||||||
|
return inst.Hostname
|
||||||
|
}
|
||||||
|
if inst.InstanceName != "" && inst.InstanceName != inst.InstanceID {
|
||||||
|
return inst.InstanceName
|
||||||
|
}
|
||||||
|
if inst.PrivateIP != "" {
|
||||||
|
return inst.PrivateIP
|
||||||
|
}
|
||||||
|
if inst.PublicIP != "" {
|
||||||
|
return inst.PublicIP
|
||||||
|
}
|
||||||
|
if inst.InstanceName != "" {
|
||||||
|
return inst.InstanceName
|
||||||
|
}
|
||||||
|
return inst.InstanceID
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloudImportAlias(inst cloud.CloudInstance) string {
|
||||||
|
// 云导入初始别名优先使用控制台实例名称,后续人工别名不被同步覆盖。 // Logan.Li
|
||||||
|
if inst.InstanceName != "" && inst.InstanceName != inst.InstanceID {
|
||||||
|
return inst.InstanceName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
ezops "ezops/model"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CustomDashboardHandler 自定义监控仪表盘 handler // Logan.Li
|
|
||||||
type CustomDashboardHandler struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCustomDashboardHandler 创建 handler
|
|
||||||
func NewCustomDashboardHandler(db *gorm.DB) *CustomDashboardHandler {
|
|
||||||
return &CustomDashboardHandler{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// DTO
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
type CreateDashboardReq struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Datasource string `json:"datasource"`
|
|
||||||
DatasourceURL string `json:"datasource_url"`
|
|
||||||
Panels string `json:"panels"` // JSON string
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateDashboardReq struct {
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Datasource *string `json:"datasource"`
|
|
||||||
DatasourceURL *string `json:"datasource_url"`
|
|
||||||
Panels *string `json:"panels"`
|
|
||||||
IsDefault *bool `json:"is_default"`
|
|
||||||
IsFavorite *bool `json:"is_favorite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// CRUD
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// List 仪表盘列表
|
|
||||||
// GET /api/v1/custom-dashboards
|
|
||||||
func (h *CustomDashboardHandler) List(c *gin.Context) {
|
|
||||||
var dashboards []ezops.CustomDashboard
|
|
||||||
if err := h.db.Order("is_default DESC, updated_at DESC").Find(&dashboards).Error; err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为前端期望的格式(id 转为 string,panels JSON 解析为数组)
|
|
||||||
result := make([]gin.H, 0, len(dashboards))
|
|
||||||
for _, d := range dashboards {
|
|
||||||
item := gin.H{
|
|
||||||
"id": strconv.Itoa(int(d.ID)),
|
|
||||||
"name": d.Name,
|
|
||||||
"description": d.Description,
|
|
||||||
"datasource": d.Datasource,
|
|
||||||
"datasourceUrl": d.DatasourceURL,
|
|
||||||
"datasource_url": d.DatasourceURL,
|
|
||||||
"createdAt": d.CreatedAt,
|
|
||||||
"updatedAt": d.UpdatedAt,
|
|
||||||
"createdBy": d.CreatedBy,
|
|
||||||
"isDefault": d.IsDefault,
|
|
||||||
"is_default": d.IsDefault,
|
|
||||||
}
|
|
||||||
// 解析 panels JSON
|
|
||||||
var panels []interface{}
|
|
||||||
if d.Panels != "" {
|
|
||||||
if err := json.Unmarshal([]byte(d.Panels), &panels); err == nil {
|
|
||||||
item["panels"] = panels
|
|
||||||
} else {
|
|
||||||
item["panels"] = []interface{}{}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item["panels"] = []interface{}{}
|
|
||||||
}
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Data: gin.H{
|
|
||||||
"list": result,
|
|
||||||
"total": len(result),
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取单个仪表盘
|
|
||||||
// GET /api/v1/custom-dashboards/:id
|
|
||||||
func (h *CustomDashboardHandler) Get(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
var d ezops.CustomDashboard
|
|
||||||
if err := h.db.First(&d, id).Error; err != nil {
|
|
||||||
c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var panels []interface{}
|
|
||||||
if d.Panels != "" {
|
|
||||||
json.Unmarshal([]byte(d.Panels), &panels)
|
|
||||||
}
|
|
||||||
if panels == nil {
|
|
||||||
panels = []interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Data: gin.H{
|
|
||||||
"id": strconv.Itoa(int(d.ID)),
|
|
||||||
"name": d.Name,
|
|
||||||
"description": d.Description,
|
|
||||||
"datasource": d.Datasource,
|
|
||||||
"datasourceUrl": d.DatasourceURL,
|
|
||||||
"datasource_url": d.DatasourceURL,
|
|
||||||
"panels": panels,
|
|
||||||
"createdAt": d.CreatedAt,
|
|
||||||
"updatedAt": d.UpdatedAt,
|
|
||||||
"createdBy": d.CreatedBy,
|
|
||||||
"isDefault": d.IsDefault,
|
|
||||||
"is_default": d.IsDefault,
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create 创建仪表盘
|
|
||||||
// POST /api/v1/custom-dashboards
|
|
||||||
func (h *CustomDashboardHandler) Create(c *gin.Context) {
|
|
||||||
var req CreateDashboardReq
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, ezops.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Datasource == "" {
|
|
||||||
req.Datasource = "prometheus"
|
|
||||||
}
|
|
||||||
if req.Panels == "" {
|
|
||||||
req.Panels = "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果设为默认,先清除其他默认
|
|
||||||
if req.IsDefault {
|
|
||||||
h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ?", true).Update("is_default", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 JWT 获取用户名
|
|
||||||
username := "admin"
|
|
||||||
if u, exists := c.Get("username"); exists {
|
|
||||||
if s, ok := u.(string); ok {
|
|
||||||
username = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d := ezops.CustomDashboard{
|
|
||||||
Name: req.Name,
|
|
||||||
Description: req.Description,
|
|
||||||
Datasource: req.Datasource,
|
|
||||||
DatasourceURL: req.DatasourceURL,
|
|
||||||
Panels: req.Panels,
|
|
||||||
CreatedBy: username,
|
|
||||||
IsDefault: req.IsDefault,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.db.Create(&d).Error; err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "创建成功", Data: gin.H{
|
|
||||||
"success": true,
|
|
||||||
"id": strconv.Itoa(int(d.ID)),
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update 更新仪表盘
|
|
||||||
// PUT /api/v1/custom-dashboards/:id
|
|
||||||
func (h *CustomDashboardHandler) Update(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
var d ezops.CustomDashboard
|
|
||||||
if err := h.db.First(&d, id).Error; err != nil {
|
|
||||||
c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req UpdateDashboardReq
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, ezops.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := map[string]interface{}{}
|
|
||||||
if req.Name != nil {
|
|
||||||
updates["name"] = *req.Name
|
|
||||||
}
|
|
||||||
if req.Description != nil {
|
|
||||||
updates["description"] = *req.Description
|
|
||||||
}
|
|
||||||
if req.Datasource != nil {
|
|
||||||
updates["datasource"] = *req.Datasource
|
|
||||||
}
|
|
||||||
if req.DatasourceURL != nil {
|
|
||||||
updates["datasource_url"] = *req.DatasourceURL
|
|
||||||
}
|
|
||||||
if req.Panels != nil {
|
|
||||||
updates["panels"] = *req.Panels
|
|
||||||
}
|
|
||||||
if req.IsFavorite != nil {
|
|
||||||
updates["is_favorite"] = *req.IsFavorite
|
|
||||||
}
|
|
||||||
if req.IsDefault != nil && *req.IsDefault {
|
|
||||||
// 设为默认前先清除其他默认
|
|
||||||
h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ? AND id != ?", true, d.ID).Update("is_default", false)
|
|
||||||
updates["is_default"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updates) > 0 {
|
|
||||||
if err := h.db.Model(&d).Updates(updates).Error; err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "更新成功", Data: gin.H{"success": true}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 删除仪表盘
|
|
||||||
// DELETE /api/v1/custom-dashboards/:id
|
|
||||||
func (h *CustomDashboardHandler) Delete(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
if err := h.db.Delete(&ezops.CustomDashboard{}, id).Error; err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "删除成功", Data: gin.H{"success": true}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefault 设为默认仪表盘
|
|
||||||
// POST /api/v1/custom-dashboards/:id/default
|
|
||||||
func (h *CustomDashboardHandler) SetDefault(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
var d ezops.CustomDashboard
|
|
||||||
if err := h.db.First(&d, id).Error; err != nil {
|
|
||||||
c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除其他默认
|
|
||||||
h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ? AND id != ?", true, d.ID).Update("is_default", false)
|
|
||||||
// 设置当前为默认
|
|
||||||
h.db.Model(&d).Update("is_default", true)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "设置成功", Data: gin.H{"success": true}})
|
|
||||||
}
|
|
||||||
@@ -359,7 +359,7 @@ func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
|
|||||||
// @Success 200 {object} model.Response{data=[]model.MetricData}
|
// @Success 200 {object} model.Response{data=[]model.MetricData}
|
||||||
// @Router /dashboard/metrics [get]
|
// @Router /dashboard/metrics [get]
|
||||||
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
||||||
hours := 6 // 默认6小时,减少扫描行数
|
hours := 6 // 默认6小时,减少扫描行数
|
||||||
if hv := c.Query("hours"); hv != "" {
|
if hv := c.Query("hours"); hv != "" {
|
||||||
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
|
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
|
||||||
hours = parsed
|
hours = parsed
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"ezops/model"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeployHandler 发布系统处理器
|
|
||||||
type DeployHandler struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDeployHandler(db *gorm.DB) *DeployHandler {
|
|
||||||
return &DeployHandler{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Git 仓库认证 ====================
|
|
||||||
|
|
||||||
func (h *DeployHandler) ListRepos(c *gin.Context) {
|
|
||||||
var items []model.PipelineRepo
|
|
||||||
h.db.Order("updated_at DESC").Find(&items)
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) GetRepo(c *gin.Context) {
|
|
||||||
var item model.PipelineRepo
|
|
||||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) CreateRepo(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
URL string `json:"url" binding:"required"`
|
|
||||||
AuthType model.RepoAuthType `json:"auth_type"`
|
|
||||||
CredentialID *uint `json:"credential_id"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.AuthType == "" { req.AuthType = model.RepoAuthToken }
|
|
||||||
item := model.PipelineRepo{
|
|
||||||
Name: req.Name, Description: req.Description, URL: req.URL,
|
|
||||||
AuthType: req.AuthType, CredentialID: req.CredentialID,
|
|
||||||
CreatedBy: c.GetString("username"),
|
|
||||||
}
|
|
||||||
h.db.Create(&item)
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) UpdateRepo(c *gin.Context) {
|
|
||||||
var item model.PipelineRepo
|
|
||||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req struct {
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
URL *string `json:"url"`
|
|
||||||
AuthType *model.RepoAuthType `json:"auth_type"`
|
|
||||||
CredentialID *uint `json:"credential_id"`
|
|
||||||
Enabled *bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
c.ShouldBindJSON(&req)
|
|
||||||
updates := map[string]interface{}{}
|
|
||||||
if req.Name != nil { updates["name"] = *req.Name }
|
|
||||||
if req.Description != nil { updates["description"] = *req.Description }
|
|
||||||
if req.URL != nil { updates["url"] = *req.URL }
|
|
||||||
if req.AuthType != nil { updates["auth_type"] = *req.AuthType }
|
|
||||||
if req.Enabled != nil { updates["enabled"] = *req.Enabled }
|
|
||||||
if req.CredentialID != nil { updates["credential_id"] = req.CredentialID }
|
|
||||||
h.db.Model(&item).Updates(updates)
|
|
||||||
h.db.First(&item, item.ID)
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) DeleteRepo(c *gin.Context) {
|
|
||||||
h.db.Delete(&model.PipelineRepo{}, c.Param("id"))
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 镜像仓库认证 ====================
|
|
||||||
|
|
||||||
func (h *DeployHandler) ListRegistries(c *gin.Context) {
|
|
||||||
var items []model.PipelineRegistry
|
|
||||||
h.db.Order("updated_at DESC").Find(&items)
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) GetRegistry(c *gin.Context) {
|
|
||||||
var item model.PipelineRegistry
|
|
||||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) CreateRegistry(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
RegistryURL string `json:"registry_url" binding:"required"`
|
|
||||||
Namespace string `json:"namespace"`
|
|
||||||
AuthType model.RegistryAuthType `json:"auth_type"`
|
|
||||||
CredentialID *uint `json:"credential_id"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.AuthType == "" { req.AuthType = model.RegistryAuthPassword }
|
|
||||||
item := model.PipelineRegistry{
|
|
||||||
Name: req.Name, Description: req.Description, RegistryURL: req.RegistryURL,
|
|
||||||
Namespace: req.Namespace, AuthType: req.AuthType, CredentialID: req.CredentialID,
|
|
||||||
CreatedBy: c.GetString("username"),
|
|
||||||
}
|
|
||||||
h.db.Create(&item)
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) UpdateRegistry(c *gin.Context) {
|
|
||||||
var item model.PipelineRegistry
|
|
||||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req struct {
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
RegistryURL *string `json:"registry_url"`
|
|
||||||
Namespace *string `json:"namespace"`
|
|
||||||
AuthType *model.RegistryAuthType `json:"auth_type"`
|
|
||||||
CredentialID *uint `json:"credential_id"`
|
|
||||||
Enabled *bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
c.ShouldBindJSON(&req)
|
|
||||||
updates := map[string]interface{}{}
|
|
||||||
if req.Name != nil { updates["name"] = *req.Name }
|
|
||||||
if req.Description != nil { updates["description"] = *req.Description }
|
|
||||||
if req.RegistryURL != nil { updates["registry_url"] = *req.RegistryURL }
|
|
||||||
if req.Namespace != nil { updates["namespace"] = *req.Namespace }
|
|
||||||
if req.AuthType != nil { updates["auth_type"] = *req.AuthType }
|
|
||||||
if req.Enabled != nil { updates["enabled"] = *req.Enabled }
|
|
||||||
if req.CredentialID != nil { updates["credential_id"] = req.CredentialID }
|
|
||||||
h.db.Model(&item).Updates(updates)
|
|
||||||
h.db.First(&item, item.ID)
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeployHandler) DeleteRegistry(c *gin.Context) {
|
|
||||||
h.db.Delete(&model.PipelineRegistry{}, c.Param("id"))
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,9 @@ func (h *PipelineHandler) CreateEnvironment(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Color == "" { req.Color = typeDefaultColor(req.Type) }
|
if req.Color == "" {
|
||||||
|
req.Color = typeDefaultColor(req.Type)
|
||||||
|
}
|
||||||
item := model.DeployEnvironment{
|
item := model.DeployEnvironment{
|
||||||
Name: req.Name, DisplayName: req.DisplayName,
|
Name: req.Name, DisplayName: req.DisplayName,
|
||||||
Type: model.EnvironmentType(req.Type), Color: req.Color,
|
Type: model.EnvironmentType(req.Type), Color: req.Color,
|
||||||
@@ -67,13 +69,27 @@ func (h *PipelineHandler) UpdateEnvironment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.ShouldBindJSON(&req)
|
c.ShouldBindJSON(&req)
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if req.Name != nil { updates["name"] = *req.Name }
|
if req.Name != nil {
|
||||||
if req.DisplayName != nil { updates["display_name"] = *req.DisplayName }
|
updates["name"] = *req.Name
|
||||||
if req.Type != nil { updates["type"] = *req.Type }
|
}
|
||||||
if req.Color != nil { updates["color"] = *req.Color }
|
if req.DisplayName != nil {
|
||||||
if req.SortOrder != nil { updates["sort_order"] = *req.SortOrder }
|
updates["display_name"] = *req.DisplayName
|
||||||
if req.Description != nil { updates["description"] = *req.Description }
|
}
|
||||||
if req.Enabled != nil { updates["enabled"] = *req.Enabled }
|
if req.Type != nil {
|
||||||
|
updates["type"] = *req.Type
|
||||||
|
}
|
||||||
|
if req.Color != nil {
|
||||||
|
updates["color"] = *req.Color
|
||||||
|
}
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
updates["sort_order"] = *req.SortOrder
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
updates["description"] = *req.Description
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
updates["enabled"] = *req.Enabled
|
||||||
|
}
|
||||||
h.db.Model(&item).Updates(updates)
|
h.db.Model(&item).Updates(updates)
|
||||||
h.db.First(&item, id)
|
h.db.First(&item, id)
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||||
@@ -87,10 +103,14 @@ func (h *PipelineHandler) DeleteEnvironment(c *gin.Context) {
|
|||||||
|
|
||||||
func typeDefaultColor(t string) string {
|
func typeDefaultColor(t string) string {
|
||||||
switch t {
|
switch t {
|
||||||
case "production": return "#EF4444"
|
case "production":
|
||||||
case "staging": return "#F59E0B"
|
return "#EF4444"
|
||||||
case "testing": return "#10B981"
|
case "staging":
|
||||||
default: return "#3B82F6" // development
|
return "#F59E0B"
|
||||||
|
case "testing":
|
||||||
|
return "#10B981"
|
||||||
|
default:
|
||||||
|
return "#3B82F6" // development
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +232,8 @@ func (h *PipelineHandler) GetRunLog(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||||
"run": run,
|
"run": run,
|
||||||
"logs": logs,
|
"logs": logs,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,12 @@ func (h *PipelineHandler) UpdateGroup(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.ShouldBindJSON(&req)
|
c.ShouldBindJSON(&req)
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if req.Name != nil { updates["name"] = *req.Name }
|
if req.Name != nil {
|
||||||
if req.Description != nil { updates["description"] = *req.Description }
|
updates["name"] = *req.Name
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
updates["description"] = *req.Description
|
||||||
|
}
|
||||||
h.db.Model(&group).Updates(updates)
|
h.db.Model(&group).Updates(updates)
|
||||||
h.db.First(&group, id)
|
h.db.First(&group, id)
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: group})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: group})
|
||||||
|
|||||||
@@ -216,28 +216,28 @@ func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
|||||||
Code: 200,
|
Code: 200,
|
||||||
Message: "success",
|
Message: "success",
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"id": cluster.ID,
|
"id": cluster.ID,
|
||||||
"name": cluster.Name,
|
"name": cluster.Name,
|
||||||
"code": cluster.Code,
|
"code": cluster.Code,
|
||||||
"version": cluster.Version,
|
"version": cluster.Version,
|
||||||
"status": cluster.Status,
|
"status": cluster.Status,
|
||||||
"config_type": cluster.ConfigType,
|
"config_type": cluster.ConfigType,
|
||||||
"api_server": cluster.APIServer,
|
"api_server": cluster.APIServer,
|
||||||
"api_endpoint": cluster.APIEndpoint,
|
"api_endpoint": cluster.APIEndpoint,
|
||||||
"description": cluster.Description,
|
"description": cluster.Description,
|
||||||
"has_token": cluster.Token != "",
|
"has_token": cluster.Token != "",
|
||||||
"has_kube_config": cluster.KubeConfig != "",
|
"has_kube_config": cluster.KubeConfig != "",
|
||||||
"node_count": cluster.NodeCount,
|
"node_count": cluster.NodeCount,
|
||||||
"pod_count": cluster.PodCount,
|
"pod_count": cluster.PodCount,
|
||||||
"service_count": cluster.ServiceCount,
|
"service_count": cluster.ServiceCount,
|
||||||
"deployment_count": cluster.DeploymentCount,
|
"deployment_count": cluster.DeploymentCount,
|
||||||
"ingress_count": cluster.IngressCount,
|
"ingress_count": cluster.IngressCount,
|
||||||
"cpu_usage": cluster.CPUUsage,
|
"cpu_usage": cluster.CPUUsage,
|
||||||
"memory_usage": cluster.MemoryUsage,
|
"memory_usage": cluster.MemoryUsage,
|
||||||
"labels": cluster.Labels,
|
"labels": cluster.Labels,
|
||||||
"last_sync_at": cluster.LastSyncAt,
|
"last_sync_at": cluster.LastSyncAt,
|
||||||
"created_at": cluster.CreatedAt,
|
"created_at": cluster.CreatedAt,
|
||||||
"updated_at": cluster.UpdatedAt,
|
"updated_at": cluster.UpdatedAt,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -320,7 +320,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
|||||||
// Sync nodes
|
// Sync nodes
|
||||||
if nodes, err := client.ListNodes(); err == nil {
|
if nodes, err := client.ListNodes(); err == nil {
|
||||||
// Use raw SQL for reliability (avoid GORM soft-delete / auto-timestamp issues)
|
// Use raw SQL for reliability (avoid GORM soft-delete / auto-timestamp issues)
|
||||||
h.db.Exec("DELETE FROM kubernetes_nodes WHERE cluster_id = ?", cluster.ID)
|
h.db.Exec("DELETE FROM k8s_nodes WHERE cluster_id = ?", cluster.ID)
|
||||||
for _, n := range nodes.Items {
|
for _, n := range nodes.Items {
|
||||||
role := "worker"
|
role := "worker"
|
||||||
if _, ok := n.Metadata.Labels["node-role.kubernetes.io/master"]; ok {
|
if _, ok := n.Metadata.Labels["node-role.kubernetes.io/master"]; ok {
|
||||||
@@ -358,7 +358,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
|||||||
memUsagePct = float64(memMi) / float64(memMB) * 100
|
memUsagePct = float64(memMi) / float64(memMB) * 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err2 := h.db.Exec(`INSERT INTO kubernetes_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, cpu_usage, memory_usage, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
err2 := h.db.Exec(`INSERT INTO k8s_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, cpu_usage, memory_usage, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
cluster.ID, n.Metadata.Name, ip, role, nodeStatus, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion,
|
cluster.ID, n.Metadata.Name, ip, role, nodeStatus, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion,
|
||||||
parseIntResource(n.Status.Capacity["cpu"]), parseMemResource(n.Status.Capacity["memory"]), parseIntResource(n.Status.Capacity["pods"]),
|
parseIntResource(n.Status.Capacity["cpu"]), parseMemResource(n.Status.Capacity["memory"]), parseIntResource(n.Status.Capacity["pods"]),
|
||||||
cpuUsagePct, memUsagePct, string(labelsJSON), now, now).Error
|
cpuUsagePct, memUsagePct, string(labelsJSON), now, now).Error
|
||||||
@@ -508,7 +508,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
Message: msg,
|
Message: msg,
|
||||||
Data: result,
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,27 +14,27 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
|||||||
h.db.Find(&clusters)
|
h.db.Find(&clusters)
|
||||||
|
|
||||||
type ClusterComparison struct {
|
type ClusterComparison struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
NodeCount int `json:"node_count"`
|
NodeCount int `json:"node_count"`
|
||||||
PodCount int `json:"pod_count"`
|
PodCount int `json:"pod_count"`
|
||||||
DeploymentCount int `json:"deployment_count"`
|
DeploymentCount int `json:"deployment_count"`
|
||||||
ServiceCount int `json:"service_count"`
|
ServiceCount int `json:"service_count"`
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
MemoryUsage float64 `json:"memory_usage"`
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
APIServer string `json:"api_server"`
|
APIServer string `json:"api_server"`
|
||||||
LastSyncAt *time.Time `json:"last_sync_at"`
|
LastSyncAt *time.Time `json:"last_sync_at"`
|
||||||
|
|
||||||
// Node-level aggregated data from kubernetes_nodes table
|
// Node-level aggregated data from k8s_nodes table
|
||||||
TotalCPUCores int `json:"total_cpu_cores"`
|
TotalCPUCores int `json:"total_cpu_cores"`
|
||||||
TotalMemoryMB int64 `json:"total_memory_mb"`
|
TotalMemoryMB int64 `json:"total_memory_mb"`
|
||||||
MasterCount int `json:"master_count"`
|
MasterCount int `json:"master_count"`
|
||||||
WorkerCount int `json:"worker_count"`
|
WorkerCount int `json:"worker_count"`
|
||||||
HealthyNodes int `json:"healthy_nodes"`
|
HealthyNodes int `json:"healthy_nodes"`
|
||||||
UnhealthyNodes int `json:"unhealthy_nodes"`
|
UnhealthyNodes int `json:"unhealthy_nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取所有节点的聚合数据(按 cluster_id 分组)
|
// 2. 获取所有节点的聚合数据(按 cluster_id 分组)
|
||||||
@@ -64,8 +64,16 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
|||||||
agg.TotalCPU += n.CPU
|
agg.TotalCPU += n.CPU
|
||||||
agg.TotalMemory += n.Memory
|
agg.TotalMemory += n.Memory
|
||||||
if n.CPUUsage > 0 || n.MemoryUsage > 0 {
|
if n.CPUUsage > 0 || n.MemoryUsage > 0 {
|
||||||
if agg.AvgCPUUsage == 0 { agg.AvgCPUUsage = n.CPUUsage } else { agg.AvgCPUUsage = (agg.AvgCPUUsage*float64(agg.NodeCount-1) + n.CPUUsage) / float64(agg.NodeCount) }
|
if agg.AvgCPUUsage == 0 {
|
||||||
if agg.AvgMemUsage == 0 { agg.AvgMemUsage = n.MemoryUsage } else { agg.AvgMemUsage = (agg.AvgMemUsage*float64(agg.NodeCount-1) + n.MemoryUsage) / float64(agg.NodeCount) }
|
agg.AvgCPUUsage = n.CPUUsage
|
||||||
|
} else {
|
||||||
|
agg.AvgCPUUsage = (agg.AvgCPUUsage*float64(agg.NodeCount-1) + n.CPUUsage) / float64(agg.NodeCount)
|
||||||
|
}
|
||||||
|
if agg.AvgMemUsage == 0 {
|
||||||
|
agg.AvgMemUsage = n.MemoryUsage
|
||||||
|
} else {
|
||||||
|
agg.AvgMemUsage = (agg.AvgMemUsage*float64(agg.NodeCount-1) + n.MemoryUsage) / float64(agg.NodeCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if n.Role == "master" {
|
if n.Role == "master" {
|
||||||
agg.MasterCount++
|
agg.MasterCount++
|
||||||
|
|||||||
@@ -14,14 +14,23 @@ import (
|
|||||||
|
|
||||||
func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ns := c.Query("namespace")
|
ns := c.Query("namespace")
|
||||||
eventType := c.Query("type") // Normal or Warning
|
eventType := c.Query("type") // Normal or Warning
|
||||||
events, err := client.ListEvents(ns)
|
events, err := client.ListEvents(ns)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type EventSummary struct {
|
type EventSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
@@ -40,7 +49,9 @@ func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []EventSummary{}
|
result := []EventSummary{}
|
||||||
for _, e := range events.Items {
|
for _, e := range events.Items {
|
||||||
if eventType != "" && e.Type != eventType { continue }
|
if eventType != "" && e.Type != eventType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
objectKind := e.InvolvedObject.Kind
|
objectKind := e.InvolvedObject.Kind
|
||||||
objectName := e.InvolvedObject.Name
|
objectName := e.InvolvedObject.Name
|
||||||
object := fmt.Sprintf("%s/%s", objectKind, objectName)
|
object := fmt.Sprintf("%s/%s", objectKind, objectName)
|
||||||
|
|||||||
@@ -246,30 +246,45 @@ func (h *KubernetesHandler) DeleteCronJobNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
hpas, err := client.ListHPAs()
|
hpas, err := client.ListHPAs()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type HPASummary struct {
|
type HPASummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
TargetKind string `json:"target_kind"`
|
TargetKind string `json:"target_kind"`
|
||||||
TargetName string `json:"target_name"`
|
TargetName string `json:"target_name"`
|
||||||
MinReplicas int `json:"min_replicas"`
|
MinReplicas int `json:"min_replicas"`
|
||||||
MaxReplicas int `json:"max_replicas"`
|
MaxReplicas int `json:"max_replicas"`
|
||||||
CPUPercent int `json:"cpu_percent"`
|
CPUPercent int `json:"cpu_percent"`
|
||||||
MemPercent int `json:"mem_percent"`
|
MemPercent int `json:"mem_percent"`
|
||||||
CurrentReplicas int `json:"current_replicas"`
|
CurrentReplicas int `json:"current_replicas"`
|
||||||
}
|
}
|
||||||
result := []HPASummary{}
|
result := []HPASummary{}
|
||||||
for _, h := range hpas.Items {
|
for _, h := range hpas.Items {
|
||||||
if h.Metadata.Namespace != ns { continue }
|
if h.Metadata.Namespace != ns {
|
||||||
cpu := 0; mem := 0
|
continue
|
||||||
|
}
|
||||||
|
cpu := 0
|
||||||
|
mem := 0
|
||||||
for _, m := range h.Spec.Metrics {
|
for _, m := range h.Spec.Metrics {
|
||||||
if m.Resource != nil {
|
if m.Resource != nil {
|
||||||
if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization }
|
if m.Resource.Name == "cpu" {
|
||||||
if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization }
|
cpu = m.Resource.Target.AverageUtilization
|
||||||
|
}
|
||||||
|
if m.Resource.Name == "memory" {
|
||||||
|
mem = m.Resource.Target.AverageUtilization
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, HPASummary{
|
result = append(result, HPASummary{
|
||||||
@@ -288,80 +303,165 @@ func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) createHPAWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) createHPAWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
var req k8sclient.CreateHPARequest
|
var req k8sclient.CreateHPARequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
if req.Namespace == "" { req.Namespace = ns }
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Namespace == "" {
|
||||||
|
req.Namespace = ns
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
hpa, err := client.CreateHPA(req)
|
hpa, err := client.CreateHPA(req)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: hpa})
|
c.JSON(200, model.Response{Code: 200, Data: hpa})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) deleteHPAWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) deleteHPAWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.DeleteHPA(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.DeleteHPA(ns, c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listCronJobsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listCronJobsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
cjs, err := client.ListCronJobs()
|
cjs, err := client.ListCronJobs()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
filtered := []k8sclient.CronJobItem{}
|
filtered := []k8sclient.CronJobItem{}
|
||||||
for _, cj := range cjs.Items { if cj.Metadata.Namespace == ns { filtered = append(filtered, cj) } }
|
for _, cj := range cjs.Items {
|
||||||
|
if cj.Metadata.Namespace == ns {
|
||||||
|
filtered = append(filtered, cj)
|
||||||
|
}
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: filtered})
|
c.JSON(200, model.Response{Code: 200, Data: filtered})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) createCronJobWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) createCronJobWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
var req k8sclient.CreateCronJobRequest
|
var req k8sclient.CreateCronJobRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
if req.Namespace == "" { req.Namespace = ns }
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Namespace == "" {
|
||||||
|
req.Namespace = ns
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
cj, err := client.CreateCronJob(req)
|
cj, err := client.CreateCronJob(req)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: cj})
|
c.JSON(200, model.Response{Code: 200, Data: cj})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) suspendCronJobWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) suspendCronJobWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
var req struct { Suspend bool `json:"suspend"` }; c.ShouldBindJSON(&req)
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
var req struct {
|
||||||
|
Suspend bool `json:"suspend"`
|
||||||
|
}
|
||||||
|
c.ShouldBindJSON(&req)
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { c.JSON(500, model.Response{Code: 500, Message: "操作失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
action := "恢复"; if req.Suspend { action = "暂停" }
|
return
|
||||||
|
}
|
||||||
|
if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "操作失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := "恢复"
|
||||||
|
if req.Suspend {
|
||||||
|
action = "暂停"
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "CronJob 已" + action})
|
c.JSON(200, model.Response{Code: 200, Message: "CronJob 已" + action})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) triggerCronJobWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) triggerCronJobWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.TriggerCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "触发失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.TriggerCronJob(ns, c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "触发失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "手动触发成功"})
|
c.JSON(200, model.Response{Code: 200, Message: "手动触发成功"})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) deleteCronJobWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) deleteCronJobWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.DeleteCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.DeleteCronJob(ns, c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) {
|
func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
hpas, err := client.ListHPAs()
|
hpas, err := client.ListHPAs()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type HPASummary struct {
|
type HPASummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -375,11 +475,16 @@ func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []HPASummary{}
|
result := []HPASummary{}
|
||||||
for _, h := range hpas.Items {
|
for _, h := range hpas.Items {
|
||||||
cpu := 0; mem := 0
|
cpu := 0
|
||||||
|
mem := 0
|
||||||
for _, m := range h.Spec.Metrics {
|
for _, m := range h.Spec.Metrics {
|
||||||
if m.Resource != nil {
|
if m.Resource != nil {
|
||||||
if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization }
|
if m.Resource.Name == "cpu" {
|
||||||
if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization }
|
cpu = m.Resource.Target.AverageUtilization
|
||||||
|
}
|
||||||
|
if m.Resource.Name == "memory" {
|
||||||
|
mem = m.Resource.Target.AverageUtilization
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = append(result, HPASummary{
|
result = append(result, HPASummary{
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
|
|||||||
|
|
||||||
// NamespaceDetail is the enriched namespace info for the management page
|
// NamespaceDetail is the enriched namespace info for the management page
|
||||||
type NamespaceDetail struct {
|
type NamespaceDetail struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Age string `json:"age"`
|
Age string `json:"age"`
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
ResourceCounts map[string]int `json:"resource_counts"`
|
ResourceCounts map[string]int `json:"resource_counts"`
|
||||||
HasResourceQuota bool `json:"has_resource_quota"`
|
HasResourceQuota bool `json:"has_resource_quota"`
|
||||||
HasLimitRange bool `json:"has_limit_range"`
|
HasLimitRange bool `json:"has_limit_range"`
|
||||||
ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"`
|
ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"`
|
||||||
LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"`
|
LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNamespacesDetail returns all namespaces with metadata, resource counts and quota info
|
// GetNamespacesDetail returns all namespaces with metadata, resource counts and quota info
|
||||||
@@ -110,11 +110,11 @@ func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) {
|
|||||||
for _, ns := range nsList.Items {
|
for _, ns := range nsList.Items {
|
||||||
name := ns.Metadata.Name
|
name := ns.Metadata.Name
|
||||||
nd := NamespaceDetail{
|
nd := NamespaceDetail{
|
||||||
Name: name,
|
Name: name,
|
||||||
Status: ns.Status.Phase,
|
Status: ns.Status.Phase,
|
||||||
Age: formatAge(ns.Metadata.CreationTimestamp),
|
Age: formatAge(ns.Metadata.CreationTimestamp),
|
||||||
Labels: ns.Metadata.Labels,
|
Labels: ns.Metadata.Labels,
|
||||||
Annotations: ns.Metadata.Annotations,
|
Annotations: ns.Metadata.Annotations,
|
||||||
ResourceCounts: map[string]int{
|
ResourceCounts: map[string]int{
|
||||||
"deployments": depCounts[name],
|
"deployments": depCounts[name],
|
||||||
"pods": podCounts[name],
|
"pods": podCounts[name],
|
||||||
@@ -181,21 +181,27 @@ func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
|
|||||||
if deps, err := client.ListDeployments(); err == nil {
|
if deps, err := client.ListDeployments(); err == nil {
|
||||||
count := 0
|
count := 0
|
||||||
for _, d := range deps.Items {
|
for _, d := range deps.Items {
|
||||||
if d.Metadata.Namespace == nsName { count++ }
|
if d.Metadata.Namespace == nsName {
|
||||||
|
count++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nd.ResourceCounts["deployments"] = count
|
nd.ResourceCounts["deployments"] = count
|
||||||
}
|
}
|
||||||
if pods, err := client.ListPods(); err == nil {
|
if pods, err := client.ListPods(); err == nil {
|
||||||
count := 0
|
count := 0
|
||||||
for _, p := range pods.Items {
|
for _, p := range pods.Items {
|
||||||
if p.Metadata.Namespace == nsName { count++ }
|
if p.Metadata.Namespace == nsName {
|
||||||
|
count++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nd.ResourceCounts["pods"] = count
|
nd.ResourceCounts["pods"] = count
|
||||||
}
|
}
|
||||||
if svcs, err := client.ListServices(); err == nil {
|
if svcs, err := client.ListServices(); err == nil {
|
||||||
count := 0
|
count := 0
|
||||||
for _, s := range svcs.Items {
|
for _, s := range svcs.Items {
|
||||||
if s.Metadata.Namespace == nsName { count++ }
|
if s.Metadata.Namespace == nsName {
|
||||||
|
count++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nd.ResourceCounts["services"] = count
|
nd.ResourceCounts["services"] = count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,20 +305,20 @@ func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detail := map[string]interface{}{
|
detail := map[string]interface{}{
|
||||||
"name": node.Metadata.Name,
|
"name": node.Metadata.Name,
|
||||||
"role": role,
|
"role": role,
|
||||||
"status": nodeStatus,
|
"status": nodeStatus,
|
||||||
"creation_time": node.Metadata.CreationTimestamp,
|
"creation_time": node.Metadata.CreationTimestamp,
|
||||||
"internal_ip": internalIP,
|
"internal_ip": internalIP,
|
||||||
"external_ip": externalIP,
|
"external_ip": externalIP,
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
"pod_cidr": node.Spec.PodCIDR,
|
"pod_cidr": node.Spec.PodCIDR,
|
||||||
"container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion,
|
"container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion,
|
||||||
"kubelet_version": node.Status.NodeInfo.KubeletVersion,
|
"kubelet_version": node.Status.NodeInfo.KubeletVersion,
|
||||||
"os_image": node.Status.NodeInfo.OSImage,
|
"os_image": node.Status.NodeInfo.OSImage,
|
||||||
"kernel_version": node.Status.NodeInfo.KernelVersion,
|
"kernel_version": node.Status.NodeInfo.KernelVersion,
|
||||||
"architecture": node.Status.NodeInfo.Architecture,
|
"architecture": node.Status.NodeInfo.Architecture,
|
||||||
"operating_system": node.Status.NodeInfo.OperatingSystem,
|
"operating_system": node.Status.NodeInfo.OperatingSystem,
|
||||||
// Capacity & Allocatable
|
// Capacity & Allocatable
|
||||||
"capacity": node.Status.Capacity,
|
"capacity": node.Status.Capacity,
|
||||||
"allocatable": node.Status.Allocatable,
|
"allocatable": node.Status.Allocatable,
|
||||||
@@ -326,18 +326,18 @@ func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
|||||||
"cpu_usage_m": cpuUsageM,
|
"cpu_usage_m": cpuUsageM,
|
||||||
"mem_usage_mi": memUsageMi,
|
"mem_usage_mi": memUsageMi,
|
||||||
// Allocation
|
// Allocation
|
||||||
"pod_count": podCount,
|
"pod_count": podCount,
|
||||||
"cpu_request_m": totalCPURequestM,
|
"cpu_request_m": totalCPURequestM,
|
||||||
"mem_request_mi": totalMemRequestMi,
|
"mem_request_mi": totalMemRequestMi,
|
||||||
"cpu_alloc_pct": cpuAllocPct,
|
"cpu_alloc_pct": cpuAllocPct,
|
||||||
"mem_alloc_pct": memAllocPct,
|
"mem_alloc_pct": memAllocPct,
|
||||||
"pod_alloc_pct": podAllocPct,
|
"pod_alloc_pct": podAllocPct,
|
||||||
"cpu_capacity": capCPU,
|
"cpu_capacity": capCPU,
|
||||||
"cpu_allocatable": allocCPU,
|
"cpu_allocatable": allocCPU,
|
||||||
"mem_capacity_mb": capMem,
|
"mem_capacity_mb": capMem,
|
||||||
"mem_allocatable_mb": allocMem,
|
"mem_allocatable_mb": allocMem,
|
||||||
"pod_capacity": capPods,
|
"pod_capacity": capPods,
|
||||||
"pod_allocatable": allocPods,
|
"pod_allocatable": allocPods,
|
||||||
// Lists
|
// Lists
|
||||||
"conditions": conditions,
|
"conditions": conditions,
|
||||||
"taints": taints,
|
"taints": taints,
|
||||||
@@ -397,20 +397,29 @@ func parseMemToMi(val string) int64 {
|
|||||||
// @Router /kubernetes-clusters/{id}/pods [get]
|
// @Router /kubernetes-clusters/{id}/pods [get]
|
||||||
func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) {
|
func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ns := c.Query("namespace")
|
ns := c.Query("namespace")
|
||||||
deps, err := client.ListDeployments()
|
deps, err := client.ListDeployments()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerResource struct {
|
type ContainerResource struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CPUReq string `json:"cpu_request"`
|
CPUReq string `json:"cpu_request"`
|
||||||
CPULim string `json:"cpu_limit"`
|
CPULim string `json:"cpu_limit"`
|
||||||
MemReq string `json:"mem_request"`
|
MemReq string `json:"mem_request"`
|
||||||
MemLim string `json:"mem_limit"`
|
MemLim string `json:"mem_limit"`
|
||||||
}
|
}
|
||||||
type CapacityRow struct {
|
type CapacityRow struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
@@ -420,7 +429,9 @@ func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []CapacityRow{}
|
result := []CapacityRow{}
|
||||||
for _, d := range deps.Items {
|
for _, d := range deps.Items {
|
||||||
if ns != "" && d.Metadata.Namespace != ns { continue }
|
if ns != "" && d.Metadata.Namespace != ns {
|
||||||
|
continue
|
||||||
|
}
|
||||||
containers := []ContainerResource{}
|
containers := []ContainerResource{}
|
||||||
for _, c := range d.Spec.Template.Spec.Containers {
|
for _, c := range d.Spec.Template.Spec.Containers {
|
||||||
cpuReq := c.Resources.Requests["cpu"]
|
cpuReq := c.Resources.Requests["cpu"]
|
||||||
|
|||||||
+139
-48
@@ -198,8 +198,8 @@ func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
|||||||
|
|
||||||
// Dial K8s API server via WebSocket with channel protocol subprotocol
|
// Dial K8s API server via WebSocket with channel protocol subprotocol
|
||||||
k8sDialer := &websocket.Dialer{
|
k8sDialer := &websocket.Dialer{
|
||||||
TLSClientConfig: client.TLSConfig,
|
TLSClientConfig: client.TLSConfig,
|
||||||
Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "v5.channel.k8s.io"},
|
Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "v5.channel.k8s.io"},
|
||||||
HandshakeTimeout: 10 * time.Second,
|
HandshakeTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
k8sHeader := http.Header{}
|
k8sHeader := http.Header{}
|
||||||
@@ -354,30 +354,46 @@ func (h *KubernetesHandler) ExecInPodNS(c *gin.Context) {
|
|||||||
// Internal helpers that resolve clusterCode and inject namespace
|
// Internal helpers that resolve clusterCode and inject namespace
|
||||||
func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
pods, err := client.ListPods()
|
pods, err := client.ListPods()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
// M2: inject pod metrics (best-effort) // Logan.Li
|
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||||
podMetrics := buildPodMetricsMap(client, ns)
|
podMetrics := buildPodMetricsMap(client, ns)
|
||||||
type PodSummary struct {
|
type PodSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
NodeName string `json:"node_name"`
|
NodeName string `json:"node_name"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
RestartCount int `json:"restart_count"`
|
RestartCount int `json:"restart_count"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
CPUUsageM string `json:"cpu_usage_m,omitempty"`
|
CPUUsageM string `json:"cpu_usage_m,omitempty"`
|
||||||
MemUsageMi string `json:"mem_usage_mi,omitempty"`
|
MemUsageMi string `json:"mem_usage_mi,omitempty"`
|
||||||
Conditions []k8sclient.PodCondition `json:"conditions,omitempty"`
|
Conditions []k8sclient.PodCondition `json:"conditions,omitempty"`
|
||||||
}
|
}
|
||||||
result := []PodSummary{}
|
result := []PodSummary{}
|
||||||
for _, p := range pods.Items {
|
for _, p := range pods.Items {
|
||||||
if p.Metadata.Namespace != ns { continue }
|
if p.Metadata.Namespace != ns {
|
||||||
image := ""; if len(p.Spec.Containers) > 0 { image = p.Spec.Containers[0].Image }
|
continue
|
||||||
restarts := 0; if len(p.Status.ContainerStatuses) > 0 { restarts = p.Status.ContainerStatuses[0].RestartCount }
|
}
|
||||||
|
image := ""
|
||||||
|
if len(p.Spec.Containers) > 0 {
|
||||||
|
image = p.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
|
restarts := 0
|
||||||
|
if len(p.Status.ContainerStatuses) > 0 {
|
||||||
|
restarts = p.Status.ContainerStatuses[0].RestartCount
|
||||||
|
}
|
||||||
ps := PodSummary{
|
ps := PodSummary{
|
||||||
Namespace: p.Metadata.Namespace,
|
Namespace: p.Metadata.Namespace,
|
||||||
Name: p.Metadata.Name,
|
Name: p.Metadata.Name,
|
||||||
@@ -398,7 +414,9 @@ func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
podName := c.Param("podName")
|
podName := c.Param("podName")
|
||||||
stream := c.Query("stream") == "true"
|
stream := c.Query("stream") == "true"
|
||||||
// 日志增强参数:container / sinceTime / previous / tailLines(兼容 tail) // Logan.Li
|
// 日志增强参数:container / sinceTime / previous / tailLines(兼容 tail) // Logan.Li
|
||||||
@@ -416,34 +434,61 @@ func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
|||||||
Previous: c.Query("previous") == "true",
|
Previous: c.Query("previous") == "true",
|
||||||
}
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
if stream {
|
if stream {
|
||||||
body, err := client.GetPodLogsStreamWithOptions(ns, podName, opts)
|
body, err := client.GetPodLogsStreamWithOptions(ns, podName, opts)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取日志流失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "获取日志流失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
defer body.Close()
|
defer body.Close()
|
||||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||||
c.Header("Cache-Control", "no-cache")
|
c.Header("Cache-Control", "no-cache")
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
buf := make([]byte, 4096); n, err := body.Read(buf)
|
buf := make([]byte, 4096)
|
||||||
if n > 0 { w.Write(buf[:n]); return true }
|
n, err := body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
w.Write(buf[:n])
|
||||||
|
return true
|
||||||
|
}
|
||||||
return err == nil
|
return err == nil
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logs, err := client.GetPodLogsWithOptions(ns, podName, opts)
|
logs, err := client.GetPodLogsWithOptions(ns, podName, opts)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取日志失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "获取日志失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}})
|
c.JSON(200, model.Response{Code: 200, Message: "success", Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
podName := c.Param("podName")
|
podName := c.Param("podName")
|
||||||
var req struct { Command []string `json:"command"`; Container string `json:"container"` }
|
var req struct {
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
Command []string `json:"command"`
|
||||||
|
Container string `json:"container"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
result, err := client.ExecInPod(ns, podName, req.Container, req.Command)
|
result, err := client.ExecInPod(ns, podName, req.Container, req.Command)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "执行失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "执行失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,11 +499,20 @@ func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) {
|
|||||||
// ListDeploymentsAllNS lists all deployments across all namespaces using clusterCode.
|
// ListDeploymentsAllNS lists all deployments across all namespaces using clusterCode.
|
||||||
func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) {
|
func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
pods, err := client.ListPods()
|
pods, err := client.ListPods()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
// M2: inject pod metrics (best-effort) // Logan.Li
|
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||||
podMetrics := buildPodMetricsMap(client, "")
|
podMetrics := buildPodMetricsMap(client, "")
|
||||||
type PodSummary struct {
|
type PodSummary struct {
|
||||||
@@ -474,8 +528,14 @@ func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []PodSummary{}
|
result := []PodSummary{}
|
||||||
for _, p := range pods.Items {
|
for _, p := range pods.Items {
|
||||||
image := ""; if len(p.Spec.Containers) > 0 { image = p.Spec.Containers[0].Image }
|
image := ""
|
||||||
restarts := 0; if len(p.Status.ContainerStatuses) > 0 { restarts = p.Status.ContainerStatuses[0].RestartCount }
|
if len(p.Spec.Containers) > 0 {
|
||||||
|
image = p.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
|
restarts := 0
|
||||||
|
if len(p.Status.ContainerStatuses) > 0 {
|
||||||
|
restarts = p.Status.ContainerStatuses[0].RestartCount
|
||||||
|
}
|
||||||
ps := PodSummary{
|
ps := PodSummary{
|
||||||
Namespace: p.Metadata.Namespace,
|
Namespace: p.Metadata.Namespace,
|
||||||
Name: p.Metadata.Name,
|
Name: p.Metadata.Name,
|
||||||
@@ -559,58 +619,80 @@ func parseMemResource(val string) int64 {
|
|||||||
|
|
||||||
// formatCpuFromNano converts K8s metrics CPU (nanocores "80410173n") to millicores "80m"
|
// formatCpuFromNano converts K8s metrics CPU (nanocores "80410173n") to millicores "80m"
|
||||||
func formatCpuFromNano(raw string) string {
|
func formatCpuFromNano(raw string) string {
|
||||||
if raw == "" { return "" }
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
val := raw
|
val := raw
|
||||||
val = strings.TrimSuffix(val, "n")
|
val = strings.TrimSuffix(val, "n")
|
||||||
var nano int64
|
var nano int64
|
||||||
fmt.Sscanf(val, "%d", &nano)
|
fmt.Sscanf(val, "%d", &nano)
|
||||||
milli := nano / 1000000
|
milli := nano / 1000000
|
||||||
if milli < 1 { milli = 1 }
|
if milli < 1 {
|
||||||
|
milli = 1
|
||||||
|
}
|
||||||
return fmt.Sprintf("%dm", milli)
|
return fmt.Sprintf("%dm", milli)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCpuNanoToMilli converts K8s metrics CPU (nanocores "100354992n") to millicores (int) // Logan.Li
|
// parseCpuNanoToMilli converts K8s metrics CPU (nanocores "100354992n") to millicores (int) // Logan.Li
|
||||||
func parseCpuNanoToMilli(raw string) int {
|
func parseCpuNanoToMilli(raw string) int {
|
||||||
if raw == "" { return 0 }
|
if raw == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
val := strings.TrimSuffix(raw, "n")
|
val := strings.TrimSuffix(raw, "n")
|
||||||
var nano int64
|
var nano int64
|
||||||
fmt.Sscanf(val, "%d", &nano)
|
fmt.Sscanf(val, "%d", &nano)
|
||||||
milli := nano / 1000000
|
milli := nano / 1000000
|
||||||
if milli < 1 { milli = 1 }
|
if milli < 1 {
|
||||||
|
milli = 1
|
||||||
|
}
|
||||||
return int(milli)
|
return int(milli)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatMemFromKi converts K8s metrics memory (Ki "2154292Ki") to Mi "2105Mi"
|
// formatMemFromKi converts K8s metrics memory (Ki "2154292Ki") to Mi "2105Mi"
|
||||||
func formatMemFromKi(raw string) string {
|
func formatMemFromKi(raw string) string {
|
||||||
if raw == "" { return "" }
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
val := strings.TrimSuffix(raw, "Ki")
|
val := strings.TrimSuffix(raw, "Ki")
|
||||||
var ki int64
|
var ki int64
|
||||||
fmt.Sscanf(val, "%d", &ki)
|
fmt.Sscanf(val, "%d", &ki)
|
||||||
mi := ki / 1024
|
mi := ki / 1024
|
||||||
if mi < 1 { mi = 1 }
|
if mi < 1 {
|
||||||
|
mi = 1
|
||||||
|
}
|
||||||
return fmt.Sprintf("%dMi", mi)
|
return fmt.Sprintf("%dMi", mi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseMemKiToMi converts K8s metrics memory (Ki "2154292Ki") to Mi (int) // Logan.Li
|
// parseMemKiToMi converts K8s metrics memory (Ki "2154292Ki") to Mi (int) // Logan.Li
|
||||||
func parseMemKiToMi(raw string) int {
|
func parseMemKiToMi(raw string) int {
|
||||||
if raw == "" { return 0 }
|
if raw == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
val := strings.TrimSuffix(raw, "Ki")
|
val := strings.TrimSuffix(raw, "Ki")
|
||||||
var ki int64
|
var ki int64
|
||||||
fmt.Sscanf(val, "%d", &ki)
|
fmt.Sscanf(val, "%d", &ki)
|
||||||
return int(ki / 1024)
|
return int(ki / 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GetResourceYAMLNS returns the YAML of a K8s resource
|
// GetResourceYAMLNS returns the YAML of a K8s resource
|
||||||
func (h *KubernetesHandler) GetPodMetrics(c *gin.Context) {
|
func (h *KubernetesHandler) GetPodMetrics(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ns := c.Query("namespace")
|
ns := c.Query("namespace")
|
||||||
metrics, err := client.ListPodMetrics(ns)
|
metrics, err := client.ListPodMetrics(ns)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "metrics API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "metrics API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerMetric struct {
|
type ContainerMetric struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -643,13 +725,22 @@ func (h *KubernetesHandler) DeletePodNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) deletePodWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) deletePodWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
podName := c.Param("podName")
|
podName := c.Param("podName")
|
||||||
if podName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 podName"}); return }
|
if podName == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "缺少 podName"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := client.DeletePod(ns, podName); err != nil {
|
if err := client.DeletePod(ns, podName); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("Pod %s 已删除", podName)})
|
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("Pod %s 已删除", podName)})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,33 @@ func (h *KubernetesHandler) ListPodFilesNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
podName := c.Param("podName")
|
podName := c.Param("podName")
|
||||||
var req struct {
|
var req struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Container string `json:"container"`
|
Container string `json:"container"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
if req.Path == "" { req.Path = "/" }
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Path == "" {
|
||||||
|
req.Path = "/"
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
// ls -la — BusyBox ignores --time-style, use default "Mon DD HH:MM" format
|
// ls -la — BusyBox ignores --time-style, use default "Mon DD HH:MM" format
|
||||||
cmd := []string{"ls", "-la", req.Path}
|
cmd := []string{"ls", "-la", req.Path}
|
||||||
result, err := client.ExecInPod(ns, podName, req.Container, cmd)
|
result, err := client.ExecInPod(ns, podName, req.Container, cmd)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -45,11 +58,17 @@ func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
|
|||||||
output := result.Stdout // WebSocket output is already clean text
|
output := result.Stdout // WebSocket output is already clean text
|
||||||
for _, line := range strings.Split(output, "\n") {
|
for _, line := range strings.Split(output, "\n") {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
if line == "" || strings.HasPrefix(line, "total ") { continue }
|
if line == "" || strings.HasPrefix(line, "total ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) < 7 { continue }
|
if len(fields) < 7 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
mode := fields[0]
|
mode := fields[0]
|
||||||
if len(mode) == 0 || (mode[0] != 'd' && mode[0] != '-' && mode[0] != 'l') { continue }
|
if len(mode) == 0 || (mode[0] != 'd' && mode[0] != '-' && mode[0] != 'l') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
size := fields[4]
|
size := fields[4]
|
||||||
// BusyBox ls date format: "Jun 23 02:17" (3 fields: month day time)
|
// BusyBox ls date format: "Jun 23 02:17" (3 fields: month day time)
|
||||||
// GNU ls --time-style: "2026-06-23T02:17:00" (1 field)
|
// GNU ls --time-style: "2026-06-23T02:17:00" (1 field)
|
||||||
@@ -64,7 +83,9 @@ func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
|
|||||||
modTime = strings.Join(fields[5:8], " ")
|
modTime = strings.Join(fields[5:8], " ")
|
||||||
name = strings.Join(fields[8:], " ")
|
name = strings.Join(fields[8:], " ")
|
||||||
}
|
}
|
||||||
if name == "" || name == "." || name == ".." { continue }
|
if name == "" || name == "." || name == ".." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
files = append(files, FileInfo{
|
files = append(files, FileInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
Size: size,
|
Size: size,
|
||||||
@@ -82,16 +103,27 @@ func (h *KubernetesHandler) DownloadPodFileNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) downloadPodFileWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) downloadPodFileWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
podName := c.Param("podName")
|
podName := c.Param("podName")
|
||||||
filePath := c.Query("path")
|
filePath := c.Query("path")
|
||||||
container := c.Query("container")
|
container := c.Query("container")
|
||||||
if filePath == "" { c.JSON(400, model.Response{Code: 400, Message: "path 参数不能为空"}); return }
|
if filePath == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "path 参数不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
cmd := []string{"cat", filePath}
|
cmd := []string{"cat", filePath}
|
||||||
result, err := client.ExecInPod(ns, podName, container, cmd)
|
result, err := client.ExecInPod(ns, podName, container, cmd)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
content := result.Stdout
|
content := result.Stdout
|
||||||
fileName := path.Base(filePath)
|
fileName := path.Base(filePath)
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ func (h *KubernetesHandler) SaveTopology(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功"})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// SyncCluster 同步集群资源(连接真实K8s API拉取数据)
|
// SyncCluster 同步集群资源(连接真实K8s API拉取数据)
|
||||||
func (h *KubernetesHandler) ListConfigMaps(c *gin.Context) {
|
func (h *KubernetesHandler) ListConfigMaps(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
@@ -313,11 +312,19 @@ func (h *KubernetesHandler) ListIngressesNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
svcs, err := client.ListServices()
|
svcs, err := client.ListServices()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type SvcPortDetail struct {
|
type SvcPortDetail struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
@@ -326,17 +333,19 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
|||||||
Protocol string `json:"protocol,omitempty"`
|
Protocol string `json:"protocol,omitempty"`
|
||||||
}
|
}
|
||||||
type SvcSummary struct {
|
type SvcSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
ClusterIP string `json:"cluster_ip"`
|
ClusterIP string `json:"cluster_ip"`
|
||||||
Ports int `json:"ports"`
|
Ports int `json:"ports"`
|
||||||
PortList []SvcPortDetail `json:"portList,omitempty"`
|
PortList []SvcPortDetail `json:"portList,omitempty"`
|
||||||
Selector map[string]string `json:"selector,omitempty"`
|
Selector map[string]string `json:"selector,omitempty"`
|
||||||
}
|
}
|
||||||
result := []SvcSummary{}
|
result := []SvcSummary{}
|
||||||
for _, s := range svcs.Items {
|
for _, s := range svcs.Items {
|
||||||
if s.Metadata.Namespace != ns { continue }
|
if s.Metadata.Namespace != ns {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ports := []SvcPortDetail{}
|
ports := []SvcPortDetail{}
|
||||||
for _, p := range s.Spec.Ports {
|
for _, p := range s.Spec.Ports {
|
||||||
ports = append(ports, SvcPortDetail{
|
ports = append(ports, SvcPortDetail{
|
||||||
@@ -345,28 +354,36 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
result = append(result, SvcSummary{
|
result = append(result, SvcSummary{
|
||||||
Namespace: s.Metadata.Namespace,
|
Namespace: s.Metadata.Namespace,
|
||||||
Name: s.Metadata.Name,
|
Name: s.Metadata.Name,
|
||||||
Type: s.Spec.Type,
|
Type: s.Spec.Type,
|
||||||
ClusterIP: s.Spec.ClusterIP,
|
ClusterIP: s.Spec.ClusterIP,
|
||||||
Ports: len(s.Spec.Ports),
|
Ports: len(s.Spec.Ports),
|
||||||
PortList: ports,
|
PortList: ports,
|
||||||
Selector: s.Spec.Selector,
|
Selector: s.Spec.Selector,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ings, err := client.ListIngresses()
|
ings, err := client.ListIngresses()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type PathDetail struct {
|
type PathDetail struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
PathType string `json:"pathType,omitempty"`
|
PathType string `json:"pathType,omitempty"`
|
||||||
BackendSvc string `json:"backendService,omitempty"`
|
BackendSvc string `json:"backendService,omitempty"`
|
||||||
BackendPort string `json:"backendPort,omitempty"`
|
BackendPort string `json:"backendPort,omitempty"`
|
||||||
}
|
}
|
||||||
type RuleDetail struct {
|
type RuleDetail struct {
|
||||||
@@ -374,20 +391,24 @@ func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) {
|
|||||||
Paths []PathDetail `json:"paths"`
|
Paths []PathDetail `json:"paths"`
|
||||||
}
|
}
|
||||||
type IngSummary struct {
|
type IngSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Hosts []string `json:"hosts"`
|
Hosts []string `json:"hosts"`
|
||||||
TLS bool `json:"tls"`
|
TLS bool `json:"tls"`
|
||||||
IngressClass string `json:"ingressClass,omitempty"`
|
IngressClass string `json:"ingressClass,omitempty"`
|
||||||
Rules []RuleDetail `json:"rules,omitempty"`
|
Rules []RuleDetail `json:"rules,omitempty"`
|
||||||
}
|
}
|
||||||
result := []IngSummary{}
|
result := []IngSummary{}
|
||||||
for _, ing := range ings.Items {
|
for _, ing := range ings.Items {
|
||||||
if ing.Metadata.Namespace != ns { continue }
|
if ing.Metadata.Namespace != ns {
|
||||||
|
continue
|
||||||
|
}
|
||||||
hosts := []string{}
|
hosts := []string{}
|
||||||
rules := []RuleDetail{}
|
rules := []RuleDetail{}
|
||||||
for _, rule := range ing.Spec.Rules {
|
for _, rule := range ing.Spec.Rules {
|
||||||
if rule.Host != "" { hosts = append(hosts, rule.Host) }
|
if rule.Host != "" {
|
||||||
|
hosts = append(hosts, rule.Host)
|
||||||
|
}
|
||||||
rd := RuleDetail{Host: rule.Host, Paths: []PathDetail{}}
|
rd := RuleDetail{Host: rule.Host, Paths: []PathDetail{}}
|
||||||
if rule.HTTP != nil {
|
if rule.HTTP != nil {
|
||||||
for _, p := range rule.HTTP.Paths {
|
for _, p := range rule.HTTP.Paths {
|
||||||
@@ -453,12 +474,21 @@ func (h *KubernetesHandler) ListIngressClasses(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
pvcs, err := client.ListPVCs(ns)
|
pvcs, err := client.ListPVCs(ns)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type PVCSummary struct {
|
type PVCSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -470,14 +500,16 @@ func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
|||||||
for _, p := range pvcs.Items {
|
for _, p := range pvcs.Items {
|
||||||
size := ""
|
size := ""
|
||||||
if p.Status.Capacity != nil {
|
if p.Status.Capacity != nil {
|
||||||
if s, ok := p.Status.Capacity["storage"]; ok { size = s }
|
if s, ok := p.Status.Capacity["storage"]; ok {
|
||||||
|
size = s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = append(result, PVCSummary{
|
result = append(result, PVCSummary{
|
||||||
Namespace: p.Metadata.Namespace,
|
Namespace: p.Metadata.Namespace,
|
||||||
Name: p.Metadata.Name,
|
Name: p.Metadata.Name,
|
||||||
Phase: p.Status.Phase,
|
Phase: p.Status.Phase,
|
||||||
Size: size,
|
Size: size,
|
||||||
StorageClass: p.Spec.StorageClassName,
|
StorageClass: p.Spec.StorageClassName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||||
@@ -486,11 +518,18 @@ func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
|||||||
// DeletePVCNS deletes a PVC (code-based)
|
// DeletePVCNS deletes a PVC (code-based)
|
||||||
func (h *KubernetesHandler) DeletePVCNS(c *gin.Context) {
|
func (h *KubernetesHandler) DeletePVCNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := client.DeletePVC(c.Param("namespace"), c.Param("name")); err != nil {
|
if err := client.DeletePVC(c.Param("namespace"), c.Param("name")); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,15 +208,25 @@ func (h *KubernetesHandler) GetDeploymentNS(c *gin.Context) {
|
|||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
deps, err := client.ListDeployments()
|
deps, err := client.ListDeployments()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, d := range deps.Items {
|
for _, d := range deps.Items {
|
||||||
if d.Metadata.Namespace == ns && d.Metadata.Name == name {
|
if d.Metadata.Namespace == ns && d.Metadata.Name == name {
|
||||||
image := ""
|
image := ""
|
||||||
if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
if len(d.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = d.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{
|
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{
|
||||||
"namespace": d.Metadata.Namespace,
|
"namespace": d.Metadata.Namespace,
|
||||||
"name": d.Metadata.Name,
|
"name": d.Metadata.Name,
|
||||||
@@ -248,11 +258,19 @@ func (h *KubernetesHandler) ResumeDeploymentNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
deps, err := client.ListDeployments()
|
deps, err := client.ListDeployments()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type DepSummary struct {
|
type DepSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -265,8 +283,13 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
result := []DepSummary{}
|
result := []DepSummary{}
|
||||||
for _, d := range deps.Items {
|
for _, d := range deps.Items {
|
||||||
if d.Metadata.Namespace != ns { continue }
|
if d.Metadata.Namespace != ns {
|
||||||
image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
continue
|
||||||
|
}
|
||||||
|
image := ""
|
||||||
|
if len(d.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = d.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
result = append(result, DepSummary{
|
result = append(result, DepSummary{
|
||||||
Namespace: d.Metadata.Namespace,
|
Namespace: d.Metadata.Namespace,
|
||||||
Name: d.Metadata.Name,
|
Name: d.Metadata.Name,
|
||||||
@@ -275,63 +298,131 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
|||||||
ReadyReplicas: d.Status.ReadyReplicas,
|
ReadyReplicas: d.Status.ReadyReplicas,
|
||||||
Image: image,
|
Image: image,
|
||||||
Paused: d.Spec.Paused,
|
Paused: d.Spec.Paused,
|
||||||
Selector: func() map[string]string { if d.Spec.Selector != nil { return d.Spec.Selector.MatchLabels }; return nil }(),
|
Selector: func() map[string]string {
|
||||||
|
if d.Spec.Selector != nil {
|
||||||
|
return d.Spec.Selector.MatchLabels
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) scaleDeploymentWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) scaleDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
var req struct { Replicas int `json:"replicas"` }
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
}
|
||||||
|
var req struct {
|
||||||
|
Replicas int `json:"replicas"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) restartDeploymentWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) restartDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.RestartDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.RestartDeployment(ns, c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "重启指令已下发"})
|
c.JSON(200, model.Response{Code: 200, Message: "重启指令已下发"})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) updateDeploymentImageWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) updateDeploymentImageWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
var req struct { Image string `json:"image"`; ContainerName string `json:"container_name"` }
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
}
|
||||||
if strings.TrimSpace(req.Image) == "" { c.JSON(400, model.Response{Code: 400, Message: "image 不能为空"}); return }
|
var req struct {
|
||||||
|
Image string `json:"image"`
|
||||||
|
ContainerName string `json:"container_name"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.Image) == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "image 不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)})
|
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) pauseDeploymentWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) pauseDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.PauseDeployment(ns, c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "已暂停滚动更新"})
|
c.JSON(200, model.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) resumeDeploymentWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) resumeDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return }
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "已恢复滚动更新"})
|
c.JSON(200, model.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||||
}
|
}
|
||||||
func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
deps, err := client.ListDeployments()
|
deps, err := client.ListDeployments()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type DepSummary struct {
|
type DepSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -342,7 +433,10 @@ func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []DepSummary{}
|
result := []DepSummary{}
|
||||||
for _, d := range deps.Items {
|
for _, d := range deps.Items {
|
||||||
image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
image := ""
|
||||||
|
if len(d.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = d.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
result = append(result, DepSummary{
|
result = append(result, DepSummary{
|
||||||
Namespace: d.Metadata.Namespace,
|
Namespace: d.Metadata.Namespace,
|
||||||
Name: d.Metadata.Name,
|
Name: d.Metadata.Name,
|
||||||
@@ -358,11 +452,20 @@ func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
|||||||
// ListPodsAllNS lists all pods across all namespaces using clusterCode.
|
// ListPodsAllNS lists all pods across all namespaces using clusterCode.
|
||||||
func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
sts, err := client.ListStatefulSets()
|
sts, err := client.ListStatefulSets()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
type StsSummary struct {
|
type StsSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
@@ -373,8 +476,13 @@ func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []StsSummary{}
|
result := []StsSummary{}
|
||||||
for _, s := range sts.Items {
|
for _, s := range sts.Items {
|
||||||
if s.Metadata.Namespace != ns { continue }
|
if s.Metadata.Namespace != ns {
|
||||||
image := ""; if len(s.Spec.Template.Spec.Containers) > 0 { image = s.Spec.Template.Spec.Containers[0].Image }
|
continue
|
||||||
|
}
|
||||||
|
image := ""
|
||||||
|
if len(s.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = s.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
result = append(result, StsSummary{
|
result = append(result, StsSummary{
|
||||||
Namespace: s.Metadata.Namespace,
|
Namespace: s.Metadata.Namespace,
|
||||||
Name: s.Metadata.Name,
|
Name: s.Metadata.Name,
|
||||||
@@ -389,11 +497,20 @@ func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
|||||||
// ListDaemonSetsNS lists DaemonSets in a namespace
|
// ListDaemonSetsNS lists DaemonSets in a namespace
|
||||||
func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ds, err := client.ListDaemonSets()
|
ds, err := client.ListDaemonSets()
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
type DsSummary struct {
|
type DsSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
@@ -405,8 +522,13 @@ func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
result := []DsSummary{}
|
result := []DsSummary{}
|
||||||
for _, d := range ds.Items {
|
for _, d := range ds.Items {
|
||||||
if d.Metadata.Namespace != ns { continue }
|
if d.Metadata.Namespace != ns {
|
||||||
image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
continue
|
||||||
|
}
|
||||||
|
image := ""
|
||||||
|
if len(d.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = d.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
result = append(result, DsSummary{
|
result = append(result, DsSummary{
|
||||||
Namespace: d.Metadata.Namespace,
|
Namespace: d.Metadata.Namespace,
|
||||||
Name: d.Metadata.Name,
|
Name: d.Metadata.Name,
|
||||||
@@ -418,18 +540,31 @@ func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScaleStatefulSetNS scales a StatefulSet (Kuboard code route)
|
// ScaleStatefulSetNS scales a StatefulSet (Kuboard code route)
|
||||||
func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) {
|
func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
var req struct{ Replicas int `json:"replicas"` }
|
var req struct {
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
Replicas int `json:"replicas"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||||
}
|
}
|
||||||
@@ -437,16 +572,30 @@ func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) {
|
|||||||
// ScaleStatefulSet scales a StatefulSet (ID route)
|
// ScaleStatefulSet scales a StatefulSet (ID route)
|
||||||
func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) {
|
func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
var req struct{ Replicas int `json:"replicas"` }
|
var req struct {
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
Replicas int `json:"replicas"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||||
}
|
}
|
||||||
@@ -454,13 +603,20 @@ func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) {
|
|||||||
// RestartStatefulSetNS restarts a StatefulSet (Kuboard code route)
|
// RestartStatefulSetNS restarts a StatefulSet (Kuboard code route)
|
||||||
func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) {
|
func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
if err := client.RestartStatefulSet(ns, name); err != nil {
|
if err := client.RestartStatefulSet(ns, name); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||||
}
|
}
|
||||||
@@ -468,14 +624,23 @@ func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) {
|
|||||||
// RestartStatefulSet restarts a StatefulSet (ID route)
|
// RestartStatefulSet restarts a StatefulSet (ID route)
|
||||||
func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) {
|
func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
if err := client.RestartStatefulSet(ns, name); err != nil {
|
if err := client.RestartStatefulSet(ns, name); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||||
}
|
}
|
||||||
@@ -483,13 +648,20 @@ func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) {
|
|||||||
// RestartDaemonSetNS restarts a DaemonSet (Kuboard code route)
|
// RestartDaemonSetNS restarts a DaemonSet (Kuboard code route)
|
||||||
func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) {
|
func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
if err := client.RestartDaemonSet(ns, name); err != nil {
|
if err := client.RestartDaemonSet(ns, name); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||||
}
|
}
|
||||||
@@ -497,14 +669,23 @@ func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) {
|
|||||||
// RestartDaemonSet restarts a DaemonSet (ID route)
|
// RestartDaemonSet restarts a DaemonSet (ID route)
|
||||||
func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) {
|
func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
if err := client.RestartDaemonSet(ns, name); err != nil {
|
if err := client.RestartDaemonSet(ns, name); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||||
}
|
}
|
||||||
@@ -512,26 +693,44 @@ func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) {
|
|||||||
// GetStatefulSetDetailNS returns a single StatefulSet detail (Kuboard code route)
|
// GetStatefulSetDetailNS returns a single StatefulSet detail (Kuboard code route)
|
||||||
func (h *KubernetesHandler) GetStatefulSetDetailNS(c *gin.Context) {
|
func (h *KubernetesHandler) GetStatefulSetDetailNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
sts, err := client.GetStatefulSet(ns, name)
|
sts, err := client.GetStatefulSet(ns, name)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: sts})
|
c.JSON(200, model.Response{Code: 200, Data: sts})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDaemonSetDetailNS returns a single DaemonSet detail (Kuboard code route)
|
// GetDaemonSetDetailNS returns a single DaemonSet detail (Kuboard code route)
|
||||||
func (h *KubernetesHandler) GetDaemonSetDetailNS(c *gin.Context) {
|
func (h *KubernetesHandler) GetDaemonSetDetailNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
ds, err := client.GetDaemonSet(ns, name)
|
ds, err := client.GetDaemonSet(ns, name)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Data: ds})
|
c.JSON(200, model.Response{Code: 200, Data: ds})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,11 +744,19 @@ func (h *KubernetesHandler) ListReplicaSetsNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
items, err := client.ListReplicaSetsRaw(ns)
|
items, err := client.ListReplicaSetsRaw(ns)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type RSSummary struct {
|
type RSSummary struct {
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -564,11 +771,16 @@ func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) {
|
|||||||
result := []RSSummary{}
|
result := []RSSummary{}
|
||||||
for _, rs := range items {
|
for _, rs := range items {
|
||||||
image := ""
|
image := ""
|
||||||
if len(rs.Spec.Template.Spec.Containers) > 0 { image = rs.Spec.Template.Spec.Containers[0].Image }
|
if len(rs.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = rs.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
revision := rs.Metadata.Annotations["deployment.kubernetes.io/revision"]
|
revision := rs.Metadata.Annotations["deployment.kubernetes.io/revision"]
|
||||||
owner := ""
|
owner := ""
|
||||||
for _, o := range rs.Metadata.OwnerReferences {
|
for _, o := range rs.Metadata.OwnerReferences {
|
||||||
if o.Kind == "Deployment" { owner = o.Name; break }
|
if o.Kind == "Deployment" {
|
||||||
|
owner = o.Name
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = append(result, RSSummary{
|
result = append(result, RSSummary{
|
||||||
Namespace: rs.Metadata.Namespace,
|
Namespace: rs.Metadata.Namespace,
|
||||||
@@ -591,13 +803,24 @@ func (h *KubernetesHandler) ListDeploymentRevisionsNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
deploymentName := c.Param("name")
|
deploymentName := c.Param("name")
|
||||||
if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
if deploymentName == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
items, err := client.ListReplicaSetsRaw(ns)
|
items, err := client.ListReplicaSetsRaw(ns)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
type Revision struct {
|
type Revision struct {
|
||||||
Revision string `json:"revision"`
|
Revision string `json:"revision"`
|
||||||
ReplicaSet string `json:"replica_set"`
|
ReplicaSet string `json:"replica_set"`
|
||||||
@@ -610,11 +833,18 @@ func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns str
|
|||||||
for _, rs := range items {
|
for _, rs := range items {
|
||||||
isOwner := false
|
isOwner := false
|
||||||
for _, o := range rs.Metadata.OwnerReferences {
|
for _, o := range rs.Metadata.OwnerReferences {
|
||||||
if o.Kind == "Deployment" && o.Name == deploymentName { isOwner = true; break }
|
if o.Kind == "Deployment" && o.Name == deploymentName {
|
||||||
|
isOwner = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isOwner {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if !isOwner { continue }
|
|
||||||
image := ""
|
image := ""
|
||||||
if len(rs.Spec.Template.Spec.Containers) > 0 { image = rs.Spec.Template.Spec.Containers[0].Image }
|
if len(rs.Spec.Template.Spec.Containers) > 0 {
|
||||||
|
image = rs.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
result = append(result, Revision{
|
result = append(result, Revision{
|
||||||
Revision: rs.Metadata.Annotations["deployment.kubernetes.io/revision"],
|
Revision: rs.Metadata.Annotations["deployment.kubernetes.io/revision"],
|
||||||
ReplicaSet: rs.Metadata.Name,
|
ReplicaSet: rs.Metadata.Name,
|
||||||
@@ -639,28 +869,44 @@ func (h *KubernetesHandler) RollbackDeploymentNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
deploymentName := c.Param("name")
|
deploymentName := c.Param("name")
|
||||||
if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
if deploymentName == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
var req struct {
|
var req struct {
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
ReplicaSet string `json:"replicaSet"`
|
ReplicaSet string `json:"replicaSet"`
|
||||||
}
|
}
|
||||||
_ = c.ShouldBindJSON(&req) // 允许空 body(但下面会校验)
|
_ = c.ShouldBindJSON(&req) // 允许空 body(但下面会校验)
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
targetRS := req.ReplicaSet
|
targetRS := req.ReplicaSet
|
||||||
// 通过 revision 定位 ReplicaSet
|
// 通过 revision 定位 ReplicaSet
|
||||||
if targetRS == "" && req.Revision > 0 {
|
if targetRS == "" && req.Revision > 0 {
|
||||||
items, err := client.ListReplicaSetsRaw(ns)
|
items, err := client.ListReplicaSetsRaw(ns)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, rs := range items {
|
for _, rs := range items {
|
||||||
isOwner := false
|
isOwner := false
|
||||||
for _, o := range rs.Metadata.OwnerReferences {
|
for _, o := range rs.Metadata.OwnerReferences {
|
||||||
if o.Kind == "Deployment" && o.Name == deploymentName { isOwner = true; break }
|
if o.Kind == "Deployment" && o.Name == deploymentName {
|
||||||
|
isOwner = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isOwner {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if !isOwner { continue }
|
|
||||||
if rs.Metadata.Annotations["deployment.kubernetes.io/revision"] == strconv.Itoa(req.Revision) {
|
if rs.Metadata.Annotations["deployment.kubernetes.io/revision"] == strconv.Itoa(req.Revision) {
|
||||||
targetRS = rs.Metadata.Name
|
targetRS = rs.Metadata.Name
|
||||||
break
|
break
|
||||||
@@ -671,18 +917,31 @@ func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if targetRS == "" { c.JSON(400, model.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"}); return }
|
if targetRS == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 取 ReplicaSet 的 template,patch 到 Deployment.spec.template 实现回滚
|
// 取 ReplicaSet 的 template,patch 到 Deployment.spec.template 实现回滚
|
||||||
rsJSON, err := client.GetReplicaSet(ns, targetRS)
|
rsJSON, err := client.GetReplicaSet(ns, targetRS)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
specMap, ok := rsJSON["spec"].(map[string]interface{})
|
specMap, ok := rsJSON["spec"].(map[string]interface{})
|
||||||
if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}); return }
|
if !ok {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 spec"})
|
||||||
|
return
|
||||||
|
}
|
||||||
template, ok := specMap["template"].(map[string]interface{})
|
template, ok := specMap["template"].(map[string]interface{})
|
||||||
if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 template"}); return }
|
if !ok {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 template"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := client.PatchDeploymentSpecTemplate(ns, deploymentName, template); err != nil {
|
if err := client.PatchDeploymentSpecTemplate(ns, deploymentName, template); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "回滚失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "回滚失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{
|
c.JSON(200, model.Response{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ import (
|
|||||||
|
|
||||||
func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
resource := c.Param("resource")
|
resource := c.Param("resource")
|
||||||
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
||||||
@@ -62,10 +68,14 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
|||||||
case "rolebindings", "rolebinding":
|
case "rolebindings", "rolebinding":
|
||||||
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
||||||
default:
|
default:
|
||||||
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
yamlBytes, err := client.GetResourceYAML(apiPath)
|
yamlBytes, err := client.GetResourceYAML(apiPath)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "获取YAML失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Header("Content-Type", "application/yaml")
|
c.Header("Content-Type", "application/yaml")
|
||||||
c.String(200, string(yamlBytes))
|
c.String(200, string(yamlBytes))
|
||||||
}
|
}
|
||||||
@@ -73,9 +83,15 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
|||||||
// UpdateResourceYAMLNS applies YAML to update a K8s resource
|
// UpdateResourceYAMLNS applies YAML to update a K8s resource
|
||||||
func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
resource := c.Param("resource")
|
resource := c.Param("resource")
|
||||||
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
||||||
@@ -90,7 +106,10 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
yamlData, err := io.ReadAll(c.Request.Body)
|
yamlData, err := io.ReadAll(c.Request.Body)
|
||||||
if err != nil { c.JSON(400, model.Response{Code: 400, Message: "读取body失败"}); return }
|
if err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "读取body失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
var apiPath string
|
var apiPath string
|
||||||
switch resource {
|
switch resource {
|
||||||
case "deployments", "deployment":
|
case "deployments", "deployment":
|
||||||
@@ -124,10 +143,12 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
|||||||
case "rolebindings", "rolebinding":
|
case "rolebindings", "rolebinding":
|
||||||
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
||||||
default:
|
default:
|
||||||
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := client.UpdateResourceYAML(apiPath, yamlData); err != nil {
|
if err := client.UpdateResourceYAML(apiPath, yamlData); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "YAML已应用"})
|
c.JSON(200, model.Response{Code: 200, Message: "YAML已应用"})
|
||||||
}
|
}
|
||||||
@@ -173,16 +194,26 @@ func resourceAPIPath(ns, resource, name string) (string, error) {
|
|||||||
// DeleteResourceYAMLNS deletes a K8s resource (Service/Ingress/ConfigMap etc.)
|
// DeleteResourceYAMLNS deletes a K8s resource (Service/Ingress/ConfigMap etc.)
|
||||||
func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) {
|
func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
ns := c.Param("namespace")
|
ns := c.Param("namespace")
|
||||||
resource := c.Param("resource")
|
resource := c.Param("resource")
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
apiPath, err := resourceAPIPath(ns, resource, name)
|
apiPath, err := resourceAPIPath(ns, resource, name)
|
||||||
if err != nil { c.JSON(400, model.Response{Code: 400, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := client.DeleteResource(apiPath); err != nil {
|
if err := client.DeleteResource(apiPath); err != nil {
|
||||||
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "资源已删除"})
|
c.JSON(200, model.Response{Code: 200, Message: "资源已删除"})
|
||||||
}
|
}
|
||||||
@@ -190,18 +221,33 @@ func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) {
|
|||||||
// getK8sClient builds a K8s API client from cluster's stored kubeconfig
|
// getK8sClient builds a K8s API client from cluster's stored kubeconfig
|
||||||
func (h *KubernetesHandler) ApplyYAML(c *gin.Context) {
|
func (h *KubernetesHandler) ApplyYAML(c *gin.Context) {
|
||||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
YAML string `json:"yaml"`
|
YAML string `json:"yaml"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
if req.YAML == "" { c.JSON(400, model.Response{Code: 400, Message: "YAML 内容不能为空"}); return }
|
c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.YAML == "" {
|
||||||
|
c.JSON(400, model.Response{Code: 400, Message: "YAML 内容不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
result, err := client.ApplyYAML(req.YAML)
|
result, err := client.ApplyYAML(req.YAML)
|
||||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "Apply 失败: " + err.Error()}); return }
|
if err != nil {
|
||||||
|
c.JSON(500, model.Response{Code: 500, Message: "Apply 失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, model.Response{Code: 200, Message: "资源已应用", Data: result})
|
c.JSON(200, model.Response{Code: 200, Message: "资源已应用", Data: result})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,4 +151,4 @@ func ldapAddADUser(conn *ldap.Conn, userDN, username, displayName, email, passwo
|
|||||||
modEnable := ldap.NewModifyRequest(userDN, nil)
|
modEnable := ldap.NewModifyRequest(userDN, nil)
|
||||||
modEnable.Replace("userAccountControl", []string{"512"})
|
modEnable.Replace("userAccountControl", []string{"512"})
|
||||||
return conn.Modify(modEnable)
|
return conn.Modify(modEnable)
|
||||||
}
|
}
|
||||||
+10
-10
@@ -60,17 +60,17 @@ func (h *LLMHandler) GetLLMConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||||
"id": cfg.ID,
|
"id": cfg.ID,
|
||||||
"provider": cfg.Provider,
|
"provider": cfg.Provider,
|
||||||
"model": cfg.Model,
|
"model": cfg.Model,
|
||||||
"api_key": cfg.APIKey,
|
"api_key": cfg.APIKey,
|
||||||
"endpoint": cfg.Endpoint,
|
"endpoint": cfg.Endpoint,
|
||||||
"temperature": cfg.Temperature,
|
"temperature": cfg.Temperature,
|
||||||
"max_tokens": cfg.MaxTokens,
|
"max_tokens": cfg.MaxTokens,
|
||||||
"system_prompt": cfg.SystemPrompt,
|
"system_prompt": cfg.SystemPrompt,
|
||||||
"enabled": cfg.Enabled,
|
"enabled": cfg.Enabled,
|
||||||
"reasoning_enabled": cfg.ReasoningEnabled,
|
"reasoning_enabled": cfg.ReasoningEnabled,
|
||||||
"configured": true,
|
"configured": true,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ func buildLLMConfigFromModel(cfg model.LLMConfig) llm.Config {
|
|||||||
// buildLLMClientFromConfig 从 model.LLMConfig 构造 LLM 客户端(OpenAI/Anthropic 自动路由) // Logan.Li
|
// buildLLMClientFromConfig 从 model.LLMConfig 构造 LLM 客户端(OpenAI/Anthropic 自动路由) // Logan.Li
|
||||||
func buildLLMClientFromConfig(cfg model.LLMConfig) llm.Client {
|
func buildLLMClientFromConfig(cfg model.LLMConfig) llm.Client {
|
||||||
return llm.NewClient(buildLLMConfigFromModel(cfg))
|
return llm.NewClient(buildLLMConfigFromModel(cfg))
|
||||||
}
|
}
|
||||||
@@ -74,10 +74,10 @@ func (h *LogHandler) CreateSource(c *gin.Context) {
|
|||||||
configJSON, _ := json.Marshal(req.Config)
|
configJSON, _ := json.Marshal(req.Config)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
item := model.LogSource{
|
item := model.LogSource{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
Config: string(configJSON),
|
Config: string(configJSON),
|
||||||
LastSeen: &now,
|
LastSeen: &now,
|
||||||
}
|
}
|
||||||
h.db.Create(&item)
|
h.db.Create(&item)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (h *LogDSHandler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
ds := model.LogDataSource{
|
ds := model.LogDataSource{
|
||||||
Name: req.Name, Type: req.Type, URL: strings.TrimRight(req.URL, "/"),
|
Name: req.Name, Type: req.Type, URL: strings.TrimRight(req.URL, "/"),
|
||||||
AuthType: normalizeLogDSAuthType(req.AuthType),
|
AuthType: normalizeLogDSAuthType(req.AuthType),
|
||||||
BasicAuth: req.BasicAuth, BasicAuthUser: req.BasicAuthUser, BasicAuthPass: req.BasicAuthPass,
|
BasicAuth: req.BasicAuth, BasicAuthUser: req.BasicAuthUser, BasicAuthPass: req.BasicAuthPass,
|
||||||
Token: req.Token, Headers: req.Headers,
|
Token: req.Token, Headers: req.Headers,
|
||||||
SkipTLSVerify: req.SkipTLSVerify, DefaultIndex: req.DefaultIndex,
|
SkipTLSVerify: req.SkipTLSVerify, DefaultIndex: req.DefaultIndex,
|
||||||
@@ -439,4 +439,4 @@ func normalizeLogDSAuthType(t string) string {
|
|||||||
default:
|
default:
|
||||||
return "none"
|
return "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,4 +477,4 @@ func formatLokiMetricTimestamp(v interface{}) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return formatLokiTimestamp(fmt.Sprintf("%v", v))
|
return formatLokiTimestamp(fmt.Sprintf("%v", v))
|
||||||
}
|
}
|
||||||
@@ -198,4 +198,4 @@ func (h *LogScopeRoleHandler) PreviewLogScope(c *gin.Context) {
|
|||||||
"matched": matched,
|
"matched": matched,
|
||||||
"total": len(matched),
|
"total": len(matched),
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
@@ -134,15 +134,15 @@ func (h *MetricsHandler) TopHosts(c *gin.Context) {
|
|||||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5"))
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5"))
|
||||||
|
|
||||||
type HostRank struct {
|
type HostRank struct {
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
MemUsage float64 `json:"memory_usage"`
|
MemUsage float64 `json:"memory_usage"`
|
||||||
DiskUsage float64 `json:"disk_usage"`
|
DiskUsage float64 `json:"disk_usage"`
|
||||||
CPUCores int `json:"cpu_cores"`
|
CPUCores int `json:"cpu_cores"`
|
||||||
MemMB int64 `json:"memory_mb"`
|
MemMB int64 `json:"memory_mb"`
|
||||||
DiskGB int64 `json:"disk_gb"`
|
DiskGB int64 `json:"disk_gb"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var servers []model.Server
|
var servers []model.Server
|
||||||
@@ -166,15 +166,15 @@ func (h *MetricsHandler) TopHosts(c *gin.Context) {
|
|||||||
result := make([]HostRank, len(servers))
|
result := make([]HostRank, len(servers))
|
||||||
for i, s := range servers {
|
for i, s := range servers {
|
||||||
result[i] = HostRank{
|
result[i] = HostRank{
|
||||||
Hostname: s.Hostname,
|
Hostname: s.Hostname,
|
||||||
IP: s.IP,
|
IP: s.IP,
|
||||||
CPUUsage: s.CPUUsage,
|
CPUUsage: s.CPUUsage,
|
||||||
MemUsage: s.MemoryUsage,
|
MemUsage: s.MemoryUsage,
|
||||||
DiskUsage: s.DiskUsage,
|
DiskUsage: s.DiskUsage,
|
||||||
CPUCores: s.CPU,
|
CPUCores: s.CPU,
|
||||||
MemMB: s.Memory,
|
MemMB: s.Memory,
|
||||||
DiskGB: s.Disk,
|
DiskGB: s.Disk,
|
||||||
Status: s.Status,
|
Status: s.Status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
// MetricsUpdate 单个主机的指标更新 // Logan.Li
|
// MetricsUpdate 单个主机的指标更新 // Logan.Li
|
||||||
type MetricsUpdate struct {
|
type MetricsUpdate struct {
|
||||||
Type string `json:"type"` // "metrics_update"
|
Type string `json:"type"` // "metrics_update"
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
CPU float64 `json:"cpu_usage"`
|
CPU float64 `json:"cpu_usage"`
|
||||||
Memory float64 `json:"memory_usage"`
|
Memory float64 `json:"memory_usage"`
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"ezops/model"
|
"ezops/model"
|
||||||
"ezops/pkg/systask"
|
"ezops/pkg/systask"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ import (
|
|||||||
// MiddlewareConfig 存储在 MonitorTarget.Config JSON 字段中
|
// MiddlewareConfig 存储在 MonitorTarget.Config JSON 字段中
|
||||||
type MiddlewareConfig struct {
|
type MiddlewareConfig struct {
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Username string `json:"username,omitempty"` // MySQL 专用
|
Username string `json:"username,omitempty"` // MySQL 专用
|
||||||
Database string `json:"database,omitempty"` // MySQL 专用
|
Database string `json:"database,omitempty"` // MySQL 专用
|
||||||
RedisMode string `json:"redis_mode,omitempty"` // Redis: standalone / cluster
|
RedisMode string `json:"redis_mode,omitempty"` // Redis: standalone / cluster
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,4 +98,4 @@ func buildRedisMetrics(info string, dbSize int64, clusterInfo, mode string) map[
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -14,12 +14,12 @@ import (
|
|||||||
|
|
||||||
// middlewareSeedResult 种子创建结果 // Logan.Li
|
// middlewareSeedResult 种子创建结果 // Logan.Li
|
||||||
type middlewareSeedResult struct {
|
type middlewareSeedResult struct {
|
||||||
Created int `json:"created"`
|
Created int `json:"created"`
|
||||||
Skipped []string `json:"skipped"`
|
Skipped []string `json:"skipped"`
|
||||||
Failed []string `json:"failed"`
|
Failed []string `json:"failed"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeedDefaultMiddlewareTargets 首次启动时创建平台默认 Redis/MySQL 监控目标 // Logan.Li
|
// SeedDefaultMiddlewareTargets 首次启动时创建平台默认 Redis/MySQL 监控目标 // Logan.Li
|
||||||
@@ -117,4 +117,4 @@ func seedDefaultMiddlewareTargets(db *gorm.DB) middlewareSeedResult {
|
|||||||
result.Message = "未创建任何默认目标"
|
result.Message = "未创建任何默认目标"
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -282,4 +282,4 @@ func (h *AlertHandler) ResolveUnifiedAlert(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复"})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复"})
|
||||||
}
|
}
|
||||||
+116
-60
@@ -6,8 +6,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ezops/model"
|
|
||||||
"ezops/middleware"
|
"ezops/middleware"
|
||||||
|
"ezops/model"
|
||||||
ezredis "ezops/pkg/redis"
|
ezredis "ezops/pkg/redis"
|
||||||
"ezops/pkg/toolinstaller"
|
"ezops/pkg/toolinstaller"
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ func NewPipelineHandler(db *gorm.DB, cache ezredis.Cache, ti *toolinstaller.Tool
|
|||||||
// PipelineListItem 列表项(含最近一次运行摘要)// Logan.Li
|
// PipelineListItem 列表项(含最近一次运行摘要)// Logan.Li
|
||||||
type PipelineListItem struct {
|
type PipelineListItem struct {
|
||||||
model.Pipeline
|
model.Pipeline
|
||||||
LastRunStages string `json:"last_run_stages,omitempty"` // JSON: [{name,status}]
|
LastRunStages string `json:"last_run_stages,omitempty"` // JSON: [{name,status}]
|
||||||
LastImageTag string `json:"last_image_tag,omitempty"`
|
LastImageTag string `json:"last_image_tag,omitempty"`
|
||||||
LastCommitID string `json:"last_commit_id,omitempty"`
|
LastCommitID string `json:"last_commit_id,omitempty"`
|
||||||
LastRunDuration int `json:"last_duration,omitempty"`
|
LastRunDuration int `json:"last_duration,omitempty"`
|
||||||
@@ -123,7 +123,7 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
// 子查询:每个 pipeline_id 的最新 run id
|
// 子查询:每个 pipeline_id 的最新 run id
|
||||||
var latestRuns []model.PipelineRun
|
var latestRuns []model.PipelineRun
|
||||||
h.db.Where("id IN (SELECT MAX(id) FROM ops_pipeline_runs WHERE pipeline_id IN ? AND status IN ('success','failed','running','pending') GROUP BY pipeline_id)", pids).
|
h.db.Where("id IN (SELECT MAX(id) FROM deploy_pipeline_runs WHERE pipeline_id IN ? AND status IN ('success','failed','running','pending') GROUP BY pipeline_id)", pids).
|
||||||
Find(&latestRuns)
|
Find(&latestRuns)
|
||||||
runMap := map[uint]*model.PipelineRun{}
|
runMap := map[uint]*model.PipelineRun{}
|
||||||
for i := range latestRuns {
|
for i := range latestRuns {
|
||||||
@@ -147,8 +147,8 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
Data: model.ListResponse{List: items, Total: total},
|
Data: model.ListResponse{List: items, Total: total},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,30 +211,28 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pipeline := model.Pipeline{
|
pipeline := model.Pipeline{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Code: req.Code,
|
Code: req.Code,
|
||||||
JobName: req.JobName,
|
JobName: req.JobName,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
Source: req.Source,
|
Source: req.Source,
|
||||||
Branch: req.Branch,
|
Branch: req.Branch,
|
||||||
Dockerfile: req.Dockerfile,
|
Dockerfile: req.Dockerfile,
|
||||||
Stages: req.Stages,
|
Stages: req.Stages,
|
||||||
Variables: req.Variables,
|
Variables: req.Variables,
|
||||||
DeployMode: req.DeployMode,
|
DeployMode: req.DeployMode,
|
||||||
DeployTarget: req.DeployTarget,
|
DeployTarget: req.DeployTarget,
|
||||||
ServerIDs: req.ServerIDs,
|
ServerIDs: req.ServerIDs,
|
||||||
DeployRemotePath: req.DeployRemotePath,
|
DeployRemotePath: req.DeployRemotePath,
|
||||||
DeployScript: req.DeployScript,
|
DeployScript: req.DeployScript,
|
||||||
K8sClusterID: req.K8sClusterID,
|
K8sClusterID: req.K8sClusterID,
|
||||||
K8sNamespace: req.K8sNamespace,
|
K8sNamespace: req.K8sNamespace,
|
||||||
K8sDeployment: req.K8sDeployment,
|
K8sDeployment: req.K8sDeployment,
|
||||||
K8sContainerName: req.K8sContainerName,
|
K8sContainerName: req.K8sContainerName,
|
||||||
RepoID: req.RepoID,
|
|
||||||
RegistryID: req.RegistryID,
|
|
||||||
GitCredentialID: req.GitCredentialID,
|
GitCredentialID: req.GitCredentialID,
|
||||||
RegistryCredentialID: req.RegistryCredentialID,
|
RegistryCredentialID: req.RegistryCredentialID,
|
||||||
EnvironmentID: req.EnvironmentID,
|
EnvironmentID: req.EnvironmentID,
|
||||||
// 触发配置
|
// 触发配置
|
||||||
TriggerMode: req.TriggerMode,
|
TriggerMode: req.TriggerMode,
|
||||||
WatchBranches: req.WatchBranches,
|
WatchBranches: req.WatchBranches,
|
||||||
@@ -246,7 +244,7 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
|||||||
// 并发控制
|
// 并发控制
|
||||||
MaxExecutors: req.MaxExecutors,
|
MaxExecutors: req.MaxExecutors,
|
||||||
BuildNodeID: req.BuildNodeID,
|
BuildNodeID: req.BuildNodeID,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.Create(&pipeline).Error; err != nil {
|
if err := h.db.Create(&pipeline).Error; err != nil {
|
||||||
@@ -301,38 +299,96 @@ func (h *PipelineHandler) UpdatePipeline(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if req.Name != "" { updates["name"] = req.Name }
|
if req.Name != "" {
|
||||||
if req.JobName != "" { updates["job_name"] = req.JobName }
|
updates["name"] = req.Name
|
||||||
if req.Description != "" { updates["description"] = req.Description }
|
}
|
||||||
if req.Type != "" { updates["type"] = req.Type }
|
if req.JobName != "" {
|
||||||
if req.Source != "" { updates["source"] = req.Source }
|
updates["job_name"] = req.JobName
|
||||||
if req.Branch != "" { updates["branch"] = req.Branch }
|
}
|
||||||
if req.Dockerfile != "" { updates["dockerfile"] = req.Dockerfile }
|
if req.Description != "" {
|
||||||
if req.Stages != "" { updates["stages"] = req.Stages }
|
updates["description"] = req.Description
|
||||||
if req.Variables != "" { updates["variables"] = req.Variables }
|
}
|
||||||
if req.DeployMode != "" { updates["deploy_mode"] = req.DeployMode }
|
if req.Type != "" {
|
||||||
if req.DeployTarget != "" { updates["deploy_target"] = req.DeployTarget }
|
updates["type"] = req.Type
|
||||||
if req.ServerIDs != "" { updates["server_ids"] = req.ServerIDs }
|
}
|
||||||
if req.DeployRemotePath != "" { updates["deploy_remote_path"] = req.DeployRemotePath }
|
if req.Source != "" {
|
||||||
if req.DeployScript != "" { updates["deploy_script"] = req.DeployScript }
|
updates["source"] = req.Source
|
||||||
if req.K8sNamespace != "" { updates["k8s_namespace"] = req.K8sNamespace }
|
}
|
||||||
if req.K8sDeployment != "" { updates["k8s_deployment"] = req.K8sDeployment }
|
if req.Branch != "" {
|
||||||
if req.K8sContainerName != "" { updates["k8s_container_name"] = req.K8sContainerName }
|
updates["branch"] = req.Branch
|
||||||
if req.K8sClusterID != nil { updates["k8s_cluster_id"] = req.K8sClusterID }
|
}
|
||||||
if req.RepoID != nil { updates["repo_id"] = req.RepoID }
|
if req.Dockerfile != "" {
|
||||||
if req.RegistryID != nil { updates["registry_id"] = req.RegistryID }
|
updates["dockerfile"] = req.Dockerfile
|
||||||
if req.GitCredentialID != nil { updates["git_credential_id"] = req.GitCredentialID }
|
}
|
||||||
if req.RegistryCredentialID != nil { updates["registry_credential_id"] = req.RegistryCredentialID }
|
if req.Stages != "" {
|
||||||
if req.EnvironmentID != nil { updates["environment_id"] = req.EnvironmentID }
|
updates["stages"] = req.Stages
|
||||||
if req.TriggerMode != "" { updates["trigger_mode"] = req.TriggerMode }
|
}
|
||||||
if req.WatchBranches != "" { updates["watch_branches"] = req.WatchBranches }
|
if req.Variables != "" {
|
||||||
if req.ExcludeBranch != "" { updates["exclude_branch"] = req.ExcludeBranch }
|
updates["variables"] = req.Variables
|
||||||
if req.CronExpr != "" { updates["cron_expr"] = req.CronExpr }
|
}
|
||||||
if req.WebhookToken != "" { updates["webhook_token"] = req.WebhookToken }
|
if req.DeployMode != "" {
|
||||||
if req.CachePaths != "" { updates["cache_paths"] = req.CachePaths }
|
updates["deploy_mode"] = req.DeployMode
|
||||||
if req.Enabled != nil { updates["enabled"] = req.Enabled }
|
}
|
||||||
if req.MaxExecutors > 0 { updates["max_executors"] = req.MaxExecutors }
|
if req.DeployTarget != "" {
|
||||||
if req.BuildNodeID != nil { updates["build_node_id"] = req.BuildNodeID }
|
updates["deploy_target"] = req.DeployTarget
|
||||||
|
}
|
||||||
|
if req.ServerIDs != "" {
|
||||||
|
updates["server_ids"] = req.ServerIDs
|
||||||
|
}
|
||||||
|
if req.DeployRemotePath != "" {
|
||||||
|
updates["deploy_remote_path"] = req.DeployRemotePath
|
||||||
|
}
|
||||||
|
if req.DeployScript != "" {
|
||||||
|
updates["deploy_script"] = req.DeployScript
|
||||||
|
}
|
||||||
|
if req.K8sNamespace != "" {
|
||||||
|
updates["k8s_namespace"] = req.K8sNamespace
|
||||||
|
}
|
||||||
|
if req.K8sDeployment != "" {
|
||||||
|
updates["k8s_deployment"] = req.K8sDeployment
|
||||||
|
}
|
||||||
|
if req.K8sContainerName != "" {
|
||||||
|
updates["k8s_container_name"] = req.K8sContainerName
|
||||||
|
}
|
||||||
|
if req.K8sClusterID != nil {
|
||||||
|
updates["k8s_cluster_id"] = req.K8sClusterID
|
||||||
|
}
|
||||||
|
if req.GitCredentialID != nil {
|
||||||
|
updates["git_credential_id"] = req.GitCredentialID
|
||||||
|
}
|
||||||
|
if req.RegistryCredentialID != nil {
|
||||||
|
updates["registry_credential_id"] = req.RegistryCredentialID
|
||||||
|
}
|
||||||
|
if req.EnvironmentID != nil {
|
||||||
|
updates["environment_id"] = req.EnvironmentID
|
||||||
|
}
|
||||||
|
if req.TriggerMode != "" {
|
||||||
|
updates["trigger_mode"] = req.TriggerMode
|
||||||
|
}
|
||||||
|
if req.WatchBranches != "" {
|
||||||
|
updates["watch_branches"] = req.WatchBranches
|
||||||
|
}
|
||||||
|
if req.ExcludeBranch != "" {
|
||||||
|
updates["exclude_branch"] = req.ExcludeBranch
|
||||||
|
}
|
||||||
|
if req.CronExpr != "" {
|
||||||
|
updates["cron_expr"] = req.CronExpr
|
||||||
|
}
|
||||||
|
if req.WebhookToken != "" {
|
||||||
|
updates["webhook_token"] = req.WebhookToken
|
||||||
|
}
|
||||||
|
if req.CachePaths != "" {
|
||||||
|
updates["cache_paths"] = req.CachePaths
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
updates["enabled"] = req.Enabled
|
||||||
|
}
|
||||||
|
if req.MaxExecutors > 0 {
|
||||||
|
updates["max_executors"] = req.MaxExecutors
|
||||||
|
}
|
||||||
|
if req.BuildNodeID != nil {
|
||||||
|
updates["build_node_id"] = req.BuildNodeID
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.db.Model(&pipeline).Updates(updates).Error; err != nil {
|
if err := h.db.Model(&pipeline).Updates(updates).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Response{
|
c.JSON(http.StatusInternalServerError, model.Response{
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func (h *PipelineHandler) executePipeline(pipeline model.Pipeline, run model.Pip
|
|||||||
}
|
}
|
||||||
|
|
||||||
runUpdates := map[string]interface{}{
|
runUpdates := map[string]interface{}{
|
||||||
"workspace_path": workspace,
|
"workspace_path": workspace,
|
||||||
"build_node_name": execCtx.NodeName,
|
"build_node_name": execCtx.NodeName,
|
||||||
}
|
}
|
||||||
if nodeID > 0 {
|
if nodeID > 0 {
|
||||||
@@ -545,13 +545,10 @@ func (h *PipelineHandler) runDeploy(workspace string, pipeline model.Pipeline, t
|
|||||||
// resolvePipelineImage 解析流水线构建镜像全名(与镜像打包阶段一致) // Logan.Li
|
// resolvePipelineImage 解析流水线构建镜像全名(与镜像打包阶段一致) // Logan.Li
|
||||||
func (h *PipelineHandler) resolvePipelineImage(pipeline model.Pipeline, tag string) string {
|
func (h *PipelineHandler) resolvePipelineImage(pipeline model.Pipeline, tag string) string {
|
||||||
var registryURL, imageName string
|
var registryURL, imageName string
|
||||||
if pipeline.RegistryID != nil {
|
if pipeline.RegistryCredentialID != nil {
|
||||||
var reg model.PipelineRegistry
|
var credential model.Credential
|
||||||
if err := h.db.First(®, *pipeline.RegistryID).Error; err == nil {
|
if err := h.db.Take(&credential, *pipeline.RegistryCredentialID).Error; err == nil {
|
||||||
registryURL = reg.RegistryURL
|
registryURL = credential.Summary
|
||||||
if reg.Namespace != "" {
|
|
||||||
imageName = reg.Namespace + "/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
appName := pipeline.Code
|
appName := pipeline.Code
|
||||||
@@ -838,7 +835,7 @@ func cleanOldWorkspaces(baseDir string, maxAge time.Duration) {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
pipelineRunningCount = make(map[uint]int) // pipelineID → 当前运行数
|
pipelineRunningCount = make(map[uint]int) // pipelineID → 当前运行数
|
||||||
pipelineQueues = make(map[uint][]uint) // pipelineID → 排队 runID 列表
|
pipelineQueues = make(map[uint][]uint) // pipelineID → 排队 runID 列表
|
||||||
queueMu sync.Mutex
|
queueMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -909,9 +906,13 @@ func (h *PipelineHandler) dequeueNext(pipelineID uint) {
|
|||||||
|
|
||||||
// 加载 pipeline + run
|
// 加载 pipeline + run
|
||||||
var pipeline model.Pipeline
|
var pipeline model.Pipeline
|
||||||
if err := h.db.First(&pipeline, pipelineID).Error; err != nil { return }
|
if err := h.db.First(&pipeline, pipelineID).Error; err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
var run model.PipelineRun
|
var run model.PipelineRun
|
||||||
if err := h.db.First(&run, nextRunID).Error; err != nil { return }
|
if err := h.db.First(&run, nextRunID).Error; err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
nodeID, canNode := h.tryAcquireBuildNode(pipeline)
|
nodeID, canNode := h.tryAcquireBuildNode(pipeline)
|
||||||
if !canNode {
|
if !canNode {
|
||||||
@@ -935,8 +936,8 @@ func (h *PipelineHandler) GetQueueStatus(pipelineID uint) map[string]interface{}
|
|||||||
running := pipelineRunningCount[pipelineID]
|
running := pipelineRunningCount[pipelineID]
|
||||||
queued := pipelineQueues[pipelineID]
|
queued := pipelineQueues[pipelineID]
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"running": running,
|
"running": running,
|
||||||
"queued": len(queued),
|
"queued": len(queued),
|
||||||
"queue_ids": queued,
|
"queue_ids": queued,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,4 +260,4 @@ func (h *PipelineHandler) hasBuildFileCtx(ctx *PipelineExecCtx, dir string) bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -32,4 +32,4 @@ func loadRDPCredential(crypt *crypto.SecretBox, server model.Server) (model.RDPS
|
|||||||
return cred, errors.New("RDP 凭据缺少用户名或密码")
|
return cred, errors.New("RDP 凭据缺少用户名或密码")
|
||||||
}
|
}
|
||||||
return cred, nil
|
return cred, nil
|
||||||
}
|
}
|
||||||
@@ -84,8 +84,8 @@ func (h *RemoteExecHandler) BatchExec(c *gin.Context) {
|
|||||||
|
|
||||||
// BatchUploadRequest 批量上传请求
|
// BatchUploadRequest 批量上传请求
|
||||||
type BatchUploadRequest struct {
|
type BatchUploadRequest struct {
|
||||||
ServerIDs []uint `json:"server_ids" binding:"required"` // 目标服务器 ID 列表
|
ServerIDs []uint `json:"server_ids" binding:"required"` // 目标服务器 ID 列表
|
||||||
LocalPath string `json:"local_path" binding:"required"` // AIOps 主机上的文件路径
|
LocalPath string `json:"local_path" binding:"required"` // AIOps 主机上的文件路径
|
||||||
RemotePath string `json:"remote_path" binding:"required"` // 远程目标路径
|
RemotePath string `json:"remote_path" binding:"required"` // 远程目标路径
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
|||||||
|
|
||||||
// 关键词搜索
|
// 关键词搜索
|
||||||
if query.Keyword != "" {
|
if query.Keyword != "" {
|
||||||
db = db.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?",
|
db = db.Where("alias LIKE ? OR cloud_instance_name LIKE ? OR hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ? OR cloud_instance_id LIKE ?",
|
||||||
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
|
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态过滤
|
// 状态过滤
|
||||||
@@ -194,23 +194,26 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server := model.Server{
|
server := model.Server{
|
||||||
Hostname: req.Hostname,
|
Hostname: req.Hostname,
|
||||||
IP: req.IP,
|
Alias: req.Alias,
|
||||||
PublicIP: req.PublicIP,
|
CloudInstanceName: req.CloudInstanceName,
|
||||||
Port: req.Port,
|
CloudInstanceID: req.CloudInstanceID,
|
||||||
Username: req.Username,
|
IP: req.IP,
|
||||||
Password: req.Password,
|
PublicIP: req.PublicIP,
|
||||||
SSHKey: req.SSHKey,
|
Port: req.Port,
|
||||||
AuthType: req.AuthType,
|
Username: req.Username,
|
||||||
OS: req.OS,
|
Password: req.Password,
|
||||||
CPU: req.CPU,
|
SSHKey: req.SSHKey,
|
||||||
Memory: req.Memory,
|
AuthType: req.AuthType,
|
||||||
Disk: req.Disk,
|
OS: req.OS,
|
||||||
RegionID: req.RegionID,
|
CPU: req.CPU,
|
||||||
CloudID: req.CloudID,
|
Memory: req.Memory,
|
||||||
Tags: req.Tags,
|
Disk: req.Disk,
|
||||||
Description: req.Description,
|
RegionID: req.RegionID,
|
||||||
Status: "stopped",
|
CloudID: req.CloudID,
|
||||||
|
Tags: req.Tags,
|
||||||
|
Description: req.Description,
|
||||||
|
Status: "stopped",
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Status != "" {
|
if req.Status != "" {
|
||||||
@@ -295,6 +298,15 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
|||||||
if req.Hostname != "" {
|
if req.Hostname != "" {
|
||||||
updates["hostname"] = req.Hostname
|
updates["hostname"] = req.Hostname
|
||||||
}
|
}
|
||||||
|
if req.Alias != nil {
|
||||||
|
updates["alias"] = *req.Alias
|
||||||
|
}
|
||||||
|
if req.CloudInstanceName != "" {
|
||||||
|
updates["cloud_instance_name"] = req.CloudInstanceName
|
||||||
|
}
|
||||||
|
if req.CloudInstanceID != "" {
|
||||||
|
updates["cloud_instance_id"] = req.CloudInstanceID
|
||||||
|
}
|
||||||
if req.IP != "" {
|
if req.IP != "" {
|
||||||
updates["ip"] = req.IP
|
updates["ip"] = req.IP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func (h *SessionCommandHandler) ListSessionCommands(c *gin.Context) {
|
|||||||
|
|
||||||
// 联表带出会话录制大小,前端可判断能否回放 // Logan.Li
|
// 联表带出会话录制大小,前端可判断能否回放 // Logan.Li
|
||||||
var items []model.SessionCommandItem
|
var items []model.SessionCommandItem
|
||||||
listQuery := h.db.Table("sys_session_commands AS sc").
|
listQuery := h.db.Table("audit_session_commands AS sc").
|
||||||
Select("sc.*, COALESCE(ts.record_size, rs.record_size, 0) AS session_record_size").
|
Select("sc.*, COALESCE(ts.record_size, rs.record_size, 0) AS session_record_size").
|
||||||
Joins("LEFT JOIN sys_terminal_sessions AS ts ON ts.id = sc.session_id AND COALESCE(sc.session_type, 'terminal') = 'terminal'").
|
Joins("LEFT JOIN sys_terminal_sessions AS ts ON ts.id = sc.session_id AND COALESCE(sc.session_type, 'terminal') = 'terminal'").
|
||||||
Joins("LEFT JOIN sys_rdp_sessions AS rs ON rs.id = sc.session_id AND sc.session_type = 'rdp'")
|
Joins("LEFT JOIN sys_rdp_sessions AS rs ON rs.id = sc.session_id AND sc.session_type = 'rdp'")
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ func (h *SFTPHandler) sftpClient(serverID uint) (*sftp.Client, *ssh.Client, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
sshConfig := &ssh.ClientConfig{
|
sshConfig := &ssh.ClientConfig{
|
||||||
User: server.Username,
|
User: server.Username,
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,4 +267,4 @@ func (h *SiteHandler) GetSiteStats(c *gin.Context) {
|
|||||||
"availability": availability,
|
"availability": availability,
|
||||||
"probe_count": totalRecent,
|
"probe_count": totalRecent,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
@@ -12,23 +12,23 @@ import (
|
|||||||
|
|
||||||
// SiteTaskView 前端拨测任务视图(字段与前端对齐)// Logan.Li
|
// SiteTaskView 前端拨测任务视图(字段与前端对齐)// Logan.Li
|
||||||
type SiteTaskView struct {
|
type SiteTaskView struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
FrequencySec int `json:"frequency_sec"`
|
FrequencySec int `json:"frequency_sec"`
|
||||||
TimeoutSec int `json:"timeout_sec"`
|
TimeoutSec int `json:"timeout_sec"`
|
||||||
ExpectedStatusCode int `json:"expected_status_code"`
|
ExpectedStatusCode int `json:"expected_status_code"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
LastStatus string `json:"last_status,omitempty"`
|
LastStatus string `json:"last_status,omitempty"`
|
||||||
LastResponseTimeMs float64 `json:"last_response_time_ms,omitempty"`
|
LastResponseTimeMs float64 `json:"last_response_time_ms,omitempty"`
|
||||||
LastCheckedAt *string `json:"last_checked_at,omitempty"`
|
LastCheckedAt *string `json:"last_checked_at,omitempty"`
|
||||||
SuccessRate float64 `json:"success_rate"`
|
SuccessRate float64 `json:"success_rate"`
|
||||||
AlertStatus string `json:"alert_status"`
|
AlertStatus string `json:"alert_status"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
SSLDaysRemaining *int `json:"ssl_days_remaining,omitempty"`
|
SSLDaysRemaining *int `json:"ssl_days_remaining,omitempty"`
|
||||||
SSLCertExpiry *string `json:"ssl_cert_expiry,omitempty"`
|
SSLCertExpiry *string `json:"ssl_cert_expiry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func toSiteTaskView(task model.SiteTask) SiteTaskView {
|
func toSiteTaskView(task model.SiteTask) SiteTaskView {
|
||||||
@@ -220,4 +220,4 @@ func parseCreateTaskRequest(raw map[string]interface{}) model.SiteTask {
|
|||||||
task.DNSResolver = strings.TrimSpace(v)
|
task.DNSResolver = strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
+12
-12
@@ -50,16 +50,16 @@ func (h *SNMPHandler) ListDevices(c *gin.Context) {
|
|||||||
// POST /api/v1/snmp/devices
|
// POST /api/v1/snmp/devices
|
||||||
func (h *SNMPHandler) CreateDevice(c *gin.Context) {
|
func (h *SNMPHandler) CreateDevice(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
IP string `json:"ip" binding:"required"`
|
IP string `json:"ip" binding:"required"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
SnmpVersion string `json:"snmp_version"`
|
SnmpVersion string `json:"snmp_version"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Community string `json:"community"`
|
Community string `json:"community"`
|
||||||
TemplateID uint `json:"template_id"`
|
TemplateID uint `json:"template_id"`
|
||||||
DeviceType string `json:"device_type"`
|
DeviceType string `json:"device_type"`
|
||||||
Vendor string `json:"vendor"`
|
Vendor string `json:"vendor"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||||
@@ -172,8 +172,8 @@ func (h *SNMPHandler) GetSNMPStats(c *gin.Context) {
|
|||||||
h.db.Model(&model.SNMPDevice{}).Where("status = ?", "online").Count(&online)
|
h.db.Model(&model.SNMPDevice{}).Where("status = ?", "online").Count(&online)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||||
"total": total,
|
"total": total,
|
||||||
"online": online,
|
"online": online,
|
||||||
"abnormal": total - online,
|
"abnormal": total - online,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
@@ -198,4 +198,4 @@ func (h *SNMPHandler) ListDeviceMetrics(c *gin.Context) {
|
|||||||
func (h *SNMPHandler) StopCollector() {
|
func (h *SNMPHandler) StopCollector() {
|
||||||
close(h.stopCh)
|
close(h.stopCh)
|
||||||
h.wg.Wait()
|
h.wg.Wait()
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ func snmpMetricLabel(name, index string) string {
|
|||||||
"h3c_fan_speed": "H3C 风扇", "hw_fan_speed": "华为 风扇",
|
"h3c_fan_speed": "H3C 风扇", "hw_fan_speed": "华为 风扇",
|
||||||
"ruijie_fan_speed": "锐捷 风扇", "dell_fan_speed": "Dell 风扇", "inspur_fan_speed": "浪潮 风扇",
|
"ruijie_fan_speed": "锐捷 风扇", "dell_fan_speed": "Dell 风扇", "inspur_fan_speed": "浪潮 风扇",
|
||||||
"ent_sensor_value": "实体传感器",
|
"ent_sensor_value": "实体传感器",
|
||||||
"if_in_octets": "入方向流量", "if_out_octets": "出方向流量",
|
"if_in_octets": "入方向流量", "if_out_octets": "出方向流量",
|
||||||
}
|
}
|
||||||
label := labels[name]
|
label := labels[name]
|
||||||
if label == "" {
|
if label == "" {
|
||||||
@@ -270,4 +270,4 @@ func snmpMetricLabel(name, index string) string {
|
|||||||
label += " #" + index
|
label += " #" + index
|
||||||
}
|
}
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func (h *SystemTaskHandler) ListSystemTasks(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
Message: "success",
|
Message: "success",
|
||||||
Data: model.ListResponse{List: list, Total: int64(len(list))},
|
Data: model.ListResponse{List: list, Total: int64(len(list))},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +50,4 @@ func (h *SystemTaskHandler) RunSystemTask(c *gin.Context) {
|
|||||||
Message: "执行完成",
|
Message: "执行完成",
|
||||||
Data: item,
|
Data: item,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -105,11 +105,11 @@ func (h *ToolConfigHandler) UpdateTool(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
InstallMethod *model.ToolInstallMethod `json:"install_method"`
|
InstallMethod *model.ToolInstallMethod `json:"install_method"`
|
||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
SortOrder *int `json:"sort_order"`
|
SortOrder *int `json:"sort_order"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||||
@@ -356,4 +356,4 @@ func getPresetVersions(toolName string) []map[string]string {
|
|||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,37 +34,37 @@ type jaegerServicesResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type jaegerTraceResp struct {
|
type jaegerTraceResp struct {
|
||||||
Data []jaegerTrace `json:"data"`
|
Data []jaegerTrace `json:"data"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jaegerTrace struct {
|
type jaegerTrace struct {
|
||||||
TraceID string `json:"traceID"`
|
TraceID string `json:"traceID"`
|
||||||
Spans []jaegerSpan `json:"spans"`
|
Spans []jaegerSpan `json:"spans"`
|
||||||
Processes map[string]jaegerProc `json:"processes"`
|
Processes map[string]jaegerProc `json:"processes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jaegerSpan struct {
|
type jaegerSpan struct {
|
||||||
TraceID string `json:"traceID"`
|
TraceID string `json:"traceID"`
|
||||||
SpanID string `json:"spanID"`
|
SpanID string `json:"spanID"`
|
||||||
OperationName string `json:"operationName"`
|
OperationName string `json:"operationName"`
|
||||||
References []jaegerRef `json:"references"`
|
References []jaegerRef `json:"references"`
|
||||||
StartTime int64 `json:"startTime"` // microseconds
|
StartTime int64 `json:"startTime"` // microseconds
|
||||||
Duration int64 `json:"duration"` // microseconds
|
Duration int64 `json:"duration"` // microseconds
|
||||||
Tags []jaegerTag `json:"tags"`
|
Tags []jaegerTag `json:"tags"`
|
||||||
ProcessID string `json:"processID"`
|
ProcessID string `json:"processID"`
|
||||||
Logs []jaegerLog `json:"logs"`
|
Logs []jaegerLog `json:"logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jaegerRef struct {
|
type jaegerRef struct {
|
||||||
RefType string `json:"refType"`
|
RefType string `json:"refType"`
|
||||||
TraceID string `json:"traceID"`
|
TraceID string `json:"traceID"`
|
||||||
SpanID string `json:"spanID"`
|
SpanID string `json:"spanID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jaegerProc struct {
|
type jaegerProc struct {
|
||||||
ServiceName string `json:"serviceName"`
|
ServiceName string `json:"serviceName"`
|
||||||
Tags []jaegerTag `json:"tags"`
|
Tags []jaegerTag `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jaegerTag struct {
|
type jaegerTag struct {
|
||||||
@@ -74,8 +74,8 @@ type jaegerTag struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type jaegerLog struct {
|
type jaegerLog struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
Fields []jaegerTag `json:"fields"`
|
Fields []jaegerTag `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 前端期望的格式 ----
|
// ---- 前端期望的格式 ----
|
||||||
@@ -115,7 +115,7 @@ type serviceMetric struct {
|
|||||||
ServiceName string `json:"serviceName"`
|
ServiceName string `json:"serviceName"`
|
||||||
RequestCount int `json:"requestCount"`
|
RequestCount int `json:"requestCount"`
|
||||||
ErrorRate float64 `json:"errorRate"`
|
ErrorRate float64 `json:"errorRate"`
|
||||||
AvgDuration float64 `json:"avgDuration"` // ms
|
AvgDuration float64 `json:"avgDuration"` // ms
|
||||||
P95Duration float64 `json:"p95Duration"`
|
P95Duration float64 `json:"p95Duration"`
|
||||||
P99Duration float64 `json:"p99Duration"`
|
P99Duration float64 `json:"p99Duration"`
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-14
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ezops/middleware"
|
|
||||||
"ezops/config"
|
"ezops/config"
|
||||||
|
"ezops/middleware"
|
||||||
"ezops/model"
|
"ezops/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -62,20 +62,31 @@ func parseUserAgent(ua string) (browser, os string) {
|
|||||||
}
|
}
|
||||||
// Browser
|
// Browser
|
||||||
switch {
|
switch {
|
||||||
case containsAny(ua, "Edg/"): browser = "Edge"
|
case containsAny(ua, "Edg/"):
|
||||||
case containsAny(ua, "Chrome"): browser = "Chrome"
|
browser = "Edge"
|
||||||
case containsAny(ua, "Safari") && !containsAny(ua, "Chrome"): browser = "Safari"
|
case containsAny(ua, "Chrome"):
|
||||||
case containsAny(ua, "Firefox"): browser = "Firefox"
|
browser = "Chrome"
|
||||||
case containsAny(ua, "curl"): browser = "curl"
|
case containsAny(ua, "Safari") && !containsAny(ua, "Chrome"):
|
||||||
case containsAny(ua, "Python"): browser = "Python"
|
browser = "Safari"
|
||||||
|
case containsAny(ua, "Firefox"):
|
||||||
|
browser = "Firefox"
|
||||||
|
case containsAny(ua, "curl"):
|
||||||
|
browser = "curl"
|
||||||
|
case containsAny(ua, "Python"):
|
||||||
|
browser = "Python"
|
||||||
}
|
}
|
||||||
// OS
|
// OS
|
||||||
switch {
|
switch {
|
||||||
case containsAny(ua, "Windows"): os = "Windows"
|
case containsAny(ua, "Windows"):
|
||||||
case containsAny(ua, "Mac OS"): os = "macOS"
|
os = "Windows"
|
||||||
case containsAny(ua, "Linux") || containsAny(ua, "X11"): os = "Linux"
|
case containsAny(ua, "Mac OS"):
|
||||||
case containsAny(ua, "Android"): os = "Android"
|
os = "macOS"
|
||||||
case containsAny(ua, "iPhone") || containsAny(ua, "iPad"): os = "iOS"
|
case containsAny(ua, "Linux") || containsAny(ua, "X11"):
|
||||||
|
os = "Linux"
|
||||||
|
case containsAny(ua, "Android"):
|
||||||
|
os = "Android"
|
||||||
|
case containsAny(ua, "iPhone") || containsAny(ua, "iPad"):
|
||||||
|
os = "iOS"
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -363,7 +374,7 @@ func collectUserPermissionCodes(db *gorm.DB, userID uint, isAdmin bool) []string
|
|||||||
if isAdmin {
|
if isAdmin {
|
||||||
return []string{"*"}
|
return []string{"*"}
|
||||||
}
|
}
|
||||||
// raw SQL: user → iam_user_roles → iam_roles → role_permissions → iam_permissions
|
// raw SQL: user → iam_user_roles → iam_roles → iam_role_permissions → iam_permissions
|
||||||
// 逐层 JOIN,任一层缺失自然返回空
|
// 逐层 JOIN,任一层缺失自然返回空
|
||||||
type permRow struct {
|
type permRow struct {
|
||||||
Code string
|
Code string
|
||||||
@@ -373,7 +384,7 @@ func collectUserPermissionCodes(db *gorm.DB, userID uint, isAdmin bool) []string
|
|||||||
SELECT DISTINCT p.code
|
SELECT DISTINCT p.code
|
||||||
FROM iam_user_roles ur
|
FROM iam_user_roles ur
|
||||||
JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1
|
JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1
|
||||||
JOIN role_permissions rp ON rp.role_id = r.id
|
JOIN iam_role_permissions rp ON rp.role_id = r.id
|
||||||
JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1
|
JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1
|
||||||
WHERE ur.user_id = ?
|
WHERE ur.user_id = ?
|
||||||
`, userID).Scan(&rows).Error
|
`, userID).Scan(&rows).Error
|
||||||
|
|||||||
@@ -396,6 +396,7 @@ func (h *UserHandler) ListRoles(c *gin.Context) {
|
|||||||
Code: 200, Data: model.ListResponse{List: roles, Total: int64(len(roles))},
|
Code: 200, Data: model.ListResponse{List: roles, Total: int64(len(roles))},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/v1/admin/users/import-template
|
// GET /api/v1/admin/users/import-template
|
||||||
func (h *UserHandler) ImportTemplate(c *gin.Context) {
|
func (h *UserHandler) ImportTemplate(c *gin.Context) {
|
||||||
// UTF-8 BOM for Excel compatibility
|
// UTF-8 BOM for Excel compatibility
|
||||||
|
|||||||
@@ -273,7 +273,9 @@ func (h *UserHandler) UpdateApiAccessKey(c *gin.Context) {
|
|||||||
// ExtendApiAccessKey 延长有效期(兼容旧接口)// Logan.Li
|
// ExtendApiAccessKey 延长有效期(兼容旧接口)// Logan.Li
|
||||||
func (h *UserHandler) ExtendApiAccessKey(c *gin.Context) {
|
func (h *UserHandler) ExtendApiAccessKey(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
userID := middleware.GetUserID(c)
|
||||||
var req struct{ DurationMonths int `json:"duration_months"` }
|
var req struct {
|
||||||
|
DurationMonths int `json:"duration_months"`
|
||||||
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||||
return
|
return
|
||||||
@@ -311,4 +313,4 @@ func (h *UserHandler) RevokeApiAccessKey(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "访问密钥已删除"})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "访问密钥已删除"})
|
||||||
}
|
}
|
||||||
@@ -60,10 +60,10 @@ func newAsciicastRecorder() *asciicastRecorder {
|
|||||||
|
|
||||||
func (r *asciicastRecorder) writeHeader() {
|
func (r *asciicastRecorder) writeHeader() {
|
||||||
h, _ := json.Marshal(map[string]interface{}{
|
h, _ := json.Marshal(map[string]interface{}{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"width": 80,
|
"width": 80,
|
||||||
"height": 24,
|
"height": 24,
|
||||||
"timestamp": r.startTime.Unix(),
|
"timestamp": r.startTime.Unix(),
|
||||||
"aiops_input_recorded": false,
|
"aiops_input_recorded": false,
|
||||||
"env": map[string]string{
|
"env": map[string]string{
|
||||||
"SHELL": "/bin/bash",
|
"SHELL": "/bin/bash",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ezops/middleware"
|
"ezops/middleware"
|
||||||
"ezops/pkg/wshub"
|
|
||||||
ezredis "ezops/pkg/redis"
|
ezredis "ezops/pkg/redis"
|
||||||
|
"ezops/pkg/wshub"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -125,7 +125,7 @@ func (h *WSPipelineLogHandler) GetLogHistory(c *gin.Context) {
|
|||||||
// 先查 Redis(执行中)
|
// 先查 Redis(执行中)
|
||||||
logKey := fmt.Sprintf("pipeline:run:%d:logs", uint(runID))
|
logKey := fmt.Sprintf("pipeline:run:%d:logs", uint(runID))
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
raw, _ := h.cache.Get(ctx, logKey)
|
raw, _ := h.cache.Get(ctx, logKey)
|
||||||
if raw != "" && !strings.HasPrefix(raw, "[") {
|
if raw != "" && !strings.HasPrefix(raw, "[") {
|
||||||
raw = "[]"
|
raw = "[]"
|
||||||
@@ -137,7 +137,7 @@ func (h *WSPipelineLogHandler) GetLogHistory(c *gin.Context) {
|
|||||||
|
|
||||||
// 查 DB(已完成)
|
// 查 DB(已完成)
|
||||||
var logs string
|
var logs string
|
||||||
h.db.Table("ops_pipeline_runs").Where("id = ?", uint(runID)).Select("logs").Scan(&logs)
|
h.db.Table("deploy_pipeline_runs").Where("id = ?", uint(runID)).Select("logs").Scan(&logs)
|
||||||
if logs == "" {
|
if logs == "" {
|
||||||
logs = "[]"
|
logs = "[]"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,24 +113,48 @@ func (h *ZabbixDSHandler) Update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if req.Name != nil { updates["name"] = *req.Name }
|
if req.Name != nil {
|
||||||
if req.URL != nil { updates["url"] = *req.URL }
|
updates["name"] = *req.Name
|
||||||
if req.Username != nil { updates["username"] = *req.Username }
|
}
|
||||||
if req.Password != nil && *req.Password != "" { updates["password"] = *req.Password }
|
if req.URL != nil {
|
||||||
if req.TrendsAfter != nil { updates["trends_after"] = *req.TrendsAfter }
|
updates["url"] = *req.URL
|
||||||
if req.TrendsRange != nil { updates["trends_range"] = *req.TrendsRange }
|
}
|
||||||
if req.TrendsCacheTTL != nil { updates["trends_cache_ttl"] = *req.TrendsCacheTTL }
|
if req.Username != nil {
|
||||||
if req.TrendsTimeout != nil { updates["trends_timeout"] = *req.TrendsTimeout }
|
updates["username"] = *req.Username
|
||||||
if req.DirectDB != nil { updates["direct_db"] = *req.DirectDB }
|
}
|
||||||
if req.DisableAcknowledges != nil { updates["disable_acknowledges"] = *req.DisableAcknowledges }
|
if req.Password != nil && *req.Password != "" {
|
||||||
if req.DisableDataAlignment != nil { updates["disable_data_alignment"] = *req.DisableDataAlignment }
|
updates["password"] = *req.Password
|
||||||
|
}
|
||||||
|
if req.TrendsAfter != nil {
|
||||||
|
updates["trends_after"] = *req.TrendsAfter
|
||||||
|
}
|
||||||
|
if req.TrendsRange != nil {
|
||||||
|
updates["trends_range"] = *req.TrendsRange
|
||||||
|
}
|
||||||
|
if req.TrendsCacheTTL != nil {
|
||||||
|
updates["trends_cache_ttl"] = *req.TrendsCacheTTL
|
||||||
|
}
|
||||||
|
if req.TrendsTimeout != nil {
|
||||||
|
updates["trends_timeout"] = *req.TrendsTimeout
|
||||||
|
}
|
||||||
|
if req.DirectDB != nil {
|
||||||
|
updates["direct_db"] = *req.DirectDB
|
||||||
|
}
|
||||||
|
if req.DisableAcknowledges != nil {
|
||||||
|
updates["disable_acknowledges"] = *req.DisableAcknowledges
|
||||||
|
}
|
||||||
|
if req.DisableDataAlignment != nil {
|
||||||
|
updates["disable_data_alignment"] = *req.DisableDataAlignment
|
||||||
|
}
|
||||||
if req.IsDefault != nil {
|
if req.IsDefault != nil {
|
||||||
updates["is_default"] = *req.IsDefault
|
updates["is_default"] = *req.IsDefault
|
||||||
if *req.IsDefault {
|
if *req.IsDefault {
|
||||||
h.db.Model(&model.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
h.db.Model(&model.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Status != nil { updates["status"] = *req.Status }
|
if req.Status != nil {
|
||||||
|
updates["status"] = *req.Status
|
||||||
|
}
|
||||||
|
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
|
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type ZabbixHostSummary struct {
|
|||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Available string `json:"available"` // 0=未知 1=可用 2=不可用
|
Available string `json:"available"` // 0=未知 1=可用 2=不可用
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
MemoryUsage float64 `json:"memory_usage"`
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
DiskUsage float64 `json:"disk_usage"`
|
DiskUsage float64 `json:"disk_usage"`
|
||||||
@@ -71,7 +71,7 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
|||||||
|
|
||||||
// 获取主机列表(含接口和组信息)
|
// 获取主机列表(含接口和组信息)
|
||||||
hosts, err := client.GetHosts(ctx, &zabbix.GetHostsParams{
|
hosts, err := client.GetHosts(ctx, &zabbix.GetHostsParams{
|
||||||
Status: 0, // 只获取被监控的主机
|
Status: 0, // 只获取被监控的主机
|
||||||
SelectInterfaces: []string{"interfaceid", "hostid", "main", "type", "useip", "ip", "dns", "port"},
|
SelectInterfaces: []string{"interfaceid", "hostid", "main", "type", "useip", "ip", "dns", "port"},
|
||||||
SelectGroups: []string{"groupid", "name"},
|
SelectGroups: []string{"groupid", "name"},
|
||||||
Output: []string{"hostid", "host", "name", "status", "available", "description"},
|
Output: []string{"hostid", "host", "name", "status", "available", "description"},
|
||||||
@@ -85,17 +85,17 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
|||||||
// 为每台主机查询监控项
|
// 为每台主机查询监控项
|
||||||
var summaries []ZabbixHostSummary
|
var summaries []ZabbixHostSummary
|
||||||
itemKeys := []string{
|
itemKeys := []string{
|
||||||
"system.cpu.util[,idle]", // CPU 空闲率
|
"system.cpu.util[,idle]", // CPU 空闲率
|
||||||
"system.cpu.util", // CPU 使用率(备用)
|
"system.cpu.util", // CPU 使用率(备用)
|
||||||
"vm.memory.size[total]", // 总内存 bytes
|
"vm.memory.size[total]", // 总内存 bytes
|
||||||
"vm.memory.size[available]", // 可用内存 bytes
|
"vm.memory.size[available]", // 可用内存 bytes
|
||||||
"vfs.fs.size[/,total]", // 总磁盘 bytes
|
"vfs.fs.size[/,total]", // 总磁盘 bytes
|
||||||
"vfs.fs.size[/,used]", // 已用磁盘 bytes
|
"vfs.fs.size[/,used]", // 已用磁盘 bytes
|
||||||
"system.cpu.load[all,avg5]", // 5分钟负载
|
"system.cpu.load[all,avg5]", // 5分钟负载
|
||||||
"net.if.in[ens18]", // 网络流入
|
"net.if.in[ens18]", // 网络流入
|
||||||
"net.if.out[ens18]", // 网络流出
|
"net.if.out[ens18]", // 网络流出
|
||||||
"system.cpu.num", // CPU 数量
|
"system.cpu.num", // CPU 数量
|
||||||
"system.uptime", // 运行时间
|
"system.uptime", // 运行时间
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
@@ -185,9 +185,9 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
|||||||
Code: 200,
|
Code: 200,
|
||||||
Message: "success",
|
Message: "success",
|
||||||
Data: gin.H{
|
Data: gin.H{
|
||||||
"version": version,
|
"version": version,
|
||||||
"hosts": summaries,
|
"hosts": summaries,
|
||||||
"total": len(summaries),
|
"total": len(summaries),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ func setupFrontend(r *gin.Engine) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(cleanPath, "assets/") {
|
||||||
|
// 构建哈希资源缺失时返回真实 404,避免 SPA fallback 把 index.html 当 JS 模块返回。 // Logan.Li
|
||||||
|
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||||
|
c.String(http.StatusNotFound, "static asset not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// SPA fallback → 直接返回 index.html 字节(避免 http.FileServer 的 301 redirect)
|
// SPA fallback → 直接返回 index.html 字节(避免 http.FileServer 的 301 redirect)
|
||||||
indexBytes, err := fs.ReadFile(staticFS, "index.html")
|
indexBytes, err := fs.ReadFile(staticFS, "index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -171,4 +171,4 @@ func GetIsAdmin(c *gin.Context) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return isAdmin.(bool)
|
return isAdmin.(bool)
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ func collectPermissionCodes(db *gorm.DB, userID uint) []string {
|
|||||||
SELECT DISTINCT p.code AS code
|
SELECT DISTINCT p.code AS code
|
||||||
FROM iam_user_roles ur
|
FROM iam_user_roles ur
|
||||||
JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1
|
JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1
|
||||||
JOIN role_permissions rp ON rp.role_id = r.id
|
JOIN iam_role_permissions rp ON rp.role_id = r.id
|
||||||
JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1
|
JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1
|
||||||
WHERE ur.user_id = ?
|
WHERE ur.user_id = ?
|
||||||
`, userID).Scan(&rows)
|
`, userID).Scan(&rows)
|
||||||
@@ -57,4 +57,4 @@ func collectPermissionCodes(db *gorm.DB, userID uint) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return codes
|
return codes
|
||||||
}
|
}
|
||||||
+60
-61
@@ -6,33 +6,33 @@ import (
|
|||||||
|
|
||||||
// AgentReport Agent 上报的原始指标数据 // Logan.Li
|
// AgentReport Agent 上报的原始指标数据 // Logan.Li
|
||||||
type AgentReport struct {
|
type AgentReport struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||||
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"`
|
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"`
|
||||||
CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"`
|
CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"`
|
||||||
CPUCores int `json:"cpu_cores"`
|
CPUCores int `json:"cpu_cores"`
|
||||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
||||||
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"`
|
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"`
|
||||||
Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"`
|
Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"`
|
||||||
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"`
|
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"`
|
||||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
||||||
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"`
|
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"`
|
||||||
MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"`
|
MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"`
|
||||||
MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"`
|
MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"`
|
||||||
DiskInfo string `gorm:"type:text" json:"disk_info"`
|
DiskInfo string `gorm:"type:text" json:"disk_info"`
|
||||||
PortInfo string `gorm:"type:text" json:"port_info"`
|
PortInfo string `gorm:"type:text" json:"port_info"`
|
||||||
NetInfo string `gorm:"type:text" json:"net_info"`
|
NetInfo string `gorm:"type:text" json:"net_info"`
|
||||||
DiskIOInfo string `gorm:"type:text" json:"disk_io_info"`
|
DiskIOInfo string `gorm:"type:text" json:"disk_io_info"`
|
||||||
// ──── v2.0 新增字段 ────
|
// ──── v2.0 新增字段 ────
|
||||||
SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON
|
SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON
|
||||||
ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON
|
ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON
|
||||||
FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON
|
FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON
|
||||||
CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON
|
CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON
|
||||||
HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON
|
HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON
|
||||||
// ──────────────────────────
|
// ──────────────────────────
|
||||||
ReportTime time.Time `json:"report_time"`
|
ReportTime time.Time `json:"report_time"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentReport) TableName() string {
|
func (AgentReport) TableName() string {
|
||||||
@@ -41,20 +41,20 @@ func (AgentReport) TableName() string {
|
|||||||
|
|
||||||
// AgentLatestResponse Agent 最新状态(含服务器元信息)
|
// AgentLatestResponse Agent 最新状态(含服务器元信息)
|
||||||
type AgentLatestResponse struct {
|
type AgentLatestResponse struct {
|
||||||
ServerID uint `json:"server_id"`
|
ServerID uint `json:"server_id"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
CPUCores int `json:"cpu_cores"`
|
CPUCores int `json:"cpu_cores"`
|
||||||
MemUsage float64 `json:"mem_usage"`
|
MemUsage float64 `json:"mem_usage"`
|
||||||
MemTotal float64 `json:"mem_total"`
|
MemTotal float64 `json:"mem_total"`
|
||||||
MemUsed float64 `json:"mem_used"`
|
MemUsed float64 `json:"mem_used"`
|
||||||
DiskUsage float64 `json:"disk_usage"`
|
DiskUsage float64 `json:"disk_usage"`
|
||||||
DiskTotal int64 `json:"disk_total"`
|
DiskTotal int64 `json:"disk_total"`
|
||||||
DiskInfo string `json:"disk_info"`
|
DiskInfo string `json:"disk_info"`
|
||||||
PortInfo string `json:"port_info"`
|
PortInfo string `json:"port_info"`
|
||||||
NetInfo string `json:"net_info"`
|
NetInfo string `json:"net_info"`
|
||||||
DiskIOInfo string `json:"disk_io_info"`
|
DiskIOInfo string `json:"disk_io_info"`
|
||||||
// ──── v2.0 新增 ────
|
// ──── v2.0 新增 ────
|
||||||
SwapInfo string `json:"swap_info"`
|
SwapInfo string `json:"swap_info"`
|
||||||
ProcessTopInfo string `json:"process_top_info"`
|
ProcessTopInfo string `json:"process_top_info"`
|
||||||
@@ -74,23 +74,23 @@ type AgentLatestResponse struct {
|
|||||||
|
|
||||||
// AgentReportRequest Agent 上报请求体
|
// AgentReportRequest Agent 上报请求体
|
||||||
type AgentReportRequest struct {
|
type AgentReportRequest struct {
|
||||||
Hostname string `json:"hostname" binding:"required"`
|
Hostname string `json:"hostname" binding:"required"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
AgentVersion string `json:"agent_version"`
|
AgentVersion string `json:"agent_version"`
|
||||||
CPU CPUMetric `json:"cpu"`
|
CPU CPUMetric `json:"cpu"`
|
||||||
Memory MemMetric `json:"memory"`
|
Memory MemMetric `json:"memory"`
|
||||||
Swap *SwapMetric `json:"swap,omitempty"`
|
Swap *SwapMetric `json:"swap,omitempty"`
|
||||||
Disks []DiskMetric `json:"disks"`
|
Disks []DiskMetric `json:"disks"`
|
||||||
Ports []PortMetric `json:"ports"`
|
Ports []PortMetric `json:"ports"`
|
||||||
Network NetworkStatModel `json:"network"`
|
Network NetworkStatModel `json:"network"`
|
||||||
DiskIO DiskIOStatModel `json:"disk_io"`
|
DiskIO DiskIOStatModel `json:"disk_io"`
|
||||||
System *SystemStatModel `json:"system,omitempty"`
|
System *SystemStatModel `json:"system,omitempty"`
|
||||||
// ──── v2.0 新增 ────
|
// ──── v2.0 新增 ────
|
||||||
ProcessTop []ProcessMetric `json:"process_top,omitempty"`
|
ProcessTop []ProcessMetric `json:"process_top,omitempty"`
|
||||||
FD *FDMetric `json:"fd,omitempty"`
|
FD *FDMetric `json:"fd,omitempty"`
|
||||||
ContextSwitches *CSMetric `json:"context_switches,omitempty"`
|
ContextSwitches *CSMetric `json:"context_switches,omitempty"`
|
||||||
Heartbeat HeartbeatMetric `json:"heartbeat"`
|
Heartbeat HeartbeatMetric `json:"heartbeat"`
|
||||||
ProbeResults []HealthProbeResult `json:"probe_results,omitempty"` // v2.1 健康探针结果
|
ProbeResults []HealthProbeResult `json:"probe_results,omitempty"` // v2.1 健康探针结果
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,14 +198,14 @@ type DiskIOStatModel struct {
|
|||||||
|
|
||||||
// AgentPortPolicy 主机端口监控策略 // Logan.Li
|
// AgentPortPolicy 主机端口监控策略 // Logan.Li
|
||||||
type AgentPortPolicy struct {
|
type AgentPortPolicy struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"`
|
Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"`
|
||||||
PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist
|
PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist
|
||||||
PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379]
|
PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentPortPolicy) TableName() string {
|
func (AgentPortPolicy) TableName() string {
|
||||||
return "monitor_agent_port_policy"
|
return "monitor_agent_port_policies"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentPortPolicyDTO 前端交互用的端口策略格式(含已解析的数组)
|
// AgentPortPolicyDTO 前端交互用的端口策略格式(含已解析的数组)
|
||||||
@@ -214,4 +214,3 @@ type AgentPortPolicyDTO struct {
|
|||||||
PortFilterMode string `json:"port_filter_mode"`
|
PortFilterMode string `json:"port_filter_mode"`
|
||||||
PortWhitelist []int `json:"port_whitelist"`
|
PortWhitelist []int `json:"port_whitelist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// agent_cli.go - Agent CLI 会话与消息持久化模型 // Logan.Li
|
|
||||||
// 记录 AI 运维 Agent 的每次任务(关联主机 + 用户)及其 ReAct 推理过程
|
|
||||||
// (思考/工具调用/输出/结果),供前端历史回放与审计。
|
|
||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// AgentCLISession Agent CLI 任务会话 // Logan.Li
|
|
||||||
type AgentCLISession struct {
|
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
|
||||||
UserID uint `gorm:"index" json:"user_id"` // 发起用户
|
|
||||||
Mode string `gorm:"size:16;default:host;index" json:"mode"` // host | kubernetes
|
|
||||||
ServerID uint `gorm:"index" json:"server_id"` // 主机模式目标主机
|
|
||||||
ServerName string `gorm:"size:128" json:"server_name"` // 主机名快照
|
|
||||||
ServerHost string `gorm:"size:64" json:"server_host"` // 主机 IP 快照
|
|
||||||
ClusterID uint `gorm:"index" json:"cluster_id"` // K8s 模式集群 ID
|
|
||||||
ClusterName string `gorm:"size:128" json:"cluster_name"` // 集群名快照
|
|
||||||
Namespace string `gorm:"size:128" json:"namespace"` // K8s 命名空间
|
|
||||||
PodName string `gorm:"size:128" json:"pod_name"` // 可选目标 Pod
|
|
||||||
Container string `gorm:"size:128" json:"container"` // 可选目标容器
|
|
||||||
Title string `gorm:"size:255" json:"title"` // 任务标题(query 截断)
|
|
||||||
Status string `gorm:"size:32;default:running" json:"status"` // running/done/aborted/error
|
|
||||||
Provider string `gorm:"size:50" json:"provider"` // LLM 提供商
|
|
||||||
Model string `gorm:"size:100" json:"model"` // 模型名
|
|
||||||
Rounds int `json:"rounds"` // ReAct 轮次
|
|
||||||
TotalTokens int `json:"total_tokens"` // token 消耗
|
|
||||||
Duration int64 `json:"duration"` // 耗时(毫秒)
|
|
||||||
Error string `gorm:"type:text" json:"error,omitempty"` // 错误信息
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 表名 // Logan.Li
|
|
||||||
func (AgentCLISession) TableName() string { return "agent_cli_sessions" }
|
|
||||||
|
|
||||||
// AgentCLIMessage Agent CLI 任务消息(ReAct 推理过程的每一步) // Logan.Li
|
|
||||||
type AgentCLIMessage struct {
|
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
|
||||||
SessionID uint `gorm:"index;not null" json:"session_id"`
|
|
||||||
Round int `json:"round"` // ReAct 轮次
|
|
||||||
Type string `gorm:"size:32" json:"type"` // thought/tool_call/tool_result/done/error/session
|
|
||||||
Role string `gorm:"size:32" json:"role"` // user/assistant/tool
|
|
||||||
Content string `gorm:"type:text" json:"content"` // 思考文本/最终答案
|
|
||||||
ToolName string `gorm:"size:64" json:"tool_name"` // 工具名
|
|
||||||
ToolArgs string `gorm:"type:text" json:"tool_args"` // 工具参数 JSON
|
|
||||||
ToolOutput string `gorm:"type:text" json:"tool_output"` // 工具输出
|
|
||||||
Risk string `gorm:"size:32" json:"risk"` // 风险等级
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Duration string `gorm:"size:32" json:"duration"` // 工具耗时
|
|
||||||
Tokens int `json:"tokens"`
|
|
||||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 表名 // Logan.Li
|
|
||||||
func (AgentCLIMessage) TableName() string { return "agent_cli_messages" }
|
|
||||||
+37
-38
@@ -2,25 +2,24 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentTask Agent 远程任务 // Logan.Li
|
// AgentTask Agent 远程任务 // Logan.Li
|
||||||
type AgentTask struct {
|
type AgentTask struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
TaskID string `gorm:"size:36;uniqueIndex;not null" json:"task_id"` // UUID
|
TaskID string `gorm:"size:36;uniqueIndex;not null" json:"task_id"` // UUID
|
||||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"` // 目标主机
|
Hostname string `gorm:"size:255;index;not null" json:"hostname"` // 目标主机
|
||||||
Type string `gorm:"size:32;not null" json:"type"` // command / script / file
|
Type string `gorm:"size:32;not null" json:"type"` // command / script / file
|
||||||
Command string `gorm:"type:text" json:"command"` // 命令或脚本内容
|
Command string `gorm:"type:text" json:"command"` // 命令或脚本内容
|
||||||
Timeout int `gorm:"default:60" json:"timeout"` // 超时秒数
|
Timeout int `gorm:"default:60" json:"timeout"` // 超时秒数
|
||||||
Status string `gorm:"size:20;default:pending;index" json:"status"` // pending/running/success/failed/timeout
|
Status string `gorm:"size:20;default:pending;index" json:"status"` // pending/running/success/failed/timeout
|
||||||
Result string `gorm:"type:text" json:"result"` // 执行输出
|
Result string `gorm:"type:text" json:"result"` // 执行输出
|
||||||
ExitCode int `gorm:"default:-1" json:"exit_code"` // 退出码
|
ExitCode int `gorm:"default:-1" json:"exit_code"` // 退出码
|
||||||
CreatedBy string `gorm:"size:255" json:"created_by"` // 创建人
|
CreatedBy string `gorm:"size:255" json:"created_by"` // 创建人
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
PickedAt *time.Time `json:"picked_at"` // Agent 拉取时间
|
PickedAt *time.Time `json:"picked_at"` // Agent 拉取时间
|
||||||
FinishedAt *time.Time `json:"finished_at"` // 完成时间
|
FinishedAt *time.Time `json:"finished_at"` // 完成时间
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentTask) TableName() string {
|
func (AgentTask) TableName() string {
|
||||||
@@ -29,17 +28,17 @@ func (AgentTask) TableName() string {
|
|||||||
|
|
||||||
// AgentRule 告警规则(基于 Agent 数据) // Logan.Li
|
// AgentRule 告警规则(基于 Agent 数据) // Logan.Li
|
||||||
type AgentRule struct {
|
type AgentRule struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:255;not null" json:"name"`
|
Name string `gorm:"size:255;not null" json:"name"`
|
||||||
Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机
|
Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机
|
||||||
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process/fd
|
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process/fd
|
||||||
Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= ==
|
Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= ==
|
||||||
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
||||||
Port int `json:"port"` // port_down 专用
|
Port int `json:"port"` // port_down 专用
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
LastTriggered *time.Time `json:"last_triggered"`
|
LastTriggered *time.Time `json:"last_triggered"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentRule) TableName() string {
|
func (AgentRule) TableName() string {
|
||||||
@@ -48,18 +47,18 @@ func (AgentRule) TableName() string {
|
|||||||
|
|
||||||
// AgentAlert Agent 告警事件 // Logan.Li
|
// AgentAlert Agent 告警事件 // Logan.Li
|
||||||
type AgentAlert struct {
|
type AgentAlert struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
RuleName string `gorm:"size:255" json:"rule_name"`
|
RuleName string `gorm:"size:255" json:"rule_name"`
|
||||||
Hostname string `gorm:"size:255;index" json:"hostname"`
|
Hostname string `gorm:"size:255;index" json:"hostname"`
|
||||||
Metric string `gorm:"size:32" json:"metric"`
|
Metric string `gorm:"size:32" json:"metric"`
|
||||||
Severity string `gorm:"size:20;default:warning" json:"severity"` // critical/warning/info
|
Severity string `gorm:"size:20;default:warning" json:"severity"` // critical/warning/info
|
||||||
Message string `gorm:"type:text" json:"message"`
|
Message string `gorm:"type:text" json:"message"`
|
||||||
Value float64 `gorm:"type:decimal(10,2)" json:"value"`
|
Value float64 `gorm:"type:decimal(10,2)" json:"value"`
|
||||||
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
||||||
Resolved bool `gorm:"default:false;index" json:"resolved"`
|
Resolved bool `gorm:"default:false;index" json:"resolved"`
|
||||||
ResolvedAt *time.Time `json:"resolved_at"`
|
ResolvedAt *time.Time `json:"resolved_at"`
|
||||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentAlert) TableName() string {
|
func (AgentAlert) TableName() string {
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// ai_runtime.go - AI 运行、工具能力和执行审计统一模型 // Logan.Li
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AIRun 统一记录 Agent CLI、告警分析、智能助手、MCP/技能调用发起的 AI 任务。 // Logan.Li
|
||||||
|
type AIRun struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
RunID string `gorm:"size:64;uniqueIndex;not null" json:"run_id"` // 对外 trace/run 标识 // Logan.Li
|
||||||
|
ParentRunID string `gorm:"size:64;index" json:"parent_run_id"` // 子任务关联 // Logan.Li
|
||||||
|
UserID uint `gorm:"index" json:"user_id"` // 发起用户 // Logan.Li
|
||||||
|
Feature string `gorm:"size:64;index;not null" json:"feature"` // agent_cli/alert_analysis/assistant/mcp // Logan.Li
|
||||||
|
Mode string `gorm:"size:32;index" json:"mode"` // host/kubernetes/aiops/general // Logan.Li
|
||||||
|
Title string `gorm:"size:255" json:"title"` // 任务标题 // Logan.Li
|
||||||
|
Status string `gorm:"size:32;index;default:running" json:"status"` // running/done/error/aborted // Logan.Li
|
||||||
|
SubjectType string `gorm:"size:64;index" json:"subject_type"` // host/k8s_cluster/namespace/pod/aiops_module/mcp_server/skill // Logan.Li
|
||||||
|
SubjectID string `gorm:"size:128;index" json:"subject_id"` // 资源 ID 或稳定键 // Logan.Li
|
||||||
|
SubjectName string `gorm:"size:255" json:"subject_name"` // 资源快照名称 // Logan.Li
|
||||||
|
SubjectScope string `gorm:"size:255;index" json:"subject_scope"` // prod/ns/app 等范围键 // Logan.Li
|
||||||
|
Provider string `gorm:"size:50" json:"provider"` // LLM 提供商 // Logan.Li
|
||||||
|
Model string `gorm:"size:100" json:"model"` // 模型名 // Logan.Li
|
||||||
|
SafetyMode string `gorm:"size:32" json:"safety_mode"` // strict/balanced/permissive // Logan.Li
|
||||||
|
ContextPackID string `gorm:"size:64;index" json:"context_pack_id"` // 上下文包引用 // Logan.Li
|
||||||
|
PromptTokens int `json:"prompt_tokens"` // 输入 token // Logan.Li
|
||||||
|
CompletionTokens int `json:"completion_tokens"` // 输出 token // Logan.Li
|
||||||
|
TotalTokens int `json:"total_tokens"` // 总 token // Logan.Li
|
||||||
|
Rounds int `json:"rounds"` // Agent 轮次 // Logan.Li
|
||||||
|
Duration int64 `json:"duration"` // 毫秒 // Logan.Li
|
||||||
|
Error string `gorm:"type:text" json:"error,omitempty"` // 错误摘要 // Logan.Li
|
||||||
|
StartedAt time.Time `gorm:"index" json:"started_at"`
|
||||||
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AIRun) TableName() string { return "ai_runs" }
|
||||||
|
|
||||||
|
// AIRunMessage 统一 AI 对话消息,负责回放,不承载工具审计明细。 // Logan.Li
|
||||||
|
type AIRunMessage struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
RunID string `gorm:"size:64;index;not null" json:"run_id"`
|
||||||
|
Round int `gorm:"index" json:"round"`
|
||||||
|
Seq int `gorm:"index" json:"seq"`
|
||||||
|
Role string `gorm:"size:32;index" json:"role"` // system/user/assistant/tool // Logan.Li
|
||||||
|
Type string `gorm:"size:32;index" json:"type"` // message/thought/tool_call/tool_result/done/error/status // Logan.Li
|
||||||
|
Content string `gorm:"type:text" json:"content"` // 脱敏内容 // Logan.Li
|
||||||
|
ToolName string `gorm:"size:128;index" json:"tool_name"` // 可选工具名 // Logan.Li
|
||||||
|
ToolCallID string `gorm:"size:128;index" json:"tool_call_id"` // LLM 工具调用 ID // Logan.Li
|
||||||
|
SpanID string `gorm:"size:64;index" json:"span_id"` // 关联 span // Logan.Li
|
||||||
|
Tokens int `json:"tokens"`
|
||||||
|
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AIRunMessage) TableName() string { return "ai_run_messages" }
|
||||||
|
|
||||||
|
// AIRunSpan 统一 AI trace span,记录 LLM、工具、guardrail、确认、审批等事件。 // Logan.Li
|
||||||
|
type AIRunSpan struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
RunID string `gorm:"size:64;index;not null" json:"run_id"`
|
||||||
|
SpanID string `gorm:"size:64;uniqueIndex;not null" json:"span_id"`
|
||||||
|
ParentSpanID string `gorm:"size:64;index" json:"parent_span_id"`
|
||||||
|
Kind string `gorm:"size:32;index;not null" json:"kind"` // llm/tool/guardrail/confirm/approval/context // Logan.Li
|
||||||
|
Name string `gorm:"size:128;index" json:"name"` // 工具名或步骤名 // Logan.Li
|
||||||
|
CapabilityID uint `gorm:"index" json:"capability_id"` // 能力注册表 ID // Logan.Li
|
||||||
|
ToolSource string `gorm:"size:32;index" json:"tool_source"` // builtin/mcp/skill/shell/kubernetes/aiops // Logan.Li
|
||||||
|
Input string `gorm:"type:text" json:"input"` // 脱敏输入 // Logan.Li
|
||||||
|
Output string `gorm:"type:text" json:"output"` // 脱敏输出摘要 // Logan.Li
|
||||||
|
OutputSize int `json:"output_size"` // 脱敏后输出长度 // Logan.Li
|
||||||
|
Risk string `gorm:"size:32;index" json:"risk"` // safe/low/medium/high/critical // Logan.Li
|
||||||
|
Decision string `gorm:"size:32;index" json:"decision"` // auto/approved/rejected/denied/escalated // Logan.Li
|
||||||
|
Status string `gorm:"size:32;index" json:"status"` // running/success/error/skipped // Logan.Li
|
||||||
|
Error string `gorm:"type:text" json:"error"` // 错误摘要 // Logan.Li
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
Duration int64 `json:"duration"` // 毫秒 // Logan.Li
|
||||||
|
StartedAt time.Time `gorm:"index" json:"started_at"`
|
||||||
|
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AIRunSpan) TableName() string { return "ai_run_spans" }
|
||||||
|
|
||||||
|
// AICapability 注册 AIOps 内置工具、MCP 工具、Skill、Shell/K8s 工具的统一能力目录。 // Logan.Li
|
||||||
|
type AICapability struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Name string `gorm:"size:128;uniqueIndex;not null" json:"name"` // 稳定工具名 // Logan.Li
|
||||||
|
DisplayName string `gorm:"size:128" json:"display_name"`
|
||||||
|
Source string `gorm:"size:32;index;not null" json:"source"` // builtin/mcp/skill/shell/kubernetes/aiops // Logan.Li
|
||||||
|
Category string `gorm:"size:64;index" json:"category"` // asset/k8s/monitor/alert/change/security // Logan.Li
|
||||||
|
RiskLevel string `gorm:"size:32;index;default:safe" json:"risk_level"`
|
||||||
|
Schema string `gorm:"type:text" json:"schema"` // JSON Schema // Logan.Li
|
||||||
|
Config string `gorm:"type:text" json:"config"` // MCP server/skill 配置快照 // Logan.Li
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
Enabled bool `gorm:"default:true;index" json:"enabled"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AICapability) TableName() string { return "ai_capabilities" }
|
||||||
|
|
||||||
|
// AICapabilityPolicy 描述工具能力在用户、角色、资源范围上的准入和审批策略。 // Logan.Li
|
||||||
|
type AICapabilityPolicy struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
|
CapabilityID uint `gorm:"index" json:"capability_id"`
|
||||||
|
SubjectType string `gorm:"size:32;index" json:"subject_type"` // user/role/all // Logan.Li
|
||||||
|
SubjectID string `gorm:"size:64;index" json:"subject_id"`
|
||||||
|
ResourceType string `gorm:"size:64;index" json:"resource_type"` // host/k8s_cluster/namespace/pod/aiops_module/mcp_server/skill // Logan.Li
|
||||||
|
ResourcePattern string `gorm:"size:255" json:"resource_pattern"` // 正则或通配符 // Logan.Li
|
||||||
|
Effect string `gorm:"size:16;index;default:allow" json:"effect"` // allow/deny // Logan.Li
|
||||||
|
RequireConfirm bool `gorm:"default:false" json:"require_confirm"`
|
||||||
|
RequireApproval bool `gorm:"default:false" json:"require_approval"`
|
||||||
|
MaxRisk string `gorm:"size:32;default:high" json:"max_risk"`
|
||||||
|
Enabled bool `gorm:"default:true;index" json:"enabled"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AICapabilityPolicy) TableName() string { return "ai_capability_policies" }
|
||||||
|
|
||||||
|
// OpsSession 统一人机执行会话,覆盖 SSH/RDP/Pod/kubectl/Agent CLI。 // Logan.Li
|
||||||
|
type OpsSession struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
SessionID string `gorm:"size:64;uniqueIndex;not null" json:"session_id"`
|
||||||
|
RunID string `gorm:"size:64;index" json:"run_id"` // AI 发起时关联 ai_runs // Logan.Li
|
||||||
|
UserID uint `gorm:"index" json:"user_id"`
|
||||||
|
SessionType string `gorm:"size:32;index;not null" json:"session_type"` // ssh/rdp/pod_exec/kubectl/agent_cli // Logan.Li
|
||||||
|
ActorType string `gorm:"size:32;index;default:user" json:"actor_type"` // user/ai/system // Logan.Li
|
||||||
|
TargetType string `gorm:"size:64;index" json:"target_type"` // host/k8s_cluster/pod // Logan.Li
|
||||||
|
TargetID string `gorm:"size:128;index" json:"target_id"`
|
||||||
|
TargetName string `gorm:"size:255" json:"target_name"`
|
||||||
|
ClientIP string `gorm:"size:64" json:"client_ip"`
|
||||||
|
Status string `gorm:"size:32;index" json:"status"` // active/closed/failed // Logan.Li
|
||||||
|
RecordRef string `gorm:"size:255" json:"record_ref"` // 录制对象存储或分片引用 // Logan.Li
|
||||||
|
RecordSize int `json:"record_size"`
|
||||||
|
Duration int64 `json:"duration"` // 秒 // Logan.Li
|
||||||
|
Error string `gorm:"type:text" json:"error,omitempty"`
|
||||||
|
StartedAt time.Time `gorm:"index" json:"started_at"`
|
||||||
|
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OpsSession) TableName() string { return "ai_ops_sessions" }
|
||||||
|
|
||||||
|
// OpsCommand 统一命令/动作审计,覆盖人执行和 AI 工具执行。 // Logan.Li
|
||||||
|
type OpsCommand struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
CommandID string `gorm:"size:64;uniqueIndex;not null" json:"command_id"`
|
||||||
|
SessionID string `gorm:"size:64;index" json:"session_id"`
|
||||||
|
RunID string `gorm:"size:64;index" json:"run_id"`
|
||||||
|
SpanID string `gorm:"size:64;index" json:"span_id"`
|
||||||
|
UserID uint `gorm:"index" json:"user_id"`
|
||||||
|
ActorType string `gorm:"size:32;index;default:user" json:"actor_type"` // user/ai/system // Logan.Li
|
||||||
|
TargetType string `gorm:"size:64;index" json:"target_type"`
|
||||||
|
TargetID string `gorm:"size:128;index" json:"target_id"`
|
||||||
|
CommandType string `gorm:"size:32;index" json:"command_type"` // shell/k8s_action/mcp/skill/api // Logan.Li
|
||||||
|
Command string `gorm:"type:text" json:"command"` // 脱敏命令或动作参数 // Logan.Li
|
||||||
|
Risk string `gorm:"size:32;index" json:"risk"`
|
||||||
|
Decision string `gorm:"size:32;index" json:"decision"`
|
||||||
|
Status string `gorm:"size:32;index" json:"status"`
|
||||||
|
ExitCode int `gorm:"default:-1" json:"exit_code"`
|
||||||
|
OutputRef string `gorm:"size:255" json:"output_ref"` // 大输出引用 // Logan.Li
|
||||||
|
OutputSize int `json:"output_size"`
|
||||||
|
Duration int64 `json:"duration"` // 毫秒 // Logan.Li
|
||||||
|
Error string `gorm:"type:text" json:"error"`
|
||||||
|
ExecutedAt time.Time `gorm:"index" json:"executed_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OpsCommand) TableName() string { return "ai_ops_commands" }
|
||||||
+112
-111
@@ -56,41 +56,41 @@ const (
|
|||||||
|
|
||||||
// AlertRule 告警规则(增强版)
|
// AlertRule 告警规则(增强版)
|
||||||
type AlertRule struct {
|
type AlertRule struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||||
Name string `gorm:"type:varchar(128);not null;comment:规则名称" json:"name"`
|
Name string `gorm:"type:varchar(128);not null;comment:规则名称" json:"name"`
|
||||||
Description string `gorm:"type:varchar(500);comment:规则描述" json:"description,omitempty"`
|
Description string `gorm:"type:varchar(500);comment:规则描述" json:"description,omitempty"`
|
||||||
// ====== 原 V1 兼容字段 ======
|
// ====== 原 V1 兼容字段 ======
|
||||||
Type string `gorm:"type:varchar(32);comment:规则类型: metric/log/event" json:"type,omitempty"`
|
Type string `gorm:"type:varchar(32);comment:规则类型: metric/log/event" json:"type,omitempty"`
|
||||||
Source string `gorm:"type:varchar(64);comment:数据源: prometheus/zabbix/custom/loki/elasticsearch" json:"source,omitempty"`
|
Source string `gorm:"type:varchar(64);comment:数据源: prometheus/zabbix/custom/loki/elasticsearch" json:"source,omitempty"`
|
||||||
Expression string `gorm:"type:text;comment:PromQL/Zabbix/LogQL/Lucene表达式" json:"expression,omitempty"`
|
Expression string `gorm:"type:text;comment:PromQL/Zabbix/LogQL/Lucene表达式" json:"expression,omitempty"`
|
||||||
DatasourceID uint `gorm:"index;comment:日志/指标数据源ID" json:"datasource_id,omitempty"`
|
DatasourceID uint `gorm:"index;comment:日志/指标数据源ID" json:"datasource_id,omitempty"`
|
||||||
Severity string `gorm:"type:varchar(32);default:'warning';comment:告警级别" json:"severity"`
|
Severity string `gorm:"type:varchar(32);default:'warning';comment:告警级别" json:"severity"`
|
||||||
Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"`
|
Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"`
|
||||||
Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"`
|
Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"`
|
||||||
// ====== V2 增强字段 ======
|
// ====== V2 增强字段 ======
|
||||||
Status AlertRuleStatus `gorm:"type:varchar(16);default:'enabled';comment:规则状态" json:"status,omitempty"`
|
Status AlertRuleStatus `gorm:"type:varchar(16);default:'enabled';comment:规则状态" json:"status,omitempty"`
|
||||||
TargetType string `gorm:"type:varchar(32);comment:目标类型" json:"target_type,omitempty"`
|
TargetType string `gorm:"type:varchar(32);comment:目标类型" json:"target_type,omitempty"`
|
||||||
TargetID uint `gorm:"index;comment:目标ID(0表示全部)" json:"target_id,omitempty"`
|
TargetID uint `gorm:"index;comment:目标ID(0表示全部)" json:"target_id,omitempty"`
|
||||||
TargetTags string `gorm:"type:varchar(256);comment:目标标签筛选" json:"target_tags,omitempty"`
|
TargetTags string `gorm:"type:varchar(256);comment:目标标签筛选" json:"target_tags,omitempty"`
|
||||||
Conditions string `gorm:"type:text;comment:触发条件JSON数组" json:"conditions,omitempty"`
|
Conditions string `gorm:"type:text;comment:触发条件JSON数组" json:"conditions,omitempty"`
|
||||||
AggWindow int `gorm:"default:300;comment:聚合窗口(秒)" json:"agg_window,omitempty"`
|
AggWindow int `gorm:"default:300;comment:聚合窗口(秒)" json:"agg_window,omitempty"`
|
||||||
AggFunction string `gorm:"type:varchar(16);default:'avg';comment:聚合函数" json:"agg_function,omitempty"`
|
AggFunction string `gorm:"type:varchar(16);default:'avg';comment:聚合函数" json:"agg_function,omitempty"`
|
||||||
RecoverCount int `gorm:"default:1;comment:恢复次数" json:"recover_count,omitempty"`
|
RecoverCount int `gorm:"default:1;comment:恢复次数" json:"recover_count,omitempty"`
|
||||||
RecoverWindow int `gorm:"default:300;comment:恢复检查窗口(秒)" json:"recover_window,omitempty"`
|
RecoverWindow int `gorm:"default:300;comment:恢复检查窗口(秒)" json:"recover_window,omitempty"`
|
||||||
Interval int `gorm:"default:60;comment:评估间隔(秒)" json:"interval,omitempty"`
|
Interval int `gorm:"default:60;comment:评估间隔(秒)" json:"interval,omitempty"`
|
||||||
CheckTimes int `gorm:"default:1;comment:连续触发次数" json:"check_times,omitempty"`
|
CheckTimes int `gorm:"default:1;comment:连续触发次数" json:"check_times,omitempty"`
|
||||||
NotifyMode string `gorm:"type:varchar(16);default:'every';comment:通知模式: every/once/upgrade" json:"notify_mode,omitempty"`
|
NotifyMode string `gorm:"type:varchar(16);default:'every';comment:通知模式: every/once/upgrade" json:"notify_mode,omitempty"`
|
||||||
ChannelIDs string `gorm:"type:varchar(256);comment:通知渠道ID列表JSON" json:"channel_ids,omitempty"`
|
ChannelIDs string `gorm:"type:varchar(256);comment:通知渠道ID列表JSON" json:"channel_ids,omitempty"`
|
||||||
NotifyChannels string `gorm:"type:text;comment:通知渠道JSON(兼容V1)" json:"notify_channels,omitempty"`
|
NotifyChannels string `gorm:"type:text;comment:通知渠道JSON(兼容V1)" json:"notify_channels,omitempty"`
|
||||||
TemplateID uint `gorm:"comment:通知模板ID" json:"template_id,omitempty"`
|
TemplateID uint `gorm:"comment:通知模板ID" json:"template_id,omitempty"`
|
||||||
UpgradeID uint `gorm:"comment:升级策略ID" json:"upgrade_id,omitempty"`
|
UpgradeID uint `gorm:"comment:升级策略ID" json:"upgrade_id,omitempty"`
|
||||||
SilenceID uint `gorm:"comment:静默规则ID" json:"silence_id,omitempty"`
|
SilenceID uint `gorm:"comment:静默规则ID" json:"silence_id,omitempty"`
|
||||||
Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"`
|
Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"`
|
||||||
Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"`
|
Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"`
|
||||||
CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by,omitempty"`
|
CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AlertRule) TableName() string {
|
func (AlertRule) TableName() string {
|
||||||
@@ -112,40 +112,40 @@ const (
|
|||||||
|
|
||||||
// AlertEvent 告警事件(增强版)
|
// AlertEvent 告警事件(增强版)
|
||||||
type AlertEvent struct {
|
type AlertEvent struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||||
RuleID uint `gorm:"not null;index:idx_rule_time;comment:规则ID" json:"rule_id"`
|
RuleID uint `gorm:"not null;index:idx_rule_time;comment:规则ID" json:"rule_id"`
|
||||||
RuleName string `gorm:"type:varchar(128);comment:规则名称" json:"rule_name,omitempty"`
|
RuleName string `gorm:"type:varchar(128);comment:规则名称" json:"rule_name,omitempty"`
|
||||||
Severity string `gorm:"type:varchar(32);comment:告警级别" json:"severity,omitempty"`
|
Severity string `gorm:"type:varchar(32);comment:告警级别" json:"severity,omitempty"`
|
||||||
Status string `gorm:"type:varchar(32);default:'firing';comment:事件状态" json:"status"`
|
Status string `gorm:"type:varchar(32);default:'firing';comment:事件状态" json:"status"`
|
||||||
// ====== 原V1兼容字段 ======
|
// ====== 原V1兼容字段 ======
|
||||||
Target string `gorm:"type:varchar(255);comment:目标标签" json:"target,omitempty"`
|
Target string `gorm:"type:varchar(255);comment:目标标签" json:"target,omitempty"`
|
||||||
Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"`
|
Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"`
|
||||||
Value float64 `gorm:"comment:触发值" json:"value,omitempty"`
|
Value float64 `gorm:"comment:触发值" json:"value,omitempty"`
|
||||||
// ====== V2增强字段 ======
|
// ====== V2增强字段 ======
|
||||||
TargetID uint `gorm:"index;comment:目标ID" json:"target_id,omitempty"`
|
TargetID uint `gorm:"index;comment:目标ID" json:"target_id,omitempty"`
|
||||||
TargetName string `gorm:"type:varchar(128);comment:目标名称" json:"target_name,omitempty"`
|
TargetName string `gorm:"type:varchar(128);comment:目标名称" json:"target_name,omitempty"`
|
||||||
TargetIP string `gorm:"type:varchar(64);comment:目标IP" json:"target_ip,omitempty"`
|
TargetIP string `gorm:"type:varchar(64);comment:目标IP" json:"target_ip,omitempty"`
|
||||||
Title string `gorm:"type:varchar(256);comment:告警标题" json:"title,omitempty"`
|
Title string `gorm:"type:varchar(256);comment:告警标题" json:"title,omitempty"`
|
||||||
MetricName string `gorm:"type:varchar(64);comment:指标名称" json:"metric_name,omitempty"`
|
MetricName string `gorm:"type:varchar(64);comment:指标名称" json:"metric_name,omitempty"`
|
||||||
Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"`
|
Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"`
|
||||||
Operator string `gorm:"type:varchar(8);comment:运算符" json:"operator,omitempty"`
|
Operator string `gorm:"type:varchar(8);comment:运算符" json:"operator,omitempty"`
|
||||||
Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"`
|
Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"`
|
||||||
Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"`
|
Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"`
|
||||||
Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"`
|
Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"`
|
||||||
StartedAt time.Time `gorm:"not null;index:idx_rule_time;comment:触发时间" json:"started_at"`
|
StartedAt time.Time `gorm:"not null;index:idx_rule_time;comment:触发时间" json:"started_at"`
|
||||||
ResolvedAt *time.Time `gorm:"comment:恢复时间" json:"resolved_at,omitempty"`
|
ResolvedAt *time.Time `gorm:"comment:恢复时间" json:"resolved_at,omitempty"`
|
||||||
Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"`
|
Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"`
|
||||||
LastEvalAt time.Time `gorm:"comment:最后评估时间" json:"last_eval_at,omitempty"`
|
LastEvalAt time.Time `gorm:"comment:最后评估时间" json:"last_eval_at,omitempty"`
|
||||||
NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count,omitempty"`
|
NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count,omitempty"`
|
||||||
LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"`
|
LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"`
|
||||||
UpgradeLevel int `gorm:"default:0;comment:当前升级级别" json:"upgrade_level,omitempty"`
|
UpgradeLevel int `gorm:"default:0;comment:当前升级级别" json:"upgrade_level,omitempty"`
|
||||||
Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"`
|
Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"`
|
||||||
AcknowledgedBy *uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"`
|
AcknowledgedBy *uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"`
|
||||||
AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"`
|
AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"`
|
||||||
AckComment string `gorm:"type:varchar(256);comment:确认备注" json:"ack_comment,omitempty"`
|
AckComment string `gorm:"type:varchar(256);comment:确认备注" json:"ack_comment,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Rule *AlertRule `json:"rule,omitempty" gorm:"foreignKey:RuleID"`
|
Rule *AlertRule `json:"rule,omitempty" gorm:"foreignKey:RuleID"`
|
||||||
@@ -160,14 +160,14 @@ func (AlertEvent) TableName() string {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
type AlertCondition struct {
|
type AlertCondition struct {
|
||||||
Metric string `json:"metric"`
|
Metric string `json:"metric"`
|
||||||
Operator Operator `json:"operator"`
|
Operator Operator `json:"operator"`
|
||||||
Threshold float64 `json:"threshold"`
|
Threshold float64 `json:"threshold"`
|
||||||
Unit string `json:"unit,omitempty"`
|
Unit string `json:"unit,omitempty"`
|
||||||
Index string `json:"index,omitempty"`
|
Index string `json:"index,omitempty"`
|
||||||
LogicOp string `json:"logic_op,omitempty"`
|
LogicOp string `json:"logic_op,omitempty"`
|
||||||
AggFunc AggFunction `json:"agg_func,omitempty"`
|
AggFunc AggFunction `json:"agg_func,omitempty"`
|
||||||
Window int `json:"window,omitempty"`
|
Window int `json:"window,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AlertCondition) TableName() string {
|
func (AlertCondition) TableName() string {
|
||||||
@@ -184,21 +184,21 @@ type AlertConditionGroup struct {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
type AlertNotifyTemplate struct {
|
type AlertNotifyTemplate struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||||
Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"`
|
Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"`
|
||||||
Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"`
|
Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"`
|
||||||
ChannelType string `gorm:"type:varchar(32);not null;comment:适用渠道类型: email/sms/webhook/dingtalk/wecom/feishu" json:"channel_type"`
|
ChannelType string `gorm:"type:varchar(32);not null;comment:适用渠道类型: email/sms/webhook/dingtalk/wecom/feishu" json:"channel_type"`
|
||||||
TitleTemplate string `gorm:"type:varchar(256);comment:标题模板" json:"title_template,omitempty"`
|
TitleTemplate string `gorm:"type:varchar(256);comment:标题模板" json:"title_template,omitempty"`
|
||||||
BodyTemplate string `gorm:"type:text;comment:正文模板" json:"body_template,omitempty"`
|
BodyTemplate string `gorm:"type:text;comment:正文模板" json:"body_template,omitempty"`
|
||||||
Variables string `gorm:"type:text;comment:可用变量说明JSON" json:"variables,omitempty"`
|
Variables string `gorm:"type:text;comment:可用变量说明JSON" json:"variables,omitempty"`
|
||||||
RecoverTitleTemplate string `gorm:"type:varchar(256);comment:恢复标题模板" json:"recover_title_template,omitempty"`
|
RecoverTitleTemplate string `gorm:"type:varchar(256);comment:恢复标题模板" json:"recover_title_template,omitempty"`
|
||||||
RecoverBodyTemplate string `gorm:"type:text;comment:恢复正文模板" json:"recover_body_template,omitempty"`
|
RecoverBodyTemplate string `gorm:"type:text;comment:恢复正文模板" json:"recover_body_template,omitempty"`
|
||||||
IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"`
|
IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"`
|
||||||
Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"`
|
Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"`
|
||||||
CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"`
|
CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"`
|
||||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AlertNotifyTemplate) TableName() string {
|
func (AlertNotifyTemplate) TableName() string {
|
||||||
@@ -249,12 +249,12 @@ func (AlertUpgrade) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertUpgradeRule struct {
|
type AlertUpgradeRule struct {
|
||||||
Level int `json:"level"`
|
Level int `json:"level"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
Severity string `json:"severity"`
|
Severity string `json:"severity"`
|
||||||
ChannelIDs []string `json:"channel_ids"`
|
ChannelIDs []string `json:"channel_ids"`
|
||||||
NotifyUsers []uint `json:"notify_users"`
|
NotifyUsers []uint `json:"notify_users"`
|
||||||
RepeatInterval int `json:"repeat_interval"`
|
RepeatInterval int `json:"repeat_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertUpgradeRecord struct {
|
type AlertUpgradeRecord struct {
|
||||||
@@ -321,19 +321,19 @@ type AlertSilenceCheckResult struct {
|
|||||||
|
|
||||||
// NotifyChannel 通知渠道模型
|
// NotifyChannel 通知渠道模型
|
||||||
type NotifyChannel struct {
|
type NotifyChannel struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:64;not null" json:"name"`
|
Name string `gorm:"size:64;not null" json:"name"`
|
||||||
Type string `gorm:"size:32;not null" json:"type"` // email, sms, webhook, dingtalk, wechat
|
Type string `gorm:"size:32;not null" json:"type"` // email, sms, webhook, dingtalk, wechat
|
||||||
Config string `gorm:"type:text" json:"config"` // JSON
|
Config string `gorm:"type:text" json:"config"` // JSON
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
TestStatus string `gorm:"size:32" json:"test_status"`
|
TestStatus string `gorm:"size:32" json:"test_status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (NotifyChannel) TableName() string {
|
func (NotifyChannel) TableName() string {
|
||||||
return "monitor_notify_channels"
|
return "monitor_alert_channels"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricData 监控指标数据
|
// MetricData 监控指标数据
|
||||||
@@ -383,14 +383,14 @@ type UpdateAlertRuleRequest struct {
|
|||||||
|
|
||||||
// AlertStats 告警统计
|
// AlertStats 告警统计
|
||||||
type AlertStats struct {
|
type AlertStats struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Firing int64 `json:"firing"`
|
Firing int64 `json:"firing"`
|
||||||
Resolved int64 `json:"resolved"`
|
Resolved int64 `json:"resolved"`
|
||||||
Critical int64 `json:"critical"`
|
Critical int64 `json:"critical"`
|
||||||
Warning int64 `json:"warning"`
|
Warning int64 `json:"warning"`
|
||||||
Info int64 `json:"info"`
|
Info int64 `json:"info"`
|
||||||
TodayCount int64 `json:"today_count"`
|
TodayCount int64 `json:"today_count"`
|
||||||
WeekCount int64 `json:"week_count"`
|
WeekCount int64 `json:"week_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcknowledgeAlertRequest 确认告警请求
|
// AcknowledgeAlertRequest 确认告警请求
|
||||||
@@ -491,7 +491,7 @@ type FeishuWebhookConfig struct {
|
|||||||
|
|
||||||
// FeishuCardMessage 飞书卡片消息
|
// FeishuCardMessage 飞书卡片消息
|
||||||
type FeishuCardMessage struct {
|
type FeishuCardMessage struct {
|
||||||
MsgType string `json:"msg_type"`
|
MsgType string `json:"msg_type"`
|
||||||
Content FeishuCardContent `json:"content"`
|
Content FeishuCardContent `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,6 +499,7 @@ type FeishuCardContent struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content [][]FeishuCardElement `json:"content"`
|
Content [][]FeishuCardElement `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FeishuCardElement 飞书卡片元素
|
// FeishuCardElement 飞书卡片元素
|
||||||
type FeishuCardElement struct {
|
type FeishuCardElement struct {
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ type AlertNotifyPolicy struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AlertNotifyPolicy) TableName() string { return "monitor_alert_notify_policies" }
|
func (AlertNotifyPolicy) TableName() string { return "monitor_alert_notify_policies" }
|
||||||
@@ -12,4 +12,4 @@ type AlertNotifySettings struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AlertNotifySettings) TableName() string { return "monitor_alert_notify_settings" }
|
func (AlertNotifySettings) TableName() string { return "monitor_alert_notify_settings" }
|
||||||
+40
-41
@@ -2,44 +2,43 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApprovalTemplate 审批模板模型
|
// ApprovalTemplate 审批模板模型
|
||||||
type ApprovalTemplate struct {
|
type ApprovalTemplate struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
Type string `gorm:"size:32;not null" json:"type"` // release, server_apply, app_deploy, permission_request
|
Type string `gorm:"size:32;not null" json:"type"` // release, server_apply, app_deploy, permission_request
|
||||||
FormSchema string `gorm:"type:text" json:"form_schema"` // JSON form fields config
|
FormSchema string `gorm:"type:text" json:"form_schema"` // JSON form fields config
|
||||||
FlowConfig string `gorm:"type:text" json:"flow_config"` // JSON approval flow steps
|
FlowConfig string `gorm:"type:text" json:"flow_config"` // JSON approval flow steps
|
||||||
Category string `gorm:"size:32;default:resource" json:"category"` // resource, app, permission
|
Category string `gorm:"size:32;default:resource" json:"category"` // resource, app, permission
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (ApprovalTemplate) TableName() string {
|
func (ApprovalTemplate) TableName() string {
|
||||||
return "sys_approval_templates"
|
return "approval_templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApprovalProcess 审批流程模型
|
// ApprovalProcess 审批流程模型
|
||||||
type ApprovalProcess struct {
|
type ApprovalProcess struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
TemplateID uint `json:"template_id"`
|
TemplateID uint `json:"template_id"`
|
||||||
Title string `gorm:"size:255;not null" json:"title"`
|
Title string `gorm:"size:255;not null" json:"title"`
|
||||||
Applicant uint `json:"applicant"`
|
Applicant uint `json:"applicant"`
|
||||||
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
|
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
|
||||||
FormData string `gorm:"type:text" json:"form_data"` // JSON
|
FormData string `gorm:"type:text" json:"form_data"` // JSON
|
||||||
CurrentStep int `gorm:"default:0" json:"current_step"`
|
CurrentStep int `gorm:"default:0" json:"current_step"`
|
||||||
TotalSteps int `json:"total_steps"`
|
TotalSteps int `json:"total_steps"`
|
||||||
ApprovedAt *time.Time `json:"approved_at"`
|
ApprovedAt *time.Time `json:"approved_at"`
|
||||||
RejectedAt *time.Time `json:"rejected_at"`
|
RejectedAt *time.Time `json:"rejected_at"`
|
||||||
RejectReason string `gorm:"size:500" json:"reject_reason"`
|
RejectReason string `gorm:"size:500" json:"reject_reason"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Template *ApprovalTemplate `gorm:"foreignKey:TemplateID" json:"template,omitempty"`
|
Template *ApprovalTemplate `gorm:"foreignKey:TemplateID" json:"template,omitempty"`
|
||||||
@@ -54,16 +53,16 @@ func (ApprovalProcess) TableName() string {
|
|||||||
|
|
||||||
// ApprovalStep 审批步骤模型
|
// ApprovalStep 审批步骤模型
|
||||||
type ApprovalStep struct {
|
type ApprovalStep struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
ProcessID uint `json:"process_id"`
|
ProcessID uint `json:"process_id"`
|
||||||
Step int `json:"step"`
|
Step int `json:"step"`
|
||||||
StepType string `gorm:"size:32;default:audit" json:"step_type"` // audit, execute
|
StepType string `gorm:"size:32;default:audit" json:"step_type"` // audit, execute
|
||||||
Approver uint `json:"approver"`
|
Approver uint `json:"approver"`
|
||||||
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
|
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
|
||||||
Comment string `gorm:"size:500" json:"comment"`
|
Comment string `gorm:"size:500" json:"comment"`
|
||||||
ApprovedAt *time.Time `json:"approved_at"`
|
ApprovedAt *time.Time `json:"approved_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
ApproverUser *User `gorm:"foreignKey:Approver" json:"approver_user,omitempty"`
|
ApproverUser *User `gorm:"foreignKey:Approver" json:"approver_user,omitempty"`
|
||||||
@@ -94,15 +93,15 @@ type CreateApprovalProcessRequest struct {
|
|||||||
|
|
||||||
// ApproveRequest 审批请求
|
// ApproveRequest 审批请求
|
||||||
type ApproveRequest struct {
|
type ApproveRequest struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApprovalStats 审批统计
|
// ApprovalStats 审批统计
|
||||||
type ApprovalStats struct {
|
type ApprovalStats struct {
|
||||||
Pending int64 `json:"pending"`
|
Pending int64 `json:"pending"`
|
||||||
Approved int64 `json:"approved"`
|
Approved int64 `json:"approved"`
|
||||||
Rejected int64 `json:"rejected"`
|
Rejected int64 `json:"rejected"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
MyPending int64 `json:"my_pending"`
|
MyPending int64 `json:"my_pending"`
|
||||||
}
|
}
|
||||||
@@ -17,52 +17,52 @@ type ApprovalWorkflow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ApprovalWorkflow) TableName() string {
|
func (ApprovalWorkflow) TableName() string {
|
||||||
return "sys_approval_workflows"
|
return "approval_workflows"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApprovalWorkflowStep 审批流程步骤
|
// ApprovalWorkflowStep 审批流程步骤
|
||||||
type ApprovalWorkflowStep struct {
|
type ApprovalWorkflowStep struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
WorkflowID uint `json:"workflow_id"`
|
WorkflowID uint `json:"workflow_id"`
|
||||||
StepNumber int `gorm:"not null" json:"step_number"`
|
StepNumber int `gorm:"not null" json:"step_number"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
StepType string `gorm:"size:32;not null" json:"step_type"` // submit, audit, execute
|
StepType string `gorm:"size:32;not null" json:"step_type"` // submit, audit, execute
|
||||||
ApproverIDs string `gorm:"type:text" json:"approver_ids"` // JSON array of user IDs
|
ApproverIDs string `gorm:"type:text" json:"approver_ids"` // JSON array of user IDs
|
||||||
IsRequired bool `gorm:"default:true" json:"is_required"`
|
IsRequired bool `gorm:"default:true" json:"is_required"`
|
||||||
TimeoutHours int `gorm:"default:24" json:"timeout_hours"`
|
TimeoutHours int `gorm:"default:24" json:"timeout_hours"`
|
||||||
AutoApprove bool `gorm:"default:false" json:"auto_approve_on_timeout"`
|
AutoApprove bool `gorm:"default:false" json:"auto_approve_on_timeout"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ApprovalWorkflowStep) TableName() string {
|
func (ApprovalWorkflowStep) TableName() string {
|
||||||
return "sys_approval_workflow_steps"
|
return "approval_workflow_steps"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWorkflowRequest 创建审批流程请求
|
// CreateWorkflowRequest 创建审批流程请求
|
||||||
type CreateWorkflowRequest struct {
|
type CreateWorkflowRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Steps []CreateWorkflowStepReq `json:"steps"`
|
Steps []CreateWorkflowStepReq `json:"steps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateWorkflowStepReq struct {
|
type CreateWorkflowStepReq struct {
|
||||||
StepNumber int `json:"step_number"`
|
StepNumber int `json:"step_number"`
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
StepType string `json:"step_type" binding:"required,oneof=submit audit execute"`
|
StepType string `json:"step_type" binding:"required,oneof=submit audit execute"`
|
||||||
ApproverIDs string `json:"approver_ids"`
|
ApproverIDs string `json:"approver_ids"`
|
||||||
IsRequired bool `json:"is_required"`
|
IsRequired bool `json:"is_required"`
|
||||||
TimeoutHours int `json:"timeout_hours"`
|
TimeoutHours int `json:"timeout_hours"`
|
||||||
AutoApproveTimeout bool `json:"auto_approve_on_timeout"`
|
AutoApproveTimeout bool `json:"auto_approve_on_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWorkflowRequest 更新审批流程请求
|
// UpdateWorkflowRequest 更新审批流程请求
|
||||||
type UpdateWorkflowRequest struct {
|
type UpdateWorkflowRequest struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Category *string `json:"category,omitempty"`
|
Category *string `json:"category,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
IsDefault *bool `json:"is_default,omitempty"`
|
IsDefault *bool `json:"is_default,omitempty"`
|
||||||
Steps []CreateWorkflowStepReq `json:"steps,omitempty"`
|
Steps []CreateWorkflowStepReq `json:"steps,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ type AuditLog struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (AuditLog) TableName() string {
|
func (AuditLog) TableName() string {
|
||||||
return "sys_audit_logs"
|
return "audit_operation_logs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuditLogQuery 审计日志查询
|
// AuditLogQuery 审计日志查询
|
||||||
@@ -48,11 +48,11 @@ type AuditLogListResponse struct {
|
|||||||
|
|
||||||
// AuditStats 审计统计
|
// AuditStats 审计统计
|
||||||
type AuditStats struct {
|
type AuditStats struct {
|
||||||
TodayCount int64 `json:"today_count"`
|
TodayCount int64 `json:"today_count"`
|
||||||
WeekCount int64 `json:"week_count"`
|
WeekCount int64 `json:"week_count"`
|
||||||
MonthCount int64 `json:"month_count"`
|
MonthCount int64 `json:"month_count"`
|
||||||
TopModules []ModuleStat `json:"top_modules"`
|
TopModules []ModuleStat `json:"top_modules"`
|
||||||
TopUsers []UserStat `json:"top_users"`
|
TopUsers []UserStat `json:"top_users"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleStat 模块统计
|
// ModuleStat 模块统计
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type AuditSettings struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AuditSettings) TableName() string { return "sys_audit_settings" }
|
func (AuditSettings) TableName() string { return "audit_settings" }
|
||||||
|
|
||||||
// AllowedAuditRetentionDays 允许的日志保留天数 // Logan.Li
|
// AllowedAuditRetentionDays 允许的日志保留天数 // Logan.Li
|
||||||
var AllowedAuditRetentionDays = []int{7, 15, 30, 90, 180, 365}
|
var AllowedAuditRetentionDays = []int{7, 15, 30, 90, 180, 365}
|
||||||
|
|||||||
+63
-64
@@ -2,30 +2,29 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CronJob 定时任务模型
|
// CronJob 定时任务模型
|
||||||
type CronJob struct {
|
type CronJob struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
CronExpr string `gorm:"size:64;not null" json:"cron_expr"`
|
CronExpr string `gorm:"size:64;not null" json:"cron_expr"`
|
||||||
Type string `gorm:"size:32;not null" json:"type"` // shell, python, command
|
Type string `gorm:"size:32;not null" json:"type"` // shell, python, command
|
||||||
Content string `gorm:"type:text" json:"content"`
|
Content string `gorm:"type:text" json:"content"`
|
||||||
ScriptID *uint `json:"script_id"`
|
ScriptID *uint `json:"script_id"`
|
||||||
TargetType string `gorm:"size:32;default:server" json:"target_type"` // server, group, tag
|
TargetType string `gorm:"size:32;default:server" json:"target_type"` // server, group, tag
|
||||||
Targets string `gorm:"type:text" json:"targets"` // JSON
|
Targets string `gorm:"type:text" json:"targets"` // JSON
|
||||||
Timeout int `gorm:"default:300" json:"timeout"`
|
Timeout int `gorm:"default:300" json:"timeout"`
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
LastRunAt *time.Time `json:"last_run_at"`
|
LastRunAt *time.Time `json:"last_run_at"`
|
||||||
LastStatus string `gorm:"size:32" json:"last_status"`
|
LastStatus string `gorm:"size:32" json:"last_status"`
|
||||||
RunCount int `gorm:"default:0" json:"run_count"`
|
RunCount int `gorm:"default:0" json:"run_count"`
|
||||||
SuccessCount int `gorm:"default:0" json:"success_count"`
|
SuccessCount int `gorm:"default:0" json:"success_count"`
|
||||||
FailCount int `gorm:"default:0" json:"fail_count"`
|
FailCount int `gorm:"default:0" json:"fail_count"`
|
||||||
NotifyOn string `gorm:"size:32;default:failure" json:"notify_on"` // always, failure, success
|
NotifyOn string `gorm:"size:32;default:failure" json:"notify_on"` // always, failure, success
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Script *Script `json:"script,omitempty"`
|
Script *Script `json:"script,omitempty"`
|
||||||
@@ -38,20 +37,20 @@ func (CronJob) TableName() string {
|
|||||||
|
|
||||||
// Script 脚本库模型
|
// Script 脚本库模型
|
||||||
type Script struct {
|
type Script struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
Type string `gorm:"size:32;default:shell" json:"type"` // shell, python
|
Type string `gorm:"size:32;default:shell" json:"type"` // shell, python
|
||||||
Content string `gorm:"type:text;not null" json:"content"`
|
Content string `gorm:"type:text;not null" json:"content"`
|
||||||
Params string `gorm:"type:text" json:"params"` // JSON
|
Params string `gorm:"type:text" json:"params"` // JSON
|
||||||
Timeout int `gorm:"default:300" json:"timeout"`
|
Timeout int `gorm:"default:300" json:"timeout"`
|
||||||
Version int `gorm:"default:1" json:"version"`
|
Version int `gorm:"default:1" json:"version"`
|
||||||
RunCount int `gorm:"default:0" json:"run_count"`
|
RunCount int `gorm:"default:0" json:"run_count"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
UpdatedBy uint `json:"updated_by"`
|
UpdatedBy uint `json:"updated_by"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
@@ -61,21 +60,21 @@ func (Script) TableName() string {
|
|||||||
|
|
||||||
// TaskExecution 任务执行记录(cron / script,不含 Ansible;Ansible 走 AnsibleJob)
|
// TaskExecution 任务执行记录(cron / script,不含 Ansible;Ansible 走 AnsibleJob)
|
||||||
type TaskExecution struct {
|
type TaskExecution struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
TaskType string `gorm:"size:32;not null" json:"task_type"` // cron, script
|
TaskType string `gorm:"size:32;not null" json:"task_type"` // cron, script
|
||||||
TaskID uint `json:"task_id"`
|
TaskID uint `json:"task_id"`
|
||||||
TaskName string `gorm:"size:128" json:"task_name"`
|
TaskName string `gorm:"size:128" json:"task_name"`
|
||||||
Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled
|
Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled
|
||||||
TargetType string `gorm:"size:32" json:"target_type"`
|
TargetType string `gorm:"size:32" json:"target_type"`
|
||||||
Targets string `gorm:"type:text" json:"targets"`
|
Targets string `gorm:"type:text" json:"targets"`
|
||||||
Output string `gorm:"type:longtext" json:"output"`
|
Output string `gorm:"type:longtext" json:"output"`
|
||||||
Error string `gorm:"type:text" json:"error"`
|
Error string `gorm:"type:text" json:"error"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
StartedAt *time.Time `json:"started_at"`
|
StartedAt *time.Time `json:"started_at"`
|
||||||
FinishedAt *time.Time `json:"finished_at"`
|
FinishedAt *time.Time `json:"finished_at"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
@@ -85,16 +84,16 @@ func (TaskExecution) TableName() string {
|
|||||||
|
|
||||||
// CreateCronJobRequest 创建定时任务请求
|
// CreateCronJobRequest 创建定时任务请求
|
||||||
type CreateCronJobRequest struct {
|
type CreateCronJobRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
CronExpr string `json:"cron_expr" binding:"required"`
|
CronExpr string `json:"cron_expr" binding:"required"`
|
||||||
Type string `json:"type" binding:"required,oneof=shell python powershell command bash"`
|
Type string `json:"type" binding:"required,oneof=shell python powershell command bash"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ScriptID *uint `json:"script_id"`
|
ScriptID *uint `json:"script_id"`
|
||||||
TargetType string `json:"target_type" binding:"omitempty,oneof=server group tag"`
|
TargetType string `json:"target_type" binding:"omitempty,oneof=server group tag"`
|
||||||
Targets []uint `json:"targets"`
|
Targets []uint `json:"targets"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
NotifyOn string `json:"notify_on" binding:"omitempty,oneof=always failure success"`
|
NotifyOn string `json:"notify_on" binding:"omitempty,oneof=always failure success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateScriptRequest 创建脚本请求
|
// CreateScriptRequest 创建脚本请求
|
||||||
@@ -117,9 +116,9 @@ type ExecuteScriptRequest struct {
|
|||||||
|
|
||||||
// AutomationStats 自动化统计
|
// AutomationStats 自动化统计
|
||||||
type AutomationStats struct {
|
type AutomationStats struct {
|
||||||
CronJobCount int64 `json:"cron_job_count"`
|
CronJobCount int64 `json:"cron_job_count"`
|
||||||
ScriptCount int64 `json:"script_count"`
|
ScriptCount int64 `json:"script_count"`
|
||||||
PlaybookCount int64 `json:"playbook_count"`
|
PlaybookCount int64 `json:"playbook_count"`
|
||||||
TodayExecutions int64 `json:"today_executions"`
|
TodayExecutions int64 `json:"today_executions"`
|
||||||
SuccessRate float64 `json:"success_rate"`
|
SuccessRate float64 `json:"success_rate"`
|
||||||
}
|
}
|
||||||
+43
-44
@@ -2,64 +2,63 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CloudBilling 云账单模型 - 每日成本记录
|
// CloudBilling 云账单模型 - 每日成本记录
|
||||||
type CloudBilling struct {
|
type CloudBilling struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
CloudAccountID *uint `gorm:"index" json:"cloud_account_id"`
|
CloudAccountID *uint `gorm:"index" json:"cloud_account_id"`
|
||||||
AccountName string `gorm:"size:128" json:"account_name"`
|
AccountName string `gorm:"size:128" json:"account_name"`
|
||||||
Provider string `gorm:"size:64;index" json:"provider"` // aliyun, tencent, aws, huawei, azure
|
Provider string `gorm:"size:64;index" json:"provider"` // aliyun, tencent, aws, huawei, azure
|
||||||
BillingDate string `gorm:"size:10;index;not null" json:"billing_date"` // YYYY-MM-DD
|
BillingDate string `gorm:"size:10;index;not null" json:"billing_date"` // YYYY-MM-DD
|
||||||
Cost float64 `gorm:"type:decimal(12,2);default:0" json:"cost"` // 费用(元)
|
Cost float64 `gorm:"type:decimal(12,2);default:0" json:"cost"` // 费用(元)
|
||||||
Currency string `gorm:"size:8;default:CNY" json:"currency"` // CNY, USD
|
Currency string `gorm:"size:8;default:CNY" json:"currency"` // CNY, USD
|
||||||
Service string `gorm:"size:128" json:"service"` // ECS, RDS, OSS, CDN 等
|
Service string `gorm:"size:128" json:"service"` // ECS, RDS, OSS, CDN 等
|
||||||
Region string `gorm:"size:64" json:"region"`
|
Region string `gorm:"size:64" json:"region"`
|
||||||
Tags string `gorm:"type:text" json:"tags"` // JSON
|
Tags string `gorm:"type:text" json:"tags"` // JSON
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (CloudBilling) TableName() string {
|
func (CloudBilling) TableName() string {
|
||||||
return "asset_cloud_billings"
|
return "asset_cloud_billing_records"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BillingAlertRule 账单预警规则
|
// BillingAlertRule 账单预警规则
|
||||||
type BillingAlertRule struct {
|
type BillingAlertRule struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Provider string `gorm:"size:64" json:"provider"` // 空=所有云厂商
|
Provider string `gorm:"size:64" json:"provider"` // 空=所有云厂商
|
||||||
CloudAccountID *uint `gorm:"index" json:"cloud_account_id"` // 空=所有账号
|
CloudAccountID *uint `gorm:"index" json:"cloud_account_id"` // 空=所有账号
|
||||||
Metric string `gorm:"size:32;default:daily" json:"metric"` // daily, monthly
|
Metric string `gorm:"size:32;default:daily" json:"metric"` // daily, monthly
|
||||||
Threshold float64 `gorm:"type:decimal(12,2)" json:"threshold"` // 阈值(元)
|
Threshold float64 `gorm:"type:decimal(12,2)" json:"threshold"` // 阈值(元)
|
||||||
Operator string `gorm:"size:8;default:>" json:"operator"` // >, >=, <, <=
|
Operator string `gorm:"size:8;default:>" json:"operator"` // >, >=, <, <=
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
LastTriggered *time.Time `json:"last_triggered"`
|
LastTriggered *time.Time `json:"last_triggered"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (BillingAlertRule) TableName() string {
|
func (BillingAlertRule) TableName() string {
|
||||||
return "monitor_billing_alert_rules"
|
return "asset_cloud_billing_alert_rules"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BillingStats 账单统计
|
// BillingStats 账单统计
|
||||||
type BillingStats struct {
|
type BillingStats struct {
|
||||||
TodayCost float64 `json:"today_cost"`
|
TodayCost float64 `json:"today_cost"`
|
||||||
MonthCost float64 `json:"month_cost"`
|
MonthCost float64 `json:"month_cost"`
|
||||||
YearCost float64 `json:"year_cost"`
|
YearCost float64 `json:"year_cost"`
|
||||||
YesterdayCost float64 `json:"yesterday_cost"`
|
YesterdayCost float64 `json:"yesterday_cost"`
|
||||||
MonthTrend []DailyCost `json:"month_trend"`
|
MonthTrend []DailyCost `json:"month_trend"`
|
||||||
ProviderDist []ProviderCost `json:"provider_dist"`
|
ProviderDist []ProviderCost `json:"provider_dist"`
|
||||||
Alerts []BillingAlert `json:"alerts"`
|
Alerts []BillingAlert `json:"alerts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailyCost 每日成本
|
// DailyCost 每日成本
|
||||||
type DailyCost struct {
|
type DailyCost struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderCost 厂商成本分布
|
// ProviderCost 厂商成本分布
|
||||||
@@ -82,15 +81,15 @@ type BillingAlert struct {
|
|||||||
|
|
||||||
// CreateBillingRequest 创建账单记录请求
|
// CreateBillingRequest 创建账单记录请求
|
||||||
type CreateBillingRequest struct {
|
type CreateBillingRequest struct {
|
||||||
CloudAccountID *uint `json:"cloud_account_id"`
|
CloudAccountID *uint `json:"cloud_account_id"`
|
||||||
AccountName string `json:"account_name"`
|
AccountName string `json:"account_name"`
|
||||||
Provider string `json:"provider" binding:"required"`
|
Provider string `json:"provider" binding:"required"`
|
||||||
BillingDate string `json:"billing_date" binding:"required"`
|
BillingDate string `json:"billing_date" binding:"required"`
|
||||||
Cost float64 `json:"cost" binding:"required"`
|
Cost float64 `json:"cost" binding:"required"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBillingAlertRequest 创建预警规则请求
|
// CreateBillingAlertRequest 创建预警规则请求
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type BuildMasterConfig struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (BuildMasterConfig) TableName() string { return "deploy_build_master_config" }
|
func (BuildMasterConfig) TableName() string { return "deploy_build_master_configs" }
|
||||||
|
|
||||||
// UpdateBuildMasterRequest 更新 Master 并发数 // Logan.Li
|
// UpdateBuildMasterRequest 更新 Master 并发数 // Logan.Li
|
||||||
type UpdateBuildMasterRequest struct {
|
type UpdateBuildMasterRequest struct {
|
||||||
@@ -58,4 +58,4 @@ type UpdateBuildNodeRequest struct {
|
|||||||
MaxExecutors int `json:"max_executors"`
|
MaxExecutors int `json:"max_executors"`
|
||||||
WorkspacePath string `json:"workspace_path"`
|
WorkspacePath string `json:"workspace_path"`
|
||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
+88
-99
@@ -2,23 +2,22 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pipeline CI/CD流水线模型
|
// Pipeline CI/CD流水线模型
|
||||||
type Pipeline struct {
|
type Pipeline struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
||||||
JobName string `gorm:"size:128;uniqueIndex;comment:Job名称(webhook URL标识, 如 cex-admin-server)" json:"job_name,omitempty"`
|
JobName string `gorm:"size:128;uniqueIndex;comment:Job名称(webhook URL标识, 如 cex-admin-server)" json:"job_name,omitempty"`
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
Type string `gorm:"size:32;default:deploy" json:"type"` // build, deploy, test
|
Type string `gorm:"size:32;default:deploy" json:"type"` // build, deploy, test
|
||||||
Source string `gorm:"size:255" json:"source"` // Git仓库地址
|
Source string `gorm:"size:255" json:"source"` // Git仓库地址
|
||||||
Branch string `gorm:"size:64;default:main" json:"branch"`
|
Branch string `gorm:"size:64;default:main" json:"branch"`
|
||||||
Dockerfile string `gorm:"size:255" json:"dockerfile"`
|
Dockerfile string `gorm:"size:255" json:"dockerfile"`
|
||||||
Stages string `gorm:"type:text" json:"stages"` // JSON
|
Stages string `gorm:"type:text" json:"stages"` // JSON
|
||||||
Variables string `gorm:"type:text" json:"variables"` // JSON
|
Variables string `gorm:"type:text" json:"variables"` // JSON
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
|
|
||||||
// 部署模式
|
// 部署模式
|
||||||
DeployMode string `gorm:"size:16;default:branch;comment:部署模式 branch/tag" json:"deploy_mode"`
|
DeployMode string `gorm:"size:16;default:branch;comment:部署模式 branch/tag" json:"deploy_mode"`
|
||||||
@@ -36,16 +35,12 @@ type Pipeline struct {
|
|||||||
DeployScript string `gorm:"type:text;comment:SSH部署后执行命令" json:"deploy_script,omitempty"`
|
DeployScript string `gorm:"type:text;comment:SSH部署后执行命令" json:"deploy_script,omitempty"`
|
||||||
|
|
||||||
// K8s 部署目标
|
// K8s 部署目标
|
||||||
K8sClusterID *uint `json:"k8s_cluster_id,omitempty"` // FK → kubernetes_clusters
|
K8sClusterID *uint `json:"k8s_cluster_id,omitempty"` // FK → k8s_clusters
|
||||||
K8sNamespace string `gorm:"size:128;comment:K8s部署命名空间" json:"k8s_namespace,omitempty"`
|
K8sNamespace string `gorm:"size:128;comment:K8s部署命名空间" json:"k8s_namespace,omitempty"`
|
||||||
K8sDeployment string `gorm:"size:128;comment:K8s Deployment 名称" json:"k8s_deployment,omitempty"`
|
K8sDeployment string `gorm:"size:128;comment:K8s Deployment 名称" json:"k8s_deployment,omitempty"`
|
||||||
K8sContainerName string `gorm:"size:128;comment:K8s更新镜像目标容器" json:"k8s_container_name,omitempty"`
|
K8sContainerName string `gorm:"size:128;comment:K8s更新镜像目标容器" json:"k8s_container_name,omitempty"`
|
||||||
|
|
||||||
// 仓库和镜像仓库关联(旧字段,待废弃)
|
// 凭证关联(直接关联统一凭证 ID)// Logan.Li
|
||||||
RepoID *uint `json:"repo_id"` // FK → deploy_pipeline_repos
|
|
||||||
RegistryID *uint `json:"registry_id"` // FK → deploy_pipeline_registries
|
|
||||||
|
|
||||||
// 凭证关联(直接关联凭证 ID)
|
|
||||||
GitCredentialID *uint `json:"git_credential_id"` // FK → iam_credentials (Git Token)
|
GitCredentialID *uint `json:"git_credential_id"` // FK → iam_credentials (Git Token)
|
||||||
RegistryCredentialID *uint `json:"registry_credential_id"` // FK → iam_credentials (Docker Password)
|
RegistryCredentialID *uint `json:"registry_credential_id"` // FK → iam_credentials (Docker Password)
|
||||||
|
|
||||||
@@ -71,39 +66,37 @@ type Pipeline struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Repo *PipelineRepo `json:"repo,omitempty" gorm:"foreignKey:RepoID"`
|
Environment *DeployEnvironment `json:"environment,omitempty" gorm:"foreignKey:EnvironmentID"`
|
||||||
Registry *PipelineRegistry `json:"registry,omitempty" gorm:"foreignKey:RegistryID"`
|
|
||||||
Environment *DeployEnvironment `json:"environment,omitempty" gorm:"foreignKey:EnvironmentID"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (Pipeline) TableName() string {
|
func (Pipeline) TableName() string {
|
||||||
return "ops_pipelines"
|
return "deploy_pipelines"
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipelineRun 流水线运行记录(CI 执行)
|
// PipelineRun 流水线运行记录(CI 执行)
|
||||||
type PipelineRun struct {
|
type PipelineRun struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
PipelineID uint `gorm:"not null" json:"pipeline_id"`
|
PipelineID uint `gorm:"not null" json:"pipeline_id"`
|
||||||
BuildNumber int `json:"build_number"`
|
BuildNumber int `json:"build_number"`
|
||||||
Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled
|
Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled
|
||||||
Branch string `gorm:"size:64" json:"branch"`
|
Branch string `gorm:"size:64" json:"branch"`
|
||||||
CommitID string `gorm:"size:64" json:"commit_id"`
|
CommitID string `gorm:"size:64" json:"commit_id"`
|
||||||
CommitMsg string `gorm:"size:500" json:"commit_msg"`
|
CommitMsg string `gorm:"size:500" json:"commit_msg"`
|
||||||
Author string `gorm:"size:64" json:"author"`
|
Author string `gorm:"size:64" json:"author"`
|
||||||
ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签
|
ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签
|
||||||
ArtifactURL string `gorm:"size:500" json:"artifact_url"` // 构建产物地址
|
ArtifactURL string `gorm:"size:500" json:"artifact_url"` // 构建产物地址
|
||||||
WorkspacePath string `gorm:"size:500" json:"workspace_path"` // 工作区路径
|
WorkspacePath string `gorm:"size:500" json:"workspace_path"` // 工作区路径
|
||||||
BuildNodeID *uint `json:"build_node_id,omitempty"` // 实际执行节点(0/nil=Master)
|
BuildNodeID *uint `json:"build_node_id,omitempty"` // 实际执行节点(0/nil=Master)
|
||||||
BuildNodeName string `gorm:"size:64" json:"build_node_name,omitempty"`
|
BuildNodeName string `gorm:"size:64" json:"build_node_name,omitempty"`
|
||||||
Duration int `json:"duration"` // 秒
|
Duration int `json:"duration"` // 秒
|
||||||
Stages string `gorm:"type:text" json:"stages"` // JSON
|
Stages string `gorm:"type:text" json:"stages"` // JSON
|
||||||
Logs string `gorm:"type:longtext" json:"logs"`
|
Logs string `gorm:"type:longtext" json:"logs"`
|
||||||
TriggeredBy string `gorm:"size:64" json:"triggered_by"`
|
TriggeredBy string `gorm:"size:64" json:"triggered_by"`
|
||||||
StartedAt *time.Time `json:"started_at"`
|
StartedAt *time.Time `json:"started_at"`
|
||||||
FinishedAt *time.Time `json:"finished_at"`
|
FinishedAt *time.Time `json:"finished_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Pipeline *Pipeline `json:"pipeline,omitempty"`
|
Pipeline *Pipeline `json:"pipeline,omitempty"`
|
||||||
@@ -111,34 +104,34 @@ type PipelineRun struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (PipelineRun) TableName() string {
|
func (PipelineRun) TableName() string {
|
||||||
return "ops_pipeline_runs"
|
return "deploy_pipeline_runs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppRelease 应用发布模型(CD:从 CI 产物部署到目标环境)
|
// AppRelease 应用发布模型(CD:从 CI 产物部署到目标环境)
|
||||||
type AppRelease struct {
|
type AppRelease struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
PipelineID *uint `json:"pipeline_id"` // 来源流水线
|
PipelineID *uint `json:"pipeline_id"` // 来源流水线
|
||||||
PipelineRunID *uint `json:"pipeline_run_id"` // 来源 CI 执行记录
|
PipelineRunID *uint `json:"pipeline_run_id"` // 来源 CI 执行记录
|
||||||
AppName string `gorm:"size:128;not null" json:"app_name"`
|
AppName string `gorm:"size:128;not null" json:"app_name"`
|
||||||
Version string `gorm:"size:64;not null" json:"version"`
|
Version string `gorm:"size:64;not null" json:"version"`
|
||||||
Environment string `gorm:"size:32;not null" json:"environment"` // dev, test, staging, prod
|
Environment string `gorm:"size:32;not null" json:"environment"` // dev, test, staging, prod
|
||||||
DeployType string `gorm:"size:32;default:server" json:"deploy_type"` // server, kubernetes
|
DeployType string `gorm:"size:32;default:server" json:"deploy_type"` // server, kubernetes
|
||||||
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected, deploying, success, failed, rolled_back
|
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected, deploying, success, failed, rolled_back
|
||||||
CommitID string `gorm:"size:64" json:"commit_id"`
|
CommitID string `gorm:"size:64" json:"commit_id"`
|
||||||
CommitMsg string `gorm:"size:500" json:"commit_msg"`
|
CommitMsg string `gorm:"size:500" json:"commit_msg"`
|
||||||
Author string `gorm:"size:64" json:"author"`
|
Author string `gorm:"size:64" json:"author"`
|
||||||
Approver string `gorm:"size:64" json:"approver"`
|
Approver string `gorm:"size:64" json:"approver"`
|
||||||
ApprovedAt *time.Time `json:"approved_at"`
|
ApprovedAt *time.Time `json:"approved_at"`
|
||||||
DeployedAt *time.Time `json:"deployed_at"`
|
DeployedAt *time.Time `json:"deployed_at"`
|
||||||
RollbackAt *time.Time `json:"rollback_at"`
|
RollbackAt *time.Time `json:"rollback_at"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签
|
ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签
|
||||||
K8sClusterID *uint `json:"k8s_cluster_id"` // Kubernetes 部署目标集群
|
K8sClusterID *uint `json:"k8s_cluster_id"` // Kubernetes 部署目标集群
|
||||||
K8sNamespace string `gorm:"size:128" json:"k8s_namespace"` // Kubernetes 部署目标命名空间
|
K8sNamespace string `gorm:"size:128" json:"k8s_namespace"` // Kubernetes 部署目标命名空间
|
||||||
Servers string `gorm:"type:text" json:"servers"` // JSON(传统服务器部署目标)
|
Servers string `gorm:"type:text" json:"servers"` // JSON(传统服务器部署目标)
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
@@ -150,7 +143,7 @@ func (AppRelease) TableName() string {
|
|||||||
type CreatePipelineRequest struct {
|
type CreatePipelineRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"`
|
Code string `json:"code" binding:"required"`
|
||||||
JobName string `json:"job_name"` // Webhook Job名称
|
JobName string `json:"job_name"` // Webhook Job名称
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type string `json:"type" binding:"omitempty,oneof=build deploy test"`
|
Type string `json:"type" binding:"omitempty,oneof=build deploy test"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
@@ -160,9 +153,9 @@ type CreatePipelineRequest struct {
|
|||||||
Variables string `json:"variables"`
|
Variables string `json:"variables"`
|
||||||
|
|
||||||
// 部署模式
|
// 部署模式
|
||||||
DeployMode string `json:"deploy_mode"` // branch / tag
|
DeployMode string `json:"deploy_mode"` // branch / tag
|
||||||
DeployTarget string `json:"deploy_target"` // ssh / k8s
|
DeployTarget string `json:"deploy_target"` // ssh / k8s
|
||||||
ServerIDs string `json:"server_ids"` // SSH 服务器ID JSON [1,2,3]
|
ServerIDs string `json:"server_ids"` // SSH 服务器ID JSON [1,2,3]
|
||||||
DeployRemotePath string `json:"deploy_remote_path"`
|
DeployRemotePath string `json:"deploy_remote_path"`
|
||||||
DeployScript string `json:"deploy_script"`
|
DeployScript string `json:"deploy_script"`
|
||||||
K8sClusterID *uint `json:"k8s_cluster_id"`
|
K8sClusterID *uint `json:"k8s_cluster_id"`
|
||||||
@@ -170,12 +163,10 @@ type CreatePipelineRequest struct {
|
|||||||
K8sDeployment string `json:"k8s_deployment"`
|
K8sDeployment string `json:"k8s_deployment"`
|
||||||
K8sContainerName string `json:"k8s_container_name"`
|
K8sContainerName string `json:"k8s_container_name"`
|
||||||
|
|
||||||
RepoID *uint `json:"repo_id"`
|
// 凭证关联 // Logan.Li
|
||||||
RegistryID *uint `json:"registry_id"`
|
|
||||||
// 凭证关联
|
|
||||||
GitCredentialID *uint `json:"git_credential_id"`
|
GitCredentialID *uint `json:"git_credential_id"`
|
||||||
RegistryCredentialID *uint `json:"registry_credential_id"`
|
RegistryCredentialID *uint `json:"registry_credential_id"`
|
||||||
EnvironmentID *uint `json:"environment_id"`
|
EnvironmentID *uint `json:"environment_id"`
|
||||||
|
|
||||||
// 触发配置
|
// 触发配置
|
||||||
TriggerMode string `json:"trigger_mode"`
|
TriggerMode string `json:"trigger_mode"`
|
||||||
@@ -196,17 +187,17 @@ type CreatePipelineRequest struct {
|
|||||||
|
|
||||||
// UpdatePipelineRequest 更新流水线请求
|
// UpdatePipelineRequest 更新流水线请求
|
||||||
type UpdatePipelineRequest struct {
|
type UpdatePipelineRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
JobName string `json:"job_name"`
|
JobName string `json:"job_name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
Dockerfile string `json:"dockerfile"`
|
Dockerfile string `json:"dockerfile"`
|
||||||
Stages string `json:"stages"`
|
Stages string `json:"stages"`
|
||||||
Variables string `json:"variables"`
|
Variables string `json:"variables"`
|
||||||
DeployMode string `json:"deploy_mode"`
|
DeployMode string `json:"deploy_mode"`
|
||||||
DeployTarget string `json:"deploy_target"`
|
DeployTarget string `json:"deploy_target"`
|
||||||
ServerIDs string `json:"server_ids"`
|
ServerIDs string `json:"server_ids"`
|
||||||
DeployRemotePath string `json:"deploy_remote_path"`
|
DeployRemotePath string `json:"deploy_remote_path"`
|
||||||
DeployScript string `json:"deploy_script"`
|
DeployScript string `json:"deploy_script"`
|
||||||
@@ -214,12 +205,10 @@ type UpdatePipelineRequest struct {
|
|||||||
K8sNamespace string `json:"k8s_namespace"`
|
K8sNamespace string `json:"k8s_namespace"`
|
||||||
K8sDeployment string `json:"k8s_deployment"`
|
K8sDeployment string `json:"k8s_deployment"`
|
||||||
K8sContainerName string `json:"k8s_container_name"`
|
K8sContainerName string `json:"k8s_container_name"`
|
||||||
RepoID *uint `json:"repo_id"`
|
// 凭证关联 // Logan.Li
|
||||||
RegistryID *uint `json:"registry_id"`
|
|
||||||
// 凭证关联
|
|
||||||
GitCredentialID *uint `json:"git_credential_id"`
|
GitCredentialID *uint `json:"git_credential_id"`
|
||||||
RegistryCredentialID *uint `json:"registry_credential_id"`
|
RegistryCredentialID *uint `json:"registry_credential_id"`
|
||||||
EnvironmentID *uint `json:"environment_id"`
|
EnvironmentID *uint `json:"environment_id"`
|
||||||
// 触发配置
|
// 触发配置
|
||||||
TriggerMode string `json:"trigger_mode"`
|
TriggerMode string `json:"trigger_mode"`
|
||||||
WatchBranches string `json:"watch_branches"`
|
WatchBranches string `json:"watch_branches"`
|
||||||
@@ -228,7 +217,7 @@ type UpdatePipelineRequest struct {
|
|||||||
WebhookToken string `json:"webhook_token"`
|
WebhookToken string `json:"webhook_token"`
|
||||||
// 缓存
|
// 缓存
|
||||||
CachePaths string `json:"cache_paths"`
|
CachePaths string `json:"cache_paths"`
|
||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
|
|
||||||
// 并发控制
|
// 并发控制
|
||||||
MaxExecutors int `json:"max_executors"`
|
MaxExecutors int `json:"max_executors"`
|
||||||
@@ -240,7 +229,7 @@ type UpdatePipelineRequest struct {
|
|||||||
// TriggerPipelineRequest 触发流水线请求
|
// TriggerPipelineRequest 触发流水线请求
|
||||||
type TriggerPipelineRequest struct {
|
type TriggerPipelineRequest struct {
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
Tag string `json:"tag"` // 发布 Tag
|
Tag string `json:"tag"` // 发布 Tag
|
||||||
Variables map[string]string `json:"variables"`
|
Variables map[string]string `json:"variables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,10 +252,10 @@ type CreateAppReleaseRequest struct {
|
|||||||
|
|
||||||
// PipelineStats 流水线统计
|
// PipelineStats 流水线统计
|
||||||
type PipelineStats struct {
|
type PipelineStats struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Success int64 `json:"success"`
|
Success int64 `json:"success"`
|
||||||
Failed int64 `json:"failed"`
|
Failed int64 `json:"failed"`
|
||||||
Running int64 `json:"running"`
|
Running int64 `json:"running"`
|
||||||
TodayCount int64 `json:"today_count"`
|
TodayCount int64 `json:"today_count"`
|
||||||
SuccessRate float64 `json:"success_rate"`
|
SuccessRate float64 `json:"success_rate"`
|
||||||
}
|
}
|
||||||
@@ -80,10 +80,10 @@ type OpsOverviewStats struct {
|
|||||||
|
|
||||||
// CostStats 成本统计
|
// CostStats 成本统计
|
||||||
type CostStats struct {
|
type CostStats struct {
|
||||||
TotalCost float64 `json:"total_cost"`
|
TotalCost float64 `json:"total_cost"`
|
||||||
MonthCost float64 `json:"month_cost"`
|
MonthCost float64 `json:"month_cost"`
|
||||||
CloudCosts []CloudCost `json:"cloud_costs"`
|
CloudCosts []CloudCost `json:"cloud_costs"`
|
||||||
MonthTrend []MonthTrend `json:"month_trend"`
|
MonthTrend []MonthTrend `json:"month_trend"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloudCost 云平台成本
|
// CloudCost 云平台成本
|
||||||
@@ -115,8 +115,8 @@ type ServerResource struct {
|
|||||||
|
|
||||||
// DomainCert 域名证书
|
// DomainCert 域名证书
|
||||||
type DomainCert struct {
|
type DomainCert struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ExpireDate string `json:"expire_date"`
|
ExpireDate string `json:"expire_date"`
|
||||||
DaysUntilExpiry int `json:"days_until_expiry"`
|
DaysUntilExpiry int `json:"days_until_expiry"`
|
||||||
}
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CustomDashboard 自定义监控仪表盘(面板配置存 JSON) // Logan.Li
|
|
||||||
type CustomDashboard struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
|
||||||
Name string `gorm:"type:varchar(128);not null;comment:仪表盘名称" json:"name"`
|
|
||||||
Description string `gorm:"type:varchar(512);comment:描述" json:"description"`
|
|
||||||
Datasource string `gorm:"type:varchar(32);default:'prometheus';comment:数据源类型" json:"datasource"`
|
|
||||||
DatasourceURL string `gorm:"type:varchar(512);comment:数据源URL(留空则用默认Prometheus)" json:"datasource_url"`
|
|
||||||
// Panels 存储面板配置 JSON 数组: [{id,title,type,query,width,height,x,y,options}]
|
|
||||||
Panels string `gorm:"type:longtext;comment:面板配置JSON" json:"panels"`
|
|
||||||
// Tags 标签(JSON 数组字符串)
|
|
||||||
Tags string `gorm:"type:varchar(512);comment:标签JSON" json:"tags,omitempty"`
|
|
||||||
// 归属
|
|
||||||
CreatedBy string `gorm:"type:varchar(64);comment:创建人" json:"created_by"`
|
|
||||||
IsDefault bool `gorm:"default:false;index;comment:是否默认仪表盘" json:"is_default"`
|
|
||||||
IsFavorite bool `gorm:"default:false;comment:是否收藏" json:"is_favorite"`
|
|
||||||
// 时间戳
|
|
||||||
CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
|
||||||
DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 指定表名
|
|
||||||
func (CustomDashboard) TableName() string {
|
|
||||||
return "monitor_custom_dashboards"
|
|
||||||
}
|
|
||||||
+10
-74
@@ -2,91 +2,27 @@ package model
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// PipelineRepo Git 仓库认证配置
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// RepoAuthType 仓库认证方式
|
|
||||||
type RepoAuthType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RepoAuthToken RepoAuthType = "token" // Personal Access Token
|
|
||||||
RepoAuthSSH RepoAuthType = "ssh" // SSH Key
|
|
||||||
RepoAuthPassword RepoAuthType = "password" // 用户名+密码
|
|
||||||
)
|
|
||||||
|
|
||||||
// PipelineRepo Git 仓库认证配置表
|
|
||||||
type PipelineRepo struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
|
||||||
Name string `gorm:"type:varchar(128);not null;comment:配置名称" json:"name"`
|
|
||||||
Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"`
|
|
||||||
URL string `gorm:"type:varchar(512);not null;comment:仓库 URL" json:"url"`
|
|
||||||
AuthType RepoAuthType `gorm:"type:varchar(16);default:'token';comment:认证方式: token/ssh/password" json:"auth_type"`
|
|
||||||
CredentialID *uint `gorm:"index;comment:凭据ID(关联 iam_credentials)" json:"credential_id,omitempty"`
|
|
||||||
Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"`
|
|
||||||
CreatedBy string `gorm:"type:varchar(64);comment:创建人" json:"created_by,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 表名
|
|
||||||
func (PipelineRepo) TableName() string {
|
|
||||||
return "deploy_pipeline_repos"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// PipelineRegistry 镜像仓库认证配置
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// RegistryAuthType 镜像仓库认证方式
|
|
||||||
type RegistryAuthType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RegistryAuthToken RegistryAuthType = "token" // Access Token
|
|
||||||
RegistryAuthPassword RegistryAuthType = "password" // 用户名+密码
|
|
||||||
)
|
|
||||||
|
|
||||||
// PipelineRegistry 镜像仓库认证配置表
|
|
||||||
type PipelineRegistry struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
|
||||||
Name string `gorm:"type:varchar(128);not null;comment:配置名称" json:"name"`
|
|
||||||
Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"`
|
|
||||||
RegistryURL string `gorm:"type:varchar(512);not null;comment:镜像仓库地址" json:"registry_url"`
|
|
||||||
Namespace string `gorm:"type:varchar(128);comment:默认命名空间" json:"namespace,omitempty"`
|
|
||||||
AuthType RegistryAuthType `gorm:"type:varchar(16);default:'password';comment:认证方式: token/password" json:"auth_type"`
|
|
||||||
CredentialID *uint `gorm:"index;comment:凭据ID(关联 iam_credentials)" json:"credential_id,omitempty"`
|
|
||||||
Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"`
|
|
||||||
CreatedBy string `gorm:"type:varchar(64);comment:创建人" json:"created_by,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 表名
|
|
||||||
func (PipelineRegistry) TableName() string {
|
|
||||||
return "deploy_pipeline_registries"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PipelineStage 流水线阶段定义(JSON)
|
// PipelineStage 流水线阶段定义(JSON)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// PipelineStage 一个阶段包含多个任务
|
// PipelineStage 一个阶段包含多个任务
|
||||||
type PipelineStage struct {
|
type PipelineStage struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"` // source/build/deploy/test/custom
|
Type string `json:"type"` // source/build/deploy/test/custom
|
||||||
Tasks []PipelineTask `json:"tasks"`
|
Tasks []PipelineTask `json:"tasks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipelineTask 阶段内的任务
|
// PipelineTask 阶段内的任务
|
||||||
type PipelineTask struct {
|
type PipelineTask struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"` // build/deploy/test/custom
|
Type string `json:"type"` // build/deploy/test/custom
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Config map[string]string `json:"config,omitempty"`
|
Config map[string]string `json:"config,omitempty"`
|
||||||
Script string `json:"script,omitempty"`
|
Script string `json:"script,omitempty"`
|
||||||
ScriptType string `json:"script_type,omitempty"` // shell/python
|
ScriptType string `json:"script_type,omitempty"` // shell/python
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package model
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// DeployTool 工具配置(懒加载/预装/禁用)
|
// DeployTool 工具配置(懒加载/预装/禁用)// Logan.Li
|
||||||
// 按 deploy_ 前缀,与 PipelineRepo/Registry 同属发布配置域
|
// 按 deploy_ 前缀归属发布配置域。// Logan.Li
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// ToolInstallMethod 安装方式
|
// ToolInstallMethod 安装方式
|
||||||
@@ -395,29 +395,29 @@ echo "[Node.js] Installed: $(/usr/local/bin/node --version) / npm $("$NVM_DIR/ve
|
|||||||
},
|
},
|
||||||
// ──── Git — 版本控制(懒加载, U4 镜像瘦身)// Logan.Li ────
|
// ──── Git — 版本控制(懒加载, U4 镜像瘦身)// Logan.Li ────
|
||||||
{
|
{
|
||||||
Name: "git",
|
Name: "git",
|
||||||
DisplayName: "Git",
|
DisplayName: "Git",
|
||||||
Description: "版本控制工具,流水线代码拉取使用(首次使用自动安装)",
|
Description: "版本控制工具,流水线代码拉取使用(首次使用自动安装)",
|
||||||
Category: "build",
|
Category: "build",
|
||||||
InstallMethod: ToolLazy,
|
InstallMethod: ToolLazy,
|
||||||
BinaryName: "git",
|
BinaryName: "git",
|
||||||
InstallPath: "/usr/bin/git",
|
InstallPath: "/usr/bin/git",
|
||||||
CheckCommand: "git --version 2>/dev/null",
|
CheckCommand: "git --version 2>/dev/null",
|
||||||
InstallScript: `apk add --no-cache git`,
|
InstallScript: `apk add --no-cache git`,
|
||||||
SortOrder: 6,
|
SortOrder: 6,
|
||||||
},
|
},
|
||||||
// ──── Docker CLI — 容器构建(懒加载, U4 镜像瘦身)// Logan.Li ────
|
// ──── Docker CLI — 容器构建(懒加载, U4 镜像瘦身)// Logan.Li ────
|
||||||
{
|
{
|
||||||
Name: "docker-cli",
|
Name: "docker-cli",
|
||||||
DisplayName: "Docker CLI",
|
DisplayName: "Docker CLI",
|
||||||
Description: "Docker 命令行工具,流水线镜像构建/推送使用(首次使用自动安装)",
|
Description: "Docker 命令行工具,流水线镜像构建/推送使用(首次使用自动安装)",
|
||||||
Category: "deploy",
|
Category: "deploy",
|
||||||
InstallMethod: ToolLazy,
|
InstallMethod: ToolLazy,
|
||||||
BinaryName: "docker",
|
BinaryName: "docker",
|
||||||
InstallPath: "/usr/bin/docker",
|
InstallPath: "/usr/bin/docker",
|
||||||
CheckCommand: "docker --version 2>/dev/null",
|
CheckCommand: "docker --version 2>/dev/null",
|
||||||
InstallScript: `apk add --no-cache docker-cli`,
|
InstallScript: `apk add --no-cache docker-cli`,
|
||||||
SortOrder: 7,
|
SortOrder: 7,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+98
-99
@@ -2,61 +2,60 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// KubernetesCluster K8s集群模型
|
// KubernetesCluster K8s集群模型
|
||||||
type KubernetesCluster struct {
|
type KubernetesCluster struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
||||||
Version string `gorm:"size:32" json:"version"`
|
Version string `gorm:"size:32" json:"version"`
|
||||||
Status string `gorm:"size:32;default:healthy" json:"status"` // healthy, warning, error
|
Status string `gorm:"size:32;default:healthy" json:"status"` // healthy, warning, error
|
||||||
ConfigType int `gorm:"default:1" json:"config_type"` // 0=token, 1=kubeconfig
|
ConfigType int `gorm:"default:1" json:"config_type"` // 0=token, 1=kubeconfig
|
||||||
APIServer string `gorm:"size:255" json:"api_server"`
|
APIServer string `gorm:"size:255" json:"api_server"`
|
||||||
APIEndpoint string `gorm:"size:255" json:"api_endpoint"`
|
APIEndpoint string `gorm:"size:255" json:"api_endpoint"`
|
||||||
Token string `gorm:"type:text" json:"-"`
|
Token string `gorm:"type:text" json:"-"`
|
||||||
KubeConfig string `gorm:"type:text" json:"-"`
|
KubeConfig string `gorm:"type:text" json:"-"`
|
||||||
NodeCount int `gorm:"default:0" json:"node_count"`
|
NodeCount int `gorm:"default:0" json:"node_count"`
|
||||||
PodCount int `gorm:"default:0" json:"pod_count"`
|
PodCount int `gorm:"default:0" json:"pod_count"`
|
||||||
ServiceCount int `gorm:"default:0" json:"service_count"`
|
ServiceCount int `gorm:"default:0" json:"service_count"`
|
||||||
DeploymentCount int `gorm:"default:0" json:"deployment_count"`
|
DeploymentCount int `gorm:"default:0" json:"deployment_count"`
|
||||||
IngressCount int `gorm:"default:0" json:"ingress_count"` // 路由/Ingress 数量 // Logan.Li
|
IngressCount int `gorm:"default:0" json:"ingress_count"` // 路由/Ingress 数量 // Logan.Li
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
MemoryUsage float64 `json:"memory_usage"`
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
Description string `gorm:"size:500" json:"description"`
|
Description string `gorm:"size:500" json:"description"`
|
||||||
Labels string `gorm:"type:text" json:"labels"` // JSON
|
Labels string `gorm:"type:text" json:"labels"` // JSON
|
||||||
LastSyncAt *time.Time `json:"last_sync_at"`
|
LastSyncAt *time.Time `json:"last_sync_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesCluster) TableName() string {
|
func (KubernetesCluster) TableName() string {
|
||||||
return "kubernetes_clusters"
|
return "k8s_clusters"
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesNode K8s节点模型
|
// KubernetesNode K8s节点模型
|
||||||
type KubernetesNode struct {
|
type KubernetesNode struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
IP string `gorm:"size:64" json:"ip"`
|
IP string `gorm:"size:64" json:"ip"`
|
||||||
Role string `gorm:"size:32" json:"role"` // master, worker
|
Role string `gorm:"size:32" json:"role"` // master, worker
|
||||||
Status string `gorm:"size:32" json:"status"`
|
Status string `gorm:"size:32" json:"status"`
|
||||||
OS string `gorm:"size:64" json:"os"`
|
OS string `gorm:"size:64" json:"os"`
|
||||||
KubeletVer string `gorm:"size:32" json:"kubelet_version"`
|
KubeletVer string `gorm:"size:32" json:"kubelet_version"`
|
||||||
CPU int `json:"cpu"`
|
CPU int `json:"cpu"`
|
||||||
Memory int64 `json:"memory"`
|
Memory int64 `json:"memory"`
|
||||||
Disk int64 `json:"disk"`
|
Disk int64 `json:"disk"`
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
MemoryUsage float64 `json:"memory_usage"`
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
DiskUsage float64 `json:"disk_usage"`
|
DiskUsage float64 `json:"disk_usage"`
|
||||||
PodCount int `json:"pod_count"`
|
PodCount int `json:"pod_count"`
|
||||||
MaxPods int `json:"max_pods"`
|
MaxPods int `json:"max_pods"`
|
||||||
Labels string `gorm:"type:text" json:"labels"`
|
Labels string `gorm:"type:text" json:"labels"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
||||||
@@ -64,25 +63,25 @@ type KubernetesNode struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesNode) TableName() string {
|
func (KubernetesNode) TableName() string {
|
||||||
return "kubernetes_nodes"
|
return "k8s_nodes"
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesPod K8s Pod模型
|
// KubernetesPod K8s Pod模型
|
||||||
type KubernetesPod struct {
|
type KubernetesPod struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
||||||
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Status string `gorm:"size:32" json:"status"` // Running, Pending, Succeeded, Failed
|
Status string `gorm:"size:32" json:"status"` // Running, Pending, Succeeded, Failed
|
||||||
NodeName string `gorm:"size:128" json:"node_name"`
|
NodeName string `gorm:"size:128" json:"node_name"`
|
||||||
IP string `gorm:"size:64" json:"ip"`
|
IP string `gorm:"size:64" json:"ip"`
|
||||||
RestartCount int `json:"restart_count"`
|
RestartCount int `json:"restart_count"`
|
||||||
Image string `gorm:"size:255" json:"image"`
|
Image string `gorm:"size:255" json:"image"`
|
||||||
CPUUsage float64 `json:"cpu_usage"`
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
MemoryUsage float64 `json:"memory_usage"`
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
Labels string `gorm:"type:text" json:"labels"`
|
Labels string `gorm:"type:text" json:"labels"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
||||||
@@ -90,23 +89,23 @@ type KubernetesPod struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesPod) TableName() string {
|
func (KubernetesPod) TableName() string {
|
||||||
return "kubernetes_pods"
|
return "k8s_pods"
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesDeployment K8s Deployment模型
|
// KubernetesDeployment K8s Deployment模型
|
||||||
type KubernetesDeployment struct {
|
type KubernetesDeployment struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
||||||
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Replicas int `json:"replicas"`
|
Replicas int `json:"replicas"`
|
||||||
AvailableReplicas int `json:"available_replicas"`
|
AvailableReplicas int `json:"available_replicas"`
|
||||||
ReadyReplicas int `json:"ready_replicas"`
|
ReadyReplicas int `json:"ready_replicas"`
|
||||||
Image string `gorm:"size:255" json:"image"`
|
Image string `gorm:"size:255" json:"image"`
|
||||||
Strategy string `gorm:"size:32" json:"strategy"`
|
Strategy string `gorm:"size:32" json:"strategy"`
|
||||||
Labels string `gorm:"type:text" json:"labels"`
|
Labels string `gorm:"type:text" json:"labels"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
||||||
@@ -114,23 +113,23 @@ type KubernetesDeployment struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesDeployment) TableName() string {
|
func (KubernetesDeployment) TableName() string {
|
||||||
return "kubernetes_deployments"
|
return "k8s_deployments"
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesService K8s Service模型
|
// KubernetesService K8s Service模型
|
||||||
type KubernetesService struct {
|
type KubernetesService struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
||||||
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Type string `gorm:"size:32" json:"type"` // ClusterIP, NodePort, LoadBalancer
|
Type string `gorm:"size:32" json:"type"` // ClusterIP, NodePort, LoadBalancer
|
||||||
ClusterIP string `gorm:"size:64" json:"cluster_ip"`
|
ClusterIP string `gorm:"size:64" json:"cluster_ip"`
|
||||||
ExternalIP string `gorm:"size:255" json:"external_ip"`
|
ExternalIP string `gorm:"size:255" json:"external_ip"`
|
||||||
Ports string `gorm:"type:text" json:"ports"` // JSON
|
Ports string `gorm:"type:text" json:"ports"` // JSON
|
||||||
Selector string `gorm:"type:text" json:"selector"`
|
Selector string `gorm:"type:text" json:"selector"`
|
||||||
Labels string `gorm:"type:text" json:"labels"`
|
Labels string `gorm:"type:text" json:"labels"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
||||||
@@ -138,22 +137,22 @@ type KubernetesService struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesService) TableName() string {
|
func (KubernetesService) TableName() string {
|
||||||
return "kubernetes_services"
|
return "k8s_services"
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesIngress K8s Ingress模型
|
// KubernetesIngress K8s Ingress模型
|
||||||
type KubernetesIngress struct {
|
type KubernetesIngress struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
ClusterID uint `gorm:"not null" json:"cluster_id"`
|
||||||
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
Namespace string `gorm:"size:64;not null" json:"namespace"`
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"`
|
||||||
Hosts string `gorm:"type:text" json:"hosts"` // JSON
|
Hosts string `gorm:"type:text" json:"hosts"` // JSON
|
||||||
Backend string `gorm:"size:255" json:"backend"`
|
Backend string `gorm:"size:255" json:"backend"`
|
||||||
TLS bool `json:"tls"`
|
TLS bool `json:"tls"`
|
||||||
TLSHosts string `gorm:"type:text" json:"tls_hosts"`
|
TLSHosts string `gorm:"type:text" json:"tls_hosts"`
|
||||||
Labels string `gorm:"type:text" json:"labels"`
|
Labels string `gorm:"type:text" json:"labels"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
Cluster *KubernetesCluster `json:"cluster,omitempty"`
|
||||||
@@ -161,7 +160,7 @@ type KubernetesIngress struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesIngress) TableName() string {
|
func (KubernetesIngress) TableName() string {
|
||||||
return "kubernetes_ingresses"
|
return "k8s_ingresses"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateKubernetesClusterRequest 创建K8s集群请求
|
// CreateKubernetesClusterRequest 创建K8s集群请求
|
||||||
@@ -169,7 +168,7 @@ type CreateKubernetesClusterRequest struct {
|
|||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"`
|
Code string `json:"code" binding:"required"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ConfigType int `json:"config_type"` // 0=token, 1=kubeconfig
|
ConfigType int `json:"config_type"` // 0=token, 1=kubeconfig
|
||||||
APIServer string `json:"api_server"`
|
APIServer string `json:"api_server"`
|
||||||
APIEndpoint string `json:"api_endpoint"`
|
APIEndpoint string `json:"api_endpoint"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@@ -201,5 +200,5 @@ type TopologyLayout struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (TopologyLayout) TableName() string {
|
func (TopologyLayout) TableName() string {
|
||||||
return "kubernetes_topology_layouts"
|
return "k8s_topology_layouts"
|
||||||
}
|
}
|
||||||
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user