perf(plugin-volcengine): 支持火山引擎VKE部署插件

- 改进Kubernetes错误处理,添加RBAC权限不足的友好提示
- 添加集群ID验证和kubeconfig解码验证
- 优化Ingress不存在时的错误提示,显示可用选项
- 移除未使用的TargetCluster选项
- 修复kubeconfig请求中集群ID未验证的问题
This commit is contained in:
xiaojunnuo
2026-05-08 00:25:00 +08:00
parent 25ad1e6f86
commit b8a64a6b5b
2 changed files with 144 additions and 10 deletions
@@ -0,0 +1,98 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { VolcengineDeployToVKE } from "./plugin-deploy-to-vke.js";
describe("VolcengineDeployToVKE", () => {
it("uses a single-select cluster field", () => {
const clusterInput = (VolcengineDeployToVKE as any).define.input.clusterId;
assert.equal(clusterInput.component.multi, false);
assert.equal(clusterInput.component.mode, "default");
});
it("sends the configured string ClusterId", async () => {
const plugin = new VolcengineDeployToVKE();
plugin.clusterId = "cc1234567890123456789";
plugin.kubeconfigType = "Public";
plugin.logger = { info: () => undefined } as any;
let requestBody: any;
const kubeconfigId = await (plugin as any).createKubeconfig({
request: async (req: any) => {
requestBody = req.body;
return { Result: { Id: "kc-123" } };
}
});
assert.equal(kubeconfigId, "kc-123");
assert.equal(requestBody.ClusterId, "cc1234567890123456789");
});
it("decodes the base64 kubeconfig returned by VKE", async () => {
const plugin = new VolcengineDeployToVKE();
plugin.clusterId = "cc1234567890123456789";
plugin.kubeconfigType = "Public";
const kubeconfig = [
"apiVersion: v1",
"clusters:",
"- cluster:",
" server: https://example.com",
" name: vke",
"contexts: []",
"current-context: vke"
].join("\n");
const result = await (plugin as any).getKubeconfig(
{
request: async () => ({
Result: {
Items: [{ Id: "kc-123", Kubeconfig: Buffer.from(kubeconfig).toString("base64") }]
}
})
},
"kc-123"
);
assert.equal(result, kubeconfig);
});
it("formats Kubernetes forbidden errors with VKE RBAC guidance", () => {
const plugin = new VolcengineDeployToVKE();
plugin.clusterId = "cc1234567890123456789";
plugin.namespace = "default";
const message = (plugin as any).formatK8sError({
status: "Failure",
message:
'secrets "aaaa" is forbidden: User "2100656669-kd7ubde6lsvqbdgsa40t0" cannot get resource "secrets" in API group "" in the namespace "default"',
reason: "Forbidden",
details: { name: "aaaa", kind: "secrets" },
code: 403
});
assert.match(message, /VKE集群RBAC权限不足/);
assert.match(message, /用户ID:2100656669/);
assert.match(message, /命名空间:default/);
assert.match(message, /secrets get\/create\/update\/patch/);
});
it("explains missing ingress names with available choices", async () => {
const plugin = new VolcengineDeployToVKE();
plugin.targetType = "ingress";
plugin.namespace = "default";
plugin.ingressName = "ingress-nginx-controller";
await assert.rejects(
() =>
(plugin as any).getTargetSecretNames({
getIngressList: async () => ({
items: [{ metadata: { name: "app-web" } }, { metadata: { name: "api-web" } }]
})
}),
/当前命名空间可用Ingress:app-web,api-web/
);
});
});
@@ -70,6 +70,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
helper: "选择要替换证书的VKE集群,也可以手动输入集群ID",
action: VolcengineDeployToVKE.prototype.onGetClusterList.name,
watches: ["accessId", "regionId"],
multi: false,
required: true
})
)
@@ -82,14 +83,13 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
name: "a-select",
options: [
{ label: "公网", value: "Public" },
{ label: "私网", value: "Private" },
{ label: "集群内", value: "TargetCluster" }
{ label: "私网", value: "Private" }
]
},
value: "Public",
required: true
})
kubeconfigType!: "Public" | "Private" | "TargetCluster";
kubeconfigType!: "Public" | "Private";
@TaskInput({
title: "命名空间",
@@ -194,7 +194,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
} catch (e) {
if (e.response?.body) {
throw new Error(JSON.stringify(e.response.body));
throw new Error(this.formatK8sError(e.response.body));
}
throw e;
} finally {
@@ -215,11 +215,12 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
}
private async createKubeconfig(vkeService: any) {
const clusterId = this.getClusterId();
const res = await vkeService.request({
action: "CreateKubeconfig",
method: "POST",
body: {
ClusterId: this.clusterId,
ClusterId: clusterId,
Type: this.kubeconfigType,
ValidDuration: 3600
}
@@ -233,12 +234,13 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
}
private async getKubeconfig(vkeService: any, kubeconfigId: string) {
const clusterId = this.getClusterId();
const res = await vkeService.request({
action: "ListKubeconfigs",
method: "POST",
body: {
Filter: {
ClusterIds: [this.clusterId],
ClusterIds: [clusterId],
Ids: [kubeconfigId],
Types: [this.kubeconfigType]
},
@@ -252,19 +254,20 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
if (!kubeconfig) {
throw new Error(`获取VKE Kubeconfig失败:${JSON.stringify(res)}`);
}
return kubeconfig;
return this.decodeKubeconfig(kubeconfig);
}
private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) {
if (!kubeconfigId) {
return;
}
const clusterId = this.getClusterId();
try {
await vkeService.request({
action: "DeleteKubeconfigs",
method: "POST",
body: {
ClusterId: this.clusterId,
ClusterId: clusterId,
Ids: [kubeconfigId]
}
});
@@ -274,6 +277,35 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
}
}
private getClusterId() {
if (!this.clusterId) {
throw new Error("VKE集群ID不能为空");
}
return this.clusterId;
}
private decodeKubeconfig(kubeconfig: string) {
const decoded = Buffer.from(kubeconfig, "base64").toString("utf8");
if (!decoded.includes("apiVersion:") || !decoded.includes("clusters:")) {
throw new Error("解析VKE Kubeconfig失败:接口返回的Kubeconfig不是有效的BASE64编码");
}
return decoded;
}
private formatK8sError(body: any) {
if (body?.code !== 403 || body?.reason !== "Forbidden") {
return JSON.stringify(body);
}
const message = body.message || "";
const user = message.match(/User "([^"]+)"/)?.[1];
const granteeId = user?.split("-")[0];
const resource = message.match(/resource "([^"]+)"/)?.[1] || body.details?.kind || "目标资源";
const namespace = message.match(/namespace "([^"]+)"/)?.[1] || this.namespace;
const userText = granteeId ? `,用户ID:${granteeId}` : "";
return `VKE集群RBAC权限不足:当前火山引擎授权${userText}在集群:${this.getClusterId()} 的命名空间:${namespace} 没有操作 ${resource} 的权限。请在火山引擎 VKE 集群权限管理中,为该用户授予此命名空间的管理员权限,或授予包含 secrets get/create/update/patch 的自定义角色。原始错误:${JSON.stringify(body)}`;
}
private async getTargetSecretNames(k8sClient: any) {
if (this.targetType === "secret") {
if (typeof this.secretName === "string") {
@@ -287,11 +319,15 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
});
const ingress = ingressList.items.find((item: any) => item.metadata.name === this.ingressName);
if (!ingress) {
throw new Error(`Ingress不存在:${this.ingressName}`);
const ingressNames = ingressList.items.map((item: any) => item.metadata.name).filter(Boolean);
const availableText = ingressNames.length > 0 ? ingressNames.join(",") : "无";
throw new Error(
`Ingress不存在:${this.ingressName}(命名空间:${this.namespace})。当前命名空间可用Ingress:${availableText}。请填写业务Ingress资源名称,不是 ingress-nginx-controller 这类控制器Service/Deployment名称;如果只想更新指定Secret,请将替换方式改为“按Secret替换”。`
);
}
const secretNames = ingress.spec?.tls?.map((tls: any) => tls.secretName).filter(Boolean) || [];
if (secretNames.length === 0) {
throw new Error(`Ingress:${this.ingressName} 未找到TLS Secret`);
throw new Error(`Ingress:${this.ingressName}(命名空间:${this.namespace})未找到spec.tls[].secretName,请确认该Ingress已配置TLS,或改用“按Secret替换”。`);
}
return secretNames;
}