mirror of
https://github.com/certd/certd.git
synced 2026-05-13 19:47:55 +08:00
perf(plugin-volcengine): 支持火山引擎VKE部署插件
- 改进Kubernetes错误处理,添加RBAC权限不足的友好提示 - 添加集群ID验证和kubeconfig解码验证 - 优化Ingress不存在时的错误提示,显示可用选项 - 移除未使用的TargetCluster选项 - 修复kubeconfig请求中集群ID未验证的问题
This commit is contained in:
+98
@@ -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/
|
||||
);
|
||||
});
|
||||
});
|
||||
+46
-10
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user