mirror of
https://github.com/certd/certd.git
synced 2026-05-14 20:17:32 +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",
|
helper: "选择要替换证书的VKE集群,也可以手动输入集群ID",
|
||||||
action: VolcengineDeployToVKE.prototype.onGetClusterList.name,
|
action: VolcengineDeployToVKE.prototype.onGetClusterList.name,
|
||||||
watches: ["accessId", "regionId"],
|
watches: ["accessId", "regionId"],
|
||||||
|
multi: false,
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -82,14 +83,13 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
|||||||
name: "a-select",
|
name: "a-select",
|
||||||
options: [
|
options: [
|
||||||
{ label: "公网", value: "Public" },
|
{ label: "公网", value: "Public" },
|
||||||
{ label: "私网", value: "Private" },
|
{ label: "私网", value: "Private" }
|
||||||
{ label: "集群内", value: "TargetCluster" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
value: "Public",
|
value: "Public",
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
kubeconfigType!: "Public" | "Private" | "TargetCluster";
|
kubeconfigType!: "Public" | "Private";
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "命名空间",
|
title: "命名空间",
|
||||||
@@ -194,7 +194,7 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
|||||||
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
|
await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response?.body) {
|
if (e.response?.body) {
|
||||||
throw new Error(JSON.stringify(e.response.body));
|
throw new Error(this.formatK8sError(e.response.body));
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -215,11 +215,12 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createKubeconfig(vkeService: any) {
|
private async createKubeconfig(vkeService: any) {
|
||||||
|
const clusterId = this.getClusterId();
|
||||||
const res = await vkeService.request({
|
const res = await vkeService.request({
|
||||||
action: "CreateKubeconfig",
|
action: "CreateKubeconfig",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
ClusterId: this.clusterId,
|
ClusterId: clusterId,
|
||||||
Type: this.kubeconfigType,
|
Type: this.kubeconfigType,
|
||||||
ValidDuration: 3600
|
ValidDuration: 3600
|
||||||
}
|
}
|
||||||
@@ -233,12 +234,13 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getKubeconfig(vkeService: any, kubeconfigId: string) {
|
private async getKubeconfig(vkeService: any, kubeconfigId: string) {
|
||||||
|
const clusterId = this.getClusterId();
|
||||||
const res = await vkeService.request({
|
const res = await vkeService.request({
|
||||||
action: "ListKubeconfigs",
|
action: "ListKubeconfigs",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
Filter: {
|
Filter: {
|
||||||
ClusterIds: [this.clusterId],
|
ClusterIds: [clusterId],
|
||||||
Ids: [kubeconfigId],
|
Ids: [kubeconfigId],
|
||||||
Types: [this.kubeconfigType]
|
Types: [this.kubeconfigType]
|
||||||
},
|
},
|
||||||
@@ -252,19 +254,20 @@ export class VolcengineDeployToVKE extends AbstractTaskPlugin {
|
|||||||
if (!kubeconfig) {
|
if (!kubeconfig) {
|
||||||
throw new Error(`获取VKE Kubeconfig失败:${JSON.stringify(res)}`);
|
throw new Error(`获取VKE Kubeconfig失败:${JSON.stringify(res)}`);
|
||||||
}
|
}
|
||||||
return kubeconfig;
|
return this.decodeKubeconfig(kubeconfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) {
|
private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) {
|
||||||
if (!kubeconfigId) {
|
if (!kubeconfigId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const clusterId = this.getClusterId();
|
||||||
try {
|
try {
|
||||||
await vkeService.request({
|
await vkeService.request({
|
||||||
action: "DeleteKubeconfigs",
|
action: "DeleteKubeconfigs",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
ClusterId: this.clusterId,
|
ClusterId: clusterId,
|
||||||
Ids: [kubeconfigId]
|
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) {
|
private async getTargetSecretNames(k8sClient: any) {
|
||||||
if (this.targetType === "secret") {
|
if (this.targetType === "secret") {
|
||||||
if (typeof this.secretName === "string") {
|
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);
|
const ingress = ingressList.items.find((item: any) => item.metadata.name === this.ingressName);
|
||||||
if (!ingress) {
|
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) || [];
|
const secretNames = ingress.spec?.tls?.map((tls: any) => tls.secretName).filter(Boolean) || [];
|
||||||
if (secretNames.length === 0) {
|
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;
|
return secretNames;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user