// 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 }