- 新增 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 五张表
376 lines
9.0 KiB
Go
376 lines
9.0 KiB
Go
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
|
|
}
|