Files
2026-07-09 14:16:56 +08:00

2075 lines
59 KiB
Go
Raw Permalink 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
}
// NewClientFromToken 支持直接用 apiServer + token 创建客户端(对应 config_type=0 场景)
// 生成一个最小 kubeconfig 复用 NewClient 解析逻辑 // Logan.Li
func NewClientFromToken(apiServer, token string) (*Client, error) {
if apiServer == "" || token == "" {
return nil, fmt.Errorf("apiServer and token are required for token auth")
}
server := strings.TrimRight(apiServer, "/")
// 为 token 模式生成最小化 kubeconfig(insecure-skip 便于测试集群;生产建议用完整 kubeconfig 携带 CA)
kubeYAML := fmt.Sprintf(`apiVersion: v1
clusters:
- cluster:
server: %s
insecure-skip-tls-verify: true
name: cluster
contexts:
- context:
cluster: cluster
user: user
name: ctx
current-context: ctx
kind: Config
preferences: {}
users:
- name: user
user:
token: %s
`, server, token)
return NewClient(kubeYAML)
}
// get performs a GET request to the K8s API
func (c *Client) get(path string, result interface{}) error {
req, err := http.NewRequest("GET", c.BaseURL+path, nil)
if err != nil {
return fmt.Errorf("GET %s: %w", path, err)
}
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
resp, err := c.HTTPClient.Do(req)
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)
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
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)
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
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"`
Taints []NodeTaint `json:"taints,omitempty"`
}
type NodeTaint struct {
Key string `json:"key"`
Value string `json:"value"`
Effect string `json:"effect"`
TimeAdded string `json:"timeAdded,omitempty"`
}
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"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
}
type NodeInfo struct {
KubeletVersion string `json:"kubeletVersion"`
OSImage string `json:"osImage"`
KernelVersion string `json:"kernelVersion"`
ContainerRuntimeVersion string `json:"containerRuntimeVersion"`
Architecture string `json:"architecture"`
OperatingSystem string `json:"operatingSystem"`
}
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"`
Conditions []PodCondition `json:"conditions,omitempty"`
}
// PodCondition for pod status conditions (Scheduled, Initialized, Ready etc)
type PodCondition struct {
Type string `json:"type"`
Status string `json:"status"`
LastTransitionTime string `json:"lastTransitionTime,omitempty"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
}
type ContainerStatus struct {
Name string `json:"name"`
RestartCount int `json:"restartCount"`
Ready bool `json:"ready"`
State ContainerStatusState `json:"state,omitempty"`
LastState ContainerStatusState `json:"lastState,omitempty"`
}
// ContainerStatusState 解析容器等待/终止状态,用于 AI 诊断 ImagePullBackOff、OOMKilled 等原因。 // Logan.Li
type ContainerStatusState struct {
Waiting *ContainerStateWaiting `json:"waiting,omitempty"`
Terminated *ContainerStateTerminated `json:"terminated,omitempty"`
}
// ContainerStateWaiting 容器等待状态。 // Logan.Li
type ContainerStateWaiting struct {
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
}
// ContainerStateTerminated 容器终止状态。 // Logan.Li
type ContainerStateTerminated struct {
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
ExitCode int `json:"exitCode,omitempty"`
}
// 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"`
Selector *LabelSelector `json:"selector,omitempty"` // matchLabels — 前端 Service 关联用
Template DeploymentTemplate `json:"template"`
Paused bool `json:"paused"`
}
// LabelSelector K8s 标签选择器
type LabelSelector struct {
MatchLabels map[string]string `json:"matchLabels,omitempty"`
}
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"`
HTTP *IngressHTTP `json:"http,omitempty"`
}
type IngressHTTP struct {
Paths []IngressPath `json:"paths"`
}
type IngressPath struct {
Path string `json:"path"`
PathType string `json:"pathType"`
Backend IngressBackend `json:"backend"`
}
type IngressBackend struct {
Service IngressBackendService `json:"service"`
}
type IngressBackendService struct {
Name string `json:"name"`
Port IngressServicePort `json:"port"`
}
type IngressServicePort struct {
Number int `json:"number,omitempty"`
Name string `json:"name,omitempty"`
}
// IngressClassList 用于 IngressClass 下拉选择
type IngressClassList struct {
Items []IngressClassItem `json:"items"`
}
type IngressClassItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec struct {
Controller string `json:"controller"`
} `json:"spec"`
}
// 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"`
Errors []string `json:"errors,omitempty"` // 各资源同步错误,便于排查 // Logan.Li
}
// 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
}
// GetNode fetches a single node by name
func (c *Client) GetNode(name string) (*NodeItem, error) {
var result NodeItem
if err := c.get("/api/v1/nodes/"+name, &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
}
// GetDeployment 获取单个 Deployment // Logan.Li
func (c *Client) GetDeployment(namespace, name string) (*DeploymentItem, error) {
var result DeploymentItem
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, name)
if err := c.get(path, &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
}
// ListIngressClasses fetches all IngressClass (cluster-scoped) for dropdown selection in Ingress visual editor
func (c *Client) ListIngressClasses() (*IngressClassList, error) {
var result IngressClassList
if err := c.get("/apis/networking.k8s.io/v1/ingressclasses", &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")
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
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")
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
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
}
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
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
}
req, err := http.NewRequest("GET", c.BaseURL+path, nil)
if err != nil {
return "", fmt.Errorf("GET logs: %w", err)
}
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
resp, err := c.HTTPClient.Do(req)
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
}
req, err := http.NewRequest("GET", c.BaseURL+path, nil)
if err != nil {
return nil, fmt.Errorf("GET logs stream: %w", err)
}
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
resp, err := c.HTTPClient.Do(req)
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 {
image = strings.TrimSpace(image)
if image == "" {
return fmt.Errorf("image 不能为空")
}
containerName = strings.TrimSpace(containerName)
if containerName == "" {
dep, err := c.GetDeployment(namespace, name)
if err != nil {
return fmt.Errorf("获取 Deployment 失败: %w", err)
}
if len(dep.Spec.Template.Spec.Containers) == 0 {
return fmt.Errorf("Deployment 无容器定义")
}
containerName = dep.Spec.Template.Spec.Containers[0].Name
}
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)
}
// PauseDeployment pauses rollout of a Deployment by setting spec.paused=true.
// A paused Deployment will not process new rollouts — useful for debugging or
// staged updates. Resume with ResumeDeployment.
func (c *Client) PauseDeployment(namespace, name string) error {
body := map[string]interface{}{
"spec": map[string]interface{}{"paused": true},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, name)
return c.patch(path, body, nil)
}
// ResumeDeployment resumes rollout of a paused Deployment by setting spec.paused=false.
func (c *Client) ResumeDeployment(namespace, name string) error {
body := map[string]interface{}{
"spec": map[string]interface{}{"paused": false},
}
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{"v4.channel.k8s.io", "v3.channel.k8s.io", "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
}
// NamespaceItem holds full namespace metadata
type NamespaceItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec NamespaceSpec `json:"spec"`
Status NamespaceStatus `json:"status"`
}
// NamespaceSpec holds namespace spec
type NamespaceSpec struct {
Finalizers []string `json:"finalizers"`
}
// NamespaceStatus holds namespace phase
type NamespaceStatus struct {
Phase string `json:"phase"`
}
// NamespaceList wraps a list of NamespaceItem
type NamespaceList struct {
Items []NamespaceItem `json:"items"`
}
// ListNamespacesDetailed returns all namespaces with full metadata
func (c *Client) ListNamespacesDetailed() (*NamespaceList, error) {
var result NamespaceList
if err := c.get("/api/v1/namespaces", &result); err != nil {
return nil, err
}
return &result, nil
}
// GetNamespace returns a single namespace by name
func (c *Client) GetNamespace(name string) (*NamespaceItem, error) {
var result NamespaceItem
path := fmt.Sprintf("/api/v1/namespaces/%s", name)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result, nil
}
// CreateNamespace creates a new namespace
func (c *Client) CreateNamespace(name string, labels map[string]string) (*NamespaceItem, error) {
body := map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{
"name": name,
"labels": labels,
},
}
var result NamespaceItem
if err := c.post("/api/v1/namespaces", body, &result); err != nil {
return nil, err
}
return &result, nil
}
// DeleteNamespace deletes a namespace by name
func (c *Client) DeleteNamespace(name string) error {
path := fmt.Sprintf("/api/v1/namespaces/%s", name)
return c.delete(path)
}
// ResourceQuotaItem represents a K8s ResourceQuota
type ResourceQuotaItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec ResourceQuotaSpec `json:"spec"`
Status ResourceQuotaStatus `json:"status"`
}
// ResourceQuotaSpec holds hard limits
type ResourceQuotaSpec struct {
Hard map[string]string `json:"hard"`
Scopes []string `json:"scopes,omitempty"`
}
// ResourceQuotaStatus holds used/hard counts
type ResourceQuotaStatus struct {
Hard map[string]string `json:"hard"`
Used map[string]string `json:"used"`
}
// ListResourceQuotas returns ResourceQuotas in a namespace, or all namespaces if ns is empty
func (c *Client) ListResourceQuotas(namespace string) ([]ResourceQuotaItem, error) {
var result struct {
Items []ResourceQuotaItem `json:"items"`
}
var path string
if namespace == "" {
path = "/api/v1/resourcequotas"
} else {
path = fmt.Sprintf("/api/v1/namespaces/%s/resourcequotas", namespace)
}
if err := c.get(path, &result); err != nil {
return nil, err
}
return result.Items, nil
}
// LimitRangeItem represents a K8s LimitRange
type LimitRangeItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec LimitRangeSpec `json:"spec"`
}
// LimitRangeSpec holds limit items
type LimitRangeSpec struct {
Limits []map[string]interface{} `json:"limits"`
}
// ListLimitRanges returns LimitRanges in a namespace, or all namespaces if ns is empty
func (c *Client) ListLimitRanges(namespace string) ([]LimitRangeItem, error) {
var result struct {
Items []LimitRangeItem `json:"items"`
}
var path string
if namespace == "" {
path = "/api/v1/limitranges"
} else {
path = fmt.Sprintf("/api/v1/namespaces/%s/limitranges", namespace)
}
if err := c.get(path, &result); err != nil {
return nil, err
}
return result.Items, 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
}
// ScaleStatefulSet scales a StatefulSet to the given replica count
func (c *Client) ScaleStatefulSet(namespace, name string, replicas int) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"replicas": replicas,
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s/scale", namespace, name)
return c.patch(path, body, nil)
}
// RestartStatefulSet triggers a rolling restart of a StatefulSet
func (c *Client) RestartStatefulSet(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/statefulsets/%s", namespace, name)
return c.patch(path, body, nil)
}
// RestartDaemonSet triggers a rolling restart of a DaemonSet
func (c *Client) RestartDaemonSet(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/daemonsets/%s", namespace, name)
return c.patch(path, body, nil)
}
// GetStatefulSet fetches a single StatefulSet by name
func (c *Client) GetStatefulSet(namespace, name string) (*StatefulSetItem, error) {
var result struct {
StatefulSetItem
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", namespace, name)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result.StatefulSetItem, nil
}
// GetDaemonSet fetches a single DaemonSet by name
func (c *Client) GetDaemonSet(namespace, name string) (*DaemonSetItem, error) {
var result struct {
DaemonSetItem
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", namespace, name)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result.DaemonSetItem, 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 EventSource struct {
Component string `json:"component"`
Host string `json:"host"`
}
type EventItem struct {
Metadata ObjectMeta `json:"metadata"`
InvolvedObject InvolvedObject `json:"involvedObject"`
Type string `json:"type"`
Reason string `json:"reason"`
Message string `json:"message"`
Source EventSource `json:"source"`
ReportingComponent string `json:"reportingComponent"`
ReportingInstance string `json:"reportingInstance"`
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
}
// ============================================================================
// RBAC — ServiceAccount / Role / RoleBinding
// ============================================================================
// --- ServiceAccount ---
type ServiceAccountList struct {
Items []ServiceAccountItem `json:"items"`
}
type ServiceAccountItem struct {
Metadata ObjectMeta `json:"metadata"`
Spec struct {
ImagePullSecrets []struct {
Name string `json:"name"`
} `json:"imagePullSecrets"`
Secrets []struct {
Name string `json:"name"`
} `json:"secrets"`
} `json:"secrets"`
}
type ServiceAccountRaw struct {
Metadata ObjectMeta `json:"metadata"`
Secrets []struct {
Name string `json:"name"`
} `json:"secrets"`
ImagePullSecrets []struct {
Name string `json:"name"`
} `json:"imagePullSecrets"`
}
// ListServiceAccounts fetches ServiceAccounts in a namespace
func (c *Client) ListServiceAccounts(namespace string) ([]ServiceAccountRaw, error) {
var result struct {
Items []ServiceAccountRaw `json:"items"`
}
path := fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts", namespace)
if err := c.get(path, &result); err != nil {
return nil, err
}
return result.Items, nil
}
// --- Role ---
type RoleList struct {
Items []RoleItem `json:"items"`
}
type RoleItem struct {
Metadata ObjectMeta `json:"metadata"`
Rules []PolicyRule `json:"rules"`
}
type PolicyRule struct {
Verbs []string `json:"verbs"`
Resources []string `json:"resources"`
APIGroups []string `json:"apiGroups"`
}
// ListRoles fetches Roles in a namespace
func (c *Client) ListRoles(namespace string) (*RoleList, error) {
var result RoleList
path := fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles", namespace)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result, nil
}
// --- RoleBinding ---
type RoleBindingList struct {
Items []RoleBindingItem `json:"items"`
}
type RoleBindingItem struct {
Metadata ObjectMeta `json:"metadata"`
RoleRef RoleRef `json:"roleRef"`
Subjects []Subject `json:"subjects"`
}
type RoleRef struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
type Subject struct {
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace"`
}
// ListRoleBindings fetches RoleBindings in a namespace
func (c *Client) ListRoleBindings(namespace string) (*RoleBindingList, error) {
var result RoleBindingList
path := fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings", 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)
}
// DeleteResource deletes a K8s resource by apiPath (generic)
func (c *Client) DeleteResource(apiPath string) error {
return c.delete(apiPath)
}
// 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) {
u := c.BaseURL + c.buildLogPath(namespace, podName, opts, false)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return "", fmt.Errorf("GET logs: %w", err)
}
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
resp, err := c.HTTPClient.Do(req)
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) {
u := c.BaseURL + c.buildLogPath(namespace, podName, opts, true)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return nil, fmt.Errorf("GET logs stream: %w", err)
}
if c.BearerToken != "" {
req.Header.Set("Authorization", "Bearer "+c.BearerToken)
}
resp, err := c.HTTPClient.Do(req)
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
}