Files
aiops/backend/pkg/kubernetesclient/client.go
T
attacker 9ab7572b8e feat(k8s): M3 Pod文件上传 + ExecInPod WebSocket协议重写
M3: Pod 文件上传
- 后端: POST .../pods/:pod/files/upload (multipart, base64分块exec写入)
- 前端: PodFileBrowser 增加上传按钮 + FormData API
- 路由: 新增 Kuboard (:clusterCode) + 旧 (:id) 两套路由

重大修复: ExecInPod 从 raw HTTP SPDY 改为 WebSocket channel.k8s.io
- 旧方案: HTTP POST + SPDY upgrade → 无法解析响应(协议头缺失 + 帧解析复杂)
- 新方案: gorilla/websocket Dialer + channel.k8s.io 子协议 → 和 ExecTerminal 一致
- 影响: 文件浏览/下载/上传全部恢复正常

修复: BusyBox ls 解析
- BusyBox 不支持 --time-style 参数,默认输出 Mon DD HH:MM 格式
- 解析器改为自动检测 GNU vs BusyBox 日期格式
2026-06-23 02:21:29 +08:00

1518 lines
43 KiB
Go
Raw 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.
package k8sclient
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
"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
KubeconfigYAML string // raw kubeconfig for kubectl subprocess
TLSConfig *tls.Config
BearerToken string
}
// 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 creds (client certs and/or bearer token)
var bearerToken string
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}
}
if u.User.Token != "" {
bearerToken = u.User.Token
}
break
}
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
}
return &Client{
BaseURL: strings.TrimRight(server, "/"),
HTTPClient: httpClient,
KubeconfigYAML: kubeconfigYAML,
TLSConfig: tlsCfg,
BearerToken: bearerToken,
}, 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
}
// getRaw performs a GET request returning raw bytes with custom Accept header
func (c *Client) getRaw(path string, accept string) ([]byte, error) {
req, err := http.NewRequest("GET", c.BaseURL+path, nil)
if err != nil { return nil, err }
req.Header.Set("Accept", accept)
resp, err := c.HTTPClient.Do(req)
if err != nil { return nil, fmt.Errorf("GET %s: %w", path, err) }
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil { return nil, fmt.Errorf("read body: %w", err) }
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 200)]))
}
return body, nil
}
// putRaw performs a PUT request with raw body and custom Content-Type
func (c *Client) putRaw(path string, contentType string, data []byte) error {
req, err := http.NewRequest("PUT", c.BaseURL+path, strings.NewReader(string(data)))
if err != nil { return err }
req.Header.Set("Content-Type", contentType)
resp, err := c.HTTPClient.Do(req)
if err != nil { return fmt.Errorf("PUT %s: %w", path, err) }
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
}
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 {
Name string `json:"name"`
Image string `json:"image"`
Resources ContainerRes `json:"resources"`
}
type ContainerRes struct {
Requests map[string]string `json:"requests"`
Limits map[string]string `json:"limits"`
}
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
}
// --- HTTP helpers (post/patch/put/delete) ---
func (c *Client) post(path string, body, result interface{}) error {
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal body: %w", err)
}
req, err := http.NewRequest("POST", c.BaseURL+path, strings.NewReader(string(bodyBytes)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("POST %s: %w", path, err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(respBody[:min(len(respBody), 300)]))
}
if result != nil {
return json.Unmarshal(respBody, result)
}
return nil
}
func (c *Client) patch(path string, body, result interface{}) error {
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal body: %w", err)
}
req, err := http.NewRequest("PATCH", c.BaseURL+path, strings.NewReader(string(bodyBytes)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/merge-patch+json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("PATCH %s: %w", path, err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(respBody[:min(len(respBody), 300)]))
}
if result != nil {
return json.Unmarshal(respBody, result)
}
return nil
}
func (c *Client) delete(path string) error {
req, err := http.NewRequest("DELETE", c.BaseURL+path, nil)
if err != nil {
return err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("DELETE %s: %w", path, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode != 404 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(respBody[:min(len(respBody), 300)]))
}
return nil
}
// --- Pod Logs ---
// GetPodLogs fetches logs for a pod/container
func (c *Client) GetPodLogs(namespace, podName, container string, tailLines int) (string, error) {
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log?tailLines=%d", namespace, podName, tailLines)
if container != "" {
path += "&container=" + container
}
url := c.BaseURL + path
resp, err := c.HTTPClient.Get(url)
if err != nil {
return "", fmt.Errorf("GET logs: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode >= 400 {
return "", fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
}
return string(body), nil
}
// GetPodLogsStream returns the raw streaming response body for follow=true
func (c *Client) GetPodLogsStream(namespace, podName, container string, tailLines int) (io.ReadCloser, error) {
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log?follow=true&tailLines=%d", namespace, podName, tailLines)
if container != "" {
path += "&container=" + container
}
url := c.BaseURL + path
resp, err := c.HTTPClient.Get(url)
if err != nil {
return nil, fmt.Errorf("GET logs stream: %w", err)
}
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
}
return resp.Body, nil
}
// --- Scale ---
// ScaleDeployment patches the replicas count for a deployment
func (c *Client) ScaleDeployment(namespace, name string, replicas int) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"replicas": replicas,
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s/scale", namespace, name)
return c.patch(path, body, nil)
}
// --- Deployment Image Update (CI) ---
// UpdateDeploymentImage patches the container image of a deployment
func (c *Client) UpdateDeploymentImage(namespace, name, containerName, image string) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{"name": containerName, "image": image},
},
},
},
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, name)
return c.patch(path, body, nil)
}
// RestartDeployment triggers a rolling restart by annotating the pod template
func (c *Client) RestartDeployment(namespace, name string) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]string{
"kubectl.kubernetes.io/restartedAt": time.Now().UTC().Format(time.RFC3339),
},
},
},
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, name)
return c.patch(path, body, nil)
}
// --- Exec (K8s WebSocket channel protocol) ---
// ExecResult holds the output of a pod exec command
type ExecResult struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
}
// ExecInPod runs a command in a pod container and returns stdout/stderr.
// Uses K8s WebSocket channel protocol (channel.k8s.io) — no kubectl needed.
func (c *Client) ExecInPod(namespace, podName, container string, command []string) (*ExecResult, error) {
// Build exec WebSocket URL
execURL := c.BaseURL
if strings.HasPrefix(execURL, "https://") {
execURL = "wss://" + strings.TrimPrefix(execURL, "https://")
} else {
execURL = "ws://" + strings.TrimPrefix(execURL, "http://")
}
params := url.Values{}
for _, cmd := range command {
params.Add("command", cmd)
}
params.Set("stdin", "false")
params.Set("stdout", "true")
params.Set("stderr", "true")
params.Set("tty", "false")
if container != "" {
params.Set("container", container)
}
execURL += fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/exec?%s",
url.PathEscape(namespace), url.PathEscape(podName), params.Encode())
dialer := websocket.Dialer{
TLSClientConfig: c.TLSConfig,
Subprotocols: []string{"channel.k8s.io"},
HandshakeTimeout: 10 * time.Second,
}
header := http.Header{}
if c.BearerToken != "" {
header.Set("Authorization", "Bearer "+c.BearerToken)
}
conn, _, err := dialer.Dial(execURL, header)
if err != nil {
return nil, fmt.Errorf("K8s exec WS dial: %w", err)
}
defer conn.Close()
var stdout, stderr bytes.Buffer
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break // connection closed = command finished
}
if len(msg) == 0 {
continue
}
channel := msg[0]
payload := msg[1:]
switch channel {
case 1:
stdout.Write(payload)
case 2:
stderr.Write(payload)
case 3:
stderr.Write(payload)
}
}
return &ExecResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
}, nil
}
// --- HPA ---
// HPAList response
type HPAList struct {
Items []HPAItem `json:"items"`
}
// HPAItem represents a HorizontalPodAutoscaler
type HPAItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec HPASpec `json:"spec"`
Status HPAStatus `json:"status"`
}
// HPASpec defines the HPA spec
type HPASpec struct {
ScaleTargetRef ScaleTargetRef `json:"scaleTargetRef"`
MinReplicas int `json:"minReplicas"`
MaxReplicas int `json:"maxReplicas"`
Metrics []HPAMetric `json:"metrics"`
}
// ScaleTargetRef references the target resource to scale
type ScaleTargetRef struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Name string `json:"name"`
}
// HPAMetric defines a metric for HPA
type HPAMetric struct {
Type string `json:"type"`
Resource *struct {
Name string `json:"name"`
Target struct {
Type string `json:"type"`
AverageUtilization int `json:"averageUtilization"`
} `json:"target"`
} `json:"resource,omitempty"`
}
// HPAStatus holds the current status of the HPA
type HPAStatus struct {
CurrentReplicas int `json:"currentReplicas"`
DesiredReplicas int `json:"desiredReplicas"`
}
// ListHPAs fetches all HPAs across namespaces
func (c *Client) ListHPAs() (*HPAList, error) {
var result HPAList
if err := c.get("/apis/autoscaling/v2/horizontalpodautoscalers", &result); err != nil {
return nil, err
}
return &result, nil
}
// CreateHPARequest is the payload for creating an HPA
type CreateHPARequest struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
TargetKind string `json:"targetKind"` // Deployment, StatefulSet
TargetName string `json:"targetName"`
MinReplicas int `json:"minReplicas"`
MaxReplicas int `json:"maxReplicas"`
CPUPercent int `json:"cpuPercent"` // target average CPU utilization
MemPercent int `json:"memPercent"` // target average memory utilization (0 = disabled)
}
// CreateHPA creates a new HPA
func (c *Client) CreateHPA(req CreateHPARequest) (*HPAItem, error) {
metrics := []HPAMetric{}
if req.CPUPercent > 0 {
metrics = append(metrics, HPAMetric{
Type: "Resource",
Resource: &struct {
Name string `json:"name"`
Target struct {
Type string `json:"type"`
AverageUtilization int `json:"averageUtilization"`
} `json:"target"`
}{
Name: "cpu",
Target: struct {
Type string `json:"type"`
AverageUtilization int `json:"averageUtilization"`
}{Type: "Utilization", AverageUtilization: req.CPUPercent},
},
})
}
if req.MemPercent > 0 {
metrics = append(metrics, HPAMetric{
Type: "Resource",
Resource: &struct {
Name string `json:"name"`
Target struct {
Type string `json:"type"`
AverageUtilization int `json:"averageUtilization"`
} `json:"target"`
}{
Name: "memory",
Target: struct {
Type string `json:"type"`
AverageUtilization int `json:"averageUtilization"`
}{Type: "Utilization", AverageUtilization: req.MemPercent},
},
})
}
body := map[string]interface{}{
"apiVersion": "autoscaling/v2",
"kind": "HorizontalPodAutoscaler",
"metadata": map[string]interface{}{
"name": req.Name,
"namespace": req.Namespace,
},
"spec": map[string]interface{}{
"scaleTargetRef": map[string]interface{}{
"apiVersion": "apps/v1",
"kind": req.TargetKind,
"name": req.TargetName,
},
"minReplicas": req.MinReplicas,
"maxReplicas": req.MaxReplicas,
"metrics": metrics,
},
}
var result HPAItem
path := fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers", req.Namespace)
if err := c.post(path, body, &result); err != nil {
return nil, err
}
return &result, nil
}
// DeleteHPA deletes an HPA
func (c *Client) DeleteHPA(namespace, name string) error {
path := fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", namespace, name)
return c.delete(path)
}
// --- CronJob ---
// CronJobList response
type CronJobList struct {
Items []CronJobItem `json:"items"`
}
// CronJobItem represents a CronJob
type CronJobItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec CronJobSpec `json:"spec"`
Status CronJobStatus `json:"status"`
}
// CronJobSpec defines the CronJob spec
type CronJobSpec struct {
Schedule string `json:"schedule"`
Suspend *bool `json:"suspend"`
ConcurrencyPolicy string `json:"concurrencyPolicy"`
JobTemplate struct {
Spec struct {
Template struct {
Spec struct {
Containers []Container `json:"containers"`
} `json:"spec"`
} `json:"template"`
} `json:"spec"`
} `json:"jobTemplate"`
}
// CronJobStatus holds the status
type CronJobStatus struct {
Active []interface{} `json:"active"`
LastScheduleTime string `json:"lastScheduleTime"`
}
// ListCronJobs fetches all CronJobs across namespaces
func (c *Client) ListCronJobs() (*CronJobList, error) {
var result CronJobList
if err := c.get("/apis/batch/v1/cronjobs", &result); err != nil {
return nil, err
}
return &result, nil
}
// CreateCronJobRequest is the payload for creating a CronJob
type CreateCronJobRequest struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Schedule string `json:"schedule"` // cron expression
Image string `json:"image"`
Command []string `json:"command"`
}
// CreateCronJob creates a new CronJob
func (c *Client) CreateCronJob(req CreateCronJobRequest) (*CronJobItem, error) {
suspend := false
body := map[string]interface{}{
"apiVersion": "batch/v1",
"kind": "CronJob",
"metadata": map[string]interface{}{
"name": req.Name,
"namespace": req.Namespace,
},
"spec": map[string]interface{}{
"schedule": req.Schedule,
"suspend": &suspend,
"concurrencyPolicy": "Forbid",
"jobTemplate": map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{
"name": req.Name,
"image": req.Image,
"command": req.Command,
},
},
"restartPolicy": "OnFailure",
},
},
},
},
},
}
var result CronJobItem
path := fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs", req.Namespace)
if err := c.post(path, body, &result); err != nil {
return nil, err
}
return &result, nil
}
// SuspendCronJob suspends or resumes a CronJob
func (c *Client) SuspendCronJob(namespace, name string, suspend bool) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"suspend": &suspend,
},
}
path := fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", namespace, name)
return c.patch(path, body, nil)
}
// DeleteCronJob deletes a CronJob
func (c *Client) DeleteCronJob(namespace, name string) error {
path := fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", namespace, name)
return c.delete(path)
}
// TriggerCronJob manually creates a Job from a CronJob
func (c *Client) TriggerCronJob(namespace, name string) error {
// Patch the CronJob with an annotation to trigger reconciliation.
triggerTime := time.Now().UTC().Format(time.RFC3339)
triggerBody := map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]string{
"aiops.io/manual-trigger": triggerTime,
},
},
}
path := fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", namespace, name)
return c.patch(path, triggerBody, nil)
}
// --- Namespace ---
// ListNamespaces fetches all namespaces
func (c *Client) ListNamespaces() ([]string, error) {
var result struct {
Items []struct {
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
} `json:"items"`
}
if err := c.get("/api/v1/namespaces", &result); err != nil {
return nil, err
}
names := make([]string, len(result.Items))
for i, ns := range result.Items {
names[i] = ns.Metadata.Name
}
return names, nil
}
// --- ConfigMap ---
// ConfigMapList response
type ConfigMapList struct {
Items []ConfigMapItem `json:"items"`
}
// ConfigMapItem represents a ConfigMap
type ConfigMapItem struct {
Metadata ObjectMeta `json:"metadata"`
Data map[string]string `json:"data"`
}
// ListConfigMaps fetches ConfigMaps in a namespace
func (c *Client) ListConfigMaps(namespace string) (*ConfigMapList, error) {
var result ConfigMapList
path := fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result, nil
}
// --- Secret ---
// SecretList response
type SecretList struct {
Items []SecretItem `json:"items"`
}
// SecretItem represents a Secret (return keys only, not values)
type SecretItem struct {
Metadata ObjectMeta `json:"metadata"`
Type string `json:"type"`
DataKeys []string `json:"-"`
}
// ListSecrets fetches Secrets in a namespace (names+types only, no data)
func (c *Client) ListSecrets(namespace string) (*SecretList, error) {
var result struct {
Items []struct {
Metadata ObjectMeta `json:"metadata"`
Type string `json:"type"`
Data map[string]string `json:"data"`
} `json:"items"`
}
path := fmt.Sprintf("/api/v1/namespaces/%s/secrets", namespace)
if err := c.get(path, &result); err != nil {
return nil, err
}
items := make([]SecretItem, len(result.Items))
for i, item := range result.Items {
keys := make([]string, 0, len(item.Data))
for k := range item.Data {
keys = append(keys, k)
}
items[i] = SecretItem{
Metadata: item.Metadata,
Type: item.Type,
DataKeys: keys,
}
}
return &SecretList{Items: items}, nil
}
// ============================================================================
// PersistentVolumeClaim (存储)
// ============================================================================
type PVCList struct {
Items []PVCItem `json:"items"`
}
type PVCItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec PVCSpec `json:"spec"`
Status struct {
Phase string `json:"phase"`
Capacity map[string]string `json:"capacity"`
} `json:"status"`
}
type PVCSpec struct {
AccessModes []string `json:"accessModes"`
Resources struct {
Requests map[string]string `json:"requests"`
} `json:"resources"`
StorageClassName string `json:"storageClassName"`
}
// ListPVCs fetches PersistentVolumeClaims in a namespace
func (c *Client) ListPVCs(namespace string) (*PVCList, error) {
var result PVCList
path := fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims", namespace)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result, nil
}
// DeletePVC deletes a PersistentVolumeClaim
func (c *Client) DeletePVC(namespace, name string) error {
path := fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", namespace, name)
return c.delete(path)
}
// ============================================================================
// StatefulSet
// ============================================================================
type StatefulSetList struct {
Items []StatefulSetItem `json:"items"`
}
type StatefulSetItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec StatefulSetSpec `json:"spec"`
Status struct {
Replicas int `json:"replicas"`
ReadyReplicas int `json:"readyReplicas"`
} `json:"status"`
}
type StatefulSetSpec struct {
Replicas int `json:"replicas"`
Template struct {
Spec struct {
Containers []struct {
Name string `json:"name"`
Image string `json:"image"`
} `json:"containers"`
} `json:"spec"`
} `json:"template"`
}
// ListStatefulSets fetches StatefulSets across all namespaces
func (c *Client) ListStatefulSets() (*StatefulSetList, error) {
var result StatefulSetList
if err := c.get("/apis/apps/v1/statefulsets", &result); err != nil {
return nil, err
}
return &result, nil
}
// ============================================================================
// DaemonSet
// ============================================================================
type DaemonSetList struct {
Items []DaemonSetItem `json:"items"`
}
type DaemonSetItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec DaemonSetSpec `json:"spec"`
Status struct {
DesiredNumberScheduled int `json:"desiredNumberScheduled"`
CurrentNumberScheduled int `json:"currentNumberScheduled"`
NumberReady int `json:"numberReady"`
} `json:"status"`
}
type DaemonSetSpec struct {
Template struct {
Spec struct {
Containers []struct {
Name string `json:"name"`
Image string `json:"image"`
} `json:"containers"`
} `json:"spec"`
} `json:"template"`
}
// ListDaemonSets fetches DaemonSets across all namespaces
func (c *Client) ListDaemonSets() (*DaemonSetList, error) {
var result DaemonSetList
if err := c.get("/apis/apps/v1/daemonsets", &result); err != nil {
return nil, err
}
return &result, nil
}
// ============================================================================
// NodeMetrics (metrics-server, optional)
// ============================================================================
type NodeMetricsList struct {
Items []NodeMetricsItem `json:"items"`
}
type NodeMetricsItem struct {
Metadata ObjectMeta `json:"metadata"`
Usage map[string]string `json:"usage"` // cpu, memory
}
// GetNodeMetrics fetches node CPU/memory usage from metrics-server
func (c *Client) GetNodeMetrics() (*NodeMetricsList, error) {
var result NodeMetricsList
if err := c.get("/apis/metrics.k8s.io/v1beta1/nodes", &result); err != nil {
return nil, err
}
return &result, nil
}
// ============================================================================
// PodMetrics (metrics-server, per-container CPU/Memory)
// ============================================================================
type PodMetricsList struct {
Items []PodMetricsItem `json:"items"`
}
type PodMetricsItem struct {
Metadata ObjectMeta `json:"metadata"`
Containers []ContainerMetrics `json:"containers"`
}
type ContainerMetrics struct {
Name string `json:"name"`
Usage map[string]string `json:"usage"` // cpu, memory
}
// ListPodMetrics fetches pod CPU/memory usage, optionally filtered by namespace
func (c *Client) ListPodMetrics(namespace string) (*PodMetricsList, error) {
var result PodMetricsList
path := "/apis/metrics.k8s.io/v1beta1/pods"
if namespace != "" {
path = fmt.Sprintf("/apis/metrics.k8s.io/v1beta1/namespaces/%s/pods", namespace)
}
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result, nil
}
// ============================================================================
// Events
// ============================================================================
type EventList struct {
Items []EventItem `json:"items"`
}
type EventItem struct {
Metadata ObjectMeta `json:"metadata"`
InvolvedObject InvolvedObject `json:"involvedObject"`
Type string `json:"type"`
Reason string `json:"reason"`
Message string `json:"message"`
Count int32 `json:"count"`
FirstTimestamp time.Time `json:"firstTimestamp"`
LastTimestamp time.Time `json:"lastTimestamp"`
EventTime time.Time `json:"eventTime"`
}
type InvolvedObject struct {
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace"`
}
// ListEvents fetches cluster events, optionally filtered by namespace
func (c *Client) ListEvents(namespace string) (*EventList, error) {
var result EventList
path := "/api/v1/events"
if namespace != "" {
path = fmt.Sprintf("/api/v1/namespaces/%s/events", namespace)
}
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result, nil
}
// ============================================================================
// YAML Apply (Dynamic resource apply)
// ============================================================================
// ApplyYAML applies a raw YAML manifest using the K8s discovery API
func (c *Client) ApplyYAML(yamlContent string) (map[string]interface{}, error) {
return c.applyRaw(yamlContent)
}
// GetResourceYAML fetches raw YAML for any K8s resource by API path
func (c *Client) GetResourceYAML(apiPath string) ([]byte, error) {
return c.getRaw(apiPath, "application/yaml")
}
// UpdateResourceYAML applies raw YAML to update a K8s resource
func (c *Client) UpdateResourceYAML(apiPath string, yamlData []byte) error {
return c.putRaw(apiPath, "application/yaml", yamlData)
}
// applyRaw parses YAML and applies it via create-or-update pattern
func (c *Client) applyRaw(yamlContent string) (map[string]interface{}, error) {
// Parse YAML to extract resource metadata
var obj struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
} `yaml:"metadata"`
}
if err := yaml.Unmarshal([]byte(yamlContent), &obj); err != nil {
return nil, fmt.Errorf("YAML 解析失败: %w", err)
}
if obj.Kind == "" || obj.Metadata.Name == "" {
return nil, fmt.Errorf("YAML 中缺少 kind 或 name")
}
// Build API path from kind
apiPath, isNamespaced := c.resourceAPIPath(obj.APIVersion, obj.Kind, obj.Metadata.Namespace, obj.Metadata.Name)
if apiPath == "" {
return nil, fmt.Errorf("不支持的资源类型: %s/%s", obj.APIVersion, obj.Kind)
}
// Try GET first — if 200, it exists, do PUT. If 404, do POST.
parentPath := apiPath[:strings.LastIndex(apiPath, "/")] // without /name suffix
_, getErr := c.getRaw(apiPath, "application/yaml")
if getErr == nil {
// Resource exists → PUT update
if err := c.putRaw(apiPath, "application/yaml", []byte(yamlContent)); err != nil {
return nil, fmt.Errorf("更新资源失败: %w", err)
}
return map[string]interface{}{"action": "updated", "path": apiPath}, nil
}
// Resource doesn't exist → POST create (for namespaced) or POST to parent
var result map[string]interface{}
if isNamespaced && obj.Metadata.Namespace == "" {
obj.Metadata.Namespace = "default"
parentPath = strings.Replace(parentPath, "/namespaces//", "/namespaces/default/", 1)
}
if err := c.post(parentPath, []byte(yamlContent), &result); err != nil {
return nil, fmt.Errorf("创建资源失败: %w", err)
}
return map[string]interface{}{"action": "created", "path": parentPath, "result": result}, nil
}
// resourceAPIPath returns the K8s REST API path for a given resource kind
func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (string, bool) {
resource := strings.ToLower(kind) + "s"
switch kind {
case "Endpoints": resource = "endpoints"
case "PodMetrics": resource = "pods"
}
var groupPrefix string
if apiVersion != "" && apiVersion != "v1" {
parts := strings.SplitN(apiVersion, "/", 2)
if len(parts) == 2 {
groupPrefix = "/apis/" + parts[0] + "/" + parts[1]
} else {
groupPrefix = "/apis/" + parts[0]
}
}
isNamespaced := true
switch kind {
case "Namespace", "Node", "ClusterRole", "ClusterRoleBinding", "PersistentVolume":
isNamespaced = false
}
if !isNamespaced {
if groupPrefix != "" {
return groupPrefix + "/" + resource + "/" + name, false
}
return "/api/v1/" + resource + "/" + name, false
}
ns := namespace
if ns == "" { ns = "default" }
if groupPrefix != "" {
switch kind {
case "Deployment", "StatefulSet", "DaemonSet", "ReplicaSet":
return groupPrefix + "/namespaces/" + ns + "/" + resource + "/" + name, true
case "HorizontalPodAutoscaler":
return groupPrefix + "/namespaces/" + ns + "/" + strings.ToLower(kind) + "s/" + name, true
case "Ingress":
return groupPrefix + "/namespaces/" + ns + "/" + resource + "/" + name, true
default:
return groupPrefix + "/namespaces/" + ns + "/" + resource + "/" + name, true
}
}
// Core v1 API
coreResources := map[string]string{
"Pod": "pods", "Service": "services", "ConfigMap": "configmaps",
"Secret": "secrets", "PersistentVolumeClaim": "persistentvolumeclaims",
}
if r, ok := coreResources[kind]; ok {
return "/api/v1/namespaces/" + ns + "/" + r + "/" + name, true
}
return "/api/v1/namespaces/" + ns + "/" + resource + "/" + name, true
}
// ============================================================================
// ReplicaSet — 用于 Deployment 回滚 / 历史版本查询 // Logan.Li
// ============================================================================
// ReplicaSetList response
type ReplicaSetList struct {
Items []ReplicaSetItem `json:"items"`
}
// ReplicaSetItem 表示一个 ReplicaSet(含完整 spec/status/metadata,用于回滚取 template)
type ReplicaSetItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec ReplicaSetSpec `json:"spec"`
Status ReplicaSetStatus `json:"status"`
}
// ReplicaSetSpec 与 DeploymentSpec 结构一致,关键是 template
type ReplicaSetSpec struct {
Replicas int `json:"replicas"`
Template ReplicaSetTemplate `json:"template"`
}
// ReplicaSetTemplate pod 模板
type ReplicaSetTemplate struct {
Metadata ObjectMeta `json:"metadata"`
Spec PodSpec `json:"spec"`
}
// ReplicaSetStatus 副本状态
type ReplicaSetStatus struct {
Replicas int `json:"replicas"`
AvailableReplicas int `json:"availableReplicas"`
ReadyReplicas int `json:"readyReplicas"`
}
// OwnerReference 资源归属引用
type OwnerReference struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
// ReplicaSetOwnerMeta 在 ListReplicaSets 时额外携带 ownerReferences
type ReplicaSetOwnerMeta struct {
Metadata 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"`
OwnerReferences []OwnerReference `json:"ownerReferences"`
} `json:"metadata"`
Spec struct {
Replicas int `json:"replicas"`
Template ReplicaSetTemplate `json:"template"`
} `json:"spec"`
Status ReplicaSetStatus `json:"status"`
}
// ListReplicaSetsRaw 列出 namespace 下的 ReplicaSet(含 ownerReferences 原始字段)
func (c *Client) ListReplicaSetsRaw(namespace string) ([]ReplicaSetOwnerMeta, error) {
var result struct {
Items []ReplicaSetOwnerMeta `json:"items"`
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets", namespace)
if err := c.get(path, &result); err != nil {
return nil, err
}
return result.Items, nil
}
// GetReplicaSet 获取单个 ReplicaSet 的完整 JSON(map 形式,便于取 template 直接 patch)
func (c *Client) GetReplicaSet(namespace, name string) (map[string]interface{}, error) {
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", namespace, name)
var result map[string]interface{}
if err := c.get(path, &result); err != nil {
return nil, err
}
return result, nil
}
// PatchDeploymentSpecTemplate 用旧 ReplicaSet 的 template 回滚 Deployment
// 通过 merge-patch 将 deployment.spec.template 整体替换为 rsTemplate
func (c *Client) PatchDeploymentSpecTemplate(namespace, deploymentName string, rsTemplate map[string]interface{}) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"template": rsTemplate,
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, deploymentName)
return c.patch(path, body, nil)
}
// ============================================================================
// Pod 删除 / 高级日志参数 // Logan.Li
// ============================================================================
// DeletePod 删除指定 namespace 下的 Pod
func (c *Client) DeletePod(namespace, name string) error {
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, name)
return c.delete(path)
}
// LogOptions 日志查询高级参数(container/sinceTime/previous/tailLines)
type LogOptions struct {
Container string
SinceTime string // RFC3339 格式,仅返回此时间之后的日志
Previous bool // 返回已终止容器(previous instance)的日志
TailLines int // 末尾 N 行,0 表示不限制
}
// buildLogPath 构造 K8s pod log API 的完整查询路径
func (c *Client) buildLogPath(namespace, podName string, opts LogOptions, follow bool) string {
q := []string{}
if opts.Container != "" {
q = append(q, "container="+opts.Container)
}
if opts.TailLines > 0 {
q = append(q, fmt.Sprintf("tailLines=%d", opts.TailLines))
}
if opts.SinceTime != "" {
q = append(q, "sinceTime="+opts.SinceTime)
}
if opts.Previous {
q = append(q, "previous=true")
}
if follow {
q = append(q, "follow=true")
}
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", namespace, podName)
if len(q) > 0 {
path += "?" + strings.Join(q, "&")
}
return path
}
// GetPodLogsWithOptions 支持高级参数的日志查询(非流式)
func (c *Client) GetPodLogsWithOptions(namespace, podName string, opts LogOptions) (string, error) {
url := c.BaseURL + c.buildLogPath(namespace, podName, opts, false)
resp, err := c.HTTPClient.Get(url)
if err != nil {
return "", fmt.Errorf("GET logs: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode >= 400 {
return "", fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
}
return string(body), nil
}
// GetPodLogsStreamWithOptions 支持高级参数的日志流式查询(follow=true)
func (c *Client) GetPodLogsStreamWithOptions(namespace, podName string, opts LogOptions) (io.ReadCloser, error) {
url := c.BaseURL + c.buildLogPath(namespace, podName, opts, true)
resp, err := c.HTTPClient.Get(url)
if err != nil {
return nil, fmt.Errorf("GET logs stream: %w", err)
}
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
}
return resp.Body, nil
}