import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; import { AliyunClient, AliyunSslClient, CasCertId } from "../../../plugin-lib/aliyun/lib/index.js"; import { AliyunClientV2 } from "../../../plugin-lib/aliyun/lib/aliyun-client-v2.js"; @IsTaskPlugin({ name: "AliyunDeployCertToALB", title: "阿里云-部署至ALB(应用负载均衡)", icon: "svg:icon-aliyun", group: pluginGroups.aliyun.key, desc: "ALB,更新监听器的默认证书", needPlus: false, default: { strategy: { runStrategy: RunStrategy.SkipWhenSucceed, }, }, }) export class AliyunDeployCertToALB extends AbstractTaskPlugin { @TaskInput({ title: "域名证书", helper: "请选择证书申请任务输出的域名证书\n或者选择前置任务“上传证书到阿里云”任务的证书ID,可以减少上传到阿里云的证书数量", component: { name: "output-selector", from: [...CertApplyPluginNames, "uploadCertToAliyun"], }, required: true, }) cert!: CertInfo | CasCertId | number; @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) certDomains!: string[]; @TaskInput({ title: "证书接入点", helper: "不会选就保持默认即可", value: "cas.aliyuncs.com", component: { name: "a-select", options: [ { value: "cas.aliyuncs.com", label: "中国大陆" }, { value: "cas.ap-southeast-1.aliyuncs.com", label: "新加坡" }, { value: "cas.eu-central-1.aliyuncs.com", label: "德国(法兰克福)" }, ], }, required: true, }) casEndpoint!: string; @TaskInput({ title: "Access授权", helper: "阿里云授权AccessKeyId、AccessKeySecret", component: { name: "access-selector", type: "aliyun", }, required: true, }) accessId!: string; @TaskInput( createRemoteSelectInputDefine({ title: "ALB所在地区", typeName: "AliyunDeployCertToALB", single: true, action: AliyunDeployCertToALB.prototype.onGetRegionList.name, watches: ["accessId"], }) ) regionId: string; @TaskInput( createRemoteSelectInputDefine({ title: "负载均衡列表", helper: "要部署证书的负载均衡ID", typeName: "AliyunDeployCertToALB", action: AliyunDeployCertToALB.prototype.onGetLoadBalanceList.name, watches: ["regionId"], }) ) loadBalancers!: string[]; @TaskInput( createRemoteSelectInputDefine({ title: "监听器列表", helper: "要部署证书的监听器列表", typeName: "AliyunDeployCertToALB", action: AliyunDeployCertToALB.prototype.onGetListenerList.name, watches: ["loadBalancers"], }) ) listeners!: string[]; @TaskInput({ title: "部署证书类型", value: "default", component: { name: "a-select", vModel: "value", options: [ { label: "默认证书", value: "default", }, { label: "扩展证书", value: "extension", }, ], }, required: true, }) deployType = "default"; @TaskInput({ title: "是否清理过期证书", value: true, component: { name: "a-switch", vModel: "checked", }, required: true, }) clearExpiredCert: boolean; async onInstance() {} async getLBClient(access: AliyunAccess, region: string) { const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); const version = "2020-06-16"; await client.init({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, //https://wafopenapi.cn-hangzhou.aliyuncs.com endpoint: `https://alb.${region}.aliyuncs.com`, apiVersion: version, }); return client; } getALBClientV2(access: AliyunAccess) { return access.getClient(`alb.${this.regionId}.aliyuncs.com`); } async execute(): Promise { this.logger.info(`开始部署证书到阿里云(alb)`); const access = await this.getAccess(this.accessId); const certId = await this.getAliyunCertId(access); //部署扩展证书 const albClientV2 = this.getALBClientV2(access); if (this.deployType === "extension") { await this.deployExtensionCert(albClientV2, certId); } else { const client = await this.getLBClient(access, this.regionId); await this.deployDefaultCert(certId, client); } if (this.clearExpiredCert !== false) { this.logger.info(`准备开始清理过期证书`); await this.ctx.utils.sleep(30000); for (const listener of this.listeners) { try { await this.clearInvalidCert(albClientV2, listener); } catch (e) { this.logger.error(`清理监听器${listener}的过期证书失败`, e); } } } this.logger.info("执行完成"); } private async deployDefaultCert(certId: any, client: AliyunClient) { for (const listener of this.listeners) { let params: any = {}; params = { ListenerId: listener, Certificates: [ { CertificateId: certId, }, ], }; const res = await client.request("UpdateListenerAttribute", params); this.checkRet(res); this.logger.info(`部署${listener}监听器证书成功`, JSON.stringify(res)); } } async deployExtensionCert(client: AliyunClientV2, certId: any) { for (const listenerId of this.listeners) { this.logger.info(`开始部署监听器${listenerId}的扩展证书`); await client.doRequest({ // 接口名称 action: "AssociateAdditionalCertificatesWithListener", // 接口版本 version: "2020-06-16", data: { query: { ListenerId: listenerId, Certificates: [ { CertificateId: certId, }, ], }, }, }); this.logger.info(`部署监听器${listenerId}的扩展证书成功`); } } async clearInvalidCert(client: AliyunClientV2, listener: string) { this.logger.info(`开始清理监听器${listener}的过期证书`); const req = { // 接口名称 action: "ListListenerCertificates", // 接口版本 version: "2020-06-16", data: { query: { ListenerId: listener, }, }, }; const res = await client.doRequest(req); const list = res.Certificates; if (list.length === 0) { this.logger.info(`监听器${listener}没有绑定证书`); return; } const sslClient = new AliyunSslClient({ access: client.access, logger: this.logger, endpoint: this.casEndpoint, }); const certIds = []; for (const item of list) { this.logger.info(`监听器${listener}绑定的证书${item.CertificateId},status:${item.Status},IsDefault:${item.IsDefault}`); if (item.Status !== "Associated") { continue; } if (item.IsDefault) { continue; } certIds.push(parseInt(item.CertificateId)); } this.logger.info(`监听器${listener}绑定的证书${certIds}`); //检查是否过期,过期则删除 const invalidCertIds = []; for (const certId of certIds) { const res = await sslClient.getCertInfo(certId); this.logger.info(`证书${certId}过期时间:${res.notAfter}`); if (res.notAfter < new Date().getTime()) { invalidCertIds.push(certId); } } if (invalidCertIds.length === 0) { this.logger.info(`监听器${listener}没有过期的证书`); return; } this.logger.info(`开始解绑过期的证书:${invalidCertIds},listener:${listener}`); await client.doRequest({ // 接口名称 action: "DissociateAdditionalCertificatesFromListener", // 接口版本 version: "2020-06-16", data: { query: { ListenerId: listener, Certificates: invalidCertIds.map(item => { return { CertificateId: item, }; }), }, }, }); this.logger.info(`解绑过期证书成功`); } async getAliyunCertId(access: AliyunAccess) { let certId: any = this.cert; if (typeof this.cert === "object") { const certInfo = this.cert as CertInfo; const casCert = this.cert as CasCertId; const sslClient = new AliyunSslClient({ access, logger: this.logger, endpoint: this.casEndpoint, }); if (certInfo.crt) { const certName = this.buildCertName(CertReader.getMainDomain(certInfo.crt)); const certIdRes = await sslClient.uploadCertificate({ name: certName, cert: certInfo, }); certId = certIdRes.certId as any; } else if (casCert.certId) { certId = casCert.certId; } else { throw new Error("证书格式错误" + JSON.stringify(this.cert)); } } return certId; } async onGetRegionList(data: any) { if (!this.accessId) { throw new Error("请选择Access授权"); } const access = await this.getAccess(this.accessId); const client = await this.getLBClient(access, "cn-shanghai"); const res = await client.request("DescribeRegions", {}); this.checkRet(res); if (!res?.Regions || res?.Regions.length === 0) { throw new Error("没有找到Regions列表"); } return res.Regions.map((item: any) => { return { label: item.LocalName, value: item.RegionId, endpoint: item.RegionEndpoint, }; }); } async onGetLoadBalanceList(data: any) { if (!this.accessId) { throw new Error("请先选择Access授权"); } if (!this.regionId) { throw new Error("请先选择地区"); } const access = await this.getAccess(this.accessId); const client = await this.getLBClient(access, this.regionId); const params = { MaxResults: 100, }; const res = await client.request("ListLoadBalancers", params); this.checkRet(res); if (!res?.LoadBalancers || res?.LoadBalancers.length === 0) { throw new Error("没有找到LoadBalancers"); } return res.LoadBalancers.map((item: any) => { const label = `${item.LoadBalancerId}<${item.LoadBalancerName}}>`; return { label: label, value: item.LoadBalancerId, }; }); } async onGetListenerList(data: any) { if (!this.accessId) { throw new Error("请先选择Access授权"); } if (!this.regionId) { throw new Error("请先选择地区"); } const access = await this.getAccess(this.accessId); const client = await this.getLBClient(access, this.regionId); const params: any = { MaxResults: 100, }; if (this.loadBalancers && this.loadBalancers.length > 0) { params.LoadBalancerIds = this.loadBalancers; } const res = await client.request("ListListeners", params); this.checkRet(res); if (!res?.Listeners || res?.Listeners.length === 0) { throw new Error("没有找到HTTPS监听器"); } return res.Listeners.map((item: any) => { const label = `${item.ListenerId}<${item.ListenerDescription}@${item.LoadBalancerId}>`; return { label: label, value: item.ListenerId, lbid: item.LoadBalancerId, }; }); } checkRet(ret: any) { if (ret.Code != null) { throw new Error(ret.Message); } } } new AliyunDeployCertToALB();