Files
aiops/backend/pkg/kubernetesclient/client.go
T
attacker a3cd0d3910 feat: PTY kubectl终端 + 侧边栏对齐修复 + 构建缓存清理
**kubectl WebShell 修复:**
- 引入 github.com/creack/pty 库实现真正的伪终端
- 替换 bash --noediting + TERM=dumb 方案为 PTY-backed bash
- 支持 xterm-256color、窗口自适应、readline 交互
- 修复键盘输入无响应问题

**侧边栏优化:**
- 独立菜单项高度从 py-1.5 改为 h-8,与父菜单对齐
- 独立菜单图标从 h-3 w-3 改为 h-3.5 w-3.5,统一 14px

**构建优化:**
- run.sh docker up 增加 docker image prune -f 清理悬空镜像
- 每次部署自动清理 builder cache + dangling images

**认证修复(历史):**
- Token 存储:Zustand + localStorage 双写,http.ts 双源 fallback
- YamlViewer/DeploymentDetail token 来源统一为 Zustand store
2026-06-19 11:22:45 +00:00

1319 lines
37 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
}
// 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 (non-interactive command) ---
// 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
func (c *Client) ExecInPod(namespace, podName, container string, command []string) (*ExecResult, error) {
cmdStr := ""
for i, c := range command {
if i > 0 {
cmdStr += "&command="
}
cmdStr += c
}
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/exec?%s&stdout=true&stderr=true",
namespace, podName, cmdStr)
if container != "" {
path += "&container=" + container
}
req, err := http.NewRequest("POST", c.BaseURL+path, nil)
if err != nil {
return nil, err
}
// SPDY is the default for exec, but modern K8s also supports WebSocket.
// Fall back to SPDY upgrade attempt.
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "SPDY/3.1")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("exec %s: %w", path, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("exec error %d: %s", resp.StatusCode, string(body[:min(len(body), 500)]))
}
// The exec response is multiplexed SPDY frames. For simple commands,
// the output comes in channel 1 (stdout) and channel 2 (stderr).
// Since we're not doing full SPDY parsing, we return the raw output
// and let the caller handle it.
return &ExecResult{
Stdout: string(body),
Stderr: "",
}, 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
}