update
This commit is contained in:
1 parent
0690285147
commit
5618cd2738
102 files changed
+3336
-1476
No files matched your search
+208
-36
@@ -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 {
|
||||||
|
if ac.isRunning() {
|
||||||
ac.abort()
|
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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,18 +282,13 @@ 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,
|
||||||
|
Alias: alias,
|
||||||
|
CloudInstanceName: inst.InstanceName,
|
||||||
|
CloudInstanceID: inst.InstanceID,
|
||||||
IP: ipAddr,
|
IP: ipAddr,
|
||||||
PublicIP: inst.PublicIP,
|
PublicIP: inst.PublicIP,
|
||||||
Port: defaultPort,
|
Port: defaultPort,
|
||||||
@@ -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}})
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
|||||||
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"`
|
||||||
@@ -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,11 +246,19 @@ 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"`
|
||||||
@@ -264,12 +272,19 @@ func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
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{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -397,13 +397,22 @@ 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"`
|
||||||
@@ -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"]
|
||||||
|
|||||||
+128
-37
@@ -354,11 +354,19 @@ 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 {
|
||||||
@@ -375,9 +383,17 @@ func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
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"`
|
||||||
@@ -336,7 +343,9 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
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{
|
||||||
@@ -358,11 +367,19 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
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"`
|
||||||
@@ -383,11 +400,15 @@ func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) {
|
|||||||
}
|
}
|
||||||
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,7 +500,9 @@ 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,
|
||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
@@ -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 {
|
||||||
@@ -230,8 +230,6 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
|||||||
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,
|
||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态过滤
|
// 状态过滤
|
||||||
@@ -195,6 +195,9 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
|||||||
|
|
||||||
server := model.Server{
|
server := model.Server{
|
||||||
Hostname: req.Hostname,
|
Hostname: req.Hostname,
|
||||||
|
Alias: req.Alias,
|
||||||
|
CloudInstanceName: req.CloudInstanceName,
|
||||||
|
CloudInstanceID: req.CloudInstanceID,
|
||||||
IP: req.IP,
|
IP: req.IP,
|
||||||
PublicIP: req.PublicIP,
|
PublicIP: req.PublicIP,
|
||||||
Port: req.Port,
|
Port: req.Port,
|
||||||
@@ -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'")
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ type AgentPortPolicy struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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" }
|
|
||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentTask Agent 远程任务 // Logan.Li
|
// AgentTask Agent 远程任务 // Logan.Li
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -333,7 +333,7 @@ type NotifyChannel struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (NotifyChannel) TableName() string {
|
func (NotifyChannel) TableName() string {
|
||||||
return "monitor_notify_channels"
|
return "monitor_alert_channels"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricData 监控指标数据
|
// MetricData 监控指标数据
|
||||||
@@ -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"`
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApprovalTemplate 审批模板模型
|
// ApprovalTemplate 审批模板模型
|
||||||
@@ -22,7 +21,7 @@ type ApprovalTemplate struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (ApprovalTemplate) TableName() string {
|
func (ApprovalTemplate) TableName() string {
|
||||||
return "sys_approval_templates"
|
return "approval_templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApprovalProcess 审批流程模型
|
// ApprovalProcess 审批流程模型
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type ApprovalWorkflow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ApprovalWorkflow) TableName() string {
|
func (ApprovalWorkflow) TableName() string {
|
||||||
return "sys_approval_workflows"
|
return "approval_workflows"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApprovalWorkflowStep 审批流程步骤
|
// ApprovalWorkflowStep 审批流程步骤
|
||||||
@@ -36,7 +36,7 @@ type ApprovalWorkflowStep struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ApprovalWorkflowStep) TableName() string {
|
func (ApprovalWorkflowStep) TableName() string {
|
||||||
return "sys_approval_workflow_steps"
|
return "approval_workflow_steps"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWorkflowRequest 创建审批流程请求
|
// CreateWorkflowRequest 创建审批流程请求
|
||||||
|
|||||||
@@ -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 审计日志查询
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CronJob 定时任务模型
|
// CronJob 定时任务模型
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CloudBilling 云账单模型 - 每日成本记录
|
// CloudBilling 云账单模型 - 每日成本记录
|
||||||
@@ -23,7 +22,7 @@ type CloudBilling struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (CloudBilling) TableName() string {
|
func (CloudBilling) TableName() string {
|
||||||
return "asset_cloud_billings"
|
return "asset_cloud_billing_records"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BillingAlertRule 账单预警规则
|
// BillingAlertRule 账单预警规则
|
||||||
@@ -42,7 +41,7 @@ type BillingAlertRule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (BillingAlertRule) TableName() string {
|
func (BillingAlertRule) TableName() string {
|
||||||
return "monitor_billing_alert_rules"
|
return "asset_cloud_billing_alert_rules"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BillingStats 账单统计
|
// BillingStats 账单统计
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+6
-17
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pipeline CI/CD流水线模型
|
// Pipeline CI/CD流水线模型
|
||||||
@@ -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,14 +66,12 @@ type Pipeline struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Repo *PipelineRepo `json:"repo,omitempty" gorm:"foreignKey:RepoID"`
|
|
||||||
Registry *PipelineRegistry `json:"registry,omitempty" gorm:"foreignKey:RegistryID"`
|
|
||||||
Environment *DeployEnvironment `json:"environment,omitempty" gorm:"foreignKey:EnvironmentID"`
|
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 执行)
|
||||||
@@ -111,7 +104,7 @@ 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 产物部署到目标环境)
|
||||||
@@ -170,9 +163,7 @@ 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"`
|
||||||
@@ -214,9 +205,7 @@ 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"`
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -2,70 +2,6 @@ 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)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package model
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// DeployTool 工具配置(懒加载/预装/禁用)
|
// DeployTool 工具配置(懒加载/预装/禁用)// Logan.Li
|
||||||
// 按 deploy_ 前缀,与 PipelineRepo/Registry 同属发布配置域
|
// 按 deploy_ 前缀归属发布配置域。// Logan.Li
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// ToolInstallMethod 安装方式
|
// ToolInstallMethod 安装方式
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// KubernetesCluster K8s集群模型
|
// KubernetesCluster K8s集群模型
|
||||||
@@ -33,7 +32,7 @@ type KubernetesCluster struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (KubernetesCluster) TableName() string {
|
func (KubernetesCluster) TableName() string {
|
||||||
return "kubernetes_clusters"
|
return "k8s_clusters"
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesNode K8s节点模型
|
// KubernetesNode K8s节点模型
|
||||||
@@ -64,7 +63,7 @@ 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模型
|
||||||
@@ -90,7 +89,7 @@ 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模型
|
||||||
@@ -114,7 +113,7 @@ 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模型
|
||||||
@@ -138,7 +137,7 @@ 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模型
|
||||||
@@ -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集群请求
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -18,19 +18,19 @@ type LogScopeRole struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (LogScopeRole) TableName() string { return "monitor_log_scope_roles" }
|
func (LogScopeRole) TableName() string { return "iam_log_scope_roles" }
|
||||||
|
|
||||||
// UserLogScopeRole 用户 ↔ 日志范围权限 // Logan.Li
|
// UserLogScopeRole 用户 ↔ 日志范围权限 // Logan.Li
|
||||||
type UserLogScopeRole struct {
|
type UserLogScopeRole struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
UserID uint `gorm:"uniqueIndex:idx_user_log_scope;not null" json:"user_id"`
|
UserID uint `gorm:"uniqueIndex:idx_iam_user_log_scope_role;not null" json:"user_id"`
|
||||||
LogScopeRoleID uint `gorm:"uniqueIndex:idx_user_log_scope;not null" json:"log_scope_role_id"`
|
LogScopeRoleID uint `gorm:"uniqueIndex:idx_iam_user_log_scope_role;not null" json:"log_scope_role_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
||||||
LogScopeRole *LogScopeRole `json:"log_scope_role,omitempty" gorm:"foreignKey:LogScopeRoleID"`
|
LogScopeRole *LogScopeRole `json:"log_scope_role,omitempty" gorm:"foreignKey:LogScopeRoleID;references:ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (UserLogScopeRole) TableName() string { return "monitor_user_log_scope_roles" }
|
func (UserLogScopeRole) TableName() string { return "iam_user_log_scope_roles" }
|
||||||
|
|
||||||
// CreateLogScopeRoleRequest 创建日志权限策略 // Logan.Li
|
// CreateLogScopeRoleRequest 创建日志权限策略 // Logan.Li
|
||||||
type CreateLogScopeRoleRequest struct {
|
type CreateLogScopeRoleRequest struct {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type LoginLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (LoginLog) TableName() string {
|
func (LoginLog) TableName() string {
|
||||||
return "sys_login_logs"
|
return "audit_login_logs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginLogQuery 登录日志查询
|
// LoginLogQuery 登录日志查询
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type MetricsAggregate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (MetricsAggregate) TableName() string {
|
func (MetricsAggregate) TableName() string {
|
||||||
return "monitor_metrics_aggregate"
|
return "monitor_metric_aggregates"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AggregateReq 聚合触发请求
|
// AggregateReq 聚合触发请求
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ type MetricValue struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (MetricValue) TableName() string {
|
func (MetricValue) TableName() string {
|
||||||
return "metric_values"
|
return "monitor_metric_values"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricHistory 指标历史数据(简化版,供监控查询使用)
|
// MetricHistory 指标历史数据(简化版,供监控查询使用)
|
||||||
@@ -314,7 +314,7 @@ type DashboardPanel struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (DashboardPanel) TableName() string {
|
func (DashboardPanel) TableName() string {
|
||||||
return "dashboard_panels"
|
return "monitor_dashboard_panels"
|
||||||
}
|
}
|
||||||
|
|
||||||
// PanelQueryConfig 面板查询配置
|
// PanelQueryConfig 面板查询配置
|
||||||
@@ -422,4 +422,3 @@ type HealthProbeResult struct {
|
|||||||
StatusCode int `json:"status_code,omitempty"` // HTTP 状态码
|
StatusCode int `json:"status_code,omitempty"` // HTTP 状态码
|
||||||
Message string `json:"message,omitempty"` // 错误/附加信息
|
Message string `json:"message,omitempty"` // 错误/附加信息
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,4 +31,4 @@ type MonitorAlertRule struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (MonitorAlertRule) TableName() string { return "monitor_alert_rules" }
|
func (MonitorAlertRule) TableName() string { return "monitor_probe_alert_rules" }
|
||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NavigationGroup 导航分组模型
|
// NavigationGroup 导航分组模型
|
||||||
@@ -45,7 +44,7 @@ type NavigationLink struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (NavigationLink) TableName() string {
|
func (NavigationLink) TableName() string {
|
||||||
return "navigation_links"
|
return "sys_navigation_links"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateNavigationGroupRequest 创建导航分组请求
|
// CreateNavigationGroupRequest 创建导航分组请求
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnsiblePlaybook Playbook 模型 // Logan.Li
|
// AnsiblePlaybook Playbook 模型 // Logan.Li
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type RDPSession struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (RDPSession) TableName() string {
|
func (RDPSession) TableName() string {
|
||||||
return "sys_rdp_sessions"
|
return "audit_rdp_sessions"
|
||||||
}
|
}
|
||||||
|
|
||||||
// RDPSessionQuery Windows RDP 图形会话查询 // Logan.Li
|
// RDPSessionQuery Windows RDP 图形会话查询 // Logan.Li
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResourceModel 资源模型 (CMDB 模型定义)
|
// ResourceModel 资源模型 (CMDB 模型定义)
|
||||||
|
|||||||
+11
-2
@@ -8,6 +8,9 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
Hostname string `gorm:"size:128;not null" json:"hostname"`
|
Hostname string `gorm:"size:128;not null" json:"hostname"`
|
||||||
|
Alias string `gorm:"size:128" json:"alias"` // 人工可读别名,展示优先级高于 hostname // Logan.Li
|
||||||
|
CloudInstanceName string `gorm:"size:128" json:"cloud_instance_name"` // 云平台实例名称,低于人工别名高于 hostname 展示 // Logan.Li
|
||||||
|
CloudInstanceID string `gorm:"size:128;index" json:"cloud_instance_id"` // 云平台实例 ID,仅做关联/去重,不作为首选展示名 // Logan.Li
|
||||||
IP string `gorm:"size:64;not null" json:"ip"`
|
IP string `gorm:"size:64;not null" json:"ip"`
|
||||||
PublicIP string `gorm:"size:64" json:"public_ip"`
|
PublicIP string `gorm:"size:64" json:"public_ip"`
|
||||||
Port int `gorm:"default:22" json:"port"`
|
Port int `gorm:"default:22" json:"port"`
|
||||||
@@ -66,7 +69,7 @@ type Cloud struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (Cloud) TableName() string {
|
func (Cloud) TableName() string {
|
||||||
return "asset_clouds"
|
return "asset_cloud_platforms"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloudAccount 云账号模型
|
// CloudAccount 云账号模型
|
||||||
@@ -106,7 +109,7 @@ type Region struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (Region) TableName() string {
|
func (Region) TableName() string {
|
||||||
return "asset_regions"
|
return "asset_cloud_regions"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Domain 域名模型
|
// Domain 域名模型
|
||||||
@@ -132,6 +135,9 @@ func (Domain) TableName() string {
|
|||||||
// CreateServerRequest 创建服务器请求
|
// CreateServerRequest 创建服务器请求
|
||||||
type CreateServerRequest struct {
|
type CreateServerRequest struct {
|
||||||
Hostname string `json:"hostname" binding:"required"`
|
Hostname string `json:"hostname" binding:"required"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
CloudInstanceName string `json:"cloud_instance_name"`
|
||||||
|
CloudInstanceID string `json:"cloud_instance_id"`
|
||||||
IP string `json:"ip" binding:"required,ip"`
|
IP string `json:"ip" binding:"required,ip"`
|
||||||
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
||||||
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
||||||
@@ -157,6 +163,9 @@ type CreateServerRequest struct {
|
|||||||
// UpdateServerRequest 更新服务器请求
|
// UpdateServerRequest 更新服务器请求
|
||||||
type UpdateServerRequest struct {
|
type UpdateServerRequest struct {
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
|
Alias *string `json:"alias"`
|
||||||
|
CloudInstanceName string `json:"cloud_instance_name"`
|
||||||
|
CloudInstanceID string `json:"cloud_instance_id"`
|
||||||
IP string `json:"ip" binding:"omitempty,ip"`
|
IP string `json:"ip" binding:"omitempty,ip"`
|
||||||
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
||||||
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type SessionCommand struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (SessionCommand) TableName() string {
|
func (SessionCommand) TableName() string {
|
||||||
return "sys_session_commands"
|
return "audit_session_commands"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionCommandQuery 会话命令查询 // Logan.Li
|
// SessionCommandQuery 会话命令查询 // Logan.Li
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ type SiteTask struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SiteTask) TableName() string {
|
func (SiteTask) TableName() string {
|
||||||
return "ops_site_tasks"
|
return "monitor_site_tasks"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -139,7 +139,7 @@ type SiteResult struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SiteResult) TableName() string {
|
func (SiteResult) TableName() string {
|
||||||
return "ops_site_results"
|
return "monitor_site_results"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -199,7 +199,7 @@ type SiteAlert struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SiteAlert) TableName() string {
|
func (SiteAlert) TableName() string {
|
||||||
return "ops_site_alerts"
|
return "monitor_site_alerts"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SiteTaskCreateRequest 拨测任务创建请求
|
// SiteTaskCreateRequest 拨测任务创建请求
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ type SNMPDevice struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SNMPDevice) TableName() string {
|
func (SNMPDevice) TableName() string {
|
||||||
return "ops_snmp_devices"
|
return "monitor_snmp_devices"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -120,7 +120,7 @@ type SNMPTemplate struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SNMPTemplate) TableName() string {
|
func (SNMPTemplate) TableName() string {
|
||||||
return "ops_snmp_templates"
|
return "monitor_snmp_templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SNMPTemplateOID OID模板条目表
|
// SNMPTemplateOID OID模板条目表
|
||||||
@@ -142,7 +142,7 @@ type SNMPTemplateOID struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SNMPTemplateOID) TableName() string {
|
func (SNMPTemplateOID) TableName() string {
|
||||||
return "ops_snmp_template_oids"
|
return "monitor_snmp_template_oids"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -170,7 +170,7 @@ type SNMPMetric struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SNMPMetric) TableName() string {
|
func (SNMPMetric) TableName() string {
|
||||||
return "ops_snmp_metrics"
|
return "monitor_snmp_samples"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -199,7 +199,7 @@ type SNMPInterface struct {
|
|||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
func (SNMPInterface) TableName() string {
|
func (SNMPInterface) TableName() string {
|
||||||
return "ops_snmp_interfaces"
|
return "monitor_snmp_interfaces"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ type SystemTaskStatus struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (SystemTaskStatus) TableName() string { return "sys_system_task_status" }
|
func (SystemTaskStatus) TableName() string { return "sys_system_task_statuses" }
|
||||||
@@ -23,7 +23,7 @@ type TerminalSession struct {
|
|||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
func (TerminalSession) TableName() string {
|
func (TerminalSession) TableName() string {
|
||||||
return "sys_terminal_sessions"
|
return "audit_terminal_sessions"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TerminalSessionQuery 终端会话查询 // Logan.Li
|
// TerminalSessionQuery 终端会话查询 // Logan.Li
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// User 用户模型
|
// User 用户模型
|
||||||
@@ -51,7 +50,7 @@ type Role struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions,omitempty"`
|
Permissions []Permission `gorm:"many2many:iam_role_permissions;" json:"permissions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 表名
|
// TableName 表名
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func NewEngine(cfg EngineConfig) (*Engine, error) {
|
|||||||
return nil, fmt.Errorf("Kubernetes 模式缺少集群客户端")
|
return nil, fmt.Errorf("Kubernetes 模式缺少集群客户端")
|
||||||
}
|
}
|
||||||
kt := cfg.Kubernetes
|
kt := cfg.Kubernetes
|
||||||
|
registry.Register(tools.NewKubernetesDiagnoseTool(kt.Client, kt.Namespace))
|
||||||
registry.Register(tools.NewKubernetesGetTool(kt.Client, kt.Namespace))
|
registry.Register(tools.NewKubernetesGetTool(kt.Client, kt.Namespace))
|
||||||
registry.Register(tools.NewPodLogsTool(kt.Client, kt.Namespace, kt.PodName, kt.Container))
|
registry.Register(tools.NewPodLogsTool(kt.Client, kt.Namespace, kt.PodName, kt.Container))
|
||||||
registry.Register(tools.NewPodExecuteTool(kt.Client, kt.Namespace, kt.PodName, kt.Container, coreCfg.ToolTimeout))
|
registry.Register(tools.NewPodExecuteTool(kt.Client, kt.Namespace, kt.PodName, kt.Container, coreCfg.ToolTimeout))
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ type Agent struct {
|
|||||||
evolver *evolver.EvolverEngine
|
evolver *evolver.EvolverEngine
|
||||||
callback EventCallback
|
callback EventCallback
|
||||||
totalTokens int
|
totalTokens int
|
||||||
|
cache map[string]string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ func NewAgent(client llm.Client, registry *tools.Registry, cfg *Config, sc *safe
|
|||||||
if cfg.MaxContextTokens <= 0 {
|
if cfg.MaxContextTokens <= 0 {
|
||||||
cfg.MaxContextTokens = 8000
|
cfg.MaxContextTokens = 8000
|
||||||
}
|
}
|
||||||
return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev}
|
return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev, cache: make(map[string]string)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCallback 注入事件回调 // Logan.Li
|
// SetCallback 注入事件回调 // Logan.Li
|
||||||
@@ -293,6 +294,14 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg := a.suppressBroadKubernetesGet(tc.Function.Name, args, history); msg != "" {
|
||||||
|
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: tools.RiskSafe.Display(), Summary: "工具执行中"})
|
||||||
|
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
|
||||||
|
Output: msg, Success: true, Duration: "0s", OutputSize: len(msg)})
|
||||||
|
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: msg})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
risk := a.toolRisk(tool, args)
|
risk := a.toolRisk(tool, args)
|
||||||
a.emit(Event{Type: EventStatus, Phase: PhaseSafety, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "安全检查"})
|
a.emit(Event{Type: EventStatus, Phase: PhaseSafety, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "安全检查"})
|
||||||
|
|
||||||
@@ -315,6 +324,16 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
|
|
||||||
// 执行
|
// 执行
|
||||||
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "工具执行中"})
|
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "工具执行中"})
|
||||||
|
if cached, ok := a.cachedToolResult(tc.Function.Name, args); ok {
|
||||||
|
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
|
||||||
|
Output: cached, Success: true, Duration: "0s", OutputSize: len(cached)})
|
||||||
|
records = append(records, evolver.ToolCallRecord{
|
||||||
|
ToolName: tc.Function.Name, Args: args, Output: cached,
|
||||||
|
Duration: 0, Success: true, RiskLevel: int(risk),
|
||||||
|
})
|
||||||
|
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: cached})
|
||||||
|
continue
|
||||||
|
}
|
||||||
toolStart := time.Now()
|
toolStart := time.Now()
|
||||||
toolCtx, cancel := context.WithTimeout(ctx, a.config.ToolTimeout)
|
toolCtx, cancel := context.WithTimeout(ctx, a.config.ToolTimeout)
|
||||||
result, execErr := tool.Execute(toolCtx, args)
|
result, execErr := tool.Execute(toolCtx, args)
|
||||||
@@ -346,6 +365,7 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
|
|
||||||
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
|
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
|
||||||
Output: output, Success: success, Duration: dur.Truncate(time.Millisecond).String(), OutputSize: len(output)})
|
Output: output, Success: success, Duration: dur.Truncate(time.Millisecond).String(), OutputSize: len(output)})
|
||||||
|
a.storeToolResult(tc.Function.Name, args, output, success)
|
||||||
|
|
||||||
records = append(records, evolver.ToolCallRecord{
|
records = append(records, evolver.ToolCallRecord{
|
||||||
ToolName: tc.Function.Name, Args: args, Output: output,
|
ToolName: tc.Function.Name, Args: args, Output: output,
|
||||||
@@ -369,6 +389,82 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
return obs, records, failInc
|
return obs, records, failInc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cachedToolResult 返回同一任务内只读工具的缓存结果,避免 LLM 重复消耗 token。 // Logan.Li
|
||||||
|
func (a *Agent) cachedToolResult(toolName string, args map[string]interface{}) (string, bool) {
|
||||||
|
if !cacheableTool(toolName) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
key := toolCacheKey(toolName, args)
|
||||||
|
if key == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if out, ok := a.cache[key]; ok {
|
||||||
|
return "已复用本轮任务缓存结果;不要重复查询同一对象,请基于首次结果总结,或改用更具体的 name/status_filter/pod 参数补查。\n\n" + out, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeToolResult 缓存成功的只读工具输出。 // Logan.Li
|
||||||
|
func (a *Agent) storeToolResult(toolName string, args map[string]interface{}, output string, success bool) {
|
||||||
|
if !success || !cacheableTool(toolName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := toolCacheKey(toolName, args)
|
||||||
|
if key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.cache[key] = output
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheableTool(toolName string) bool {
|
||||||
|
switch toolName {
|
||||||
|
case "kubernetes_diagnose", "kubernetes_get", "pod_logs", "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolCacheKey(toolName string, args map[string]interface{}) string {
|
||||||
|
b, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return toolName + ":" + string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// suppressBroadKubernetesGet 在聚合诊断后拦截全量 K8s 查询,迫使模型按异常对象下钻。 // Logan.Li
|
||||||
|
func (a *Agent) suppressBroadKubernetesGet(toolName string, args map[string]interface{}, history map[string]int) string {
|
||||||
|
if a.config.Mode != "kubernetes" && a.config.Mode != "k8s" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if toolName != "kubernetes_get" || !hasDiagnoseHistory(history) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
resource := strings.ToLower(strings.TrimSpace(fmt.Sprint(args["resource"])))
|
||||||
|
name := strings.TrimSpace(fmt.Sprint(args["name"]))
|
||||||
|
labelSelector := strings.TrimSpace(fmt.Sprint(args["label_selector"]))
|
||||||
|
statusFilter := strings.TrimSpace(fmt.Sprint(args["status_filter"]))
|
||||||
|
if name != "" && name != "<nil>" || labelSelector != "" && labelSelector != "<nil>" || statusFilter != "" && statusFilter != "<nil>" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch resource {
|
||||||
|
case "pods", "pod", "deployments", "deployment", "deploy", "services", "service", "svc", "nodes", "node", "events", "event":
|
||||||
|
return "已拦截全量 Kubernetes 查询:本轮已经执行过 kubernetes_diagnose,里面包含异常 Workload、异常 Pod、Top Events 和节点摘要。请不要再次拉全量资源;如需补证据,请按诊断结果中的具体对象调用 kubernetes_get(name/status_filter) 或 pod_logs(pod)。"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDiagnoseHistory(history map[string]int) bool {
|
||||||
|
for key, count := range history {
|
||||||
|
if count > 0 && strings.HasPrefix(key, "kubernetes_diagnose:") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// triggerEvolve 异步触发进化(带超时 + panic 恢复,绝不阻塞主循环) // Logan.Li
|
// triggerEvolve 异步触发进化(带超时 + panic 恢复,绝不阻塞主循环) // Logan.Li
|
||||||
func (a *Agent) triggerEvolve(ctx context.Context, query, serverHost, answer string, records []evolver.ToolCallRecord, success bool, start time.Time) {
|
func (a *Agent) triggerEvolve(ctx context.Context, query, serverHost, answer string, records []evolver.ToolCallRecord, success bool, start time.Time) {
|
||||||
if a.evolver == nil {
|
if a.evolver == nil {
|
||||||
@@ -522,34 +618,46 @@ func (a *Agent) trimMessages(msgs []llm.Message) []llm.Message {
|
|||||||
return ensureToolMessageIntegrity(result)
|
return ensureToolMessageIntegrity(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureToolMessageIntegrity 确保 tool 消息前面有对应 assistant(tool_calls) // Logan.Li
|
// ensureToolMessageIntegrity 确保 assistant(tool_calls) 后紧跟完整 tool 结果组 // Logan.Li
|
||||||
func ensureToolMessageIntegrity(msgs []llm.Message) []llm.Message {
|
func ensureToolMessageIntegrity(msgs []llm.Message) []llm.Message {
|
||||||
validIDs := make(map[string]bool)
|
|
||||||
for _, m := range msgs {
|
|
||||||
if m.Role == "assistant" {
|
|
||||||
for _, tc := range m.ToolCalls {
|
|
||||||
validIDs[tc.ID] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out := make([]llm.Message, 0, len(msgs))
|
out := make([]llm.Message, 0, len(msgs))
|
||||||
for i, m := range msgs {
|
for i := 0; i < len(msgs); i++ {
|
||||||
|
m := msgs[i]
|
||||||
if m.Role == "tool" {
|
if m.Role == "tool" {
|
||||||
if !validIDs[m.ToolCallID] || i == 0 || msgs[i-1].Role != "assistant" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
matched := false
|
if m.Role != "assistant" || len(m.ToolCalls) == 0 {
|
||||||
for _, tc := range msgs[i-1].ToolCalls {
|
|
||||||
if tc.ID == m.ToolCallID {
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = append(out, m)
|
out = append(out, m)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
required := make(map[string]bool, len(m.ToolCalls))
|
||||||
|
for _, tc := range m.ToolCalls {
|
||||||
|
if tc.ID != "" {
|
||||||
|
required[tc.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(required) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]bool, len(required))
|
||||||
|
tools := make([]llm.Message, 0, len(required))
|
||||||
|
j := i + 1
|
||||||
|
for ; j < len(msgs) && msgs[j].Role == "tool"; j++ {
|
||||||
|
tm := msgs[j]
|
||||||
|
if !required[tm.ToolCallID] || seen[tm.ToolCallID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[tm.ToolCallID] = true
|
||||||
|
tools = append(tools, tm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(seen) == len(required) {
|
||||||
|
out = append(out, m)
|
||||||
|
out = append(out, tools...)
|
||||||
|
}
|
||||||
|
i = j - 1
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"ezops/pkg/agentcli/llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsureToolMessageIntegrityKeepsCompleteMultiToolGroup(t *testing.T) {
|
||||||
|
msgs := []llm.Message{
|
||||||
|
{Role: "system", Content: "sys"},
|
||||||
|
{Role: "user", Content: "查看异常副本"},
|
||||||
|
{Role: "assistant", Content: "并行检查", ToolCalls: []llm.ToolCall{
|
||||||
|
{ID: "call-1", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
|
||||||
|
{ID: "call-2", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
|
||||||
|
{ID: "call-3", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
|
||||||
|
}},
|
||||||
|
{Role: "tool", ToolCallID: "call-1", Name: "kubernetes_get", Content: "deployments"},
|
||||||
|
{Role: "tool", ToolCallID: "call-2", Name: "kubernetes_get", Content: "statefulsets"},
|
||||||
|
{Role: "tool", ToolCallID: "call-3", Name: "kubernetes_get", Content: "pods"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ensureToolMessageIntegrity(msgs)
|
||||||
|
if len(got) != len(msgs) {
|
||||||
|
t.Fatalf("完整多工具结果组应全部保留,got=%d want=%d", len(got), len(msgs))
|
||||||
|
}
|
||||||
|
for i := 3; i < len(got); i++ {
|
||||||
|
if got[i].Role != "tool" || got[i].ToolCallID == "" {
|
||||||
|
t.Fatalf("第 %d 条工具结果异常: %+v", i, got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureToolMessageIntegrityDropsIncompleteToolGroup(t *testing.T) {
|
||||||
|
msgs := []llm.Message{
|
||||||
|
{Role: "system", Content: "sys"},
|
||||||
|
{Role: "assistant", Content: "并行检查", ToolCalls: []llm.ToolCall{
|
||||||
|
{ID: "call-1", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
|
||||||
|
{ID: "call-2", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
|
||||||
|
}},
|
||||||
|
{Role: "tool", ToolCallID: "call-1", Name: "kubernetes_get", Content: "deployments"},
|
||||||
|
{Role: "user", Content: "继续"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ensureToolMessageIntegrity(msgs)
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("不完整工具结果组应整体丢弃,got=%d want=2: %+v", len(got), got)
|
||||||
|
}
|
||||||
|
if got[0].Role != "system" || got[1].Role != "user" {
|
||||||
|
t.Fatalf("剩余消息异常: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuppressBroadKubernetesGetAfterDiagnose(t *testing.T) {
|
||||||
|
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
|
||||||
|
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx"}`: 1}
|
||||||
|
msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{"resource": "pods"}, history)
|
||||||
|
if msg == "" {
|
||||||
|
t.Fatal("expected broad pods query to be suppressed after diagnose")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuppressBroadKubernetesGetBlocksAllPodAliasesAfterDiagnose(t *testing.T) {
|
||||||
|
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
|
||||||
|
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx","focus":"all"}`: 1}
|
||||||
|
for _, resource := range []string{"pods", "pod"} {
|
||||||
|
msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{"resource": resource}, history)
|
||||||
|
if msg == "" {
|
||||||
|
t.Fatalf("expected broad %s query to be suppressed after diagnose", resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuppressBroadKubernetesGetAllowsFilteredQuery(t *testing.T) {
|
||||||
|
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
|
||||||
|
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx"}`: 1}
|
||||||
|
msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{
|
||||||
|
"resource": "pods", "status_filter": "NotRunning",
|
||||||
|
}, history)
|
||||||
|
if msg != "" {
|
||||||
|
t.Fatalf("expected filtered pods query to pass, got %q", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuppressBroadKubernetesGetAllowsStatusFilteredPodsAfterDiagnose(t *testing.T) {
|
||||||
|
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
|
||||||
|
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx","focus":"pods"}`: 1}
|
||||||
|
cases := []map[string]interface{}{
|
||||||
|
{"resource": "pods", "status_filter": "NotRunning"},
|
||||||
|
{"resource": "pod", "status_filter": "CrashLoopBackOff"},
|
||||||
|
}
|
||||||
|
for _, args := range cases {
|
||||||
|
msg := agent.suppressBroadKubernetesGet("kubernetes_get", args, history)
|
||||||
|
if msg != "" {
|
||||||
|
t.Fatalf("expected status_filter query to pass for args=%v, got %q", args, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Package guardrail 提供 Agent CLI 输入输出治理能力。 // Logan.Li
|
||||||
|
package guardrail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sensitivePatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`(?i)(bearer\s+)[a-z0-9._~+/=-]{16,}`),
|
||||||
|
regexp.MustCompile(`(?i)((?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|secret[_-]?key)\s*[:=]\s*)[^\s'"&]+`),
|
||||||
|
regexp.MustCompile(`(?i)((?:mysql|postgres|postgresql|redis|mongodb)://[^:\s/@]+:)[^@\s]+(@)`),
|
||||||
|
regexp.MustCompile(`(?is)-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----`),
|
||||||
|
regexp.MustCompile(`(?i)((?:Cookie|Set-Cookie):\s*)[^\r\n]+`),
|
||||||
|
regexp.MustCompile(`(?i)(AKIA[0-9A-Z]{16})`),
|
||||||
|
}
|
||||||
|
|
||||||
|
var sensitiveKeys = map[string]bool{
|
||||||
|
"password": true, "passwd": true, "pwd": true, "secret": true,
|
||||||
|
"token": true, "api_key": true, "apikey": true, "access_key": true,
|
||||||
|
"secret_key": true, "private_key": true, "cookie": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeText 脱敏工具输出、LLM 文本和错误信息,避免敏感信息进入前端/DB/记忆。 // Logan.Li
|
||||||
|
func SanitizeText(input string) string {
|
||||||
|
if input == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
out := input
|
||||||
|
for _, re := range sensitivePatterns {
|
||||||
|
out = re.ReplaceAllStringFunc(out, func(match string) string {
|
||||||
|
if strings.Contains(match, "PRIVATE KEY") {
|
||||||
|
return "[REDACTED_PRIVATE_KEY]"
|
||||||
|
}
|
||||||
|
sub := re.FindStringSubmatch(match)
|
||||||
|
if len(sub) >= 3 {
|
||||||
|
return sub[1] + "[REDACTED]" + sub[2]
|
||||||
|
}
|
||||||
|
if len(sub) >= 2 {
|
||||||
|
return sub[1] + "[REDACTED]"
|
||||||
|
}
|
||||||
|
return "[REDACTED]"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeJSON 脱敏 JSON 文本;非法 JSON 时退回文本脱敏。 // Logan.Li
|
||||||
|
func SanitizeJSON(raw string) string {
|
||||||
|
if strings.TrimSpace(raw) == "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
var v interface{}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
||||||
|
return SanitizeText(raw)
|
||||||
|
}
|
||||||
|
v = sanitizeValue(v)
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return SanitizeText(raw)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeValue(v interface{}) interface{} {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
out := make(map[string]interface{}, len(val))
|
||||||
|
for k, item := range val {
|
||||||
|
if sensitiveKeys[strings.ToLower(strings.ReplaceAll(k, "-", "_"))] {
|
||||||
|
out[k] = "[REDACTED]"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[k] = sanitizeValue(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case []interface{}:
|
||||||
|
for i := range val {
|
||||||
|
val[i] = sanitizeValue(val[i])
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
case string:
|
||||||
|
return SanitizeText(val)
|
||||||
|
default:
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTraceID 生成短 trace id,便于串联一次 Agent 任务。 // Logan.Li
|
||||||
|
func NewTraceID() string {
|
||||||
|
return "tr-" + randomHex(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSpanID 生成短 span id,便于串联工具和消息事件。 // Logan.Li
|
||||||
|
func NewSpanID() string {
|
||||||
|
return "sp-" + randomHex(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomHex(n int) string {
|
||||||
|
buf := make([]byte, n)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "0000000000000000"
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(buf)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package guardrail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSanitizeTextMasksCommonSecrets(t *testing.T) {
|
||||||
|
input := "Authorization: Bearer abcdefghijklmnopqrstuvwxyz password=secret123 mysql://root:pass@db/prod"
|
||||||
|
got := SanitizeText(input)
|
||||||
|
for _, leak := range []string{"abcdefghijklmnopqrstuvwxyz", "secret123", "root:pass@db"} {
|
||||||
|
if strings.Contains(got, leak) {
|
||||||
|
t.Fatalf("敏感信息未脱敏: %s in %s", leak, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "[REDACTED]") {
|
||||||
|
t.Fatalf("脱敏标记缺失: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeJSONMasksSensitiveKeys(t *testing.T) {
|
||||||
|
got := SanitizeJSON(`{"command":"echo ok","password":"p1","nested":{"api_key":"k1"}}`)
|
||||||
|
for _, leak := range []string{"p1", "k1"} {
|
||||||
|
if strings.Contains(got, leak) {
|
||||||
|
t.Fatalf("JSON 敏感字段未脱敏: %s in %s", leak, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, `"command":"echo ok"`) {
|
||||||
|
t.Fatalf("非敏感字段不应被改写: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,9 +51,10 @@ func BuildSystemPrompt(mode, targetInfo, toolDescs, evolveContext string) string
|
|||||||
sb.WriteString("1. 破坏性命令(rm -rf /、mkfs、dd 覆写磁盘等)被严格禁止,绝不执行。\n")
|
sb.WriteString("1. 破坏性命令(rm -rf /、mkfs、dd 覆写磁盘等)被严格禁止,绝不执行。\n")
|
||||||
sb.WriteString("2. 高危操作(删除 Pod、重启 Deployment、扩缩容、换镜像等)需要用户确认后才能执行。\n")
|
sb.WriteString("2. 高危操作(删除 Pod、重启 Deployment、扩缩容、换镜像等)需要用户确认后才能执行。\n")
|
||||||
if mode == runModeKubernetes {
|
if mode == runModeKubernetes {
|
||||||
sb.WriteString("3. 排障时优先使用只读工具:kubernetes_get 查资源状态、pod_logs 看日志;确认根因后再 pod_execute 或 kubernetes_action。\n")
|
sb.WriteString("3. 排障时优先使用只读工具;涉及异常副本、集群健康、Pod 异常、ImagePullBackOff、CrashLoopBackOff、探针失败时,第一步必须调用 kubernetes_diagnose。\n")
|
||||||
sb.WriteString("4. 容器内可能没有 systemctl、宿主机路径;不要假设节点级命令可用。\n")
|
sb.WriteString("4. kubernetes_diagnose 已聚合 Workload/Pod/Event/Node,不要重复拉全量 pods/deployments/events;只针对异常对象补查 pod_logs 或 kubernetes_get name。\n")
|
||||||
sb.WriteString("5. 未选定 Pod 时,用 kubernetes_get 和 pod_logs(带 pod 参数)排障;选定 Pod 后可直接 pod_execute。\n")
|
sb.WriteString("5. 容器内可能没有 systemctl、宿主机路径;不要假设节点级命令可用。\n")
|
||||||
|
sb.WriteString("6. 未选定 Pod 时,用 kubernetes_diagnose 和必要的 pod_logs(带 pod 参数)排障;选定 Pod 后可直接 pod_execute。\n")
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("3. 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息,确认根因后再动手修复。\n")
|
sb.WriteString("3. 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息,确认根因后再动手修复。\n")
|
||||||
sb.WriteString("4. 不要执行任何你不确定后果的命令;宁可多问用户,也不盲目操作。\n")
|
sb.WriteString("4. 不要执行任何你不确定后果的命令;宁可多问用户,也不盲目操作。\n")
|
||||||
@@ -70,7 +71,7 @@ func BuildSystemPrompt(mode, targetInfo, toolDescs, evolveContext string) string
|
|||||||
sb.WriteString("你采用「思考-行动-观察」循环解决任务:\n")
|
sb.WriteString("你采用「思考-行动-观察」循环解决任务:\n")
|
||||||
sb.WriteString("1. 思考:分析当前状态,决定下一步。在回复中先用简短文字说明你的判断和计划。\n")
|
sb.WriteString("1. 思考:分析当前状态,决定下一步。在回复中先用简短文字说明你的判断和计划。\n")
|
||||||
if mode == runModeKubernetes {
|
if mode == runModeKubernetes {
|
||||||
sb.WriteString("2. 行动:优先 kubernetes_get / pod_logs;需要容器内信息时用 pod_execute;变更用 kubernetes_action(一次一步)。\n")
|
sb.WriteString("2. 行动:健康巡检/异常副本先用 kubernetes_diagnose;需要细节时对异常对象调用 kubernetes_get / pod_logs;需要容器内信息时用 pod_execute;变更用 kubernetes_action(一次一步)。\n")
|
||||||
sb.WriteString("3. 观察:仔细阅读工具输出;pod_execute 输出含 [exit_code=0] 表示成功。\n")
|
sb.WriteString("3. 观察:仔细阅读工具输出;pod_execute 输出含 [exit_code=0] 表示成功。\n")
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("2. 行动:调用 ssh_execute 执行命令(一次只做关键的、可验证的一步)。\n")
|
sb.WriteString("2. 行动:调用 ssh_execute 执行命令(一次只做关键的、可验证的一步)。\n")
|
||||||
|
|||||||
@@ -0,0 +1,601 @@
|
|||||||
|
// kubernetes_diagnose.go - Kubernetes 异常副本与健康诊断聚合工具 // Logan.Li
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultDiagnoseLimit = 8
|
||||||
|
|
||||||
|
// KubernetesDiagnoseTool 聚合查询异常 Workload、Pod、Event 和节点健康,减少 LLM 反复拉全量资源。 // Logan.Li
|
||||||
|
type KubernetesDiagnoseTool struct {
|
||||||
|
client *k8sclient.Client
|
||||||
|
namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKubernetesDiagnoseTool 创建 Kubernetes 聚合诊断工具。 // Logan.Li
|
||||||
|
func NewKubernetesDiagnoseTool(client *k8sclient.Client, namespace string) *KubernetesDiagnoseTool {
|
||||||
|
return &KubernetesDiagnoseTool{client: client, namespace: namespace}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *KubernetesDiagnoseTool) Name() string { return "kubernetes_diagnose" }
|
||||||
|
|
||||||
|
func (t *KubernetesDiagnoseTool) Description() string {
|
||||||
|
return "一键诊断 Kubernetes 命名空间健康和异常副本(只读)。优先用于“异常副本、集群健康、Pod 异常、Deployment 不可用、ImagePullBackOff、CrashLoopBackOff”等任务。" +
|
||||||
|
"工具会聚合 Deployment/StatefulSet/DaemonSet/Pod/Event/Node,只返回异常摘要、可能根因和建议补充检查,避免反复查询全量资源。"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *KubernetesDiagnoseTool) Parameters() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"namespace": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "命名空间;空则使用会话绑定命名空间",
|
||||||
|
},
|
||||||
|
"focus": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "关注点:replicas|pods|events|image|crash|probe|all,默认 all",
|
||||||
|
},
|
||||||
|
"limit": map[string]interface{}{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "每类最多返回数量,默认 8,最大 20",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *KubernetesDiagnoseTool) RiskLevel() RiskLevel { return RiskSafe }
|
||||||
|
|
||||||
|
func (t *KubernetesDiagnoseTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
|
||||||
|
ns := strings.TrimSpace(StringParam(args, "namespace"))
|
||||||
|
if ns == "" {
|
||||||
|
ns = t.namespace
|
||||||
|
}
|
||||||
|
if ns == "" {
|
||||||
|
return &Result{Success: false, Error: "namespace 不能为空"}, nil
|
||||||
|
}
|
||||||
|
limit := IntParam(args, "limit")
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = defaultDiagnoseLimit
|
||||||
|
}
|
||||||
|
if limit > 20 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
focus := strings.ToLower(strings.TrimSpace(StringParam(args, "focus")))
|
||||||
|
if focus == "" {
|
||||||
|
focus = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := t.buildReport(ns, focus, limit)
|
||||||
|
if err != nil {
|
||||||
|
return &Result{Success: false, Error: err.Error(), Output: err.Error()}, nil
|
||||||
|
}
|
||||||
|
b, err := json.MarshalIndent(report, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("诊断结果 JSON 序列化失败: %w", err)
|
||||||
|
}
|
||||||
|
return &Result{Success: true, Output: string(b)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type k8sDiagnoseReport struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Focus string `json:"focus"`
|
||||||
|
Summary k8sDiagnoseSummary `json:"summary"`
|
||||||
|
AbnormalWorkloads []k8sWorkloadFinding `json:"abnormal_workloads"`
|
||||||
|
AbnormalPods []k8sPodFinding `json:"abnormal_pods"`
|
||||||
|
TopEvents []k8sEventFinding `json:"top_events"`
|
||||||
|
NodeHealth []k8sNodeFinding `json:"node_health,omitempty"`
|
||||||
|
LikelyRootCauses []string `json:"likely_root_causes"`
|
||||||
|
RecommendedActions []string `json:"recommended_actions"`
|
||||||
|
NextTools []map[string]interface{} `json:"next_tools"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type k8sDiagnoseSummary struct {
|
||||||
|
Deployments int `json:"deployments"`
|
||||||
|
StatefulSets int `json:"statefulsets"`
|
||||||
|
DaemonSets int `json:"daemonsets"`
|
||||||
|
Pods int `json:"pods"`
|
||||||
|
AbnormalWorkloads int `json:"abnormal_workloads"`
|
||||||
|
AbnormalPods int `json:"abnormal_pods"`
|
||||||
|
WarningEvents int `json:"warning_events"`
|
||||||
|
NotReadyNodes int `json:"not_ready_nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type k8sWorkloadFinding struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Desired int `json:"desired"`
|
||||||
|
Ready int `json:"ready"`
|
||||||
|
Available int `json:"available,omitempty"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type k8sPodFinding struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Restarts int `json:"restarts"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
Messages []string `json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type k8sEventFinding struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Count int32 `json:"count"`
|
||||||
|
LastSeen string `json:"last_seen,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type k8sNodeFinding struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Ready string `json:"ready"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *KubernetesDiagnoseTool) buildReport(ns, focus string, limit int) (*k8sDiagnoseReport, error) {
|
||||||
|
pods, err := t.client.ListPods()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取 Pod 失败: %w", err)
|
||||||
|
}
|
||||||
|
deployments, err := t.client.ListDeployments()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取 Deployment 失败: %w", err)
|
||||||
|
}
|
||||||
|
statefulsets, err := t.client.ListStatefulSets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取 StatefulSet 失败: %w", err)
|
||||||
|
}
|
||||||
|
daemonsets, err := t.client.ListDaemonSets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取 DaemonSet 失败: %w", err)
|
||||||
|
}
|
||||||
|
events, err := t.client.ListEvents(ns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取 Event 失败: %w", err)
|
||||||
|
}
|
||||||
|
nodes, _ := t.client.ListNodes()
|
||||||
|
|
||||||
|
report := &k8sDiagnoseReport{Namespace: ns, Focus: focus}
|
||||||
|
report.AbnormalWorkloads = diagnoseWorkloads(ns, deployments.Items, statefulsets.Items, daemonsets.Items)
|
||||||
|
report.AbnormalPods = diagnosePods(ns, pods.Items, deployments.Items)
|
||||||
|
report.TopEvents = diagnoseEvents(events.Items, limit)
|
||||||
|
report.NodeHealth = diagnoseNodes(nodes)
|
||||||
|
report.Summary = k8sDiagnoseSummary{
|
||||||
|
Deployments: countDeployments(ns, deployments.Items), StatefulSets: countStatefulSets(ns, statefulsets.Items),
|
||||||
|
DaemonSets: countDaemonSets(ns, daemonsets.Items), Pods: countPods(ns, pods.Items),
|
||||||
|
AbnormalWorkloads: len(report.AbnormalWorkloads), AbnormalPods: len(report.AbnormalPods),
|
||||||
|
WarningEvents: countWarningEvents(events.Items), NotReadyNodes: len(report.NodeHealth),
|
||||||
|
}
|
||||||
|
report.LikelyRootCauses = inferK8sRootCauses(report.AbnormalPods, report.TopEvents)
|
||||||
|
report.RecommendedActions = recommendK8sActions(report.AbnormalPods, report.AbnormalWorkloads)
|
||||||
|
report.NextTools = recommendK8sNextTools(report.AbnormalPods, report.TopEvents, limit)
|
||||||
|
report.AbnormalWorkloads = limitWorkloadFindings(report.AbnormalWorkloads, limit)
|
||||||
|
report.AbnormalPods = limitPodFindings(report.AbnormalPods, limit)
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnoseWorkloads(ns string, deployments []k8sclient.DeploymentItem, statefulsets []k8sclient.StatefulSetItem, daemonsets []k8sclient.DaemonSetItem) []k8sWorkloadFinding {
|
||||||
|
var out []k8sWorkloadFinding
|
||||||
|
for _, d := range deployments {
|
||||||
|
if d.Metadata.Namespace != ns || d.Spec.Replicas == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if d.Status.ReadyReplicas < d.Spec.Replicas || d.Status.AvailableReplicas < d.Spec.Replicas {
|
||||||
|
out = append(out, k8sWorkloadFinding{
|
||||||
|
Kind: "Deployment", Namespace: d.Metadata.Namespace, Name: d.Metadata.Name,
|
||||||
|
Desired: d.Spec.Replicas, Ready: d.Status.ReadyReplicas, Available: d.Status.AvailableReplicas,
|
||||||
|
Reason: "ready/available replicas less than desired", Image: firstDeploymentImage(d),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range statefulsets {
|
||||||
|
if s.Metadata.Namespace != ns || s.Spec.Replicas == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Status.ReadyReplicas < s.Spec.Replicas {
|
||||||
|
out = append(out, k8sWorkloadFinding{
|
||||||
|
Kind: "StatefulSet", Namespace: s.Metadata.Namespace, Name: s.Metadata.Name,
|
||||||
|
Desired: s.Spec.Replicas, Ready: s.Status.ReadyReplicas,
|
||||||
|
Reason: "ready replicas less than desired", Image: firstStatefulSetImage(s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, d := range daemonsets {
|
||||||
|
if d.Metadata.Namespace != ns || d.Status.DesiredNumberScheduled == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if d.Status.NumberReady < d.Status.DesiredNumberScheduled {
|
||||||
|
out = append(out, k8sWorkloadFinding{
|
||||||
|
Kind: "DaemonSet", Namespace: d.Metadata.Namespace, Name: d.Metadata.Name,
|
||||||
|
Desired: d.Status.DesiredNumberScheduled, Ready: d.Status.NumberReady,
|
||||||
|
Reason: "ready pods less than desired scheduled pods", Image: firstDaemonSetImage(d),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
if out[i].Kind == out[j].Kind {
|
||||||
|
return out[i].Name < out[j].Name
|
||||||
|
}
|
||||||
|
return out[i].Kind < out[j].Kind
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnosePods(ns string, pods []k8sclient.PodItem, deployments []k8sclient.DeploymentItem) []k8sPodFinding {
|
||||||
|
var out []k8sPodFinding
|
||||||
|
for _, p := range pods {
|
||||||
|
if p.Metadata.Namespace != ns {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, reason, restarts, messages := podDiagnosisStatus(p)
|
||||||
|
if !isAbnormalPod(status, reason, restarts) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, k8sPodFinding{
|
||||||
|
Namespace: p.Metadata.Namespace, Name: p.Metadata.Name, Status: status, Reason: reason,
|
||||||
|
Restarts: restarts, Node: p.Spec.NodeName, IP: p.Status.PodIP, Image: firstPodImage(p),
|
||||||
|
Owner: inferDeploymentOwner(p, deployments), Messages: messages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
if out[i].Reason == out[j].Reason {
|
||||||
|
return out[i].Name < out[j].Name
|
||||||
|
}
|
||||||
|
return podReasonRank(out[i].Reason) < podReasonRank(out[j].Reason)
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func podDiagnosisStatus(p k8sclient.PodItem) (string, string, int, []string) {
|
||||||
|
status := p.Status.Phase
|
||||||
|
reason := ""
|
||||||
|
restarts := 0
|
||||||
|
var messages []string
|
||||||
|
for _, cs := range p.Status.ContainerStatuses {
|
||||||
|
if cs.RestartCount > restarts {
|
||||||
|
restarts = cs.RestartCount
|
||||||
|
}
|
||||||
|
if cs.State.Waiting != nil && cs.State.Waiting.Reason != "" {
|
||||||
|
status = cs.State.Waiting.Reason
|
||||||
|
reason = cs.State.Waiting.Reason
|
||||||
|
if cs.State.Waiting.Message != "" {
|
||||||
|
messages = append(messages, truncateK8sText(cs.State.Waiting.Message, 220))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cs.LastState.Terminated != nil && cs.LastState.Terminated.Reason != "" {
|
||||||
|
if reason == "" {
|
||||||
|
reason = cs.LastState.Terminated.Reason
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf("last terminated: %s exit=%d", cs.LastState.Terminated.Reason, cs.LastState.Terminated.ExitCode)
|
||||||
|
messages = append(messages, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range p.Status.Conditions {
|
||||||
|
if c.Type == "Ready" && c.Status != "True" {
|
||||||
|
if reason == "" && c.Reason != "" {
|
||||||
|
reason = c.Reason
|
||||||
|
}
|
||||||
|
if c.Message != "" {
|
||||||
|
messages = append(messages, truncateK8sText(c.Message, 220))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reason == "" {
|
||||||
|
reason = status
|
||||||
|
}
|
||||||
|
return status, reason, restarts, dedupeStrings(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnoseEvents(events []k8sclient.EventItem, limit int) []k8sEventFinding {
|
||||||
|
sort.Slice(events, func(i, j int) bool {
|
||||||
|
return eventTime(events[i]).After(eventTime(events[j]))
|
||||||
|
})
|
||||||
|
var out []k8sEventFinding
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, e := range events {
|
||||||
|
if e.Type != "Warning" && !isImportantEventReason(e.Reason) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s/%s/%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Reason, e.Message)
|
||||||
|
if seen[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
out = append(out, k8sEventFinding{
|
||||||
|
Type: e.Type, Reason: e.Reason,
|
||||||
|
Object: fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name),
|
||||||
|
Count: e.Count, LastSeen: eventTime(e).Format(time.RFC3339),
|
||||||
|
Message: truncateK8sText(e.Message, 260),
|
||||||
|
})
|
||||||
|
if len(out) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnoseNodes(nodes *k8sclient.NodeList) []k8sNodeFinding {
|
||||||
|
if nodes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []k8sNodeFinding
|
||||||
|
for _, n := range nodes.Items {
|
||||||
|
for _, c := range n.Status.Conditions {
|
||||||
|
if c.Type == "Ready" && c.Status != "True" {
|
||||||
|
out = append(out, k8sNodeFinding{Name: n.Metadata.Name, Ready: c.Status, Reason: c.Reason, Message: truncateK8sText(c.Message, 220)})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func inferK8sRootCauses(pods []k8sPodFinding, events []k8sEventFinding) []string {
|
||||||
|
flags := make(map[string]bool)
|
||||||
|
for _, p := range pods {
|
||||||
|
r := strings.ToLower(p.Reason + " " + p.Status)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r, "imagepull") || strings.Contains(r, "errimagepull"):
|
||||||
|
flags["存在镜像拉取失败,优先检查镜像 tag、Harbor 仓库、imagePullSecrets 和节点到仓库网络。"] = true
|
||||||
|
case strings.Contains(r, "crashloop"):
|
||||||
|
flags["存在容器反复崩溃,优先查看 previous logs、启动参数、依赖连接和最近变更。"] = true
|
||||||
|
case strings.Contains(r, "oom"):
|
||||||
|
flags["存在 OOMKilled 风险,优先检查内存 limit、最近内存趋势和应用内存泄漏。"] = true
|
||||||
|
case strings.Contains(r, "containersnotready") || strings.Contains(r, "unready"):
|
||||||
|
flags["存在容器未就绪,需结合 readiness/liveness probe、事件和应用日志确认。"] = true
|
||||||
|
case strings.Contains(r, "pending") || strings.Contains(r, "failedscheduling"):
|
||||||
|
flags["存在调度失败或 Pending,优先检查节点资源、污点、亲和性和 PVC。"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range events {
|
||||||
|
msg := strings.ToLower(e.Reason + " " + e.Message)
|
||||||
|
if strings.Contains(msg, "liveness probe") || strings.Contains(msg, "readiness probe") {
|
||||||
|
flags["事件显示健康探针失败,需检查探针路径、超时时间、应用响应耗时和依赖服务。"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedMapKeys(flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recommendK8sActions(pods []k8sPodFinding, workloads []k8sWorkloadFinding) []string {
|
||||||
|
var out []string
|
||||||
|
if len(workloads) > 0 {
|
||||||
|
out = append(out, "先处理 ready/available 副本不足的 Workload,再回查副本是否恢复。")
|
||||||
|
}
|
||||||
|
if hasPodReason(pods, "ImagePullBackOff") || hasPodReason(pods, "ErrImagePull") {
|
||||||
|
out = append(out, "对镜像拉取失败的 Pod,确认镜像 tag 是否存在、仓库权限是否有效、节点能否访问镜像仓库。")
|
||||||
|
}
|
||||||
|
if hasPodReason(pods, "CrashLoopBackOff") {
|
||||||
|
out = append(out, "对 CrashLoopBackOff 的 Pod,补查 previous logs 和启动命令,避免只看当前空日志。")
|
||||||
|
}
|
||||||
|
if hasPodReason(pods, "ContainersNotReady") {
|
||||||
|
out = append(out, "对未就绪 Pod,查看事件中的 readiness/liveness probe 失败原因和应用健康端点耗时。")
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
out = append(out, "当前聚合诊断未发现明显异常;如仍有业务问题,继续按服务名查看日志、Ingress、Service selector 和指标。")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func recommendK8sNextTools(pods []k8sPodFinding, events []k8sEventFinding, limit int) []map[string]interface{} {
|
||||||
|
var out []map[string]interface{}
|
||||||
|
for _, p := range pods {
|
||||||
|
if len(out) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(p.Reason), "imagepull") {
|
||||||
|
out = append(out, map[string]interface{}{"tool": "kubernetes_get", "args": map[string]interface{}{"resource": "events", "name": p.Name}})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.Status == "CrashLoopBackOff" || p.Restarts >= 5 || strings.Contains(strings.ToLower(p.Reason), "notready") {
|
||||||
|
out = append(out, map[string]interface{}{"tool": "pod_logs", "args": map[string]interface{}{"namespace": p.Namespace, "pod": p.Name, "tail": 120}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 && len(events) > 0 {
|
||||||
|
out = append(out, map[string]interface{}{"tool": "kubernetes_get", "args": map[string]interface{}{"resource": "events"}})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAbnormalPod(status, reason string, restarts int) bool {
|
||||||
|
s := strings.ToLower(status + " " + reason)
|
||||||
|
if status != "Running" && status != "Succeeded" && status != "PodCompleted" && status != "Completed" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "notready") || strings.Contains(s, "crashloop") || strings.Contains(s, "imagepull") || strings.Contains(s, "oom") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return restarts >= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func podReasonRank(reason string) int {
|
||||||
|
r := strings.ToLower(reason)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r, "imagepull"):
|
||||||
|
return 1
|
||||||
|
case strings.Contains(r, "crashloop"):
|
||||||
|
return 2
|
||||||
|
case strings.Contains(r, "oom"):
|
||||||
|
return 3
|
||||||
|
case strings.Contains(r, "notready"):
|
||||||
|
return 4
|
||||||
|
default:
|
||||||
|
return 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inferDeploymentOwner(p k8sclient.PodItem, deployments []k8sclient.DeploymentItem) string {
|
||||||
|
for _, d := range deployments {
|
||||||
|
if d.Metadata.Namespace != p.Metadata.Namespace || d.Spec.Selector == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if labelsMatch(p.Metadata.Labels, d.Spec.Selector.MatchLabels) {
|
||||||
|
return "Deployment/" + d.Metadata.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstDeploymentImage(d k8sclient.DeploymentItem) string {
|
||||||
|
if len(d.Spec.Template.Spec.Containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return d.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstStatefulSetImage(s k8sclient.StatefulSetItem) string {
|
||||||
|
if len(s.Spec.Template.Spec.Containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstDaemonSetImage(d k8sclient.DaemonSetItem) string {
|
||||||
|
if len(d.Spec.Template.Spec.Containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return d.Spec.Template.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstPodImage(p k8sclient.PodItem) string {
|
||||||
|
if len(p.Spec.Containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return p.Spec.Containers[0].Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventTime(e k8sclient.EventItem) time.Time {
|
||||||
|
if !e.LastTimestamp.IsZero() {
|
||||||
|
return e.LastTimestamp
|
||||||
|
}
|
||||||
|
if !e.EventTime.IsZero() {
|
||||||
|
return e.EventTime
|
||||||
|
}
|
||||||
|
return e.FirstTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func isImportantEventReason(reason string) bool {
|
||||||
|
r := strings.ToLower(reason)
|
||||||
|
return strings.Contains(r, "failed") || strings.Contains(r, "backoff") || strings.Contains(r, "unhealthy")
|
||||||
|
}
|
||||||
|
|
||||||
|
func countDeployments(ns string, items []k8sclient.DeploymentItem) int {
|
||||||
|
n := 0
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Metadata.Namespace == ns {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func countStatefulSets(ns string, items []k8sclient.StatefulSetItem) int {
|
||||||
|
n := 0
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Metadata.Namespace == ns {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func countDaemonSets(ns string, items []k8sclient.DaemonSetItem) int {
|
||||||
|
n := 0
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Metadata.Namespace == ns {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func countPods(ns string, items []k8sclient.PodItem) int {
|
||||||
|
n := 0
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Metadata.Namespace == ns {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func countWarningEvents(items []k8sclient.EventItem) int {
|
||||||
|
n := 0
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Type == "Warning" {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPodReason(pods []k8sPodFinding, reason string) bool {
|
||||||
|
needle := strings.ToLower(reason)
|
||||||
|
for _, p := range pods {
|
||||||
|
if strings.Contains(strings.ToLower(p.Reason+" "+p.Status), needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func limitWorkloadFindings(items []k8sWorkloadFinding, limit int) []k8sWorkloadFinding {
|
||||||
|
if len(items) <= limit {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
return items[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
func limitPodFindings(items []k8sPodFinding, limit int) []k8sPodFinding {
|
||||||
|
if len(items) <= limit {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
return items[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateK8sText(s string, max int) string {
|
||||||
|
s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
|
||||||
|
if len([]rune(s)) <= max {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
r := []rune(s)
|
||||||
|
return string(r[:max]) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeStrings(items []string) []string {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var out []string
|
||||||
|
for _, item := range items {
|
||||||
|
if item == "" || seen[item] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item] = true
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedMapKeys(m map[string]bool) []string {
|
||||||
|
out := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPodDiagnosisStatusDetectsWaitingReason(t *testing.T) {
|
||||||
|
pod := k8sclient.PodItem{
|
||||||
|
Status: k8sclient.PodStatus{
|
||||||
|
Phase: "Pending",
|
||||||
|
ContainerStatuses: []k8sclient.ContainerStatus{{
|
||||||
|
Name: "app",
|
||||||
|
State: k8sclient.ContainerStatusState{Waiting: &k8sclient.ContainerStateWaiting{
|
||||||
|
Reason: "ImagePullBackOff", Message: "Back-off pulling image",
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
status, reason, restarts, messages := podDiagnosisStatus(pod)
|
||||||
|
if status != "ImagePullBackOff" || reason != "ImagePullBackOff" {
|
||||||
|
t.Fatalf("unexpected status=%s reason=%s", status, reason)
|
||||||
|
}
|
||||||
|
if restarts != 0 {
|
||||||
|
t.Fatalf("unexpected restarts=%d", restarts)
|
||||||
|
}
|
||||||
|
if len(messages) != 1 {
|
||||||
|
t.Fatalf("expected waiting message, got %v", messages)
|
||||||
|
}
|
||||||
|
if !isAbnormalPod(status, reason, restarts) {
|
||||||
|
t.Fatal("ImagePullBackOff should be abnormal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodDiagnosisStatusDetectsOOMKilled(t *testing.T) {
|
||||||
|
pod := k8sclient.PodItem{
|
||||||
|
Status: k8sclient.PodStatus{
|
||||||
|
Phase: "Running",
|
||||||
|
ContainerStatuses: []k8sclient.ContainerStatus{{
|
||||||
|
Name: "app", Ready: true, RestartCount: 3,
|
||||||
|
LastState: k8sclient.ContainerStatusState{Terminated: &k8sclient.ContainerStateTerminated{
|
||||||
|
Reason: "OOMKilled", ExitCode: 137,
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
status, reason, restarts, _ := podDiagnosisStatus(pod)
|
||||||
|
if status != "Running" || reason != "OOMKilled" || restarts != 3 {
|
||||||
|
t.Fatalf("unexpected status=%s reason=%s restarts=%d", status, reason, restarts)
|
||||||
|
}
|
||||||
|
if !isAbnormalPod(status, reason, restarts) {
|
||||||
|
t.Fatal("OOMKilled should be abnormal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAbnormalPodByRestartThreshold(t *testing.T) {
|
||||||
|
if !isAbnormalPod("Running", "Running", 10) {
|
||||||
|
t.Fatal("high restart pod should be abnormal")
|
||||||
|
}
|
||||||
|
if isAbnormalPod("Running", "Running", 2) {
|
||||||
|
t.Fatal("healthy running pod should not be abnormal")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,7 +96,6 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&model.AlertNotifyPolicy{}, // 告警集合通知策略 // Logan.Li
|
&model.AlertNotifyPolicy{}, // 告警集合通知策略 // Logan.Li
|
||||||
&model.AlertNotifySettings{}, // 告警消息模版 // Logan.Li
|
&model.AlertNotifySettings{}, // 告警消息模版 // Logan.Li
|
||||||
&model.NotifyChannel{},
|
&model.NotifyChannel{},
|
||||||
&model.MetricData{},
|
|
||||||
&model.Pipeline{},
|
&model.Pipeline{},
|
||||||
&model.PipelineRun{},
|
&model.PipelineRun{},
|
||||||
&model.BuildNode{},
|
&model.BuildNode{},
|
||||||
@@ -121,11 +120,10 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&model.AgentTask{},
|
&model.AgentTask{},
|
||||||
&model.AgentRule{},
|
&model.AgentRule{},
|
||||||
&model.AgentAlert{},
|
&model.AgentAlert{},
|
||||||
|
&model.AgentPortPolicy{}, // Agent 端口策略 // Logan.Li
|
||||||
&model.AnsiblePlaybook{},
|
&model.AnsiblePlaybook{},
|
||||||
&model.AnsibleJob{},
|
&model.AnsibleJob{},
|
||||||
&model.AnsibleInventory{},
|
&model.AnsibleInventory{},
|
||||||
&model.LogSource{},
|
|
||||||
&model.LogEntry{},
|
|
||||||
&model.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
|
&model.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
|
||||||
&model.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
|
&model.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
|
||||||
&model.RDPSession{}, // RDP 图形会话审计 // Logan.Li
|
&model.RDPSession{}, // RDP 图形会话审计 // Logan.Li
|
||||||
@@ -136,7 +134,6 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&model.MetricsAggregate{}, // 指标时序聚合(Redis → MySQL)// Logan.Li
|
&model.MetricsAggregate{}, // 指标时序聚合(Redis → MySQL)// Logan.Li
|
||||||
&model.MonitorTarget{}, // 监控目标(主机/中间件/设备/站点) // Logan.Li
|
&model.MonitorTarget{}, // 监控目标(主机/中间件/设备/站点) // Logan.Li
|
||||||
&model.MonitorMetric{}, // 监控指标定义 // Logan.Li
|
&model.MonitorMetric{}, // 监控指标定义 // Logan.Li
|
||||||
&model.MetricValue{}, // 指标当前值 // Logan.Li
|
|
||||||
&model.MetricHistory{}, // 指标历史时序 // Logan.Li
|
&model.MetricHistory{}, // 指标历史时序 // Logan.Li
|
||||||
&model.HostGroup{}, // 主机组 // Logan.Li
|
&model.HostGroup{}, // 主机组 // Logan.Li
|
||||||
&model.SNMPDevice{}, // SNMP设备 // Logan.Li
|
&model.SNMPDevice{}, // SNMP设备 // Logan.Li
|
||||||
@@ -152,8 +149,6 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&model.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li
|
&model.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li
|
||||||
&model.LogDataSource{}, // Loki / ES 日志数据源 // Logan.Li
|
&model.LogDataSource{}, // Loki / ES 日志数据源 // Logan.Li
|
||||||
&model.Credential{}, // 凭据管理(SSH密钥/API Token) // Logan.Li
|
&model.Credential{}, // 凭据管理(SSH密钥/API Token) // Logan.Li
|
||||||
&model.PipelineRepo{}, // Git 仓库认证 // Logan.Li
|
|
||||||
&model.PipelineRegistry{}, // 镜像仓库认证 // Logan.Li
|
|
||||||
&model.DeployEnvironment{}, // 发布环境分组 // Logan.Li
|
&model.DeployEnvironment{}, // 发布环境分组 // Logan.Li
|
||||||
&model.DeployPipelineMember{}, // 流水线成员 // Logan.Li
|
&model.DeployPipelineMember{}, // 流水线成员 // Logan.Li
|
||||||
&model.DeployUserGroup{}, // 用户组 // Logan.Li
|
&model.DeployUserGroup{}, // 用户组 // Logan.Li
|
||||||
@@ -172,9 +167,13 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&model.HealthProbe{}, // 健康探针配置 // Logan.Li
|
&model.HealthProbe{}, // 健康探针配置 // Logan.Li
|
||||||
&model.ApprovalWorkflow{}, // 审批流程定义 // Logan.Li
|
&model.ApprovalWorkflow{}, // 审批流程定义 // Logan.Li
|
||||||
&model.ApprovalWorkflowStep{}, // 审批流程步骤 // Logan.Li
|
&model.ApprovalWorkflowStep{}, // 审批流程步骤 // Logan.Li
|
||||||
&model.AgentCLISession{}, // Agent CLI 任务会话 // Logan.Li
|
&model.AIRun{}, // AI 任务统一运行记录 // Logan.Li
|
||||||
&model.AgentCLIMessage{}, // Agent CLI 推理消息 // Logan.Li
|
&model.AIRunMessage{}, // AI 任务回放消息 // Logan.Li
|
||||||
&model.CustomDashboard{}, // 自定义监控仪表盘 // Logan.Li
|
&model.AIRunSpan{}, // AI 工具/LLM/Guardrail Trace Span // Logan.Li
|
||||||
|
&model.AICapability{}, // AI 内置/MCP/技能能力注册 // Logan.Li
|
||||||
|
&model.AICapabilityPolicy{}, // AI 工具能力策略 // Logan.Li
|
||||||
|
&model.OpsSession{}, // 人机统一执行会话 // Logan.Li
|
||||||
|
&model.OpsCommand{}, // 人机统一命令审计 // Logan.Li
|
||||||
&model.TraceDataSource{}, // 链路追踪数据源 Jaeger/Tempo // Logan.Li
|
&model.TraceDataSource{}, // 链路追踪数据源 Jaeger/Tempo // Logan.Li
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,8 +230,10 @@ func applySchemaPatches(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ensureTable(&model.LogScopeRole{}, "monitor_log_scope_roles")
|
ensureTable(&model.LogScopeRole{}, "iam_log_scope_roles")
|
||||||
ensureTable(&model.UserLogScopeRole{}, "monitor_user_log_scope_roles")
|
if !m.HasTable("iam_user_log_scope_roles") {
|
||||||
|
createUserLogScopeRolesTable(db)
|
||||||
|
}
|
||||||
|
|
||||||
// LDAP 目录类型列(openldap | ad) // Logan.Li
|
// LDAP 目录类型列(openldap | ad) // Logan.Li
|
||||||
if m.HasTable(&model.LDAPConfig{}) && !m.HasColumn(&model.LDAPConfig{}, "server_type") {
|
if m.HasTable(&model.LDAPConfig{}) && !m.HasColumn(&model.LDAPConfig{}, "server_type") {
|
||||||
@@ -252,6 +253,9 @@ func applySchemaPatches(db *gorm.DB) {
|
|||||||
patchServerColumn("OSType", "os_type")
|
patchServerColumn("OSType", "os_type")
|
||||||
patchServerColumn("Protocol", "protocol")
|
patchServerColumn("Protocol", "protocol")
|
||||||
patchServerColumn("RDPPort", "rdp_port")
|
patchServerColumn("RDPPort", "rdp_port")
|
||||||
|
patchServerColumn("Alias", "alias")
|
||||||
|
patchServerColumn("CloudInstanceName", "cloud_instance_name")
|
||||||
|
patchServerColumn("CloudInstanceID", "cloud_instance_id")
|
||||||
|
|
||||||
// 将历史仅填 os=windows 的记录补齐协议字段 // Logan.Li
|
// 将历史仅填 os=windows 的记录补齐协议字段 // Logan.Li
|
||||||
if m.HasColumn(&model.Server{}, "os_type") && m.HasColumn(&model.Server{}, "protocol") {
|
if m.HasColumn(&model.Server{}, "os_type") && m.HasColumn(&model.Server{}, "protocol") {
|
||||||
@@ -263,3 +267,26 @@ func applySchemaPatches(db *gorm.DB) {
|
|||||||
`).Error
|
`).Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createUserLogScopeRolesTable(db *gorm.DB) {
|
||||||
|
sql := `CREATE TABLE IF NOT EXISTS iam_user_log_scope_roles (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
log_scope_role_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
created_at DATETIME(3) NULL,
|
||||||
|
UNIQUE KEY idx_iam_user_log_scope_role (user_id, log_scope_role_id),
|
||||||
|
KEY idx_iam_user_log_scope_role_role (log_scope_role_id)
|
||||||
|
)`
|
||||||
|
if db.Dialector.Name() == "sqlite" {
|
||||||
|
sql = `CREATE TABLE IF NOT EXISTS iam_user_log_scope_roles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
log_scope_role_id INTEGER NOT NULL,
|
||||||
|
created_at DATETIME,
|
||||||
|
UNIQUE(user_id, log_scope_role_id)
|
||||||
|
)`
|
||||||
|
}
|
||||||
|
if err := db.Exec(sql).Error; err != nil {
|
||||||
|
log.Printf("[DB] 创建 iam_user_log_scope_roles 失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,12 +51,12 @@ func CleanupSNMPMetricsSQL(driver string, retentionDays int) string {
|
|||||||
switch driver {
|
switch driver {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"DELETE FROM ops_snmp_metrics WHERE timestamp < CAST(strftime('%%s', 'now', '-%d days') AS INTEGER)",
|
"DELETE FROM monitor_snmp_samples WHERE timestamp < CAST(strftime('%%s', 'now', '-%d days') AS INTEGER)",
|
||||||
retentionDays,
|
retentionDays,
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"DELETE FROM ops_snmp_metrics WHERE timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL %d DAY))",
|
"DELETE FROM monitor_snmp_samples WHERE timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL %d DAY))",
|
||||||
retentionDays,
|
retentionDays,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,16 +203,22 @@ func (c *Client) get(path string, result interface{}) error {
|
|||||||
// getRaw performs a GET request returning raw bytes with custom Accept header
|
// getRaw performs a GET request returning raw bytes with custom Accept header
|
||||||
func (c *Client) getRaw(path string, accept string) ([]byte, error) {
|
func (c *Client) getRaw(path string, accept string) ([]byte, error) {
|
||||||
req, err := http.NewRequest("GET", c.BaseURL+path, nil)
|
req, err := http.NewRequest("GET", c.BaseURL+path, nil)
|
||||||
if err != nil { return nil, err }
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
req.Header.Set("Accept", accept)
|
req.Header.Set("Accept", accept)
|
||||||
if c.BearerToken != "" {
|
if c.BearerToken != "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
|
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
|
||||||
}
|
}
|
||||||
resp, err := c.HTTPClient.Do(req)
|
resp, err := c.HTTPClient.Do(req)
|
||||||
if err != nil { return nil, fmt.Errorf("GET %s: %w", path, err) }
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GET %s: %w", path, err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil { return nil, fmt.Errorf("read body: %w", err) }
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read body: %w", err)
|
||||||
|
}
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return nil, fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 200)]))
|
return nil, fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 200)]))
|
||||||
}
|
}
|
||||||
@@ -222,13 +228,17 @@ func (c *Client) getRaw(path string, accept string) ([]byte, error) {
|
|||||||
// putRaw performs a PUT request with raw body and custom Content-Type
|
// putRaw performs a PUT request with raw body and custom Content-Type
|
||||||
func (c *Client) putRaw(path string, contentType string, data []byte) error {
|
func (c *Client) putRaw(path string, contentType string, data []byte) error {
|
||||||
req, err := http.NewRequest("PUT", c.BaseURL+path, strings.NewReader(string(data)))
|
req, err := http.NewRequest("PUT", c.BaseURL+path, strings.NewReader(string(data)))
|
||||||
if err != nil { return err }
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
if c.BearerToken != "" {
|
if c.BearerToken != "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
|
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
|
||||||
}
|
}
|
||||||
resp, err := c.HTTPClient.Do(req)
|
resp, err := c.HTTPClient.Do(req)
|
||||||
if err != nil { return fmt.Errorf("PUT %s: %w", path, err) }
|
if err != nil {
|
||||||
|
return fmt.Errorf("PUT %s: %w", path, err)
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
@@ -337,8 +347,30 @@ type PodCondition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ContainerStatus struct {
|
type ContainerStatus struct {
|
||||||
|
Name string `json:"name"`
|
||||||
RestartCount int `json:"restartCount"`
|
RestartCount int `json:"restartCount"`
|
||||||
Ready bool `json:"ready"`
|
Ready bool `json:"ready"`
|
||||||
|
State ContainerStatusState `json:"state,omitempty"`
|
||||||
|
LastState ContainerStatusState `json:"lastState,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerStatusState 解析容器等待/终止状态,用于 AI 诊断 ImagePullBackOff、OOMKilled 等原因。 // Logan.Li
|
||||||
|
type ContainerStatusState struct {
|
||||||
|
Waiting *ContainerStateWaiting `json:"waiting,omitempty"`
|
||||||
|
Terminated *ContainerStateTerminated `json:"terminated,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerStateWaiting 容器等待状态。 // Logan.Li
|
||||||
|
type ContainerStateWaiting struct {
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerStateTerminated 容器终止状态。 // Logan.Li
|
||||||
|
type ContainerStateTerminated struct {
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
ExitCode int `json:"exitCode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeploymentList response
|
// DeploymentList response
|
||||||
@@ -1801,8 +1833,10 @@ func (c *Client) applyRaw(yamlContent string) (map[string]interface{}, error) {
|
|||||||
func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (string, bool) {
|
func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (string, bool) {
|
||||||
resource := strings.ToLower(kind) + "s"
|
resource := strings.ToLower(kind) + "s"
|
||||||
switch kind {
|
switch kind {
|
||||||
case "Endpoints": resource = "endpoints"
|
case "Endpoints":
|
||||||
case "PodMetrics": resource = "pods"
|
resource = "endpoints"
|
||||||
|
case "PodMetrics":
|
||||||
|
resource = "pods"
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupPrefix string
|
var groupPrefix string
|
||||||
@@ -1829,7 +1863,9 @@ func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
ns := namespace
|
ns := namespace
|
||||||
if ns == "" { ns = "default" }
|
if ns == "" {
|
||||||
|
ns = "default"
|
||||||
|
}
|
||||||
|
|
||||||
if groupPrefix != "" {
|
if groupPrefix != "" {
|
||||||
switch kind {
|
switch kind {
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ var (
|
|||||||
// GetUserLogScopeRoles 获取用户已启用的日志权限策略 // Logan.Li
|
// GetUserLogScopeRoles 获取用户已启用的日志权限策略 // Logan.Li
|
||||||
func GetUserLogScopeRoles(db *gorm.DB, userID uint) ([]model.LogScopeRole, error) {
|
func GetUserLogScopeRoles(db *gorm.DB, userID uint) ([]model.LogScopeRole, error) {
|
||||||
var roles []model.LogScopeRole
|
var roles []model.LogScopeRole
|
||||||
err := db.Table("monitor_log_scope_roles").
|
err := db.Table("iam_log_scope_roles").
|
||||||
Joins("JOIN monitor_user_log_scope_roles ON monitor_user_log_scope_roles.log_scope_role_id = monitor_log_scope_roles.id").
|
Joins("JOIN iam_user_log_scope_roles ON iam_user_log_scope_roles.log_scope_role_id = iam_log_scope_roles.id").
|
||||||
Where("monitor_user_log_scope_roles.user_id = ? AND monitor_log_scope_roles.status = 1", userID).
|
Where("iam_user_log_scope_roles.user_id = ? AND iam_log_scope_roles.status = 1", userID).
|
||||||
Find(&roles).Error
|
Find(&roles).Error
|
||||||
return roles, err
|
return roles, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -668,24 +668,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
releases.POST("/:id/rollback", pipelineHandler.RollbackRelease)
|
releases.POST("/:id/rollback", pipelineHandler.RollbackRelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仓库认证 & 镜像仓库认证路由
|
|
||||||
deployHandler := controller.NewDeployHandler(db)
|
|
||||||
{
|
|
||||||
repos := authorized.Group("/deploy/repos")
|
|
||||||
repos.GET("", deployHandler.ListRepos)
|
|
||||||
repos.POST("", deployHandler.CreateRepo)
|
|
||||||
repos.GET("/:id", deployHandler.GetRepo)
|
|
||||||
repos.PUT("/:id", deployHandler.UpdateRepo)
|
|
||||||
repos.DELETE("/:id", deployHandler.DeleteRepo)
|
|
||||||
|
|
||||||
registries := authorized.Group("/deploy/registries")
|
|
||||||
registries.GET("", deployHandler.ListRegistries)
|
|
||||||
registries.POST("", deployHandler.CreateRegistry)
|
|
||||||
registries.GET("/:id", deployHandler.GetRegistry)
|
|
||||||
registries.PUT("/:id", deployHandler.UpdateRegistry)
|
|
||||||
registries.DELETE("/:id", deployHandler.DeleteRegistry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具配置路由(发布配置 Tab ④)// Logan.Li
|
// 工具配置路由(发布配置 Tab ④)// Logan.Li
|
||||||
toolConfigHandler := controller.NewToolConfigHandler(db, ti)
|
toolConfigHandler := controller.NewToolConfigHandler(db, ti)
|
||||||
tools := authorized.Group("/deploy/tools")
|
tools := authorized.Group("/deploy/tools")
|
||||||
@@ -1053,18 +1035,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection)
|
zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义监控仪表盘 — CRUD + 设默认 // Logan.Li
|
|
||||||
customDashHandler := controller.NewCustomDashboardHandler(db)
|
|
||||||
customDashGroup := authorized.Group("/custom-dashboards")
|
|
||||||
{
|
|
||||||
customDashGroup.GET("", customDashHandler.List)
|
|
||||||
customDashGroup.GET("/:id", customDashHandler.Get)
|
|
||||||
customDashGroup.POST("", customDashHandler.Create)
|
|
||||||
customDashGroup.PUT("/:id", customDashHandler.Update)
|
|
||||||
customDashGroup.DELETE("/:id", customDashHandler.Delete)
|
|
||||||
customDashGroup.POST("/:id/default", customDashHandler.SetDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 链路追踪数据源管理 — CRUD + 测试连接 + 设默认 // Logan.Li
|
// 链路追踪数据源管理 — CRUD + 测试连接 + 设默认 // Logan.Li
|
||||||
traceDSHandler := controller.NewTraceDSHandler(db)
|
traceDSHandler := controller.NewTraceDSHandler(db)
|
||||||
traceProxyHandler := controller.NewTraceProxyHandler(traceDSHandler)
|
traceProxyHandler := controller.NewTraceProxyHandler(traceDSHandler)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package seed
|
package seed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
snmppkg "ezops/pkg/snmp"
|
|
||||||
"ezops/model"
|
"ezops/model"
|
||||||
|
snmppkg "ezops/pkg/snmp"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
# 数据表与菜单域审计记录
|
||||||
|
|
||||||
|
> 审计范围:`backend/model`、`backend/pkg/database/database.go`、主要 controller/service/repository 引用、`backend/seed/seed.go` 默认菜单权限。
|
||||||
|
> 目标:按菜单域整理表清单,记录表名前缀、设计合理性、疑似旧表/无用表和后续整改建议。
|
||||||
|
|
||||||
|
## 重建版目标设计
|
||||||
|
|
||||||
|
测试环境允许直接重建时,建议采用 **Schema v2 全局统一方案**:按菜单域和平台基础域重新归类,旧表不做兼容 rename,直接调整 `TableName()`、AutoMigrate 清单、种子数据和相关查询引用,然后清空数据库重建。
|
||||||
|
|
||||||
|
### 全局命名原则
|
||||||
|
|
||||||
|
1. 表名统一使用:`<domain>_<resource_plural>`。
|
||||||
|
2. 领域前缀与菜单域保持一致,避免同一功能散落在 `sys_`、`ops_`、`monitor_`。
|
||||||
|
3. 关联表统一使用:`<left_domain>_<left>_<right>` 或同域 `<domain>_<left>_<right>`。
|
||||||
|
4. 配置类表可用单数语义但表名仍用复数,例如 `deploy_build_master_configs`。
|
||||||
|
5. 时序/日志大表必须明确后缀:`*_samples`、`*_histories`、`*_events`、`*_logs`。
|
||||||
|
6. 不再新增无前缀表。
|
||||||
|
|
||||||
|
### 目标前缀
|
||||||
|
|
||||||
|
| 前缀 | 菜单/领域 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `iam_` | 身份与权限 | 用户、角色、权限、凭据、LDAP、资源级授权 |
|
||||||
|
| `sys_` | 系统基础配置 | 系统任务、站内导航、全局配置 |
|
||||||
|
| `audit_` | 安全审计 | 操作审计、登录日志、终端/RDP 会话、命令审计 |
|
||||||
|
| `asset_` | 资产/云资源 | 服务器、云账号、云区域、域名、账单、资源型号 |
|
||||||
|
| `k8s_` | Kubernetes | 集群与同步缓存资源 |
|
||||||
|
| `deploy_` | 应用发布 | 流水线、运行记录、发布、环境、构建节点、工具 |
|
||||||
|
| `monitor_` | 监控告警 | 告警、指标、Agent、SNMP、站点拨测、中间件、数据源 |
|
||||||
|
| `ops_` | 自动化运维 | 定时任务、脚本、远程执行、Ansible 如保留 |
|
||||||
|
| `approval_` | 资源申请/审批 | 模板、流程、实例、步骤 |
|
||||||
|
| `ai_` | AIOps/AI | LLM、会话、分析轨迹、Agent CLI 运行记录 |
|
||||||
|
|
||||||
|
### 目标表清单
|
||||||
|
|
||||||
|
#### IAM / 系统基础
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `iam_users` | 保留 |
|
||||||
|
| `iam_roles` | 保留 |
|
||||||
|
| `iam_permissions` | 保留 |
|
||||||
|
| `iam_user_roles` | 保留 |
|
||||||
|
| `iam_role_permissions` | 替换 `role_permissions` |
|
||||||
|
| `iam_credentials` | 保留,作为全局凭据唯一主表 |
|
||||||
|
| `iam_ldap_configs` | 保留 |
|
||||||
|
| `iam_item_roles` | 保留 |
|
||||||
|
| `iam_user_item_roles` | 保留 |
|
||||||
|
| `iam_log_scope_roles` | 从 `monitor_log_scope_roles` 调整,日志查询权限本质是权限控制 |
|
||||||
|
| `iam_user_log_scope_roles` | 从 `monitor_user_log_scope_roles` 调整 |
|
||||||
|
| `sys_system_task_statuses` | 从 `sys_system_task_status` 调整为复数 |
|
||||||
|
| `sys_navigation_groups` | 保留 |
|
||||||
|
| `sys_navigation_links` | 替换 `navigation_links` |
|
||||||
|
|
||||||
|
#### 安全审计
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `audit_operation_logs` | 替换 `sys_audit_logs` |
|
||||||
|
| `audit_settings` | 替换 `sys_audit_settings` |
|
||||||
|
| `audit_login_logs` | 替换 `sys_login_logs` |
|
||||||
|
| `audit_terminal_sessions` | 替换 `sys_terminal_sessions` |
|
||||||
|
| `audit_rdp_sessions` | 替换 `sys_rdp_sessions` |
|
||||||
|
| `audit_session_commands` | 替换 `sys_session_commands` |
|
||||||
|
|
||||||
|
#### 资产 / 云资源
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `asset_servers` | 保留 |
|
||||||
|
| `asset_host_groups` | 保留 |
|
||||||
|
| `asset_cloud_platforms` | 替换 `asset_clouds` |
|
||||||
|
| `asset_cloud_accounts` | 保留,敏感字段统一转凭据 ID |
|
||||||
|
| `asset_cloud_regions` | 替换 `asset_regions` |
|
||||||
|
| `asset_domains` | 保留 |
|
||||||
|
| `asset_resource_models` | 保留 |
|
||||||
|
| `asset_cloud_billing_records` | 替换 `asset_cloud_billings`,语义更明确 |
|
||||||
|
| `asset_cloud_billing_alert_rules` | 替换 `monitor_billing_alert_rules`,账单告警属于资产账单域配置,触发事件再进监控事件 |
|
||||||
|
|
||||||
|
#### Kubernetes
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `k8s_clusters` | 替换 `kubernetes_clusters` |
|
||||||
|
| `k8s_nodes` | 替换 `kubernetes_nodes` |
|
||||||
|
| `k8s_pods` | 替换 `kubernetes_pods` |
|
||||||
|
| `k8s_deployments` | 替换 `kubernetes_deployments` |
|
||||||
|
| `k8s_services` | 替换 `kubernetes_services` |
|
||||||
|
| `k8s_ingresses` | 替换 `kubernetes_ingresses` |
|
||||||
|
| `k8s_topology_layouts` | 替换 `kubernetes_topology_layouts` |
|
||||||
|
|
||||||
|
#### 应用发布
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `deploy_pipelines` | 替换 `ops_pipelines` |
|
||||||
|
| `deploy_pipeline_runs` | 替换 `ops_pipeline_runs` |
|
||||||
|
| `deploy_app_releases` | 保留 |
|
||||||
|
| `deploy_environments` | 保留 |
|
||||||
|
| `deploy_build_nodes` | 保留 |
|
||||||
|
| `deploy_build_master_configs` | 替换 `deploy_build_master_config` |
|
||||||
|
| `deploy_pipeline_members` | 保留 |
|
||||||
|
| `deploy_user_groups` | 保留 |
|
||||||
|
| `deploy_user_group_members` | 保留 |
|
||||||
|
| `deploy_tools` | 保留 |
|
||||||
|
| 已删除 `deploy_pipeline_repos` | 发布配置直接使用 `iam_credentials` + Pipeline 凭据字段 |
|
||||||
|
| 已删除 `deploy_pipeline_registries` | 发布配置直接使用 `iam_credentials` + Pipeline 凭据字段 |
|
||||||
|
|
||||||
|
#### 监控告警
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `monitor_targets` | 保留 |
|
||||||
|
| `monitor_metrics` | 保留,作为指标定义表 |
|
||||||
|
| `monitor_metric_samples` | 合并/替换 `metric_values` 和 `monitor_metric_histories` |
|
||||||
|
| `monitor_metric_aggregates` | 替换 `monitor_metrics_aggregate` |
|
||||||
|
| `monitor_metric_tags` | 保留 |
|
||||||
|
| `monitor_alert_rules` | 保留但只保留一个模型定义 |
|
||||||
|
| `monitor_alert_conditions` | 保留 |
|
||||||
|
| `monitor_alert_events` | 保留 |
|
||||||
|
| `monitor_alert_channels` | 替换 `monitor_notify_channels` |
|
||||||
|
| `monitor_alert_contacts` | 保留 |
|
||||||
|
| `monitor_alert_contact_groups` | 保留 |
|
||||||
|
| `monitor_alert_notify_policies` | 保留 |
|
||||||
|
| `monitor_alert_notify_templates` | 保留 |
|
||||||
|
| `monitor_alert_notify_settings` | 保留 |
|
||||||
|
| `monitor_alert_upgrades` | 保留 |
|
||||||
|
| `monitor_alert_upgrade_records` | 保留 |
|
||||||
|
| `monitor_alert_silences` | 保留 |
|
||||||
|
| `monitor_agent_reports` | 保留 |
|
||||||
|
| `monitor_agent_tasks` | 保留 |
|
||||||
|
| `monitor_agent_rules` | 保留 |
|
||||||
|
| `monitor_agent_alerts` | 保留 |
|
||||||
|
| `monitor_agent_port_policies` | 替换 `monitor_agent_port_policy`,并加入迁移 |
|
||||||
|
| `monitor_health_probes` | 保留 |
|
||||||
|
| `monitor_prometheus_datasources` | 保留 |
|
||||||
|
| `monitor_zabbix_datasources` | 保留 |
|
||||||
|
| `monitor_log_datasources` | 保留 |
|
||||||
|
| `monitor_trace_datasources` | 保留 |
|
||||||
|
| `monitor_site_tasks` | 替换 `ops_site_tasks` |
|
||||||
|
| `monitor_site_results` | 替换 `ops_site_results` |
|
||||||
|
| `monitor_site_alerts` | 替换 `ops_site_alerts` |
|
||||||
|
| `monitor_snmp_devices` | 替换 `ops_snmp_devices` |
|
||||||
|
| `monitor_snmp_templates` | 替换 `ops_snmp_templates` |
|
||||||
|
| `monitor_snmp_template_oids` | 替换 `ops_snmp_template_oids` |
|
||||||
|
| `monitor_snmp_samples` | 替换 `ops_snmp_metrics`,删除 `snmp_history` |
|
||||||
|
| `monitor_snmp_interfaces` | 替换 `ops_snmp_interfaces` |
|
||||||
|
| `monitor_dashboards` | 保留一个大屏主表 |
|
||||||
|
| `monitor_dashboard_panels` | 替换 `dashboard_panels` |
|
||||||
|
| 已删除 `monitor_custom_dashboards` | 合并到 `monitor_dashboards`,用 `type/custom` 区分 |
|
||||||
|
| 删除 `monitor_metric_data` | 用 `monitor_metrics` 替代 |
|
||||||
|
|
||||||
|
#### 自动化运维
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ops_cron_jobs` | 保留 |
|
||||||
|
| `ops_scripts` | 保留 |
|
||||||
|
| `ops_task_executions` | 保留 |
|
||||||
|
| `ops_remote_executions` | 建议新增,记录远程批量执行任务级审计 |
|
||||||
|
| `ops_ansible_playbooks` | 如保留 Ansible 页面则保留,否则删除 |
|
||||||
|
| `ops_ansible_jobs` | 同上 |
|
||||||
|
| `ops_ansible_inventories` | 同上 |
|
||||||
|
|
||||||
|
#### 审批
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `approval_templates` | 替换 `sys_approval_templates` |
|
||||||
|
| `approval_processes` | 保留 |
|
||||||
|
| `approval_steps` | 保留 |
|
||||||
|
| `approval_workflows` | 替换 `sys_approval_workflows` |
|
||||||
|
| `approval_workflow_steps` | 替换 `sys_approval_workflow_steps` |
|
||||||
|
|
||||||
|
建议:模板保留表单 schema,工作流表保留审批流定义,流程实例引用 `workflow_id`,逐步弱化 `flow_config` JSON。
|
||||||
|
|
||||||
|
#### AIOps / AI
|
||||||
|
|
||||||
|
| 目标表 | 来源/处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ai_llm_configs` | 保留 |
|
||||||
|
| `ai_chat_histories` | 保留 |
|
||||||
|
| `ai_analysis_traces` | 保留 |
|
||||||
|
| `ai_runs` | 保留 |
|
||||||
|
| `ai_run_messages` | 保留 |
|
||||||
|
| `ai_run_spans` | 保留 |
|
||||||
|
| `ai_capabilities` | 保留 |
|
||||||
|
| `ai_capability_policies` | 保留 |
|
||||||
|
| `ai_ops_sessions` | 替换 `ops_sessions` |
|
||||||
|
| `ai_ops_commands` | 替换 `ops_commands` |
|
||||||
|
|
||||||
|
### 重建执行策略
|
||||||
|
|
||||||
|
1. 修改所有 `TableName()`,把目标表名一次性统一。
|
||||||
|
2. 修改 GORM many2many tag:`role_permissions` -> `iam_role_permissions`。
|
||||||
|
3. 修改硬编码 `db.Table(...)` 和 SQL 清理逻辑。
|
||||||
|
4. 精简 AutoMigrate:删除已合并/废弃模型,补上漏迁移模型。
|
||||||
|
5. 更新 seed:默认权限、审批模板、工具配置、演示数据按新表生成。
|
||||||
|
6. 删除测试库数据卷或 SQLite 文件,重新启动服务让 AutoMigrate 建表。
|
||||||
|
7. 跑 `go build`、`npx tsc --noEmit`、集成测试和关键页面验证。
|
||||||
|
|
||||||
|
## 总体结论
|
||||||
|
|
||||||
|
当前表名前缀没有形成统一分层,主要使用 `asset_`、`monitor_`、`ops_`、`deploy_`、`sys_`、`iam_`、`ai_`、`kubernetes_`。整体可以运行,但存在几类需要后续整改的问题:
|
||||||
|
|
||||||
|
1. **无前缀或前缀不一致表**:`approval_processes`、`approval_steps`、`navigation_links`、`metric_values`、`dashboard_panels`、`snmp_history`、`role_permissions`。
|
||||||
|
2. **同一职责多套表并存**:审批有 `sys_approval_templates / approval_processes / approval_steps` 与 `sys_approval_workflows / sys_approval_workflow_steps`;监控大屏有 `monitor_dashboards / dashboard_panels` 与 `monitor_custom_dashboards`;告警规则有 `AlertRule` 和 `MonitorAlertRule` 共用 `monitor_alert_rules`。
|
||||||
|
3. **表定义但迁移/引用不完整**:`SNMPHistory` 定义了 `snmp_history`,但未进入 AutoMigrate,代码实际查询 `ops_snmp_metrics`;`AgentPortPolicy` 有控制器引用,但未进入 AutoMigrate。
|
||||||
|
4. **疑似旧表/低价值表**:`sys_log_sources`、`sys_log_entries` 属于旧日志模块,与当前日志中心 `monitor_log_datasources` + 外部 Loki/ES 架构重复;`ops_ansible_*` 有种子数据但“远程执行”现已主要走 SSH/脚本执行,需要确认是否仍保留 Ansible 页面。
|
||||||
|
5. **设计一致性问题**:大量 JSON 字符串字段承载结构化配置,短期可接受,但要明确边界;高频时序表缺少统一保留策略和分区方案;部分关联表缺少显式外键/唯一约束命名规范。
|
||||||
|
|
||||||
|
## 现状前缀评估
|
||||||
|
|
||||||
|
> 本节是现状评估口径;重建整改时以“重建版目标设计”为准。
|
||||||
|
|
||||||
|
| 领域 | 当前/建议前缀 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| IAM/权限/凭据 | `iam_` | 用户、角色、权限、LDAP、凭据、ItemRole |
|
||||||
|
| 系统基础配置 | `sys_` | 系统任务、站内导航、全局配置 |
|
||||||
|
| 安全审计 | `audit_` | 重建后从 `sys_` 拆出,避免系统配置与审计数据混在一起 |
|
||||||
|
| 资产/云资源 | `asset_` | 服务器、云账号、域名、账单、资源模型 |
|
||||||
|
| Kubernetes | `k8s_` | 重建后统一短前缀,替换当前 `kubernetes_` |
|
||||||
|
| 发布/部署 | `deploy_` | 环境、凭据、工具、发布记录、成员权限 |
|
||||||
|
| 自动化执行 | `ops_` | 定时任务、脚本、任务执行、远程执行记录 |
|
||||||
|
| 监控告警 | `monitor_` | 指标、目标、告警、数据源、站点拨测、SNMP、Agent |
|
||||||
|
| 审批 | `approval_` | 重建后模板、流程、实例统一归入审批域 |
|
||||||
|
| AI/AIOps | `ai_` | LLM 配置、会话、分析轨迹、能力策略 |
|
||||||
|
|
||||||
|
## 问题清单
|
||||||
|
|
||||||
|
| 编号 | 严重度 | 表/模型 | 问题 | 建议 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| DB-001 | 高 | `AlertRule` / `MonitorAlertRule` -> `monitor_alert_rules` | 两个模型返回同一张表,字段定义不同。AutoMigrate 会叠加字段,后续维护容易误删/误覆盖。 | 合并为一个模型或拆成 `monitor_alert_rules` 与 `monitor_probe_alert_rules`,迁移前确认线上字段使用。 |
|
||||||
|
| DB-002 | 高 | `AgentPortPolicy` -> `monitor_agent_port_policy` | 控制器有读写,但未加入 AutoMigrate,新库端口策略接口可能报“表不存在”。 | 加入 AutoMigrate;表名建议改为复数 `monitor_agent_port_policies`,如改名需迁移旧表。 |
|
||||||
|
| DB-003 | 高 | `SNMPHistory` -> `snmp_history` | 定义未迁移,代码未引用;清理逻辑清的是 `ops_snmp_metrics`。 | 若 `ops_snmp_metrics` 已兼任历史表,删除 `SNMPHistory`;否则新增迁移并把查询/清理改到 `monitor_snmp_histories`。 |
|
||||||
|
| DB-004 | 中 | `approval_processes`、`approval_steps` | 审批流程表无前缀,而模板表是 `sys_approval_templates`。 | 重建后统一为 `approval_templates`、`approval_processes`、`approval_steps`、`approval_workflows`。 |
|
||||||
|
| DB-005 | 中 | `sys_approval_workflows`、`sys_approval_workflow_steps` | 与 `sys_approval_templates.flow_config` 和 `approval_steps` 职责有重叠。 | 明确“模板流程配置”和“流程定义管理”的关系;如果新工作流已替代模板内 JSON,应迁移数据并下线旧字段。 |
|
||||||
|
| DB-006 | 中 | `navigation_links` | 导航组是 `sys_navigation_groups`,链接表无前缀。 | 重建后改为 `sys_navigation_links`。 |
|
||||||
|
| DB-007 | 中 | `metric_values`、`monitor_metric_histories`、`monitor_metrics_aggregate` | 监控时序有三套表:原始值、简化历史、聚合;当前控制器大量使用 `monitor_metric_histories`,`metric_values` 引用少。 | 重建后合并为 `monitor_metric_samples` + `monitor_metric_aggregates`。 |
|
||||||
|
| DB-008 | 已处理 | `monitor_dashboards`、`monitor_dashboard_panels` | 自定义大屏旧模型与路由已删除,统一使用 `monitor_dashboards` + `monitor_dashboard_panels`。 | 后续仅补齐新大屏 API/页面。 |
|
||||||
|
| DB-009 | 中 | `role_permissions` | GORM many2many 明确指定为 `role_permissions`,无 `iam_` 前缀。 | 重建后改为 `iam_role_permissions`。 |
|
||||||
|
| DB-010 | 中 | `sys_log_sources`、`sys_log_entries` | 旧日志采集/入库表,与当前日志中心外部数据源架构重复。 | 重建后删除,日志中心只保留 `monitor_log_datasources` 和外部查询结果。 |
|
||||||
|
| DB-011 | 中 | `ops_site_*`、`ops_snmp_*` | 站点拨测和 SNMP 属于监控域,但表名前缀使用 `ops_`。 | 重建后改为 `monitor_site_*`、`monitor_snmp_*`。 |
|
||||||
|
| DB-012 | 中 | `ai_runs`、`ops_sessions`、`ops_commands` | AI 运行表和人机执行会话跨 `ai_` / `ops_`,边界不清。 | 重建后改为 `ai_ops_sessions`、`ai_ops_commands`。 |
|
||||||
|
| DB-013 | 低 | `backend/model/monitor.go`、`deploy_tool.go` | 模型文件超过或接近项目红线,维护成本高。 | 后续按子域拆分:target/metric/dashboard/health_probe/tool_registry。 |
|
||||||
|
|
||||||
|
## 菜单域表清单
|
||||||
|
|
||||||
|
### 仪表盘
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 首页统计 | 多域聚合,无独立主表 | 统计来自资产、告警、K8s、流水线等表,合理。 |
|
||||||
|
| 自定义监控仪表盘 | `monitor_dashboards`、`monitor_dashboard_panels` | 已统一到监控大屏主表和面板表。 |
|
||||||
|
|
||||||
|
### 资产管理 / 云资源
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 服务器管理 | `asset_servers` | 前缀规范;字段较多但与 Agent/RDP/SSH 场景匹配。 |
|
||||||
|
| 主机组 | `asset_host_groups` | 放资产域合理。 |
|
||||||
|
| 云平台 | `asset_clouds` | 命名偏泛,实际是云平台类型/供应商。可考虑改为 `asset_cloud_platforms`。 |
|
||||||
|
| 云账号 | `asset_cloud_accounts` | 前缀规范;凭证字段需确认是否已统一走 `iam_credentials` 加密。 |
|
||||||
|
| 云区域 | `asset_regions` | 前缀规范;如果仅云区域,建议 `asset_cloud_regions` 更清晰。 |
|
||||||
|
| 域名管理 | `asset_domains` | 前缀规范。 |
|
||||||
|
| 资源型号 | `asset_resource_models` | 前缀规范。 |
|
||||||
|
| 云账单 | `asset_cloud_billings` | 前缀规范。 |
|
||||||
|
| 账单告警规则 | `monitor_billing_alert_rules` | 归入监控域合理,但与资产账单强关联,需文档化。 |
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 集群管理 | `kubernetes_clusters` | 当前统一 `kubernetes_` 前缀,虽较长但一致。 |
|
||||||
|
| 节点缓存 | `kubernetes_nodes` | 从集群同步缓存,合理。 |
|
||||||
|
| Pod 缓存 | `kubernetes_pods` | 从集群同步缓存,合理。 |
|
||||||
|
| Deployment 缓存 | `kubernetes_deployments` | 从集群同步缓存,合理。 |
|
||||||
|
| Service 缓存 | `kubernetes_services` | 从集群同步缓存,合理。 |
|
||||||
|
| Ingress 缓存 | `kubernetes_ingresses` | 从集群同步缓存,合理。 |
|
||||||
|
| 拓扑布局 | `kubernetes_topology_layouts` | 用户布局数据,合理。 |
|
||||||
|
|
||||||
|
建议:重建后统一改为 `k8s_`,后续不再新增 `kubernetes_` 表。
|
||||||
|
|
||||||
|
### 应用发布 / CI/CD
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 流水线 | `ops_pipelines` | 历史命名属 `ops_`,但该域现在已经有 `deploy_` 前缀,建议长期统一。 |
|
||||||
|
| 流水线运行 | `ops_pipeline_runs` | 同上;日志存在 `logs longtext`,数据增长需清理策略。 |
|
||||||
|
| 应用发布 | `deploy_app_releases` | 前缀规范。 |
|
||||||
|
| Git 仓库配置 | `deploy_pipeline_repos` | 可能与统一凭据 `iam_credentials` 存在职责重叠,当前通过 `credential_id` 关联可接受。 |
|
||||||
|
| 镜像仓库配置 | `deploy_pipeline_registries` | 同上。 |
|
||||||
|
| 发布环境 | `deploy_environments` | 前缀规范。 |
|
||||||
|
| 构建节点 | `deploy_build_nodes`、`deploy_build_master_config` | 前缀规范。 |
|
||||||
|
| 流水线成员 | `deploy_pipeline_members` | 前缀规范;直接用户 + 用户组双授权设计合理。 |
|
||||||
|
| 发布用户组 | `deploy_user_groups`、`deploy_user_group_members` | 前缀规范,但与 IAM 角色/组职责需边界清晰。 |
|
||||||
|
| ItemRole | `iam_item_roles`、`iam_user_item_roles` | 放 IAM 域合理,用于流水线正则授权。 |
|
||||||
|
| 工具配置 | `deploy_tools` | 前缀规范。 |
|
||||||
|
|
||||||
|
建议:重建后统一改为 `deploy_pipelines`、`deploy_pipeline_runs`。
|
||||||
|
|
||||||
|
### 监控告警
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 告警规则/事件 | `monitor_alert_rules`、`monitor_alert_events` | 主表合理;但规则模型重复是高风险问题。 |
|
||||||
|
| 告警条件/模板/升级/静默 | `monitor_alert_conditions`、`monitor_alert_notify_templates`、`monitor_alert_upgrades`、`monitor_alert_upgrade_records`、`monitor_alert_silences` | 前缀规范;需确认哪些已接入 UI/引擎。 |
|
||||||
|
| 通知渠道/联系人 | `monitor_notify_channels`、`monitor_alert_contacts`、`monitor_alert_contact_groups`、`monitor_alert_notify_policies`、`monitor_alert_notify_settings` | 前缀规范;通知设置/策略/模板职责较多,建议后续画数据流。 |
|
||||||
|
| Agent 指标 | `monitor_agent_reports`、`monitor_metrics_aggregate` | 前缀规范;按 hostname 关联服务器,删除服务器时有清理。 |
|
||||||
|
| Agent 任务/规则/告警 | `monitor_agent_tasks`、`monitor_agent_rules`、`monitor_agent_alerts` | 前缀规范。 |
|
||||||
|
| Agent 端口策略 | `monitor_agent_port_policy` | 未迁移,且表名非复数。 |
|
||||||
|
| 监控目标/指标 | `monitor_targets`、`monitor_metrics`、`monitor_metric_histories`、`monitor_metric_tags`、`metric_values` | 需要统一时序表职责和命名前缀。 |
|
||||||
|
| 健康探针 | `monitor_health_probes` | 前缀规范;结果不独立入库,随 Agent 心跳更新。 |
|
||||||
|
| Prometheus/Zabbix/日志/链路数据源 | `monitor_prometheus_datasources`、`monitor_zabbix_datasources`、`monitor_log_datasources`、`monitor_trace_datasources` | 前缀规范。 |
|
||||||
|
| 站点拨测 | `ops_site_tasks`、`ops_site_results`、`ops_site_alerts` | 功能属监控,前缀却是 `ops_`。 |
|
||||||
|
| SNMP | `ops_snmp_devices`、`ops_snmp_templates`、`ops_snmp_template_oids`、`ops_snmp_metrics`、`ops_snmp_interfaces`、`snmp_history` | 功能属监控;`snmp_history` 未迁移/未引用。 |
|
||||||
|
| 指标目录旧表 | `monitor_metric_data` | 需确认是否仍有 UI/引擎依赖;可能与 `monitor_metrics` 重复。 |
|
||||||
|
|
||||||
|
### 自动化运维
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 定时任务 | `ops_cron_jobs` | 前缀规范。 |
|
||||||
|
| 脚本库 | `ops_scripts` | 前缀规范。 |
|
||||||
|
| 执行记录 | `ops_task_executions` | 前缀规范。 |
|
||||||
|
| Ansible | `ops_ansible_playbooks`、`ops_ansible_jobs`、`ops_ansible_inventories` | 有模型和种子数据;需确认 UI 是否仍作为主功能。若已由 Go-native SSH 替代,可降级为 deprecated。 |
|
||||||
|
|
||||||
|
### AIOps / AI
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| LLM 配置 | `ai_llm_configs` | 前缀规范。 |
|
||||||
|
| 对话历史 | `ai_chat_histories` | 前缀规范。 |
|
||||||
|
| 告警分析轨迹 | `ai_analysis_traces` | 前缀规范。 |
|
||||||
|
| Agent CLI 运行 | `ai_runs`、`ai_run_messages`、`ai_run_spans` | 前缀规范。 |
|
||||||
|
| 能力注册/策略 | `ai_capabilities`、`ai_capability_policies` | 前缀规范。 |
|
||||||
|
| 人机执行会话 | `ops_sessions`、`ops_commands` | 与 AI Agent 强关联但用 `ops_`,边界需确认。 |
|
||||||
|
|
||||||
|
### 资源申请 / 审批
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 审批模板 | `sys_approval_templates` | 前缀规范。 |
|
||||||
|
| 审批实例 | `approval_processes` | 无前缀。 |
|
||||||
|
| 审批步骤 | `approval_steps` | 无前缀。 |
|
||||||
|
| 审批工作流 | `sys_approval_workflows`、`sys_approval_workflow_steps` | 前缀规范,但与模板内 `flow_config` 和实例步骤存在职责重叠。 |
|
||||||
|
|
||||||
|
建议:先确认现行前端使用旧模板流程还是新 workflow;再决定迁移方向。
|
||||||
|
|
||||||
|
### 安全审计
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 操作审计 | `sys_audit_logs` | 前缀规范。 |
|
||||||
|
| 审计策略 | `sys_audit_settings` | 前缀规范。 |
|
||||||
|
| 登录日志 | `sys_login_logs` | 前缀规范。 |
|
||||||
|
| 终端会话 | `sys_terminal_sessions` | 前缀规范。 |
|
||||||
|
| RDP 会话 | `sys_rdp_sessions` | 前缀规范。 |
|
||||||
|
| 会话命令 | `sys_session_commands` | 前缀规范。 |
|
||||||
|
|
||||||
|
### 系统管理 / IAM
|
||||||
|
|
||||||
|
| 功能 | 表 | 评价 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 用户 | `iam_users` | 前缀规范。 |
|
||||||
|
| 角色 | `iam_roles` | 前缀规范。 |
|
||||||
|
| 权限 | `iam_permissions` | 前缀规范。 |
|
||||||
|
| 用户角色关系 | `iam_user_roles` | GORM many2many 指定,前缀规范。 |
|
||||||
|
| 角色权限关系 | `role_permissions` | GORM many2many 指定,无 `iam_` 前缀。 |
|
||||||
|
| LDAP | `iam_ldap_configs` | 前缀规范。 |
|
||||||
|
| 凭据 | `iam_credentials` | 前缀规范;建议继续作为全局凭据统一入口。 |
|
||||||
|
| 日志范围权限 | `monitor_log_scope_roles`、`monitor_user_log_scope_roles` | 属日志中心/监控域,放 `monitor_` 合理。 |
|
||||||
|
| 系统任务状态 | `sys_system_task_status` | 前缀规范。 |
|
||||||
|
| 站内导航 | `sys_navigation_groups`、`navigation_links` | 链接表无前缀。 |
|
||||||
|
|
||||||
|
## 疑似可清理候选
|
||||||
|
|
||||||
|
测试库重建时可直接删除;生产整改前仍需查数据量、最近写入时间、controller/frontend 是否仍有入口。
|
||||||
|
|
||||||
|
| 表/模型 | 判断依据 | 建议动作 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `snmp_history` / `SNMPHistory` | 未 AutoMigrate,未被业务引用。 | 重建后删除模型。 |
|
||||||
|
| `sys_log_sources`、`sys_log_entries` | 旧日志入库模块,与日志中心外部数据源重复。 | 重建后删除。 |
|
||||||
|
| `monitor_custom_dashboards` | 与 `monitor_dashboards` 重复。 | 已删除模型与路由,重建后不再创建。 |
|
||||||
|
| `metric_values`、`monitor_metric_histories` | 时序职责重复。 | 重建后合并为 `monitor_metric_samples`。 |
|
||||||
|
| `ops_ansible_*` | 远程执行主路径已偏 Go-native SSH。 | 若确认无页面入口,重建后删除;否则保留在 `ops_`。 |
|
||||||
|
| `deploy_pipeline_repos`、`deploy_pipeline_registries` | Pipeline 已直接使用 `GitCredentialID` / `RegistryCredentialID`。 | 已删除模型、路由和前端旧配置入口,重建后不再创建。 |
|
||||||
|
|
||||||
|
## 后续整改顺序
|
||||||
|
|
||||||
|
1. **先统一命名和模型归属**:按 Schema v2 修改 `TableName()`、many2many tag、硬编码 SQL。
|
||||||
|
2. **处理重复模型**:合并 `AlertRule` / `MonitorAlertRule`、大屏表、指标时序表、审批 workflow/模板关系。
|
||||||
|
3. **删除废弃模型**:`SNMPHistory`、旧日志入库、旧仓库/镜像仓库配置、确认不用的 Ansible 表。
|
||||||
|
4. **重建测试库**:删除 SQLite 文件或 Docker 数据卷,让 AutoMigrate 从零建表。
|
||||||
|
5. **补充数据生命周期**:流水线日志、Agent 报告、指标样本、SNMP 样本、站点拨测结果、审计日志都需要保留期和清理任务。
|
||||||
+53
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense, lazy } from 'react';
|
import { Suspense, lazy, useEffect, useRef } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
@@ -10,6 +10,7 @@ import { PageTransition } from '@/components/layout/PageTransition';
|
|||||||
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
// Lazy load page components for code splitting
|
// Lazy load page components for code splitting
|
||||||
const Dashboard = lazy(() => import('@/components/dashboard/Dashboard').then(m => ({ default: m.Dashboard })));
|
const Dashboard = lazy(() => import('@/components/dashboard/Dashboard').then(m => ({ default: m.Dashboard })));
|
||||||
@@ -104,12 +105,63 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function AssetVersionGuard() {
|
||||||
|
const currentVersionRef = useRef<string>('');
|
||||||
|
const notifiedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const readVersion = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/version?t=${Date.now()}`, { cache: 'no-store' });
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const data = await resp.json();
|
||||||
|
const nextVersion = [data.version, data.git_commit, data.build_time].filter(Boolean).join(':');
|
||||||
|
if (!nextVersion) return;
|
||||||
|
if (!currentVersionRef.current) {
|
||||||
|
currentVersionRef.current = nextVersion;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentVersionRef.current !== nextVersion && !notifiedRef.current) {
|
||||||
|
notifiedRef.current = true;
|
||||||
|
toast.info(i18n.t('common.appUpdated'), {
|
||||||
|
description: i18n.t('common.appUpdatedDesc'),
|
||||||
|
action: {
|
||||||
|
label: i18n.t('common.reloadPage'),
|
||||||
|
onClick: () => window.location.reload(),
|
||||||
|
},
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 版本检查失败不影响业务页面,后续定时器会重试。 // Logan.Li
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void readVersion();
|
||||||
|
const timer = window.setInterval(readVersion, 60_000);
|
||||||
|
const handleFocus = () => void readVersion();
|
||||||
|
const handleVisibility = () => {
|
||||||
|
if (document.visibilityState === 'visible') void readVersion();
|
||||||
|
};
|
||||||
|
window.addEventListener('focus', handleFocus);
|
||||||
|
document.addEventListener('visibilitychange', handleVisibility);
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timer);
|
||||||
|
window.removeEventListener('focus', handleFocus);
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibility);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ConfirmProvider>
|
<ConfirmProvider>
|
||||||
<Toaster position="top-right" richColors closeButton />
|
<Toaster position="top-right" richColors closeButton />
|
||||||
|
<AssetVersionGuard />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|||||||
@@ -2,44 +2,47 @@
|
|||||||
import { http } from '@/utils/request';
|
import { http } from '@/utils/request';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
// 会话与消息类型(与后端 model.AgentCLISession/Message 对应) // Logan.Li
|
// 会话与消息类型(与后端 model.AIRun/AIRunMessage 对应) // Logan.Li
|
||||||
export interface AgentCliSession {
|
export interface AgentCliSession {
|
||||||
id: number;
|
id: number;
|
||||||
|
run_id: string;
|
||||||
|
parent_run_id: string;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
feature: string;
|
||||||
mode: string; // host | kubernetes
|
mode: string; // host | kubernetes
|
||||||
server_id: number;
|
subject_type: string;
|
||||||
server_name: string;
|
subject_id: string;
|
||||||
server_host: string;
|
subject_name: string;
|
||||||
cluster_id: number;
|
subject_scope: string;
|
||||||
cluster_name: string;
|
|
||||||
namespace: string;
|
|
||||||
pod_name: string;
|
|
||||||
container: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
status: string; // running/done/aborted/error
|
status: string; // running/done/aborted/error
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
safety_mode: string;
|
||||||
|
context_pack_id: string;
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
rounds: number;
|
rounds: number;
|
||||||
total_tokens: number;
|
total_tokens: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
started_at: string;
|
||||||
|
finished_at?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentCliMessage {
|
export interface AgentCliMessage {
|
||||||
id: number;
|
id: number;
|
||||||
session_id: number;
|
run_id: string;
|
||||||
round: number;
|
round: number;
|
||||||
|
seq: number;
|
||||||
type: string; // thought/tool_call/tool_result/done/error/user
|
type: string; // thought/tool_call/tool_result/done/error/user
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
tool_name: string;
|
tool_name: string;
|
||||||
tool_args: string;
|
tool_call_id: string;
|
||||||
tool_output: string;
|
span_id: string;
|
||||||
risk: string;
|
|
||||||
success: boolean;
|
|
||||||
duration: string;
|
|
||||||
tokens: number;
|
tokens: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,6 @@
|
|||||||
import { http, type ListResponse } from '@/utils/request';
|
import { http, type ListResponse } from '@/utils/request';
|
||||||
|
|
||||||
export const deployApi = {
|
export const deployApi = {
|
||||||
// Git 仓库认证
|
|
||||||
getRepos: () => http.get<ListResponse>('/deploy/repos'),
|
|
||||||
getRepo: (id: number | string) => http.get(`/deploy/repos/${id}`),
|
|
||||||
createRepo: ( data: Record<string, unknown>) => http.post('/deploy/repos', data),
|
|
||||||
updateRepo: (id: number | string, data: Record<string, unknown>) => http.put(`/deploy/repos/${id}`, data),
|
|
||||||
deleteRepo: (id: number | string) => http.delete(`/deploy/repos/${id}`),
|
|
||||||
// 镜像仓库认证
|
|
||||||
getRegistries: () => http.get<ListResponse>('/deploy/registries'),
|
|
||||||
getRegistry: (id: number | string) => http.get(`/deploy/registries/${id}`),
|
|
||||||
createRegistry: ( data: Record<string, unknown>) => http.post('/deploy/registries', data),
|
|
||||||
updateRegistry: (id: number | string, data: Record<string, unknown>) => http.put(`/deploy/registries/${id}`, data),
|
|
||||||
deleteRegistry: (id: number | string) => http.delete(`/deploy/registries/${id}`),
|
|
||||||
// 环境管理
|
// 环境管理
|
||||||
getEnvironments: () => http.get<ListResponse>('/deploy/environments'),
|
getEnvironments: () => http.get<ListResponse>('/deploy/environments'),
|
||||||
createEnvironment: ( data: Record<string, unknown>) => http.post('/deploy/environments', data),
|
createEnvironment: ( data: Record<string, unknown>) => http.post('/deploy/environments', data),
|
||||||
|
|||||||
@@ -54,17 +54,17 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] {
|
|||||||
items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content });
|
items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content });
|
||||||
break;
|
break;
|
||||||
case 'status':
|
case 'status':
|
||||||
items.push({ id: nextId(), kind: 'status', round: m.round, phase: m.duration || undefined, content: m.content, toolName: m.tool_name, risk: m.risk });
|
items.push({ id: nextId(), kind: 'status', round: m.round, content: m.content, toolName: m.tool_name });
|
||||||
break;
|
break;
|
||||||
case 'tool_call': {
|
case 'tool_call': {
|
||||||
let command = '';
|
let command = '';
|
||||||
try {
|
try {
|
||||||
const args = JSON.parse(m.tool_args || '{}') as Record<string, unknown>;
|
const args = JSON.parse(m.content || '{}') as Record<string, unknown>;
|
||||||
command = String(args.command || '');
|
command = String(args.command || (args.action ? `${args.action} ${args.name || ''}` : ''));
|
||||||
} catch { /* 忽略解析失败 */ }
|
} catch { /* 忽略解析失败 */ }
|
||||||
items.push({
|
items.push({
|
||||||
id: nextId(), kind: 'tool', round: m.round,
|
id: nextId(), kind: 'tool', round: m.round,
|
||||||
toolName: m.tool_name, command, risk: m.risk, pending: true,
|
toolName: m.tool_name, command, pending: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -73,9 +73,8 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] {
|
|||||||
if (items[i].kind === 'tool' && items[i].pending && items[i].toolName === m.tool_name) {
|
if (items[i].kind === 'tool' && items[i].pending && items[i].toolName === m.tool_name) {
|
||||||
items[i] = {
|
items[i] = {
|
||||||
...items[i],
|
...items[i],
|
||||||
output: m.content || m.tool_output,
|
output: m.content,
|
||||||
success: m.success,
|
success: true,
|
||||||
duration: m.duration,
|
|
||||||
pending: false,
|
pending: false,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Plus, Key, Globe, Container } from 'lucide-react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { credentialApi, deployApi } from '@/api';
|
|
||||||
import type { JsonData } from '@/types/common';
|
|
||||||
|
|
||||||
interface Repo {
|
|
||||||
id: number; name: string; url: string; credential_id: number | null;
|
|
||||||
}
|
|
||||||
interface Registry {
|
|
||||||
id: number; name: string; registry_url: string; namespace: string; credential_id: number | null;
|
|
||||||
}
|
|
||||||
interface Credential {
|
|
||||||
id: number; name: string; secret_type: string; summary: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (v: boolean) => void;
|
|
||||||
type: 'repo' | 'registry';
|
|
||||||
editId: number | null;
|
|
||||||
repos: Repo[];
|
|
||||||
registries: Registry[];
|
|
||||||
onSave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CredentialFormDialog({ open, onOpenChange, type, editId, repos, registries, onSave }: Props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const isRepo = type === 'repo';
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [desc, setDesc] = useState('');
|
|
||||||
const [url, setUrl] = useState('');
|
|
||||||
const [namespace, setNamespace] = useState('');
|
|
||||||
const [credentialId, setCredentialId] = useState<number | string>('');
|
|
||||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
|
|
||||||
// Docker 仓库 → docker 类型;Git 仓库 → api 或 docker 类型
|
|
||||||
const filteredCredentials = credentials.filter(c =>
|
|
||||||
isRepo ? true : c.secret_type === 'docker'
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadCredentials = async () => {
|
|
||||||
try {
|
|
||||||
const res: JsonData = await credentialApi.list({ page_size: 100 });
|
|
||||||
setCredentials(res?.list || []);
|
|
||||||
} catch { /* silent */ }
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadCredentials();
|
|
||||||
if (editId) {
|
|
||||||
if (isRepo) {
|
|
||||||
const r = repos.find(x => x.id === editId);
|
|
||||||
if (r) { setName(r.name); setDesc(''); setUrl(r.url); setCredentialId(r.credential_id || ''); }
|
|
||||||
} else {
|
|
||||||
const r = registries.find(x => x.id === editId);
|
|
||||||
if (r) { setName(r.name); setDesc(''); setUrl(r.registry_url); setNamespace(r.namespace || ''); setCredentialId(r.credential_id || ''); }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setName(''); setDesc(''); setUrl(''); setNamespace(''); setCredentialId('');
|
|
||||||
}
|
|
||||||
}, [editId, open, isRepo]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!name.trim() || !url.trim()) { toast.error(t('deploy.form.nameAddrRequired')); return; }
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
const cid = credentialId ? Number(credentialId) : null;
|
|
||||||
const payload = isRepo
|
|
||||||
? { name: name.trim(), description: desc.trim(), url: url.trim(), credential_id: cid }
|
|
||||||
: { name: name.trim(), description: desc.trim(), registry_url: url.trim(), namespace: namespace.trim(), credential_id: cid };
|
|
||||||
if (editId) {
|
|
||||||
if (isRepo) await deployApi.updateRepo(editId, payload);
|
|
||||||
else await deployApi.updateRegistry(editId, payload);
|
|
||||||
toast.success(t('deploy.toast.updated'));
|
|
||||||
} else {
|
|
||||||
if (isRepo) await deployApi.createRepo(payload);
|
|
||||||
else await deployApi.createRegistry(payload);
|
|
||||||
toast.success(t('deploy.toast.created'));
|
|
||||||
}
|
|
||||||
onOpenChange(false);
|
|
||||||
onSave();
|
|
||||||
} catch { toast.error(t('deploy.toast.saveFailed')); }
|
|
||||||
finally { setSaving(false); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const CredIcon = isRepo ? Globe : Container;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="sm:max-w-[480px] bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
||||||
<CredIcon className="h-5 w-5 text-[var(--dg-blue)]" />
|
|
||||||
{editId ? t('common.edit') : t('common.add')} {isRepo ? t('deploy.credForm.gitRepo') : t('deploy.credForm.imageRegistry')}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-2">
|
|
||||||
<div className="space-y-1"><Label>{t('deploy.form.nameStar')}</Label>
|
|
||||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder={isRepo ? t('deploy.credForm.repoNamePlaceholder') : t('deploy.credForm.registryNamePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
|
|
||||||
<div className="space-y-1"><Label>{isRepo ? t('deploy.form.repoUrlStar') : 'Registry URL *'}</Label>
|
|
||||||
<Input value={url} onChange={e => setUrl(e.target.value)} placeholder={isRepo ? 'http://10.10.10.72:9000/group/repo.git' : 'http://10.10.10.72'} className="font-mono text-sm bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
|
|
||||||
{!isRepo && <div className="space-y-1"><Label>{t('deploy.form.defaultNamespace')}</Label>
|
|
||||||
<Input value={namespace} onChange={e => setNamespace(e.target.value)} placeholder={t('deploy.credForm.namespacePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>}
|
|
||||||
|
|
||||||
{/* 关联凭证 */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label>{t('deploy.cred.linkedCred')} <span className="text-xs text-[var(--dg-text-secondary)] opacity-70">{t('deploy.credForm.credHint')}</span></Label>
|
|
||||||
<Select value={String(credentialId)} onValueChange={v => setCredentialId(v === '__none' ? '' : v)}>
|
|
||||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
|
||||||
<SelectValue placeholder={isRepo ? t('deploy.cred.selectApiCred') : t('deploy.cred.selectDockerPwd')} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__none">{t('deploy.cred.notLinkedCred')}</SelectItem>
|
|
||||||
{filteredCredentials.map(c => (
|
|
||||||
<SelectItem key={c.id} value={String(c.id)}>
|
|
||||||
<span className="flex items-center gap-1.5">
|
|
||||||
<Key className="h-3 w-3 opacity-60" />
|
|
||||||
{c.name} {c.summary && <span className="text-[10px] opacity-50">({c.summary})</span>}
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{filteredCredentials.length === 0 && (
|
|
||||||
<p className="text-xs dg-text-yellow flex items-center gap-1 mt-1">
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
{t('deploy.credForm.noCredHint', { type: isRepo ? '' : t('deploy.credForm.dockerPwd') })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
|
|
||||||
<Button onClick={handleSave} disabled={saving}>{saving ? t('deploy.pipeline.saving') : t('common.save')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Plus, Pencil, Trash2, RefreshCw, GitBranch, Container, Globe, Settings2 } from 'lucide-react';
|
import { Plus, Pencil, Trash2, RefreshCw, Globe, Settings2 } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -12,24 +12,12 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import BuildNodesManager from './BuildNodesManager';
|
import BuildNodesManager from './BuildNodesManager';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { deployApi } from '@/api';
|
import { deployApi } from '@/api';
|
||||||
import CredentialFormDialog from './CredentialFormDialog';
|
|
||||||
import ToolConfigTab from './ToolConfigTab';
|
import ToolConfigTab from './ToolConfigTab';
|
||||||
import type { JsonData } from '@/types/common';
|
|
||||||
|
|
||||||
interface Repo {
|
|
||||||
id: number; name: string; description: string; url: string;
|
|
||||||
auth_type: string; credential_id: number | null; enabled: boolean;
|
|
||||||
created_by: string; updated_at: string;
|
|
||||||
}
|
|
||||||
interface Registry {
|
|
||||||
id: number; name: string; description: string; registry_url: string; namespace: string;
|
|
||||||
auth_type: string; credential_id: number | null; enabled: boolean;
|
|
||||||
created_by: string; updated_at: string;
|
|
||||||
}
|
|
||||||
interface Environment {
|
interface Environment {
|
||||||
id: number; name: string; display_name: string; type: string; color: string;
|
id: number; name: string; display_name: string; type: string; color: string;
|
||||||
sort_order: number; description: string; enabled: boolean;
|
sort_order: number; description: string; enabled: boolean;
|
||||||
@@ -74,7 +62,10 @@ function EnvFormDialog({ open, onClose, onSaved, editData }: { open: boolean; on
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader><DialogTitle>{editData ? t('deploy.env.editEnv') : t('deploy.env.addEnv')}</DialogTitle></DialogHeader>
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editData ? t('deploy.env.editEnv') : t('deploy.env.addEnv')}</DialogTitle>
|
||||||
|
<DialogDescription>{t('deploy.label.envManage')}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-1"><Label>{t('deploy.env.envLabel')}</Label><Input value={name} onChange={e => setName(e.target.value)} placeholder="production" /></div>
|
<div className="space-y-1"><Label>{t('deploy.env.envLabel')}</Label><Input value={name} onChange={e => setName(e.target.value)} placeholder="production" /></div>
|
||||||
<div className="space-y-1"><Label>{t('deploy.table.displayName')}</Label><Input value={displayName} onChange={e => setDisplayName(e.target.value)} placeholder={t('deploy.env.prodEnv')} /></div>
|
<div className="space-y-1"><Label>{t('deploy.table.displayName')}</Label><Input value={displayName} onChange={e => setDisplayName(e.target.value)} placeholder={t('deploy.env.prodEnv')} /></div>
|
||||||
@@ -102,46 +93,42 @@ function EnvFormDialog({ open, onClose, onSaved, editData }: { open: boolean; on
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_TABS = ['repos', 'registries', 'environments', 'tools', 'build-nodes'] as const;
|
const CONFIG_TABS = ['environments', 'build-nodes', 'tools'] as const;
|
||||||
|
|
||||||
export default function DeployCredentials() {
|
export default function DeployCredentials() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const _confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const tabFromUrl = searchParams.get('tab');
|
const tabFromUrl = searchParams.get('tab');
|
||||||
const [repos, setRepos] = useState<Repo[]>([]);
|
|
||||||
const [registries, setRegistries] = useState<Registry[]>([]);
|
|
||||||
const [environments, setEnvironments] = useState<Environment[]>([]);
|
const [environments, setEnvironments] = useState<Environment[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editorOpen, setEditorOpen] = useState(false);
|
|
||||||
const [editType, setEditType] = useState<'repo' | 'registry'>('repo');
|
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
|
||||||
const [envDialogOpen, setEnvDialogOpen] = useState(false);
|
const [envDialogOpen, setEnvDialogOpen] = useState(false);
|
||||||
const [editEnv, setEditEnv] = useState<Environment | null>(null);
|
const [editEnv, setEditEnv] = useState<Environment | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState(
|
const [activeTab, setActiveTab] = useState(
|
||||||
tabFromUrl && (CONFIG_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'repos',
|
tabFromUrl && (CONFIG_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'environments',
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchAll = async () => {
|
const fetchAll = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]);
|
const rEnv = await deployApi.getEnvironments();
|
||||||
setRepos((rR as JsonData)?.list || []);
|
setEnvironments((rEnv as { list?: Environment[] })?.list || []);
|
||||||
setRegistries((rReg as JsonData)?.list || []);
|
|
||||||
setEnvironments((rEnv as JsonData)?.list || []);
|
|
||||||
} catch { toast.error(t('deploy.toast.getDataFailed')); }
|
} catch { toast.error(t('deploy.toast.getDataFailed')); }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { fetchAll(); }, []);
|
useEffect(() => { fetchAll(); }, []);
|
||||||
|
|
||||||
const handleDelete = async (type: 'repo' | 'registry' | 'env', item: JsonData) => {
|
const handleDeleteEnv = async (item: Environment) => {
|
||||||
const ok = await _confirm({ title: t('common.delete'), description: t('deploy.toast.deleteConfirm', { name: item.name || item.display_name }), confirmText: t('common.delete'), variant: 'destructive' });
|
const ok = await confirm({
|
||||||
|
title: t('common.delete'),
|
||||||
|
description: t('deploy.toast.deleteConfirm', { name: item.display_name || item.name }),
|
||||||
|
confirmText: t('common.delete'),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
if (type === 'repo') await deployApi.deleteRepo(item.id);
|
await deployApi.deleteEnvironment(item.id);
|
||||||
else if (type === 'registry') await deployApi.deleteRegistry(item.id);
|
|
||||||
else await deployApi.deleteEnvironment(item.id);
|
|
||||||
toast.success(t('deploy.toast.deleted'));
|
toast.success(t('deploy.toast.deleted'));
|
||||||
fetchAll();
|
fetchAll();
|
||||||
} catch { toast.error(t('deploy.toast.deleteFailed')); }
|
} catch { toast.error(t('deploy.toast.deleteFailed')); }
|
||||||
@@ -150,8 +137,6 @@ export default function DeployCredentials() {
|
|||||||
const envTypeLabel: Record<string, string> = { production: t('deploy.env.production'), staging: t('deploy.env.staging'), testing: t('deploy.env.testing'), development: t('deploy.env.development') };
|
const envTypeLabel: Record<string, string> = { production: t('deploy.env.production'), staging: t('deploy.env.staging'), testing: t('deploy.env.testing'), development: t('deploy.env.development') };
|
||||||
|
|
||||||
const pageTabs = [
|
const pageTabs = [
|
||||||
{ id: 'repos', label: t('deploy.credForm.gitRepo'), badge: repos.length || undefined },
|
|
||||||
{ id: 'registries', label: t('deploy.credForm.imageRegistry'), badge: registries.length || undefined },
|
|
||||||
{ id: 'environments', label: t('deploy.label.envManage'), badge: environments.length || undefined },
|
{ id: 'environments', label: t('deploy.label.envManage'), badge: environments.length || undefined },
|
||||||
{ id: 'build-nodes', label: t('deploy.deployNav.buildNodes') },
|
{ id: 'build-nodes', label: t('deploy.deployNav.buildNodes') },
|
||||||
{ id: 'tools', label: t('deploy.tool.title') },
|
{ id: 'tools', label: t('deploy.tool.title') },
|
||||||
@@ -168,109 +153,25 @@ export default function DeployCredentials() {
|
|||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onTabChange={setActiveTab}
|
onTabChange={setActiveTab}
|
||||||
actions={activeTab === 'build-nodes' ? undefined : (
|
actions={activeTab === 'build-nodes' ? undefined : (
|
||||||
|
activeTab === 'environments' ? (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchAll}>
|
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchAll}>
|
||||||
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditType('repo'); setEditingId(null); setEditorOpen(true); }}>
|
|
||||||
<Plus className="w-4 h-4 mr-1" />{t('deploy.credForm.newRepo')}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onClick={() => { setEditType('registry'); setEditingId(null); setEditorOpen(true); }}>
|
|
||||||
<Plus className="w-4 h-4 mr-1" />{t('deploy.credForm.newRegistry')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
contentClassName="space-y-4"
|
|
||||||
>
|
|
||||||
{activeTab === 'repos' && (
|
|
||||||
repos.length === 0 ? (
|
|
||||||
<PageEmpty icon={<GitBranch className="h-10 w-10" />} title={t('deploy.credForm.noRepo')} />
|
|
||||||
) : (
|
|
||||||
<ContentPanel noPadding>
|
|
||||||
<div className="overflow-hidden">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
|
|
||||||
<tr>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.name')}</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">{t('deploy.table.repoUrl')}</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.cred.authMethod')}</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.status')}</th>
|
|
||||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('common.actions')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{repos.map(r => (
|
|
||||||
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
|
||||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">{r.name}</td>
|
|
||||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] hidden md:table-cell font-mono text-xs max-w-[300px] truncate">{r.url}</td>
|
|
||||||
<td className="px-3 py-2"><Badge variant="outline" className="text-xs">{r.auth_type}</Badge></td>
|
|
||||||
<td className="px-3 py-2">
|
|
||||||
<Badge className={r.enabled ? 'bg-emerald-500/20 dg-text-green border-emerald-500/30' : 'bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)]'}>{r.enabled ? t('common.enable') : t('common.disable')}</Badge>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 text-right">
|
|
||||||
<div className="flex justify-end gap-1">
|
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditType('repo'); setEditingId(r.id); setEditorOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDelete('repo', r)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</ContentPanel>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'registries' && (
|
|
||||||
registries.length === 0 ? (
|
|
||||||
<PageEmpty icon={<Container className="h-10 w-10" />} title={t('deploy.credForm.noRegistry')} />
|
|
||||||
) : (
|
|
||||||
<ContentPanel noPadding>
|
|
||||||
<div className="overflow-hidden">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
|
|
||||||
<tr>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.name')}</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">Registry URL</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] hidden sm:table-cell">{t('deploy.credForm.namespace')}</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.cred.authMethod')}</th>
|
|
||||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.status')}</th>
|
|
||||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('common.actions')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{registries.map(r => (
|
|
||||||
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
|
||||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">{r.name}</td>
|
|
||||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] hidden md:table-cell font-mono text-xs max-w-[250px] truncate">{r.registry_url}</td>
|
|
||||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] hidden sm:table-cell">{r.namespace || '-'}</td>
|
|
||||||
<td className="px-3 py-2"><Badge variant="outline" className="text-xs">{r.auth_type}</Badge></td>
|
|
||||||
<td className="px-3 py-2">
|
|
||||||
<Badge className={r.enabled ? 'bg-emerald-500/20 dg-text-green border-emerald-500/30' : 'bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)]'}>{r.enabled ? t('common.enable') : t('common.disable')}</Badge>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 text-right">
|
|
||||||
<div className="flex justify-end gap-1">
|
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditType('registry'); setEditingId(r.id); setEditorOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDelete('registry', r)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</ContentPanel>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'environments' && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-end mb-3">
|
|
||||||
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditEnv(null); setEnvDialogOpen(true); }}>
|
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditEnv(null); setEnvDialogOpen(true); }}>
|
||||||
<Plus className="w-4 h-4 mr-1" />新增环境
|
<Plus className="w-4 h-4 mr-1" />新增环境
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchAll}>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
contentClassName="space-y-4"
|
||||||
|
>
|
||||||
|
{activeTab === 'environments' && (
|
||||||
|
<>
|
||||||
{environments.length === 0 ? (
|
{environments.length === 0 ? (
|
||||||
<PageEmpty
|
<PageEmpty
|
||||||
icon={<Globe className="h-10 w-10" />}
|
icon={<Globe className="h-10 w-10" />}
|
||||||
@@ -316,7 +217,7 @@ export default function DeployCredentials() {
|
|||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditEnv(env); setEnvDialogOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditEnv(env); setEnvDialogOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDelete('env', env)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDeleteEnv(env)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -333,11 +234,6 @@ export default function DeployCredentials() {
|
|||||||
|
|
||||||
{activeTab === 'tools' && <ToolConfigTab />}
|
{activeTab === 'tools' && <ToolConfigTab />}
|
||||||
|
|
||||||
<CredentialFormDialog
|
|
||||||
open={editorOpen} onOpenChange={setEditorOpen} type={editType} editId={editingId}
|
|
||||||
repos={repos} registries={registries} onSave={fetchAll}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EnvFormDialog open={envDialogOpen} onClose={() => setEnvDialogOpen(false)} onSaved={fetchAll} editData={editEnv} />
|
<EnvFormDialog open={envDialogOpen} onClose={() => setEnvDialogOpen(false)} onSaved={fetchAll} editData={editEnv} />
|
||||||
</PageShell>
|
</PageShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Container,
|
|
||||||
Globe,
|
|
||||||
History,
|
History,
|
||||||
Server,
|
Server,
|
||||||
Network,
|
Network,
|
||||||
@@ -54,8 +52,6 @@ export function PipelineCard({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const stages = parseStages(pipeline.stages);
|
const stages = parseStages(pipeline.stages);
|
||||||
const repo = pipeline.repo as Record<string, unknown> | undefined;
|
|
||||||
const registry = pipeline.registry as Record<string, unknown> | undefined;
|
|
||||||
const environment = pipeline.environment as Record<string, unknown> | undefined;
|
const environment = pipeline.environment as Record<string, unknown> | undefined;
|
||||||
const code = (pipeline.code || pipeline.id) as string;
|
const code = (pipeline.code || pipeline.id) as string;
|
||||||
const deployTarget = String(pipeline.deploy_target || 'ssh');
|
const deployTarget = String(pipeline.deploy_target || 'ssh');
|
||||||
@@ -151,8 +147,6 @@ export function PipelineCard({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{repo && <span className="flex items-center gap-1"><Globe className="h-3 w-3" />{String(repo.name)}</span>}
|
|
||||||
{registry && <span className="flex items-center gap-1"><Container className="h-3 w-3" />{String(registry.name)}</span>}
|
|
||||||
{pipeline.last_run_at && (
|
{pipeline.last_run_at && (
|
||||||
<span><Clock className="h-3 w-3 inline mr-0.5" />{new Date(pipeline.last_run_at as string).toLocaleString('zh-CN')}</span>
|
<span><Clock className="h-3 w-3 inline mr-0.5" />{new Date(pipeline.last_run_at as string).toLocaleString('zh-CN')}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ interface State {
|
|||||||
errorInfo: ErrorInfo | null;
|
errorInfo: ErrorInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChunkLoadError(error: Error | null) {
|
||||||
|
// 发版后旧入口引用已删除 chunk 时,浏览器会抛动态 import 加载失败。 // Logan.Li
|
||||||
|
const message = error?.message || '';
|
||||||
|
return message.includes('Failed to fetch dynamically imported module') ||
|
||||||
|
message.includes('Importing a module script failed') ||
|
||||||
|
message.includes('ChunkLoadError') ||
|
||||||
|
message.includes('Loading chunk');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ErrorBoundary - 全局错误边界组件
|
* ErrorBoundary - 全局错误边界组件
|
||||||
*
|
*
|
||||||
@@ -38,6 +47,10 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRetry = () => {
|
handleRetry = () => {
|
||||||
|
if (isChunkLoadError(this.state.error)) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({ hasError: false, error: null, errorInfo: null });
|
this.setState({ hasError: false, error: null, errorInfo: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +73,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
if (this.props.fallback) {
|
if (this.props.fallback) {
|
||||||
return this.props.fallback;
|
return this.props.fallback;
|
||||||
}
|
}
|
||||||
|
const chunkLoadError = isChunkLoadError(this.state.error);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[60vh] flex items-center justify-center p-6">
|
<div className="min-h-[60vh] flex items-center justify-center p-6">
|
||||||
@@ -75,7 +89,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
{i18n.t('common.ebPageError')}
|
{i18n.t('common.ebPageError')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-[var(--dg-text-muted)] mt-2">
|
<p className="text-sm text-[var(--dg-text-muted)] mt-2">
|
||||||
{i18n.t('common.ebDescription')}
|
{chunkLoadError ? i18n.t('common.ebChunkDescription') : i18n.t('common.ebDescription')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,7 +130,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
onClick={this.handleRetry}
|
onClick={this.handleRetry}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-4 w-4 mr-1.5" />
|
<RotateCcw className="h-4 w-4 mr-1.5" />
|
||||||
{i18n.t('common.reload')}
|
{chunkLoadError ? i18n.t('common.reloadPage') : i18n.t('common.reload')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -283,6 +283,13 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
|
|||||||
r.code?.toLowerCase().includes(regionSearch.toLowerCase())
|
r.code?.toLowerCase().includes(regionSearch.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getImportHostname = (server: any) => {
|
||||||
|
// 阿里云 HostName 常是 iZ 开头的系统名,导入预览优先展示控制台实例名称。 // Logan.Li
|
||||||
|
if (server.instance_name && server.instance_name !== server.instance_id) return server.instance_name;
|
||||||
|
if (server.hostname && server.hostname !== server.instance_id) return server.hostname;
|
||||||
|
return server.private_ip || server.public_ip || server.instance_name || server.instance_id || '-';
|
||||||
|
};
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ id: 1, title: t('server.cloudProvider'), icon: Cloud },
|
{ id: 1, title: t('server.cloudProvider'), icon: Cloud },
|
||||||
{ id: 2, title: t('server.cloudAccount'), icon: Key },
|
{ id: 2, title: t('server.cloudAccount'), icon: Key },
|
||||||
@@ -620,7 +627,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
|
|||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<Checkbox checked={selectedServers.includes(server.instance_id)} />
|
<Checkbox checked={selectedServers.includes(server.instance_id)} />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-primary)]">{server.instance_name || server.hostname || '-'}</td>
|
<td className="px-3 py-2 text-sm text-[var(--dg-text-primary)]">{getImportHostname(server)}</td>
|
||||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">
|
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">
|
||||||
{server.private_ip || server.public_ip || '-'}
|
{server.private_ip || server.public_ip || '-'}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import type { JsonData } from '@/types/common';
|
|||||||
// 表单数据结构
|
// 表单数据结构
|
||||||
export interface ServerFormData {
|
export interface ServerFormData {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
alias: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
public_ip: string;
|
public_ip: string;
|
||||||
port: string;
|
port: string;
|
||||||
@@ -55,6 +56,7 @@ export interface ServerFormData {
|
|||||||
|
|
||||||
export const EMPTY_FORM: ServerFormData = {
|
export const EMPTY_FORM: ServerFormData = {
|
||||||
hostname: '',
|
hostname: '',
|
||||||
|
alias: '',
|
||||||
ip: '',
|
ip: '',
|
||||||
public_ip: '',
|
public_ip: '',
|
||||||
port: '22',
|
port: '22',
|
||||||
@@ -183,6 +185,17 @@ export function ServerForm({
|
|||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
{/* 基础信息 */}
|
{/* 基础信息 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-[var(--dg-text-secondary)]">
|
||||||
|
{t('server.formAlias')}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t('server.aliasPlaceholder')}
|
||||||
|
value={formData.alias}
|
||||||
|
onChange={(e) => setFormData({ ...formData, alias: e.target.value })}
|
||||||
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-[var(--dg-text-secondary)]">
|
<Label className="text-[var(--dg-text-secondary)]">
|
||||||
{t('server.formHostname')} <span className="dg-text-red">*</span>
|
{t('server.formHostname')} <span className="dg-text-red">*</span>
|
||||||
|
|||||||
@@ -122,10 +122,13 @@ export function ServerManager() {
|
|||||||
const servers = data?.list ?? [];
|
const servers = data?.list ?? [];
|
||||||
const total = data?.total ?? 0;
|
const total = data?.total ?? 0;
|
||||||
|
|
||||||
|
const getServerDisplayName = (server: JsonData) =>
|
||||||
|
server.alias || server.cloud_instance_name || server.hostname || server.ip || "";
|
||||||
|
|
||||||
const openTerminal = (server: JsonData) => {
|
const openTerminal = (server: JsonData) => {
|
||||||
setTerminalServer({
|
setTerminalServer({
|
||||||
id: server.id,
|
id: server.id,
|
||||||
name: server.hostname,
|
name: getServerDisplayName(server),
|
||||||
ip: server.ip,
|
ip: server.ip,
|
||||||
hasAuth: server.has_auth ?? true,
|
hasAuth: server.has_auth ?? true,
|
||||||
});
|
});
|
||||||
@@ -133,12 +136,12 @@ export function ServerManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openRdp = (server: JsonData) => {
|
const openRdp = (server: JsonData) => {
|
||||||
setRdpServer({ id: server.id, name: server.hostname, ip: server.ip });
|
setRdpServer({ id: server.id, name: getServerDisplayName(server), ip: server.ip });
|
||||||
setRdpOpen(true);
|
setRdpOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFileBrowser = (server: JsonData) => {
|
const openFileBrowser = (server: JsonData) => {
|
||||||
setFileBrowserServer({ id: server.id, name: server.hostname });
|
setFileBrowserServer({ id: server.id, name: getServerDisplayName(server) });
|
||||||
setFileBrowserOpen(true);
|
setFileBrowserOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,9 +174,13 @@ export function ServerManager() {
|
|||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
const server = servers.find((s) => String(s.id) === String(id));
|
const server = servers.find((s) => String(s.id) === String(id));
|
||||||
|
const name = server ? getServerDisplayName(server) : id;
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: t("server.deleteTitle"),
|
title: t("server.deleteTitle"),
|
||||||
description: `${server?.hostname || id}`,
|
description: t("server.deleteConfirm", {
|
||||||
|
name,
|
||||||
|
ip: server?.ip || "-",
|
||||||
|
}),
|
||||||
confirmText: t("common.delete"),
|
confirmText: t("common.delete"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@@ -192,7 +199,9 @@ export function ServerManager() {
|
|||||||
if (selectedServers.length === 0) return;
|
if (selectedServers.length === 0) return;
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: t("server.batchDeleteTitle"),
|
title: t("server.batchDeleteTitle"),
|
||||||
description: `${selectedServers.length}`,
|
description: t("server.batchDeleteConfirm", {
|
||||||
|
count: selectedServers.length,
|
||||||
|
}),
|
||||||
confirmText: t("server.batchDelete"),
|
confirmText: t("server.batchDelete"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@@ -224,6 +233,7 @@ export function ServerManager() {
|
|||||||
setEditingId(server.id);
|
setEditingId(server.id);
|
||||||
setFormData({
|
setFormData({
|
||||||
hostname: server.hostname || "",
|
hostname: server.hostname || "",
|
||||||
|
alias: server.alias || "",
|
||||||
ip: server.ip || "",
|
ip: server.ip || "",
|
||||||
public_ip: server.public_ip || "",
|
public_ip: server.public_ip || "",
|
||||||
port: String(server.port || 22),
|
port: String(server.port || 22),
|
||||||
@@ -256,6 +266,7 @@ export function ServerManager() {
|
|||||||
setFormSaving(true);
|
setFormSaving(true);
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
hostname: formData.hostname,
|
hostname: formData.hostname,
|
||||||
|
alias: formData.alias,
|
||||||
ip: formData.ip,
|
ip: formData.ip,
|
||||||
public_ip: formData.public_ip || undefined,
|
public_ip: formData.public_ip || undefined,
|
||||||
port: Number(formData.port) || 22,
|
port: Number(formData.port) || 22,
|
||||||
@@ -306,6 +317,7 @@ export function ServerManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headers = [
|
const headers = [
|
||||||
|
t("server.csv.name"),
|
||||||
t("server.csv.hostname"),
|
t("server.csv.hostname"),
|
||||||
t("server.csv.ip"),
|
t("server.csv.ip"),
|
||||||
t("server.csv.publicIp"),
|
t("server.csv.publicIp"),
|
||||||
@@ -321,6 +333,7 @@ export function ServerManager() {
|
|||||||
const memGB = s.memory ? Math.round(Number(s.memory) / 1024) : 0;
|
const memGB = s.memory ? Math.round(Number(s.memory) / 1024) : 0;
|
||||||
const tags = getTags(s).join(";");
|
const tags = getTags(s).join(";");
|
||||||
return [
|
return [
|
||||||
|
getServerDisplayName(s),
|
||||||
s.hostname || "",
|
s.hostname || "",
|
||||||
s.ip || "",
|
s.ip || "",
|
||||||
s.public_ip || "",
|
s.public_ip || "",
|
||||||
@@ -482,7 +495,7 @@ export function ServerManager() {
|
|||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-[var(--dg-text-secondary)]">
|
<TableHead className="text-[var(--dg-text-secondary)]">
|
||||||
{t("server.hostname")}
|
{t("server.assetName")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-[var(--dg-text-secondary)]">
|
<TableHead className="text-[var(--dg-text-secondary)]">
|
||||||
{t("server.ipAddress")}
|
{t("server.ipAddress")}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export function ServerTableRow({
|
|||||||
const tags = getTags(server);
|
const tags = getTags(server);
|
||||||
const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
||||||
const isRdp = isRdpServer(server);
|
const isRdp = isRdpServer(server);
|
||||||
|
const displayName = server.alias || server.cloud_instance_name || server.hostname || server.ip;
|
||||||
|
const secondaryName = server.hostname && server.hostname !== displayName ? server.hostname : server.os;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
<TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||||
@@ -66,9 +68,12 @@ export function ServerTableRow({
|
|||||||
<ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
<ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-[var(--dg-text-primary)]">
|
<p className="font-medium text-[var(--dg-text-primary)]">
|
||||||
{server.hostname}
|
{displayName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[var(--dg-text-muted)]">
|
||||||
|
{secondaryName}
|
||||||
|
{server.cloud_instance_id ? ` · ${server.cloud_instance_id}` : ""}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"analyzing": "Analyzing",
|
"analyzing": "Analyzing",
|
||||||
"annotation": "Annotation",
|
"annotation": "Annotation",
|
||||||
"appName": "AIOps Platform",
|
"appName": "AIOps Platform",
|
||||||
|
"appUpdated": "System updated",
|
||||||
|
"appUpdatedDesc": "A new frontend version is available. Refresh the page before continuing.",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"applyFilter": "Apply Filter",
|
"applyFilter": "Apply Filter",
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
@@ -144,6 +146,7 @@
|
|||||||
},
|
},
|
||||||
"ebCaughtError": "[ErrorBoundary] Caught error:",
|
"ebCaughtError": "[ErrorBoundary] Caught error:",
|
||||||
"ebCopyError": "Copy Error",
|
"ebCopyError": "Copy Error",
|
||||||
|
"ebChunkDescription": "Page assets may have been updated or temporarily unavailable. Please refresh to load the latest version.",
|
||||||
"ebDescription": "Sorry, this page encountered an unexpected error. You can try reloading or returning to home.",
|
"ebDescription": "Sorry, this page encountered an unexpected error. You can try reloading or returning to home.",
|
||||||
"ebErrorInfo": "[ErrorBoundary] Error info:",
|
"ebErrorInfo": "[ErrorBoundary] Error info:",
|
||||||
"ebGoHome": "Back to Home",
|
"ebGoHome": "Back to Home",
|
||||||
@@ -759,8 +762,9 @@
|
|||||||
"csvImport": "CSV Import",
|
"csvImport": "CSV Import",
|
||||||
"csvImportHint": "hostname + IP only",
|
"csvImportHint": "hostname + IP only",
|
||||||
"addServer": "Add Server",
|
"addServer": "Add Server",
|
||||||
"searchPlaceholder": "Search hostname or IP...",
|
"searchPlaceholder": "Search alias, cloud name, hostname or IP...",
|
||||||
"allStatus": "All Status",
|
"allStatus": "All Status",
|
||||||
|
"assetName": "Asset Name",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"ipAddress": "IP Address",
|
"ipAddress": "IP Address",
|
||||||
"publicIp": "Public IP",
|
"publicIp": "Public IP",
|
||||||
@@ -774,8 +778,10 @@
|
|||||||
"items": "items",
|
"items": "items",
|
||||||
"totalItems": "{{count}} total",
|
"totalItems": "{{count}} total",
|
||||||
"deleteTitle": "Delete Server",
|
"deleteTitle": "Delete Server",
|
||||||
|
"deleteConfirm": "Delete server \"{{name}}\" ({{ip}})? This action cannot be undone.",
|
||||||
"deleted": "Server deleted",
|
"deleted": "Server deleted",
|
||||||
"batchDeleteTitle": "Batch Delete Servers",
|
"batchDeleteTitle": "Batch Delete Servers",
|
||||||
|
"batchDeleteConfirm": "Delete the selected {{count}} servers? This action cannot be undone.",
|
||||||
"updated": "Server updated",
|
"updated": "Server updated",
|
||||||
"created": "Server created",
|
"created": "Server created",
|
||||||
"saveFailed": "Save failed, please retry",
|
"saveFailed": "Save failed, please retry",
|
||||||
@@ -785,6 +791,7 @@
|
|||||||
"deleteFailed": "Delete failed",
|
"deleteFailed": "Delete failed",
|
||||||
"batchDeleteFailed": "Batch delete failed",
|
"batchDeleteFailed": "Batch delete failed",
|
||||||
"csv": {
|
"csv": {
|
||||||
|
"name": "Asset Name",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"ip": "IP Address",
|
"ip": "IP Address",
|
||||||
"publicIp": "Public IP",
|
"publicIp": "Public IP",
|
||||||
@@ -1076,6 +1083,8 @@
|
|||||||
"editServer": "Edit Server",
|
"editServer": "Edit Server",
|
||||||
"editServerDesc": "Modify server asset information",
|
"editServerDesc": "Modify server asset information",
|
||||||
"addServerDesc": "Add a new server asset to CMDB",
|
"addServerDesc": "Add a new server asset to CMDB",
|
||||||
|
"formAlias": "Alias",
|
||||||
|
"aliasPlaceholder": "e.g. prod-gateway, order-svc-01",
|
||||||
"formHostname": "Hostname",
|
"formHostname": "Hostname",
|
||||||
"formIpAddress": "IP Address",
|
"formIpAddress": "IP Address",
|
||||||
"osType": "OS Type",
|
"osType": "OS Type",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"analyzing": "分析中",
|
"analyzing": "分析中",
|
||||||
"annotation": "注解",
|
"annotation": "注解",
|
||||||
"appName": "AIOps平台",
|
"appName": "AIOps平台",
|
||||||
|
"appUpdated": "系统已更新",
|
||||||
|
"appUpdatedDesc": "检测到前端资源已有新版本,建议刷新页面后继续操作。",
|
||||||
"apply": "应用",
|
"apply": "应用",
|
||||||
"applyFilter": "应用筛选",
|
"applyFilter": "应用筛选",
|
||||||
"approve": "通过",
|
"approve": "通过",
|
||||||
@@ -144,6 +146,7 @@
|
|||||||
},
|
},
|
||||||
"ebCaughtError": "[ErrorBoundary] 捕获到错误:",
|
"ebCaughtError": "[ErrorBoundary] 捕获到错误:",
|
||||||
"ebCopyError": "复制错误",
|
"ebCopyError": "复制错误",
|
||||||
|
"ebChunkDescription": "页面资源可能已更新或短暂不可用,请刷新页面重新加载最新资源。",
|
||||||
"ebDescription": "抱歉,该页面遇到了意外错误。您可以尝试重新加载或返回首页。",
|
"ebDescription": "抱歉,该页面遇到了意外错误。您可以尝试重新加载或返回首页。",
|
||||||
"ebErrorInfo": "[ErrorBoundary] 错误信息:",
|
"ebErrorInfo": "[ErrorBoundary] 错误信息:",
|
||||||
"ebGoHome": "返回首页",
|
"ebGoHome": "返回首页",
|
||||||
@@ -759,8 +762,9 @@
|
|||||||
"csvImport": "CSV导入",
|
"csvImport": "CSV导入",
|
||||||
"csvImportHint": "hostname + IP 即可",
|
"csvImportHint": "hostname + IP 即可",
|
||||||
"addServer": "添加服务器",
|
"addServer": "添加服务器",
|
||||||
"searchPlaceholder": "搜索主机名或IP...",
|
"searchPlaceholder": "搜索别名、云实例名、主机名或IP...",
|
||||||
"allStatus": "全部状态",
|
"allStatus": "全部状态",
|
||||||
|
"assetName": "资产名称",
|
||||||
"hostname": "主机名",
|
"hostname": "主机名",
|
||||||
"ipAddress": "IP地址",
|
"ipAddress": "IP地址",
|
||||||
"publicIp": "公网IP",
|
"publicIp": "公网IP",
|
||||||
@@ -774,8 +778,10 @@
|
|||||||
"items": "条",
|
"items": "条",
|
||||||
"totalItems": "共 {{count}} 条",
|
"totalItems": "共 {{count}} 条",
|
||||||
"deleteTitle": "删除服务器",
|
"deleteTitle": "删除服务器",
|
||||||
|
"deleteConfirm": "确认删除服务器「{{name}}」({{ip}})?此操作不可撤销。",
|
||||||
"deleted": "服务器已删除",
|
"deleted": "服务器已删除",
|
||||||
"batchDeleteTitle": "批量删除服务器",
|
"batchDeleteTitle": "批量删除服务器",
|
||||||
|
"batchDeleteConfirm": "确认删除选中的 {{count}} 台服务器?此操作不可撤销。",
|
||||||
"updated": "服务器已更新",
|
"updated": "服务器已更新",
|
||||||
"created": "服务器已创建",
|
"created": "服务器已创建",
|
||||||
"saveFailed": "保存失败,请重试",
|
"saveFailed": "保存失败,请重试",
|
||||||
@@ -785,6 +791,7 @@
|
|||||||
"deleteFailed": "删除失败",
|
"deleteFailed": "删除失败",
|
||||||
"batchDeleteFailed": "批量删除失败",
|
"batchDeleteFailed": "批量删除失败",
|
||||||
"csv": {
|
"csv": {
|
||||||
|
"name": "资产名称",
|
||||||
"hostname": "主机名",
|
"hostname": "主机名",
|
||||||
"ip": "IP地址",
|
"ip": "IP地址",
|
||||||
"publicIp": "公网IP",
|
"publicIp": "公网IP",
|
||||||
@@ -1076,6 +1083,8 @@
|
|||||||
"editServer": "编辑服务器",
|
"editServer": "编辑服务器",
|
||||||
"editServerDesc": "修改服务器资产信息",
|
"editServerDesc": "修改服务器资产信息",
|
||||||
"addServerDesc": "录入新的服务器资产到 CMDB",
|
"addServerDesc": "录入新的服务器资产到 CMDB",
|
||||||
|
"formAlias": "别名",
|
||||||
|
"aliasPlaceholder": "如:生产网关、订单服务-01",
|
||||||
"formHostname": "主机名",
|
"formHostname": "主机名",
|
||||||
"formIpAddress": "IP地址",
|
"formIpAddress": "IP地址",
|
||||||
"osType": "系统类型",
|
"osType": "系统类型",
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import App from './App.tsx'
|
|||||||
// 首屏前应用主题,避免闪烁 // Logan.Li
|
// 首屏前应用主题,避免闪烁 // Logan.Li
|
||||||
applyThemeToDocument(getInitialTheme())
|
applyThemeToDocument(getInitialTheme())
|
||||||
|
|
||||||
|
window.addEventListener('vite:preloadError', () => {
|
||||||
|
// 任一左侧子菜单懒加载资源在发版后失效时,统一整页刷新获取最新 index/chunk。 // Logan.Li
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user