fix: 使用 HTTP API 代理方式调用 K8S
- 重写 k8s-client.ts,改用 HTTP API 代理方式而非 SDK 直连 - @kubernetes/client-node 仅用于类型定义 - 修复部署功能,正确调用 K8S API 代理服务 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
/**
|
||||
* K8S 客户端 - 通过 HTTP API 代理调用 K8S
|
||||
* @kubernetes/client-node 仅用于类型定义
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
|
||||
// 导出类型
|
||||
export type V1Deployment = k8s.V1Deployment;
|
||||
export type V1DeploymentList = k8s.V1DeploymentList;
|
||||
export type V1Ingress = k8s.V1Ingress;
|
||||
@@ -8,131 +15,303 @@ export type V1Pod = k8s.V1Pod;
|
||||
export type V1Service = k8s.V1Service;
|
||||
export type V1ServicePort = k8s.V1ServicePort;
|
||||
|
||||
// 构建 URL 路径
|
||||
const buildUrl = (baseUrl: string, path: string, params?: Record<string, string>) => {
|
||||
let url = baseUrl.replace(/\/+$/, '') + '/api/v1/kubernetes/' + path.replace(/^\/+/, '');
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
url = url.replace(`:${key}`, value);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
// 构建查询字符串
|
||||
const buildQuery = (query?: Record<string, string | number | boolean | undefined>) => {
|
||||
if (!query) return '';
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== undefined) {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
return params.toString() ? '?' + params.toString() : '';
|
||||
};
|
||||
|
||||
// HTTP 请求封装
|
||||
const request = async (
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
method: string,
|
||||
path: string,
|
||||
args?: {
|
||||
param?: Record<string, string>;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
json?: unknown;
|
||||
}
|
||||
) => {
|
||||
let url = buildUrl(baseUrl, path, args?.param);
|
||||
url += buildQuery(args?.query);
|
||||
|
||||
const response = await axios.request({
|
||||
url,
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: args?.json ? JSON.stringify(args.json) : undefined,
|
||||
validateStatus: () => true, // 不抛出 HTTP 错误
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* K8S 客户端类
|
||||
*/
|
||||
export class K8sClient {
|
||||
private kubeConfig: k8s.KubeConfig;
|
||||
private baseUrl: string;
|
||||
private token: string;
|
||||
|
||||
constructor(baseUrl: string, token: string) {
|
||||
this.kubeConfig = new k8s.KubeConfig();
|
||||
this.kubeConfig.loadFromOptions({
|
||||
clusters: [{ name: 'cluster', server: baseUrl, skipTLSVerify: true }],
|
||||
users: [{ name: 'user', authProvider: { name: 'token', config: { token } } }],
|
||||
contexts: [{ name: 'ctx', cluster: 'cluster', user: 'user' }],
|
||||
currentContext: 'ctx',
|
||||
});
|
||||
this.baseUrl = baseUrl;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
private getCoreV1Api() {
|
||||
return this.kubeConfig.makeApiClient(k8s.CoreV1Api);
|
||||
}
|
||||
// ========== Pod 操作 ==========
|
||||
|
||||
private getAppsV1Api() {
|
||||
return this.kubeConfig.makeApiClient(k8s.AppsV1Api);
|
||||
}
|
||||
|
||||
private getNetworkingV1Api() {
|
||||
return this.kubeConfig.makeApiClient(k8s.NetworkingV1Api);
|
||||
}
|
||||
|
||||
async getPods(args: { param: { namespace?: string }; query?: { labelSelector?: string; fieldSelector?: string; limit?: number; continue?: string } }): Promise<{ pods: k8s.V1Pod[] }> {
|
||||
async getPods(args: {
|
||||
param: { namespace?: string };
|
||||
query?: { labelSelector?: string; fieldSelector?: string; limit?: number; continue?: string };
|
||||
}): Promise<{ pods: V1Pod[] }> {
|
||||
try {
|
||||
const ns = args.param.namespace || 'default';
|
||||
const res = await this.getCoreV1Api().listNamespacedPod({ namespace: ns, labelSelector: args.query?.labelSelector, fieldSelector: args.query?.fieldSelector, limit: args.query?.limit, _continue: args.query?.continue });
|
||||
return { pods: res.items || [] };
|
||||
} catch { return { pods: [] }; }
|
||||
const namespace = args.param.namespace || 'default';
|
||||
const res = await request(this.baseUrl, this.token, 'GET', 'pods/:namespace', {
|
||||
param: { namespace },
|
||||
query: args.query,
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.data?.message || '获取 Pods 失败');
|
||||
}
|
||||
return { pods: res.data?.pods || [] };
|
||||
} catch (error) {
|
||||
console.error('获取 Pods 失败:', error);
|
||||
return { pods: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async getPod(args: { param: { namespace: string; podName: string } }): Promise<k8s.V1Pod> {
|
||||
return this.getCoreV1Api().readNamespacedPod({ name: args.param.podName, namespace: args.param.namespace });
|
||||
async getPod(args: { param: { namespace: string; podName: string } }): Promise<V1Pod> {
|
||||
const res = await request(this.baseUrl, this.token, 'GET', 'pods/:namespace/:podName', {
|
||||
param: { namespace: args.param.namespace, podName: args.param.podName },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.data?.message || '获取 Pod 失败');
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async createPod(args: { json: { namespace: string; pod: V1Pod } }): Promise<{ success: boolean; message: string; pod?: V1Pod }> {
|
||||
try {
|
||||
const res = await this.getCoreV1Api().createNamespacedPod({ namespace: args.json.namespace, body: args.json.pod });
|
||||
return { success: true, message: 'Pod 创建成功', pod: res };
|
||||
} catch (e) { return { success: false, message: `创建 Pod 失败: ${e}` }; }
|
||||
const res = await request(this.baseUrl, this.token, 'POST', 'pods', { json: args.json });
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '创建 Pod 失败' };
|
||||
}
|
||||
return { success: true, message: 'Pod 创建成功', pod: res.data };
|
||||
} catch (error) {
|
||||
return { success: false, message: `创建 Pod 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async deletePod(args: { param: { namespace: string; podName: string } }): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.getCoreV1Api().deleteNamespacedPod({ name: args.param.podName, namespace: args.param.namespace });
|
||||
const res = await request(this.baseUrl, this.token, 'DELETE', 'pods/:namespace/:podName', {
|
||||
param: { namespace: args.param.namespace, podName: args.param.podName },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '删除 Pod 失败' };
|
||||
}
|
||||
return { success: true, message: 'Pod 删除成功' };
|
||||
} catch (e) { return { success: false, message: `删除 Pod 失败: ${e}` }; }
|
||||
} catch (error) {
|
||||
return { success: false, message: `删除 Pod 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async getDeployments(args: { param: { namespace: string }; query?: { limit?: number; continue?: string } }): Promise<V1DeploymentList> {
|
||||
try {
|
||||
const res = await this.getAppsV1Api().listNamespacedDeployment({ namespace: args.param.namespace, limit: args.query?.limit, _continue: args.query?.continue });
|
||||
return { items: res.items || [], metadata: res.metadata || {} };
|
||||
} catch { return { items: [], metadata: {} }; }
|
||||
}
|
||||
// ========== Deployment 操作 ==========
|
||||
|
||||
async getDeployment(args: { param: { namespace: string; name: string } }): Promise<V1Deployment> {
|
||||
return this.getAppsV1Api().readNamespacedDeployment({ name: args.param.name, namespace: args.param.namespace });
|
||||
const res = await request(this.baseUrl, this.token, 'GET', 'deployments/:namespace/:name', {
|
||||
param: { namespace: args.param.namespace, name: args.param.name },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
const error = new Error(res.data?.message || `Deployment ${args.param.name} not found`);
|
||||
(error as any).code = res.status;
|
||||
throw error;
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async createDeployment(args: { json: { namespace: string; deployment: V1Deployment } }): Promise<{ success: boolean; message: string; deployment?: V1Deployment }> {
|
||||
async getDeployments(args: {
|
||||
param: { namespace: string };
|
||||
query?: { limit?: number; continue?: string };
|
||||
}): Promise<V1DeploymentList> {
|
||||
try {
|
||||
const res = await this.getAppsV1Api().createNamespacedDeployment({ namespace: args.json.namespace, body: args.json.deployment });
|
||||
return { success: true, message: 'Deployment 创建成功', deployment: res };
|
||||
} catch (e) { return { success: false, message: `创建 Deployment 失败: ${e}` }; }
|
||||
const res = await request(this.baseUrl, this.token, 'GET', 'deployments/:namespace', {
|
||||
param: { namespace: args.param.namespace },
|
||||
query: args.query,
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return { items: [], metadata: {} };
|
||||
}
|
||||
return { items: res.data?.items || [], metadata: res.data?.metadata || {} };
|
||||
} catch (error) {
|
||||
console.error('获取 Deployments 失败:', error);
|
||||
return { items: [], metadata: {} };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDeployment(args: { param: { namespace: string; name: string } }): Promise<{ success: boolean; message: string }> {
|
||||
async createDeployment(args: {
|
||||
json: { namespace: string; deployment: V1Deployment };
|
||||
}): Promise<{ success: boolean; message: string; deployment?: V1Deployment }> {
|
||||
try {
|
||||
await this.getAppsV1Api().deleteNamespacedDeployment({ name: args.param.name, namespace: args.param.namespace });
|
||||
const res = await request(this.baseUrl, this.token, 'POST', 'deployments', { json: args.json });
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '创建 Deployment 失败' };
|
||||
}
|
||||
return { success: true, message: 'Deployment 创建成功', deployment: res.data };
|
||||
} catch (error) {
|
||||
return { success: false, message: `创建 Deployment 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDeployment(args: {
|
||||
param: { namespace: string; name: string };
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const res = await request(this.baseUrl, this.token, 'DELETE', 'deployments/:namespace/:name', {
|
||||
param: { namespace: args.param.namespace, name: args.param.name },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '删除 Deployment 失败' };
|
||||
}
|
||||
return { success: true, message: 'Deployment 删除成功' };
|
||||
} catch (e) { return { success: false, message: `删除 Deployment 失败: ${e}` }; }
|
||||
} catch (error) {
|
||||
return { success: false, message: `删除 Deployment 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async restartDeployment(args: { param: { namespace: string; name: string } }): Promise<{ success: boolean; message: string }> {
|
||||
async restartDeployment(args: {
|
||||
param: { namespace: string; name: string };
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const pods = await this.getPods({ param: { namespace: args.param.namespace }, query: { labelSelector: `app=${args.param.name}` } });
|
||||
const pods = await this.getPods({
|
||||
param: { namespace: args.param.namespace },
|
||||
query: { labelSelector: `app=${args.param.name}` },
|
||||
});
|
||||
for (const pod of pods.pods) {
|
||||
if (pod.metadata?.name) await this.deletePod({ param: { namespace: args.param.namespace, podName: pod.metadata.name } });
|
||||
if (pod.metadata?.name) {
|
||||
await this.deletePod({ param: { namespace: args.param.namespace, podName: pod.metadata.name } });
|
||||
}
|
||||
}
|
||||
return { success: true, message: 'Deployment 重启成功' };
|
||||
} catch (e) { return { success: false, message: `重启 Deployment 失败: ${e}` }; }
|
||||
} catch (error) {
|
||||
return { success: false, message: `重启 Deployment 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async createService(args: { json: { namespace: string; service: V1Service } }): Promise<{ success: boolean; message: string; service?: V1Service }> {
|
||||
// ========== Service 操作 ==========
|
||||
|
||||
async createService(args: {
|
||||
json: { namespace: string; service: V1Service };
|
||||
}): Promise<{ success: boolean; message: string; service?: V1Service }> {
|
||||
try {
|
||||
const res = await this.getCoreV1Api().createNamespacedService({ namespace: args.json.namespace, body: args.json.service });
|
||||
return { success: true, message: 'Service 创建成功', service: res };
|
||||
} catch (e) { return { success: false, message: `创建 Service 失败: ${e}` }; }
|
||||
const res = await request(this.baseUrl, this.token, 'POST', 'services', { json: args.json });
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '创建 Service 失败' };
|
||||
}
|
||||
return { success: true, message: 'Service 创建成功', service: res.data };
|
||||
} catch (error) {
|
||||
return { success: false, message: `创建 Service 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteService(args: { param: { namespace: string; serviceName: string } }): Promise<{ success: boolean; message: string }> {
|
||||
async deleteService(args: {
|
||||
param: { namespace: string; serviceName: string };
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.getCoreV1Api().deleteNamespacedService({ name: args.param.serviceName, namespace: args.param.namespace });
|
||||
const res = await request(this.baseUrl, this.token, 'DELETE', 'services/:namespace/:serviceName', {
|
||||
param: { namespace: args.param.namespace, serviceName: args.param.serviceName },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '删除 Service 失败' };
|
||||
}
|
||||
return { success: true, message: 'Service 删除成功' };
|
||||
} catch (e) { return { success: false, message: `删除 Service 失败: ${e}` }; }
|
||||
} catch (error) {
|
||||
return { success: false, message: `删除 Service 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async getService(args: { param: { namespace: string; serviceName: string } }): Promise<{ service?: V1Service }> {
|
||||
async getService(args: {
|
||||
param: { namespace: string; serviceName: string };
|
||||
}): Promise<{ service?: V1Service }> {
|
||||
try {
|
||||
const res = await this.getCoreV1Api().readNamespacedService({ name: args.param.serviceName, namespace: args.param.namespace });
|
||||
return { service: res };
|
||||
} catch { return {}; }
|
||||
const res = await request(this.baseUrl, this.token, 'GET', 'services/:namespace/:serviceName', {
|
||||
param: { namespace: args.param.namespace, serviceName: args.param.serviceName },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return {};
|
||||
}
|
||||
return { service: res.data };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async createIngress(args: { json: { namespace: string; ingress: V1Ingress } }): Promise<{ success: boolean; message: string; ingress?: V1Ingress }> {
|
||||
// ========== Ingress 操作 ==========
|
||||
|
||||
async createIngress(args: {
|
||||
json: { namespace: string; ingress: V1Ingress };
|
||||
}): Promise<{ success: boolean; message: string; ingress?: V1Ingress }> {
|
||||
try {
|
||||
const res = await this.getNetworkingV1Api().createNamespacedIngress({ namespace: args.json.namespace, body: args.json.ingress });
|
||||
return { success: true, message: 'Ingress 创建成功', ingress: res };
|
||||
} catch (e) { return { success: false, message: `创建 Ingress 失败: ${e}` }; }
|
||||
const res = await request(this.baseUrl, this.token, 'POST', 'ingresses', { json: args.json });
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '创建 Ingress 失败' };
|
||||
}
|
||||
return { success: true, message: 'Ingress 创建成功', ingress: res.data };
|
||||
} catch (error) {
|
||||
return { success: false, message: `创建 Ingress 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteIngress(args: { param: { namespace: string; ingressName: string } }): Promise<{ success: boolean; message: string }> {
|
||||
async deleteIngress(args: {
|
||||
param: { namespace: string; ingressName: string };
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.getNetworkingV1Api().deleteNamespacedIngress({ name: args.param.ingressName, namespace: args.param.namespace });
|
||||
const res = await request(this.baseUrl, this.token, 'DELETE', 'ingresses/:namespace/:ingressName', {
|
||||
param: { namespace: args.param.namespace, ingressName: args.param.ingressName },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return { success: false, message: res.data?.message || '删除 Ingress 失败' };
|
||||
}
|
||||
return { success: true, message: 'Ingress 删除成功' };
|
||||
} catch (e) { return { success: false, message: `删除 Ingress 失败: ${e}` }; }
|
||||
} catch (error) {
|
||||
return { success: false, message: `删除 Ingress 失败: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
async getIngress(args: { param: { namespace: string; ingressName: string } }): Promise<{ ingress?: V1Ingress }> {
|
||||
async getIngress(args: {
|
||||
param: { namespace: string; ingressName: string };
|
||||
}): Promise<{ ingress?: V1Ingress }> {
|
||||
try {
|
||||
const res = await this.getNetworkingV1Api().readNamespacedIngress({ name: args.param.ingressName, namespace: args.param.namespace });
|
||||
return { ingress: res };
|
||||
} catch { return {}; }
|
||||
const res = await request(this.baseUrl, this.token, 'GET', 'ingresses/:namespace/:ingressName', {
|
||||
param: { namespace: args.param.namespace, ingressName: args.param.ingressName },
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
return {};
|
||||
}
|
||||
return { ingress: res.data };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user