diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.test.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.test.ts new file mode 100644 index 000000000..5f15a623e --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.test.ts @@ -0,0 +1,123 @@ +/// + +import assert from "node:assert/strict"; +import { AliyunDeployCertToESA } from "./index.js"; + +describe("AliyunDeployCertToESA", () => { + it("has deployMode field with default value 'edge'", () => { + const input = (AliyunDeployCertToESA as any).define.input.deployMode; + assert.equal(input.value, "edge"); + assert.equal(input.component.name, "a-radio-group"); + assert.deepEqual(input.component.options, [ + { label: "边缘证书", value: "edge" }, + { label: "SaaS证书", value: "saas" }, + ]); + }); + + it("has saasDomainIds field with conditional show", () => { + const input = (AliyunDeployCertToESA as any).define.input.saasDomainIds; + assert.equal(input.component.name, "remote-select"); + assert.equal(input.component.action, "onGetCustomHostnameList"); + assert.equal(input.required, false); + assert.match(input.mergeScript, /form.deployMode === 'saas'/); + }); + + it("executeSaaS throws error when no site is selected", async () => { + const plugin = new AliyunDeployCertToESA(); + plugin.logger = { info: () => undefined } as any; + plugin.deployMode = "saas"; + plugin.siteIds = []; + + await assert.rejects( + () => (plugin as any).executeSaaS(null, null, 1, "test"), + /SaaS证书模式下请先选择站点/ + ); + }); + + it("executeSaaS throws error when multiple sites are selected", async () => { + const plugin = new AliyunDeployCertToESA(); + plugin.logger = { info: () => undefined } as any; + plugin.deployMode = "saas"; + plugin.siteIds = ["site1", "site2"]; + + await assert.rejects( + () => (plugin as any).executeSaaS(null, null, 1, "test"), + /SaaS证书模式下站点只能单选/ + ); + }); + + it("executeSaaS throws error when no SaaS domains selected", async () => { + const plugin = new AliyunDeployCertToESA(); + plugin.logger = { info: () => undefined } as any; + plugin.deployMode = "saas"; + plugin.siteIds = ["site1"]; + plugin.saasDomainIds = []; + + await assert.rejects( + () => (plugin as any).executeSaaS(null, null, 1, "test"), + /SaaS证书模式下请选择要部署的SaaS域名/ + ); + }); + + it("executeSaaS calls UpdateCustomHostname for each selected SaaS domain", async () => { + const plugin = new AliyunDeployCertToESA(); + plugin.logger = { info: () => undefined, error: () => undefined } as any; + plugin.deployMode = "saas"; + plugin.siteIds = ["site1"]; + plugin.saasDomainIds = ["1001", "1002"]; + plugin.regionId = "cn-hangzhou"; + + const calledHostnameIds: number[] = []; + const mockClient = { + doRequest: async (req: any) => { + calledHostnameIds.push(req.data.body.HostnameId); + return {}; + }, + }; + + await (plugin as any).executeSaaS(mockClient, null, 12345, "test-cert"); + + assert.deepEqual(calledHostnameIds, [1001, 1002]); + }); + + it("executeSaaS handles Certificate.Duplicated error gracefully", async () => { + const plugin = new AliyunDeployCertToESA(); + plugin.logger = { info: () => undefined, error: () => undefined } as any; + plugin.deployMode = "saas"; + plugin.siteIds = ["site1"]; + plugin.saasDomainIds = ["1001"]; + plugin.regionId = "cn-hangzhou"; + + let callCount = 0; + const mockClient = { + doRequest: async (req: any) => { + callCount++; + throw new Error("Certificate.Duplicated"); + }, + }; + + await (plugin as any).executeSaaS(mockClient, null, 12345, "test-cert"); + assert.equal(callCount, 1); + }); + + it("executeEdge calls SetCertificate for each site", async () => { + const plugin = new AliyunDeployCertToESA(); + plugin.logger = { info: () => undefined, error: () => undefined } as any; + plugin.siteIds = ["site1", "site2"]; + plugin.certLimit = 2; + + const calledSites: string[] = []; + const mockClient = { + doRequest: async (req: any) => { + if (req.action === "SetCertificate") { + calledSites.push(req.data.body.SiteId); + } + return { Result: [] }; + }, + }; + + await (plugin as any).executeEdge(mockClient, 12345, "test-cert"); + + assert.deepEqual(calledSites, ["site1", "site2"]); + }); +}); \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts index c72c1acdf..043a5701d 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts @@ -11,7 +11,7 @@ import dayjs from "dayjs"; title: "阿里云-部署至ESA", icon: "svg:icon-aliyun", group: pluginGroups.aliyun.key, - desc: "部署证书到阿里云ESA(边缘安全加速),自动删除过期证书", + desc: "部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式", needPlus: false, default: { strategy: { @@ -76,6 +76,22 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin { }) accessId!: string; + @TaskInput({ + title: "部署模式", + value: "edge", + component: { + name: "a-radio-group", + vModel: "value", + options: [ + { label: "边缘证书", value: "edge" }, + { label: "SaaS证书", value: "saas" }, + ], + }, + helper: "边缘证书:将证书部署到站点的边缘节点;SaaS证书:将证书部署到站点的SaaS域名", + required: true, + }) + deployMode!: "edge" | "saas"; + @TaskInput( createRemoteSelectInputDefine({ title: "站点", @@ -86,6 +102,29 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin { ) siteIds!: string[]; + @TaskInput( + createRemoteSelectInputDefine({ + title: "SaaS域名", + helper: "请选择要部署证书的SaaS域名(SaaS证书模式下必选)", + action: AliyunDeployCertToESA.prototype.onGetCustomHostnameList.name, + watches: ["siteIds", "accessId", "regionId"], + required: false, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.deployMode === 'saas' + }), + component:{ + form: ctx.compute(({form})=>{ + return form + }) + }, + } + `, + }) + ) + saasDomainIds!: string[]; + @TaskInput({ title: "免费证书数量限制", value: 2, @@ -135,20 +174,27 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin { } async execute(): Promise { - this.logger.info("开始部署证书到阿里云"); + this.logger.info("开始部署证书到阿里云ESA"); const access = await this.getAccess(this.accessId); const client = await this.getClient(access); const { certId, certName } = await this.getAliyunCertId(access); + if (this.deployMode === "saas") { + await this.executeSaaS(client, certId, certName); + } else { + await this.executeEdge(client, certId, certName); + } + } + + async executeEdge(client: AliyunClientV2, certId: number, certName: string) { + this.logger.info("边缘证书模式"); for (const siteId of this.siteIds) { await this.clearSiteLimitCert(client, siteId); try { const res = await client.doRequest({ - // 接口名称 action: "SetCertificate", - // 接口版本 version: "2024-09-10", data: { body: { @@ -159,7 +205,7 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin { }, }, }); - this.logger.info(`部署站点[${siteId}]证书成功:${JSON.stringify(res)}`); + this.logger.info(`部署站点[${siteId}]边缘证书成功:${JSON.stringify(res)}`); } catch (e) { if (e.message.includes("Certificate.Duplicated")) { this.logger.info(`站点[${siteId}]证书已存在,无需重复部署`); @@ -176,6 +222,47 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin { } } + async executeSaaS(client: AliyunClientV2, certId: number, certName: string) { + this.logger.info("SaaS证书模式"); + + if (!this.siteIds || this.siteIds.length === 0) { + throw new Error("SaaS证书模式下请先选择站点"); + } + if (this.siteIds.length > 1) { + throw new Error(`SaaS证书模式下站点只能单选,当前已选择 ${this.siteIds.length} 个站点,请修改为只选择一个站点`); + } + + if (!this.saasDomainIds || this.saasDomainIds.length === 0) { + throw new Error("SaaS证书模式下请选择要部署的SaaS域名"); + } + + for (const hostnameId of this.saasDomainIds) { + this.logger.info(`开始更新SaaS域名[${hostnameId}]证书`); + try { + const res = await client.doRequest({ + action: "UpdateCustomHostname", + version: "2024-09-10", + data: { + body: { + HostnameId: parseInt(hostnameId, 10), + SslFlag: "on", + CertType: "cas", + CasId: certId, + CasRegion: this.regionId, + }, + }, + }); + this.logger.info(`更新SaaS域名[${hostnameId}]证书成功:${JSON.stringify(res)}`); + } catch (e) { + if (e.message?.includes("Certificate.Duplicated")) { + this.logger.info(`SaaS域名[${hostnameId}]证书已存在,无需重复部署`); + } else { + throw e; + } + } + } + } + async getClient(access: AliyunAccess) { const endpoint = `esa.${this.regionId}.aliyuncs.com`; return access.getClient(endpoint); @@ -210,6 +297,48 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin { return this.ctx.utils.options.buildGroupOptions(options, this.certDomains); } + async onGetCustomHostnameList(data: any) { + if (!this.accessId) { + throw new Error("请选择Access授权"); + } + if (!this.siteIds || this.siteIds.length === 0) { + throw new Error("请先选择站点"); + } + if (this.siteIds.length > 1) { + throw new Error("SaaS模式下站点只能单选,请先修改站点选择"); + } + + const siteId = this.siteIds[0]; + const access = await this.getAccess(this.accessId); + const client = await this.getClient(access); + + const res = await client.doRequest({ + action: "ListCustomHostnames", + version: "2024-09-10", + data: { + body: { + SiteId: parseInt(siteId, 10), + PageSize: 500, + PageNumber: 1, + }, + }, + }); + + const hostnames = res?.Hostnames; + if (!hostnames || hostnames.length === 0) { + throw new Error("该站点下没有找到SaaS域名,请先在ESA控制台添加SaaS域名"); + } + + const options = hostnames.map((item: any) => { + return { + label: `${item.Hostname}(${item.Status})`, + value: String(item.HostnameId), + domain: item.Hostname, + }; + }); + return this.ctx.utils.options.buildGroupOptions(options, this.certDomains); + } + async clearSiteExpiredCert(client: AliyunClientV2, siteId: string) { this.logger.info(`开始清理站点[${siteId}]过期证书`); const certListRes = await client.doRequest({