Files
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- 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>
2026-07-04 01:50:36 +08:00

526 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}