feat(k8s): M3 Pod文件上传 + ExecInPod WebSocket协议重写

M3: Pod 文件上传
- 后端: POST .../pods/:pod/files/upload (multipart, base64分块exec写入)
- 前端: PodFileBrowser 增加上传按钮 + FormData API
- 路由: 新增 Kuboard (:clusterCode) + 旧 (:id) 两套路由

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

修复: BusyBox ls 解析
- BusyBox 不支持 --time-style 参数,默认输出 Mon DD HH:MM 格式
- 解析器改为自动检测 GNU vs BusyBox 日期格式
This commit is contained in:
attacker committed 2026-06-23 02:21:29 +08:00
1 parent 69df073d42
commit 9ab7572b8e
11 files changed
+389 -126

No files matched your search

+31 -2
View File
@@ -249,12 +249,13 @@ export const kubernetesApi = {
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/pods/${podName}/exec`, { command, container });
},
// 🆕 Pod 级 exec WebSocket URL (Logan <admin@attacker.club>)
// ws://host/ws/kubernetes-exec/:clusterId/namespaces/:ns/pods/:pod?container=&token=
// ws://host/ws/kubernetes-exec/:clusterId/namespaces/:ns/pods/:pod?container=&command=&token=
buildPodExecWsUrl: (
clusterId: number | string,
namespace: string,
podName: string,
container?: string
container?: string,
command?: string
) => {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const baseUrl = import.meta.env.VITE_API_BASE_URL || `${proto}//${window.location.host}`;
@@ -262,6 +263,7 @@ export const kubernetesApi = {
const token = useUserStore.getState().token || '';
const params = new URLSearchParams();
if (container) params.set('container', container);
if (command) params.set('command', command);
if (token) params.set('token', token);
const qs = params.toString();
return `${wsBase}/ws/kubernetes-exec/${clusterId}/namespaces/${namespace}/pods/${podName}${qs ? `?${qs}` : ''}`;
@@ -309,6 +311,33 @@ export const kubernetesApi = {
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.text();
},
// 上传文件到 Pod 容器内指定目录
uploadPodFile: async (
clusterCode: string,
namespace: string,
podName: string,
targetPath: string,
file: File,
container?: string,
) => {
const formData = new FormData();
formData.append('file', file);
formData.append('path', targetPath);
if (container) formData.append('container', container);
const token = useUserStore.getState().token || localStorage.getItem('token') || '';
const baseUrl = import.meta.env.VITE_API_BASE_URL || `${window.location.protocol}//${window.location.host}`;
const resp = await fetch(
`${baseUrl}/api/v1/kubernetes/${clusterCode}/namespace/${namespace}/pods/${podName}/files/upload`,
{
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData,
},
);
const json = await resp.json();
if (!resp.ok || json.code !== 200) throw new Error(json.message || `HTTP ${resp.status}`);
return json.data;
},
scaleDeployment: async (clusterCode: string, namespace: string, name: string, replicas: number) => {
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/scale`, { replicas });
},