From 77b802445322d576d54d194f7c505da49e0e824c Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sat, 6 Jun 2026 02:12:47 +0800 Subject: [PATCH] =?UTF-8?q?perf(volcengine-vke):=20=E7=81=AB=E5=B1=B1VKE?= =?UTF-8?q?=E9=9B=86=E7=BE=A4=E8=AF=81=E4=B9=A6=E6=94=AF=E6=8C=81=E4=B8=A4?= =?UTF-8?q?=E7=A7=8D=E7=B1=BB=E5=9E=8B=E7=9A=84=E8=AF=81=E4=B9=A6=E4=BF=9D?= =?UTF-8?q?=E5=AF=86=E5=AD=97=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deploy_VolcengineDeployToVKE.yaml | 28 ++- .../plugins/plugin-deploy-to-vke.test.ts | 62 +++++++ .../plugins/plugin-deploy-to-vke.ts | 159 +++++++++++++++--- .../plugins/plugin-volcengine/ve-client.ts | 2 +- 4 files changed, 217 insertions(+), 34 deletions(-) diff --git a/packages/ui/certd-server/metadata/deploy_VolcengineDeployToVKE.yaml b/packages/ui/certd-server/metadata/deploy_VolcengineDeployToVKE.yaml index 628d0b716..fadb6625e 100644 --- a/packages/ui/certd-server/metadata/deploy_VolcengineDeployToVKE.yaml +++ b/packages/ui/certd-server/metadata/deploy_VolcengineDeployToVKE.yaml @@ -6,7 +6,7 @@ name: VolcengineDeployToVKE title: 火山引擎-替换VKE证书 icon: svg:icon-volcengine group: volcengine -desc: 替换火山引擎VKE集群中的TLS Secret证书 +desc: 将证书上传至火山引擎证书中心后,通过 cert_center 方式替换VKE集群中的Secret input: cert: title: 域名证书 @@ -15,6 +15,7 @@ input: name: output-selector from: - ':cert:' + - 'VolcengineUploadToCertCenter' required: true order: 0 certDomains: @@ -140,23 +141,38 @@ input: secretName: title: Secret名称 required: true - helper: 存储TLS证书的Secret名称,可填写多个 + helper: 选择要替换的Secret,可多选 component: - name: a-select + name: remote-select vModel: value mode: tags - open: false + type: plugin + action: onGetSecretList + search: false + pager: false + single: false + watches: + - certDomains + - accessId + - regionId + - clusterId + - kubeconfigType + - namespace mergeScript: |2- return { show: ctx.compute(({form}) => form.targetType === 'secret'), - required: ctx.compute(({form}) => form.targetType === 'secret') + required: ctx.compute(({form}) => form.targetType === 'secret'), + component: { + form: ctx.compute(({form}) => form) + } } order: 0 + createOnNotFound: title: Secret自动创建 - helper: 如果Secret不存在,则创建kubernetes.io/tls类型Secret + helper: 如果Secret不存在,则创建Opaque类型Secret value: false component: name: a-switch diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts index 748ca73dc..32bd28a07 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { VolcengineDeployToVKE } from "./plugin-deploy-to-vke.js"; +import { CertInfo } from "@certd/plugin-cert"; describe("VolcengineDeployToVKE", () => { it("uses a single-select cluster field", () => { @@ -86,4 +87,65 @@ describe("VolcengineDeployToVKE", () => { /当前命名空间可用Ingress:app-web,api-web/ ); }); + + it("creates Secret with cert_center format for new non-tls secrets", async () => { + const plugin = new VolcengineDeployToVKE(); + plugin.namespace = "default"; + plugin.targetType = "secret"; + plugin.secretName = "test-tls"; + plugin.createOnNotFound = true; + plugin.logger = { info: () => undefined } as any; + plugin.appendTimeSuffix = (s: string) => s + "-test"; + + let secretBody: any; + await (plugin as any).patchCertSecret({ + certId: "cert-abc123", + k8sClient: { + patchSecret: async (opts: any) => { + secretBody = opts.body; + return {}; + }, + client: { + readNamespacedSecret: async () => { + throw Object.assign(new Error("Not Found"), { response: { body: { code: 404 } } }); + }, + }, + }, + secretNames: ["test-tls"], + }); + + assert.equal(secretBody.type, "Opaque"); + assert.equal(secretBody.data["cert_id"], Buffer.from("cert-abc123").toString("base64")); + assert.equal(secretBody.data["cert_source"], Buffer.from("cert_center").toString("base64")); + }); + + it("uses tls.crt/tls.key format for kubernetes.io/tls secrets", async () => { + const plugin = new VolcengineDeployToVKE(); + plugin.namespace = "default"; + plugin.targetType = "secret"; + plugin.secretName = "test-tls"; + plugin.logger = { info: () => undefined } as any; + plugin.appendTimeSuffix = (s: string) => s + "-test"; + plugin.cert = { crt: "MY_CRT", key: "MY_KEY" } as CertInfo; + + let secretBody: any; + await (plugin as any).patchCertSecret({ + certId: "cert-abc123", + k8sClient: { + patchSecret: async (opts: any) => { + secretBody = opts.body; + return {}; + }, + client: { + readNamespacedSecret: async () => ({ + body: { type: "kubernetes.io/tls" }, + }), + }, + }, + secretNames: ["test-tls"], + }); + + assert.equal(secretBody.data["tls.crt"], Buffer.from("MY_CRT").toString("base64")); + assert.equal(secretBody.data["tls.key"], Buffer.from("MY_KEY").toString("base64")); + }); }); diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts index d314898a4..ae6246080 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts @@ -34,11 +34,11 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { helper: "请选择前置任务输出的域名证书", component: { name: "output-selector", - from: [...CertApplyPluginNames], + from: [...CertApplyPluginNames, "VolcengineUploadToCertCenter"], }, required: true, }) - cert!: CertInfo; + cert!: CertInfo | string; @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) certDomains!: string[]; @@ -109,11 +109,11 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { component: { name: "a-select", options: [ - { label: "按Ingress替换", value: "ingress" }, { label: "按Secret替换", value: "secret" }, + { label: "按Ingress替换", value: "ingress" }, ], }, - value: "ingress", + value: "secret", required: true, }) targetType!: "ingress" | "secret"; @@ -134,17 +134,25 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { @TaskInput({ title: "Secret名称", required: true, - helper: "存储TLS证书的Secret名称,可填写多个", + helper: "选择要替换的Secret,可多选", component: { - name: "a-select", + name: "remote-select", vModel: "value", mode: "tags", - open: false, + type: "plugin", + action: "onGetSecretList", + search: false, + pager: false, + single: false, + watches: ["certDomains", "accessId", "regionId", "clusterId", "kubeconfigType", "namespace"], }, mergeScript: ` return { show: ctx.compute(({form}) => form.targetType === 'secret'), - required: ctx.compute(({form}) => form.targetType === 'secret') + required: ctx.compute(({form}) => form.targetType === 'secret'), + component: { + form: ctx.compute(({form}) => form) + } } `, }) @@ -152,7 +160,7 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { @TaskInput({ title: "Secret自动创建", - helper: "如果Secret不存在,则创建kubernetes.io/tls类型Secret", + helper: "如果Secret不存在,则创建Opaque类型Secret", value: false, component: { name: "a-switch", @@ -183,6 +191,23 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { this.logger.info("开始替换火山引擎VKE证书"); const access = await this.getAccess(this.accessId); const vkeService = await this.getVkeService(access); + + // 上传证书到证书中心 + let certId: string; + if (typeof this.cert !== "string") { + const certInfo = this.cert as CertInfo; + this.logger.info("开始上传证书到证书中心"); + const certService = await this.getCertService(access); + certId = await certService.ImportCertificate({ + certName: this.appendTimeSuffix("certd"), + cert: certInfo, + }); + this.logger.info("上传证书到证书中心成功:" + certId); + } else { + certId = this.cert; + this.logger.info("使用已有证书中心ID:" + certId); + } + const kubeconfigId = await this.createKubeconfig(vkeService); try { @@ -193,7 +218,7 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { skipTLSVerify: this.skipTLSVerify, }); const secretNames = await this.getTargetSecretNames(k8sClient); - await this.patchCertSecret({ cert: this.cert, k8sClient, secretNames }); + await this.patchCertSecret({ certId, k8sClient, secretNames }); } catch (e) { if (e.response?.body) { throw new Error(this.formatK8sError(e.response.body)); @@ -207,6 +232,15 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { this.logger.info("VKE证书替换完成"); } + private async getCertService(access: VolcengineAccess) { + const client = new VolcengineClient({ + logger: this.logger, + access, + http: this.http, + }); + return await client.getCertCenterService(); + } + private async getVkeService(access: VolcengineAccess) { const client = new VolcengineClient({ logger: this.logger, @@ -295,6 +329,11 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { } private formatK8sError(body: any) { + if (body?.code === 422 && body?.message?.includes("field is immutable")) { + const secretName = body.details?.name || "未知"; + return `Secret类型不可变:Secret ${secretName} 已是kubernetes.io/tls类型,type字段不可修改。\n请删除该Secret后重试,或选择正确的Secret。\n原始错误:${JSON.stringify(body)}`; + } + if (body?.code !== 403 || body?.reason !== "Forbidden") { return JSON.stringify(body); } @@ -336,34 +375,71 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { return secretNames; } - private async patchCertSecret(options: { cert: CertInfo; k8sClient: any; secretNames: string[] }) { - const { cert, k8sClient, secretNames } = options; + private async patchCertSecret(options: { certId: string; k8sClient: any; secretNames: string[] }) { + const { certId, k8sClient, secretNames } = options; if (!secretNames || secretNames.length === 0) { throw new Error("Secret名称不能为空"); } - const body: any = { - data: { - "tls.crt": Buffer.from(cert.crt).toString("base64"), - "tls.key": Buffer.from(cert.key).toString("base64"), - }, - metadata: { - labels: { - certd: this.appendTimeSuffix("certd"), - }, - }, - }; - for (const secretName of secretNames) { + let useTlsFormat = false; + try { + const res = await k8sClient.client.readNamespacedSecret(secretName, this.namespace); + useTlsFormat = res.body?.type === "kubernetes.io/tls"; + } catch (e) { + // Secret 不存在,将走创建逻辑 + } + + let body: any; + if (useTlsFormat) { + let crt: string; + let key: string; + if (typeof this.cert === "string") { + const access = await this.getAccess(this.accessId); + const certService = await this.getCertService(access); + const detail = await certService.GetCertificateDetail(this.cert); + crt = detail.CertificateChain || ""; + key = detail.PrivateKey || ""; + this.logger.info("从证书中心获取证书详情成功"); + } else { + crt = this.cert.crt; + key = this.cert.key; + } + body = { + data: { + "tls.crt": Buffer.from(crt).toString("base64"), + "tls.key": Buffer.from(key).toString("base64"), + }, + metadata: { + labels: { + certd: this.appendTimeSuffix("certd"), + }, + }, + }; + } else { + body = { + type: "Opaque", + data: { + cert_id: Buffer.from(certId).toString("base64"), + cert_source: Buffer.from("cert_center").toString("base64"), + }, + metadata: { + labels: { + certd: this.appendTimeSuffix("certd"), + }, + }, + }; + } + body.metadata.name = secretName; - this.logger.info(`开始更新VKE Secret:${secretName}`); + this.logger.info("开始更新VKE Secret:" + secretName); await k8sClient.patchSecret({ namespace: this.namespace, secretName, body, createOnNotFound: this.createOnNotFound, }); - this.logger.info(`VKE Secret已更新:${secretName}`); + this.logger.info("VKE Secret已更新:" + secretName); } if (this.targetType === "ingress" && this.ingressName) { @@ -391,6 +467,35 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { value: item.Id, })); } + async onGetSecretList() { + if (!this.accessId) { + throw new Error("请选择Access授权"); + } + if (!this.clusterId) { + throw new Error("请选择VKE集群"); + } + const access = await this.getAccess(this.accessId); + const vkeService = await this.getVkeService(access); + const kubeconfigId = await this.createKubeconfig(vkeService); + + try { + const kubeconfig = await this.getKubeconfig(vkeService, kubeconfigId); + const k8sClient = new this.K8sClient({ + kubeConfigStr: kubeconfig, + logger: this.logger, + skipTLSVerify: this.skipTLSVerify, + }); + const res = await k8sClient.getSecrets({ namespace: this.namespace || "default" }); + const list = res.body?.items || res.items || []; + return list.map((item: any) => ({ + label: item.metadata.name, + value: item.metadata.name, + })); + } finally { + await this.deleteKubeconfig(vkeService, kubeconfigId); + } + } + } -new VolcengineDeployToVKE(); +new VolcengineDeployToVKE(); \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts index c07bdf1f8..e3049e3c1 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts @@ -1,4 +1,4 @@ -import { VolcengineAccess } from "./access.js"; +import { VolcengineAccess } from "./access.js"; import { HttpClient, ILogger } from "@certd/basic"; export type VolcengineOpts = {