主要变更: - RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、 backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API - Windows SMB 文件传输:backend/pkg/winsmb - 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情 - Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs - 服务器导入助手:server_import_helpers、ImportCredentialSettings - 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块) - 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-* - .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库) Co-Authored-By: Claude <noreply@anthropic.com>
526 lines
14 KiB
Go
526 lines
14 KiB
Go
// kubernetes_get.go - Kubernetes 集群/命名空间资源统一只读查询 // Logan.Li
|
||
package tools
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
k8sclient "ezops/pkg/kubernetesclient"
|
||
)
|
||
|
||
// KubernetesGetTool 统一只读查询工具 // Logan.Li
|
||
type KubernetesGetTool struct {
|
||
client *k8sclient.Client
|
||
namespace string // 会话默认命名空间
|
||
}
|
||
|
||
// NewKubernetesGetTool 创建查询工具 // Logan.Li
|
||
func NewKubernetesGetTool(client *k8sclient.Client, namespace string) *KubernetesGetTool {
|
||
return &KubernetesGetTool{client: client, namespace: namespace}
|
||
}
|
||
|
||
// Name 工具名 // Logan.Li
|
||
func (t *KubernetesGetTool) Name() string { return "kubernetes_get" }
|
||
|
||
// Description 给 LLM 的说明 // Logan.Li
|
||
func (t *KubernetesGetTool) Description() string {
|
||
return "查询 Kubernetes 集群或命名空间资源状态(只读)。" +
|
||
"resource 支持: nodes, pods, deployments, statefulsets, daemonsets, services, ingresses, events, namespaces, hpa, cronjobs。" +
|
||
"用于盘点节点/Pod 健康、副本状态、事件告警、服务路由等。返回结构化摘要,非原始 JSON。"
|
||
}
|
||
|
||
// Parameters JSON Schema // Logan.Li
|
||
func (t *KubernetesGetTool) Parameters() map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"resource": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "资源类型: nodes|pods|deployments|statefulsets|daemonsets|services|ingresses|events|namespaces|hpa|cronjobs",
|
||
},
|
||
"namespace": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "命名空间;pods/deployments 等命名空间级资源需要;空则用会话绑定命名空间",
|
||
},
|
||
"name": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "可选,查询单个资源名",
|
||
},
|
||
"label_selector": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "可选,标签筛选,格式 app=nginx 或 app=nginx,env=prod",
|
||
},
|
||
"status_filter": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "可选,Pod 状态筛选,如 Running、Pending、CrashLoopBackOff、NotRunning",
|
||
},
|
||
},
|
||
"required": []string{"resource"},
|
||
}
|
||
}
|
||
|
||
// RiskLevel 只读 // Logan.Li
|
||
func (t *KubernetesGetTool) RiskLevel() RiskLevel { return RiskSafe }
|
||
|
||
// Execute 执行查询 // Logan.Li
|
||
func (t *KubernetesGetTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
|
||
resource := strings.ToLower(strings.TrimSpace(StringParam(args, "resource")))
|
||
if resource == "" {
|
||
return &Result{Success: false, Error: "resource 不能为空"}, nil
|
||
}
|
||
ns := strings.TrimSpace(StringParam(args, "namespace"))
|
||
if ns == "" {
|
||
ns = t.namespace
|
||
}
|
||
name := strings.TrimSpace(StringParam(args, "name"))
|
||
labelSel := parseLabelSelector(StringParam(args, "label_selector"))
|
||
statusFilter := strings.TrimSpace(StringParam(args, "status_filter"))
|
||
|
||
var output string
|
||
var err error
|
||
switch resource {
|
||
case "nodes", "node":
|
||
output, err = t.formatNodes(name)
|
||
case "pods", "pod":
|
||
output, err = t.formatPods(ns, name, labelSel, statusFilter)
|
||
case "deployments", "deployment", "deploy":
|
||
output, err = t.formatDeployments(ns, name)
|
||
case "statefulsets", "statefulset", "sts":
|
||
output, err = t.formatStatefulSets(ns, name)
|
||
case "daemonsets", "daemonset", "ds":
|
||
output, err = t.formatDaemonSets(ns, name)
|
||
case "services", "service", "svc":
|
||
output, err = t.formatServices(ns, name)
|
||
case "ingresses", "ingress", "ing":
|
||
output, err = t.formatIngresses(ns, name)
|
||
case "events", "event":
|
||
output, err = t.formatEvents(ns, name)
|
||
case "namespaces", "namespace", "ns":
|
||
output, err = t.formatNamespaces(name)
|
||
case "hpa", "hpas":
|
||
output, err = t.formatHPAs(ns, name)
|
||
case "cronjobs", "cronjob":
|
||
output, err = t.formatCronJobs(ns, name)
|
||
default:
|
||
return &Result{Success: false, Error: fmt.Sprintf("不支持的 resource: %s", resource)}, nil
|
||
}
|
||
if err != nil {
|
||
return &Result{Success: false, Error: err.Error()}, nil
|
||
}
|
||
return &Result{Success: true, Output: output}, nil
|
||
}
|
||
|
||
func parseLabelSelector(raw string) map[string]string {
|
||
out := make(map[string]string)
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return out
|
||
}
|
||
for _, part := range strings.Split(raw, ",") {
|
||
part = strings.TrimSpace(part)
|
||
if kv := strings.SplitN(part, "=", 2); len(kv) == 2 {
|
||
out[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func labelsMatch(labels, selector map[string]string) bool {
|
||
if len(selector) == 0 {
|
||
return true
|
||
}
|
||
for k, v := range selector {
|
||
if labels[k] != v {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func podDisplayStatus(p k8sclient.PodItem) string {
|
||
phase := p.Status.Phase
|
||
for _, cs := range p.Status.ContainerStatuses {
|
||
if !cs.Ready && cs.RestartCount > 0 {
|
||
return "CrashLoopBackOff"
|
||
}
|
||
}
|
||
for _, c := range p.Status.Conditions {
|
||
if c.Type == "Ready" && c.Status != "True" && c.Reason != "" {
|
||
return c.Reason
|
||
}
|
||
}
|
||
return phase
|
||
}
|
||
|
||
func matchPodStatus(status, filter string) bool {
|
||
if filter == "" {
|
||
return true
|
||
}
|
||
f := strings.ToLower(filter)
|
||
s := strings.ToLower(status)
|
||
if f == "notrunning" {
|
||
return s != "running" && s != "succeeded" && s != "completed"
|
||
}
|
||
return strings.Contains(s, f)
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatNodes(name string) (string, error) {
|
||
list, err := t.client.ListNodes()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("Nodes (%d):\n", len(list.Items)))
|
||
for _, n := range list.Items {
|
||
if name != "" && n.Metadata.Name != name {
|
||
continue
|
||
}
|
||
ready := "Unknown"
|
||
for _, c := range n.Status.Conditions {
|
||
if c.Type == "Ready" {
|
||
ready = c.Status
|
||
break
|
||
}
|
||
}
|
||
ip := ""
|
||
for _, a := range n.Status.Addresses {
|
||
if a.Type == "InternalIP" {
|
||
ip = a.Address
|
||
break
|
||
}
|
||
}
|
||
sb.WriteString(fmt.Sprintf("- %s Ready=%s IP=%s OS=%s kubelet=%s\n",
|
||
n.Metadata.Name, ready, ip, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion))
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatPods(ns, name string, labelSel map[string]string, statusFilter string) (string, error) {
|
||
list, err := t.client.ListPods()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
type row struct{ line string }
|
||
var rows []row
|
||
for _, p := range list.Items {
|
||
if ns != "" && p.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && p.Metadata.Name != name {
|
||
continue
|
||
}
|
||
if !labelsMatch(p.Metadata.Labels, labelSel) {
|
||
continue
|
||
}
|
||
st := podDisplayStatus(p)
|
||
if !matchPodStatus(st, statusFilter) {
|
||
continue
|
||
}
|
||
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
|
||
}
|
||
rows = append(rows, row{line: fmt.Sprintf("- %s/%s %s restarts=%d node=%s ip=%s image=%s\n",
|
||
p.Metadata.Namespace, p.Metadata.Name, st, restarts, p.Spec.NodeName, p.Status.PodIP, image)})
|
||
}
|
||
sort.Slice(rows, func(i, j int) bool { return rows[i].line < rows[j].line })
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("Pods (%d):\n", len(rows)))
|
||
for _, r := range rows {
|
||
sb.WriteString(r.line)
|
||
}
|
||
if len(rows) == 0 {
|
||
sb.WriteString("(无匹配 Pod)\n")
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatDeployments(ns, name string) (string, error) {
|
||
list, err := t.client.ListDeployments()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var sb strings.Builder
|
||
count := 0
|
||
sb.WriteString("Deployments:\n")
|
||
for _, d := range list.Items {
|
||
if ns != "" && d.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && d.Metadata.Name != name {
|
||
continue
|
||
}
|
||
count++
|
||
sb.WriteString(fmt.Sprintf("- %s/%s replicas=%d/%d ready=%d available=%d paused=%v\n",
|
||
d.Metadata.Namespace, d.Metadata.Name,
|
||
d.Status.ReadyReplicas, d.Spec.Replicas,
|
||
d.Status.ReadyReplicas, d.Status.AvailableReplicas, d.Spec.Paused))
|
||
}
|
||
if count == 0 {
|
||
sb.WriteString("(无匹配 Deployment)\n")
|
||
} else {
|
||
sb.Reset()
|
||
sb.WriteString(fmt.Sprintf("Deployments (%d):\n", count))
|
||
for _, d := range list.Items {
|
||
if ns != "" && d.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && d.Metadata.Name != name {
|
||
continue
|
||
}
|
||
sb.WriteString(fmt.Sprintf("- %s/%s replicas=%d/%d ready=%d available=%d paused=%v\n",
|
||
d.Metadata.Namespace, d.Metadata.Name,
|
||
d.Status.ReadyReplicas, d.Spec.Replicas,
|
||
d.Status.ReadyReplicas, d.Status.AvailableReplicas, d.Spec.Paused))
|
||
}
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatStatefulSets(ns, name string) (string, error) {
|
||
list, err := t.client.ListStatefulSets()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var lines []string
|
||
for _, s := range list.Items {
|
||
if ns != "" && s.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && s.Metadata.Name != name {
|
||
continue
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s/%s replicas=%d/%d ready=%d\n",
|
||
s.Metadata.Namespace, s.Metadata.Name, s.Status.ReadyReplicas, s.Spec.Replicas, s.Status.ReadyReplicas))
|
||
}
|
||
sort.Strings(lines)
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("StatefulSets (%d):\n", len(lines)))
|
||
if len(lines) == 0 {
|
||
sb.WriteString("(无匹配 StatefulSet)\n")
|
||
} else {
|
||
for _, l := range lines {
|
||
sb.WriteString(l + "\n")
|
||
}
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatDaemonSets(ns, name string) (string, error) {
|
||
list, err := t.client.ListDaemonSets()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var lines []string
|
||
for _, d := range list.Items {
|
||
if ns != "" && d.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && d.Metadata.Name != name {
|
||
continue
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s/%s desired=%d ready=%d\n",
|
||
d.Metadata.Namespace, d.Metadata.Name, d.Status.DesiredNumberScheduled,
|
||
d.Status.NumberReady))
|
||
}
|
||
sort.Strings(lines)
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("DaemonSets (%d):\n", len(lines)))
|
||
for _, l := range lines {
|
||
sb.WriteString(l + "\n")
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatServices(ns, name string) (string, error) {
|
||
list, err := t.client.ListServices()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var lines []string
|
||
for _, s := range list.Items {
|
||
if ns != "" && s.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && s.Metadata.Name != name {
|
||
continue
|
||
}
|
||
ports := ""
|
||
for i, p := range s.Spec.Ports {
|
||
if i > 0 {
|
||
ports += ","
|
||
}
|
||
ports += fmt.Sprintf("%d", p.Port)
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s/%s type=%s clusterIP=%s ports=[%s] selector=%v\n",
|
||
s.Metadata.Namespace, s.Metadata.Name, s.Spec.Type, s.Spec.ClusterIP, ports, s.Spec.Selector))
|
||
}
|
||
sort.Strings(lines)
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("Services (%d):\n", len(lines)))
|
||
for _, l := range lines {
|
||
sb.WriteString(l)
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatIngresses(ns, name string) (string, error) {
|
||
list, err := t.client.ListIngresses()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var lines []string
|
||
for _, ing := range list.Items {
|
||
if ns != "" && ing.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && ing.Metadata.Name != name {
|
||
continue
|
||
}
|
||
hosts := []string{}
|
||
for _, r := range ing.Spec.Rules {
|
||
if r.Host != "" {
|
||
hosts = append(hosts, r.Host)
|
||
}
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s/%s hosts=%v\n", ing.Metadata.Namespace, ing.Metadata.Name, hosts))
|
||
}
|
||
sort.Strings(lines)
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("Ingresses (%d):\n", len(lines)))
|
||
for _, l := range lines {
|
||
sb.WriteString(l + "\n")
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatEvents(ns, involvedName string) (string, error) {
|
||
list, err := t.client.ListEvents(ns)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
type evRow struct {
|
||
ts time.Time
|
||
line string
|
||
}
|
||
var rows []evRow
|
||
for _, e := range list.Items {
|
||
if involvedName != "" && e.InvolvedObject.Name != involvedName {
|
||
continue
|
||
}
|
||
ts := e.LastTimestamp
|
||
if ts.IsZero() {
|
||
ts = e.EventTime
|
||
}
|
||
rows = append(rows, evRow{
|
||
ts: ts,
|
||
line: fmt.Sprintf("[%s] %s %s/%s: %s — %s (x%d)\n",
|
||
e.Type, e.Reason, e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Message,
|
||
e.InvolvedObject.Namespace, e.Count),
|
||
})
|
||
}
|
||
sort.Slice(rows, func(i, j int) bool { return rows[i].ts.After(rows[j].ts) })
|
||
if len(rows) > 30 {
|
||
rows = rows[:30]
|
||
}
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("Events (recent %d):\n", len(rows)))
|
||
for _, r := range rows {
|
||
sb.WriteString(r.line)
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatNamespaces(name string) (string, error) {
|
||
names, err := t.client.ListNamespaces()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
sort.Strings(names)
|
||
var sb strings.Builder
|
||
count := 0
|
||
sb.WriteString("Namespaces:\n")
|
||
for _, n := range names {
|
||
if name != "" && n != name {
|
||
continue
|
||
}
|
||
count++
|
||
sb.WriteString(fmt.Sprintf("- %s\n", n))
|
||
}
|
||
if count == 0 {
|
||
sb.WriteString("(无匹配命名空间)\n")
|
||
} else {
|
||
sb.Reset()
|
||
sb.WriteString(fmt.Sprintf("Namespaces (%d):\n", count))
|
||
for _, n := range names {
|
||
if name != "" && n != name {
|
||
continue
|
||
}
|
||
sb.WriteString(fmt.Sprintf("- %s\n", n))
|
||
}
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatHPAs(ns, name string) (string, error) {
|
||
list, err := t.client.ListHPAs()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var lines []string
|
||
for _, h := range list.Items {
|
||
if ns != "" && h.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && h.Metadata.Name != name {
|
||
continue
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s/%s target=%s/%s min=%d max=%d current=%d\n",
|
||
h.Metadata.Namespace, h.Metadata.Name,
|
||
h.Spec.ScaleTargetRef.Kind, h.Spec.ScaleTargetRef.Name,
|
||
h.Spec.MinReplicas, h.Spec.MaxReplicas, h.Status.CurrentReplicas))
|
||
}
|
||
sort.Strings(lines)
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("HPAs (%d):\n", len(lines)))
|
||
for _, l := range lines {
|
||
sb.WriteString(l)
|
||
}
|
||
return sb.String(), nil
|
||
}
|
||
|
||
func (t *KubernetesGetTool) formatCronJobs(ns, name string) (string, error) {
|
||
list, err := t.client.ListCronJobs()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var lines []string
|
||
for _, cj := range list.Items {
|
||
if ns != "" && cj.Metadata.Namespace != ns {
|
||
continue
|
||
}
|
||
if name != "" && cj.Metadata.Name != name {
|
||
continue
|
||
}
|
||
suspended := false
|
||
if cj.Spec.Suspend != nil {
|
||
suspended = *cj.Spec.Suspend
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s/%s schedule=%s suspend=%v active=%d\n",
|
||
cj.Metadata.Namespace, cj.Metadata.Name, cj.Spec.Schedule, suspended, len(cj.Status.Active)))
|
||
}
|
||
sort.Strings(lines)
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("CronJobs (%d):\n", len(lines)))
|
||
for _, l := range lines {
|
||
sb.WriteString(l)
|
||
}
|
||
return sb.String(), nil
|
||
} |