From b25ad8705f815500a1e134cb83b7d8d8d5af7164 Mon Sep 17 00:00:00 2001 From: attacker Date: Fri, 19 Jun 2026 06:32:18 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20K8s=20=E9=9B=86=E7=BE=A4=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E4=BB=A3=E7=90=86=20=E2=80=94=20=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=B0=83=E7=94=A8=20K8s=20API=20=E4=B8=8D=E5=AD=98=E5=82=A8=20?= =?UTF-8?q?MySQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 pkg/k8sclient 轻量级 K8s REST 客户端(解析 kubeconfig TLS 证书) - ListNodes/Pods/Deployments/Services/Ingresses 改为实时代理 K8s API - 集群同步端点 POST /:id/sync(拉取后仍存 MySQL 用于离线缓存储备) - 前端 k8sApi 新增 syncCluster 方法 - 修复 SNMP MaxRepetitions uint8→uint32 类型错误 - 修复 ServicePort.TargetPort 兼容 int/string 双类型 - 创建 k8s_nodes/pods/deployments/services/ingresses 五张表 --- backend/go.mod | 2 +- backend/internal/api/handlers/k8s.go | 491 +++++++++++++++++++++++--- backend/internal/api/routes/routes.go | 1 + backend/pkg/k8sclient/client.go | 375 ++++++++++++++++++++ backend/pkg/snmp/collector.go | 2 +- frontend/src/services/api.ts | 3 + 6 files changed, 821 insertions(+), 53 deletions(-) create mode 100644 backend/pkg/k8sclient/client.go diff --git a/backend/go.mod b/backend/go.mod index 8d2be4a..741e8ce 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -14,6 +14,7 @@ require ( github.com/spf13/viper v1.18.2 go.uber.org/zap v1.26.0 golang.org/x/crypto v0.46.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.2 gorm.io/driver/postgres v1.5.4 gorm.io/driver/sqlite v1.6.0 @@ -68,5 +69,4 @@ require ( golang.org/x/text v0.32.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/internal/api/handlers/k8s.go b/backend/internal/api/handlers/k8s.go index c80a28e..5724c74 100644 --- a/backend/internal/api/handlers/k8s.go +++ b/backend/internal/api/handlers/k8s.go @@ -1,10 +1,15 @@ package handlers import ( + "encoding/json" + "fmt" + "log" "net/http" "strconv" + "time" "ezops/internal/models" + "ezops/pkg/k8sclient" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -224,14 +229,65 @@ func (h *K8sHandler) DeleteCluster(c *gin.Context) { func (h *K8sHandler) ListNodes(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) - var nodes []models.K8sNode - h.db.Where("cluster_id = ?", clusterID).Find(&nodes) + client, _, err := h.getK8sClient(clusterID) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()}) + return + } + nodes, err := client.ListNodes() + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } - c.JSON(http.StatusOK, models.Response{ - Code: 200, - Message: "success", - Data: nodes, - }) + // Convert to simplified response + type NodeSummary struct { + Name string `json:"name"` + IP string `json:"ip"` + Role string `json:"role"` + Status string `json:"status"` + OS string `json:"os"` + Kubelet string `json:"kubelet_version"` + CPU int `json:"cpu"` + Memory int64 `json:"memory_mb"` + MaxPods int `json:"max_pods"` + } + result := []NodeSummary{} + for _, n := range nodes.Items { + role := "worker" + for k := range n.Metadata.Labels { + if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" { + role = "master" + break + } + } + nodeStatus := "NotReady" + for _, cond := range n.Status.Conditions { + if cond.Type == "Ready" && cond.Status == "True" { + nodeStatus = "Ready" + break + } + } + ip := "" + for _, addr := range n.Status.Addresses { + if addr.Type == "InternalIP" { + ip = addr.Address + break + } + } + result = append(result, NodeSummary{ + Name: n.Metadata.Name, + IP: ip, + Role: role, + Status: nodeStatus, + OS: n.Status.NodeInfo.OSImage, + Kubelet: n.Status.NodeInfo.KubeletVersion, + CPU: parseIntResource(n.Status.Capacity["cpu"]), + Memory: parseMemResource(n.Status.Capacity["memory"]), + MaxPods: parseIntResource(n.Status.Capacity["pods"]), + }) + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result}) } // ListPods 获取Pod列表 @@ -248,19 +304,50 @@ func (h *K8sHandler) ListPods(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) namespace := c.Query("namespace") - db := h.db.Where("cluster_id = ?", clusterID) - if namespace != "" { - db = db.Where("namespace = ?", namespace) + client, _, err := h.getK8sClient(clusterID) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()}) + return + } + pods, err := client.ListPods() + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return } - var pods []models.K8sPod - db.Find(&pods) - - c.JSON(http.StatusOK, models.Response{ - Code: 200, - Message: "success", - Data: pods, - }) + type PodSummary struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Status string `json:"status"` + NodeName string `json:"node_name"` + IP string `json:"ip"` + RestartCount int `json:"restart_count"` + Image string `json:"image"` + } + result := []PodSummary{} + for _, p := range pods.Items { + if namespace != "" && p.Metadata.Namespace != namespace { + 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 + } + result = append(result, PodSummary{ + Namespace: p.Metadata.Namespace, + Name: p.Metadata.Name, + Status: p.Status.Phase, + NodeName: p.Spec.NodeName, + IP: p.Status.PodIP, + RestartCount: restarts, + Image: image, + }) + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result}) } // ListDeployments 获取Deployment列表 @@ -277,19 +364,44 @@ func (h *K8sHandler) ListDeployments(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) namespace := c.Query("namespace") - db := h.db.Where("cluster_id = ?", clusterID) - if namespace != "" { - db = db.Where("namespace = ?", namespace) + client, _, err := h.getK8sClient(clusterID) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()}) + return + } + deps, err := client.ListDeployments() + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return } - var deployments []models.K8sDeployment - db.Find(&deployments) - - c.JSON(http.StatusOK, models.Response{ - Code: 200, - Message: "success", - Data: deployments, - }) + type DepSummary struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Replicas int `json:"replicas"` + AvailableReplicas int `json:"available_replicas"` + ReadyReplicas int `json:"ready_replicas"` + Image string `json:"image"` + } + result := []DepSummary{} + for _, d := range deps.Items { + if namespace != "" && d.Metadata.Namespace != namespace { + continue + } + image := "" + if len(d.Spec.Template.Spec.Containers) > 0 { + image = d.Spec.Template.Spec.Containers[0].Image + } + result = append(result, DepSummary{ + Namespace: d.Metadata.Namespace, + Name: d.Metadata.Name, + Replicas: d.Spec.Replicas, + AvailableReplicas: d.Status.AvailableReplicas, + ReadyReplicas: d.Status.ReadyReplicas, + Image: image, + }) + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result}) } // ListServices 获取Service列表 @@ -306,19 +418,38 @@ func (h *K8sHandler) ListServices(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) namespace := c.Query("namespace") - db := h.db.Where("cluster_id = ?", clusterID) - if namespace != "" { - db = db.Where("namespace = ?", namespace) + client, _, err := h.getK8sClient(clusterID) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()}) + return + } + svcs, err := client.ListServices() + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return } - var services []models.K8sService - db.Find(&services) - - c.JSON(http.StatusOK, models.Response{ - Code: 200, - Message: "success", - Data: services, - }) + type SvcSummary struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Type string `json:"type"` + ClusterIP string `json:"cluster_ip"` + Ports int `json:"port_count"` + } + result := []SvcSummary{} + for _, s := range svcs.Items { + if namespace != "" && s.Metadata.Namespace != namespace { + continue + } + result = append(result, SvcSummary{ + Namespace: s.Metadata.Namespace, + Name: s.Metadata.Name, + Type: s.Spec.Type, + ClusterIP: s.Spec.ClusterIP, + Ports: len(s.Spec.Ports), + }) + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result}) } // ListIngresses 获取Ingress列表 @@ -335,19 +466,42 @@ func (h *K8sHandler) ListIngresses(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) namespace := c.Query("namespace") - db := h.db.Where("cluster_id = ?", clusterID) - if namespace != "" { - db = db.Where("namespace = ?", namespace) + client, _, err := h.getK8sClient(clusterID) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()}) + return + } + ings, err := client.ListIngresses() + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return } - var ingresses []models.K8sIngress - db.Find(&ingresses) - - c.JSON(http.StatusOK, models.Response{ - Code: 200, - Message: "success", - Data: ingresses, - }) + type IngSummary struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Hosts []string `json:"hosts"` + TLS bool `json:"tls"` + } + result := []IngSummary{} + for _, ing := range ings.Items { + if namespace != "" && ing.Metadata.Namespace != namespace { + continue + } + hosts := []string{} + for _, rule := range ing.Spec.Rules { + if rule.Host != "" { + hosts = append(hosts, rule.Host) + } + } + result = append(result, IngSummary{ + Namespace: ing.Metadata.Namespace, + Name: ing.Metadata.Name, + Hosts: hosts, + TLS: len(ing.Spec.TLS) > 0, + }) + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result}) } // GetTopology 获取集群拓扑布局 // Logan.Li @@ -385,3 +539,238 @@ func (h *K8sHandler) SaveTopology(c *gin.Context) { c.JSON(http.StatusOK, models.Response{Code: 200, Message: "保存成功"}) } + + +// SyncCluster 同步集群资源(连接真实K8s API拉取数据) +func (h *K8sHandler) SyncCluster(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + + var cluster models.K8sCluster + if err := h.db.First(&cluster, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "集群不存在"}) + return + } + + if cluster.KubeConfig == "" && cluster.Token == "" { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "集群未配置凭证"}) + return + } + + client, err := k8sclient.NewClient(cluster.KubeConfig) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "连接K8s失败: " + err.Error()}) + return + } + + result := k8sclient.SyncResult{} + + // Sync nodes + if nodes, err := client.ListNodes(); err == nil { + // Use raw SQL for reliability (avoid GORM soft-delete / auto-timestamp issues) + h.db.Exec("DELETE FROM k8s_nodes WHERE cluster_id = ?", cluster.ID) + for _, n := range nodes.Items { + role := "worker" + if _, ok := n.Metadata.Labels["node-role.kubernetes.io/master"]; ok { + role = "master" + } else if _, ok := n.Metadata.Labels["node-role.kubernetes.io/control-plane"]; ok { + role = "master" + } + nodeStatus := "NotReady" + for _, cond := range n.Status.Conditions { + if cond.Type == "Ready" && cond.Status == "True" { + nodeStatus = "Ready" + break + } + } + ip := "" + for _, addr := range n.Status.Addresses { + if addr.Type == "InternalIP" { + ip = addr.Address + break + } + } + labelsJSON, _ := json.Marshal(n.Metadata.Labels) + now := time.Now() + err2 := h.db.Exec(`INSERT INTO k8s_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + 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"]), + string(labelsJSON), now, now).Error + if err2 != nil { + log.Printf("[K8s] insert node %s error: %v", n.Metadata.Name, err2) + } else { + result.Nodes++ + } + } + } else { + log.Printf("[K8s] sync nodes error: %v", err) + } + + // Sync pods + if pods, err := client.ListPods(); err == nil { + h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.K8sPod{}) + for _, p := range pods.Items { + 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 + } + labelsJSON, _ := json.Marshal(p.Metadata.Labels) + h.db.Create(&models.K8sPod{ + ClusterID: cluster.ID, + Namespace: p.Metadata.Namespace, + Name: p.Metadata.Name, + Status: p.Status.Phase, + NodeName: p.Spec.NodeName, + IP: p.Status.PodIP, + Image: image, + RestartCount: restarts, + Labels: string(labelsJSON), + }) + result.Pods++ + } + } else { + log.Printf("[K8s] sync pods error: %v", err) + } + + // Sync deployments + if deps, err := client.ListDeployments(); err == nil { + h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.K8sDeployment{}) + for _, d := range deps.Items { + image := "" + if len(d.Spec.Template.Spec.Containers) > 0 { + image = d.Spec.Template.Spec.Containers[0].Image + } + labelsJSON, _ := json.Marshal(d.Metadata.Labels) + h.db.Create(&models.K8sDeployment{ + ClusterID: cluster.ID, + Namespace: d.Metadata.Namespace, + Name: d.Metadata.Name, + Replicas: d.Spec.Replicas, + AvailableReplicas: d.Status.AvailableReplicas, + ReadyReplicas: d.Status.ReadyReplicas, + Image: image, + Labels: string(labelsJSON), + }) + result.Deployments++ + } + } else { + log.Printf("[K8s] sync deps error: %v", err) + } + + // Sync services + if svcs, err := client.ListServices(); err == nil { + h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.K8sService{}) + for _, s := range svcs.Items { + portsJSON, _ := json.Marshal(s.Spec.Ports) + selectorJSON, _ := json.Marshal(s.Spec.Selector) + labelsJSON, _ := json.Marshal(s.Metadata.Labels) + h.db.Create(&models.K8sService{ + ClusterID: cluster.ID, + Namespace: s.Metadata.Namespace, + Name: s.Metadata.Name, + Type: s.Spec.Type, + ClusterIP: s.Spec.ClusterIP, + Ports: string(portsJSON), + Selector: string(selectorJSON), + Labels: string(labelsJSON), + }) + result.Services++ + } + } + + // Sync ingresses + if ings, err := client.ListIngresses(); err == nil { + h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.K8sIngress{}) + for _, ing := range ings.Items { + hosts := []string{} + for _, rule := range ing.Spec.Rules { + hosts = append(hosts, rule.Host) + } + hostsJSON, _ := json.Marshal(hosts) + tls := len(ing.Spec.TLS) > 0 + tlsHosts := []string{} + if tls { + for _, t := range ing.Spec.TLS { + tlsHosts = append(tlsHosts, t.Hosts...) + } + } + tlsHostsJSON, _ := json.Marshal(tlsHosts) + labelsJSON, _ := json.Marshal(ing.Metadata.Labels) + h.db.Create(&models.K8sIngress{ + ClusterID: cluster.ID, + Namespace: ing.Metadata.Namespace, + Name: ing.Metadata.Name, + Hosts: string(hostsJSON), + TLS: tls, + TLSHosts: string(tlsHostsJSON), + Labels: string(labelsJSON), + }) + result.Ingresses++ + } + } + + // Update cluster stats + now := time.Now() + h.db.Model(&cluster).Updates(map[string]interface{}{ + "node_count": result.Nodes, + "pod_count": result.Pods, + "deployment_count": result.Deployments, + "service_count": result.Services, + "last_sync_at": &now, + }) + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: fmt.Sprintf("同步完成: %d nodes, %d pods, %d deployments, %d services, %d ingresses", + result.Nodes, result.Pods, result.Deployments, result.Services, result.Ingresses), + Data: result, + }) +} + +func parseIntResource(val string) int { + if val == "" { + return 0 + } + var i int + fmt.Sscanf(val, "%d", &i) + return i +} + +func parseMemResource(val string) int64 { + if val == "" { + return 0 + } + var num float64 + var unit string + fmt.Sscanf(val, "%f%s", &num, &unit) + switch unit { + case "Ki": + return int64(num / 1024) + case "Mi": + return int64(num) + case "Gi": + return int64(num * 1024) + default: + return int64(num / 1024 / 1024) + } +} + + +// getK8sClient builds a K8s API client from cluster's stored kubeconfig +func (h *K8sHandler) getK8sClient(clusterID int) (*k8sclient.Client, *models.K8sCluster, error) { + var cluster models.K8sCluster + if err := h.db.First(&cluster, clusterID).Error; err != nil { + return nil, nil, fmt.Errorf("cluster not found") + } + if cluster.KubeConfig == "" { + return nil, nil, fmt.Errorf("no kubeconfig") + } + client, err := k8sclient.NewClient(cluster.KubeConfig) + if err != nil { + return nil, nil, err + } + return client, &cluster, nil +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index a3d8292..970cd08 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -239,6 +239,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c clusters.POST("", k8sHandler.CreateCluster) clusters.PUT("/:id", k8sHandler.UpdateCluster) clusters.DELETE("/:id", k8sHandler.DeleteCluster) + clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li clusters.GET("/:id/nodes", k8sHandler.ListNodes) clusters.GET("/:id/pods", k8sHandler.ListPods) clusters.GET("/:id/deployments", k8sHandler.ListDeployments) diff --git a/backend/pkg/k8sclient/client.go b/backend/pkg/k8sclient/client.go new file mode 100644 index 0000000..a06b1df --- /dev/null +++ b/backend/pkg/k8sclient/client.go @@ -0,0 +1,375 @@ +package k8sclient + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +// KubeConfig represents a parsed kubeconfig +type KubeConfig struct { + APIVersion string `yaml:"apiVersion"` + Clusters []struct { + Name string `yaml:"name"` + Cluster struct { + Server string `yaml:"server"` + CertificateAuthorityData string `yaml:"certificate-authority-data"` + InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify"` + } `yaml:"cluster"` + } `yaml:"clusters"` + Users []struct { + Name string `yaml:"name"` + User struct { + ClientCertificateData string `yaml:"client-certificate-data"` + ClientKeyData string `yaml:"client-key-data"` + Token string `yaml:"token"` + } `yaml:"user"` + } `yaml:"users"` + Contexts []struct { + Name string `yaml:"name"` + Context struct { + Cluster string `yaml:"cluster"` + User string `yaml:"user"` + } `yaml:"context"` + } `yaml:"contexts"` + CurrentContext string `yaml:"current-context"` +} + +// Client wraps an HTTP client for K8s API calls +type Client struct { + BaseURL string + HTTPClient *http.Client +} + +// NewClient creates a K8s API client from kubeconfig YAML +func NewClient(kubeconfigYAML string) (*Client, error) { + var kcfg KubeConfig + if err := yaml.Unmarshal([]byte(kubeconfigYAML), &kcfg); err != nil { + return nil, fmt.Errorf("parse kubeconfig: %w", err) + } + + // Find current context + ctxName := kcfg.CurrentContext + var clusterName, userName string + for _, ctx := range kcfg.Contexts { + if ctx.Name == ctxName { + clusterName = ctx.Context.Cluster + userName = ctx.Context.User + break + } + } + if clusterName == "" { + return nil, fmt.Errorf("current context %q not found", ctxName) + } + + // Find cluster + var server string + var caData string + for _, cl := range kcfg.Clusters { + if cl.Name == clusterName { + server = cl.Cluster.Server + caData = cl.Cluster.CertificateAuthorityData + break + } + } + if server == "" { + return nil, fmt.Errorf("cluster %q not found", clusterName) + } + + // Build TLS config + tlsCfg := &tls.Config{} + if caData != "" { + caBytes, err := base64.StdEncoding.DecodeString(caData) + if err != nil { + return nil, fmt.Errorf("decode CA cert: %w", err) + } + caPool := x509.NewCertPool() + caPool.AppendCertsFromPEM(caBytes) + tlsCfg.RootCAs = caPool + } else { + tlsCfg.InsecureSkipVerify = true + } + + // Find user certs + for _, u := range kcfg.Users { + if u.Name == userName { + if u.User.ClientCertificateData != "" && u.User.ClientKeyData != "" { + certBytes, _ := base64.StdEncoding.DecodeString(u.User.ClientCertificateData) + keyBytes, _ := base64.StdEncoding.DecodeString(u.User.ClientKeyData) + cert, err := tls.X509KeyPair(certBytes, keyBytes) + if err != nil { + return nil, fmt.Errorf("parse client cert: %w", err) + } + tlsCfg.Certificates = []tls.Certificate{cert} + } + break + } + } + + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsCfg, + }, + } + + return &Client{ + BaseURL: strings.TrimRight(server, "/"), + HTTPClient: httpClient, + }, nil +} + +// get performs a GET request to the K8s API +func (c *Client) get(path string, result interface{}) error { + url := c.BaseURL + path + resp, err := c.HTTPClient.Get(url) + if err != nil { + return fmt.Errorf("GET %s: %w", path, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read body: %w", err) + } + + if resp.StatusCode >= 400 { + return fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 200)])) + } + + if err := json.Unmarshal(body, result); err != nil { + return fmt.Errorf("unmarshal: %w (body: %s)", err, string(body[:min(len(body), 100)])) + } + return nil +} + +// --- K8s API response types --- + +// NodeList response +type NodeList struct { + Items []NodeItem `json:"items"` +} + +type NodeItem struct { + Metadata ObjectMeta `json:"metadata"` + Status NodeStatus `json:"status"` + Spec NodeSpec `json:"spec"` +} + +type NodeSpec struct { + PodCIDR string `json:"podCIDR"` +} + +type NodeStatus struct { + Capacity ResourceList `json:"capacity"` + Allocatable ResourceList `json:"allocatable"` + Conditions []NodeCondition `json:"conditions"` + NodeInfo NodeInfo `json:"nodeInfo"` + Addresses []NodeAddress `json:"addresses"` +} + +type ResourceList map[string]string + +type NodeCondition struct { + Type string `json:"type"` + Status string `json:"status"` +} + +type NodeInfo struct { + KubeletVersion string `json:"kubeletVersion"` + OSImage string `json:"osImage"` + KernelVersion string `json:"kernelVersion"` + ContainerRuntimeVersion string `json:"containerRuntimeVersion"` +} + +type NodeAddress struct { + Type string `json:"type"` + Address string `json:"address"` +} + +// PodList response +type PodList struct { + Items []PodItem `json:"items"` +} + +type PodItem struct { + Metadata ObjectMeta `json:"metadata"` + Status PodStatus `json:"status"` + Spec PodSpec `json:"spec"` +} + +type PodSpec struct { + NodeName string `json:"nodeName"` + Containers []Container `json:"containers"` +} + +type Container struct { + Image string `json:"image"` +} + +type PodStatus struct { + Phase string `json:"phase"` + PodIP string `json:"podIP"` + ContainerStatuses []ContainerStatus `json:"containerStatuses"` +} + +type ContainerStatus struct { + RestartCount int `json:"restartCount"` + Ready bool `json:"ready"` +} + +// DeploymentList response +type DeploymentList struct { + Items []DeploymentItem `json:"items"` +} + +type DeploymentItem struct { + Metadata ObjectMeta `json:"metadata"` + Spec DeploymentSpec `json:"spec"` + Status DeploymentStatus `json:"status"` +} + +type DeploymentSpec struct { + Replicas int `json:"replicas"` + Template DeploymentTemplate `json:"template"` +} + +type DeploymentTemplate struct { + Spec PodSpec `json:"spec"` +} + +type DeploymentStatus struct { + Replicas int `json:"replicas"` + AvailableReplicas int `json:"availableReplicas"` + ReadyReplicas int `json:"readyReplicas"` +} + +// ServiceList response +type ServiceList struct { + Items []ServiceItem `json:"items"` +} + +type ServiceItem struct { + Metadata ObjectMeta `json:"metadata"` + Spec ServiceSpec `json:"spec"` +} + +type ServiceSpec struct { + Type string `json:"type"` + ClusterIP string `json:"clusterIP"` + Ports []ServicePort `json:"ports"` + Selector map[string]string `json:"selector"` +} + +type ServicePort struct { + Name string `json:"name"` + Port int `json:"port"` + TargetPort interface{} `json:"targetPort"` + NodePort int `json:"nodePort"` + Protocol string `json:"protocol"` +} + +// IngressList response +type IngressList struct { + Items []IngressItem `json:"items"` +} + +type IngressItem struct { + Metadata ObjectMeta `json:"metadata"` + Spec IngressSpec `json:"spec"` +} + +type IngressSpec struct { + TLS []IngressTLS `json:"tls"` + Rules []IngressRule `json:"rules"` +} + +type IngressTLS struct { + Hosts []string `json:"hosts"` +} + +type IngressRule struct { + Host string `json:"host"` +} + +// ObjectMeta +type ObjectMeta struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + CreationTimestamp string `json:"creationTimestamp"` +} + +// SyncResult holds sync counts +type SyncResult struct { + Nodes int `json:"nodes"` + Pods int `json:"pods"` + Deployments int `json:"deployments"` + Services int `json:"services"` + Ingresses int `json:"ingresses"` +} + +// GetServerVersion returns K8s server version +func (c *Client) GetServerVersion() (string, error) { + var result struct { + GitVersion string `json:"gitVersion"` + } + if err := c.get("/version", &result); err != nil { + return "", err + } + return result.GitVersion, nil +} + +// ListNodes fetches all nodes +func (c *Client) ListNodes() (*NodeList, error) { + var result NodeList + if err := c.get("/api/v1/nodes", &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListPods fetches all pods across namespaces +func (c *Client) ListPods() (*PodList, error) { + var result PodList + if err := c.get("/api/v1/pods", &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListDeployments fetches all deployments across namespaces +func (c *Client) ListDeployments() (*DeploymentList, error) { + var result DeploymentList + if err := c.get("/apis/apps/v1/deployments", &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListServices fetches all services across namespaces +func (c *Client) ListServices() (*ServiceList, error) { + var result ServiceList + if err := c.get("/api/v1/services", &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListIngresses fetches all ingresses across namespaces +func (c *Client) ListIngresses() (*IngressList, error) { + var result IngressList + if err := c.get("/apis/networking.k8s.io/v1/ingresses", &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/backend/pkg/snmp/collector.go b/backend/pkg/snmp/collector.go index 48e8d71..f595a5e 100644 --- a/backend/pkg/snmp/collector.go +++ b/backend/pkg/snmp/collector.go @@ -85,7 +85,7 @@ func NewCollector(config *CollectorConfig) (*Collector, error) { Version: config.Version, Timeout: config.Timeout, Retries: config.Retries, - MaxRepetitions: config.MaxRepetitions, + MaxRepetitions: uint32(config.MaxRepetitions), ExponentialTimeout: true, } diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 8b92790..45faf01 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -187,6 +187,9 @@ export const k8sApi = { getIngresses: async (clusterId: number | string) => { return http.get(`/k8s-clusters/${clusterId}/ingresses`); }, + syncCluster: async (clusterId: number | string) => { + return http.post(`/k8s-clusters/${clusterId}/sync`); + }, getTopology: async (clusterId: number | string) => { // Logan.Li return http.get(`/k8s-clusters/${clusterId}/topology`); },