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
This commit is contained in:
attacker committed 2026-06-19 11:22:45 +00:00
1 parent cac1414917
commit a3cd0d3910
29 files changed
+2954 -180

No files matched your search

+372 -1
View File
@@ -151,6 +151,37 @@ func (c *Client) get(path string, result interface{}) error {
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
@@ -212,7 +243,14 @@ type PodSpec struct {
}
type Container struct {
Image string `json:"image"`
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 {
@@ -945,3 +983,336 @@ func (c *Client) ListSecrets(namespace string) (*SecretList, error) {
}
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
}