Files
certd/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts
T

316 lines
8.7 KiB
TypeScript
Raw Normal View History

2025-05-22 23:21:32 +08:00
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 { AliyunSslClient, CasCertId } from "../../../plugin-lib/aliyun/lib/ssl-client.js";
import { AliyunClientV2 } from "../../../plugin-lib/aliyun/lib/aliyun-client-v2.js";
import dayjs from "dayjs";
2025-05-22 23:21:32 +08:00
@IsTaskPlugin({
name: "AliyunDeployCertToESA",
title: "阿里云-部署至ESA",
icon: "svg:icon-aliyun",
group: pluginGroups.aliyun.key,
2025-10-22 23:07:33 +08:00
desc: "部署证书到阿里云ESA(边缘安全加速),自动删除过期证书",
2025-05-22 23:21:32 +08:00
needPlus: false,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
export class AliyunDeployCertToESA extends AbstractTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择证书申请任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames, 'uploadCertToAliyun']
2025-05-22 23:21:32 +08:00
},
required: true
})
cert!: CertInfo | CasCertId | number;
2025-05-22 23:21:32 +08:00
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: "大区",
value: "cn-hangzhou",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "cn-hangzhou", label: "华东1(杭州)" },
{ value: "ap-southeast-1", label: "新加坡" }
]
},
required: true
})
regionId!: string;
@TaskInput({
title: "证书接入点",
helper: "不会选就保持默认即可",
value: "cas.aliyuncs.com",
component: {
name: "a-select",
options: [
{ value: "cas.aliyuncs.com", label: "中国大陆" },
2025-07-23 17:55:08 +08:00
{ value: "cas.ap-southeast-1.aliyuncs.com", label: "新加坡" }
// { value: "cas.eu-central-1.aliyuncs.com", label: "德国(法兰克福)" }
2025-05-22 23:21:32 +08:00
]
},
required: true
})
casEndpoint!: string;
@TaskInput({
title: "Access授权",
helper: "阿里云授权AccessKeyId、AccessKeySecret",
component: {
name: "access-selector",
type: "aliyun"
},
required: true
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: "站点",
helper: "请选择要部署证书的站点",
action: AliyunDeployCertToESA.prototype.onGetSiteList.name,
watches: ["accessId", "regionId"]
})
)
siteIds!: string[];
@TaskInput({
title: "免费证书数量限制",
value: 2,
component: {
name: "a-input-number",
vModel: "value"
},
helper: "将检查证书数量限制,如果超限将删除最旧的那张证书",
required: true
})
certLimit: number = 2;
2025-05-22 23:21:32 +08:00
async onInstance() {
}
async getAliyunCertId(access: AliyunAccess) {
let certId: any = this.cert;
let certName: any = this.appendTimeSuffix("certd");
if (typeof this.cert === "object") {
const sslClient = new AliyunSslClient({
access,
logger: this.logger,
endpoint: this.casEndpoint
});
const certInfo = this.cert as CertInfo;
const casCert = this.cert as CasCertId;
if (casCert.certId) {
certId = casCert.certId;
certName = casCert.certName;
} else if (certInfo.crt) {
2026-04-11 23:10:51 +08:00
certName = this.buildCertName(CertReader.getMainDomain(certInfo.crt),"certd");
const certIdRes = await sslClient.uploadCertificate({
name: certName,
cert: certInfo
});
certId = certIdRes.certId as any;
this.logger.info("上传证书成功", certId, certName);
}else{
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
2025-05-22 23:21:32 +08:00
}
return {
certId,
certName
};
}
async execute(): Promise<void> {
this.logger.info("开始部署证书到阿里云");
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getClient(access);
2026-02-01 15:40:35 +08:00
const { certId, certName } = await this.getAliyunCertId(access);
2025-05-22 23:21:32 +08:00
for (const siteId of this.siteIds) {
await this.clearSiteLimitCert(client, siteId);
2026-02-01 15:40:35 +08:00
try {
const res = await client.doRequest({
// 接口名称
action: "SetCertificate",
// 接口版本
version: "2024-09-10",
data: {
body: {
SiteId: siteId,
CasId: certId,
Type: "cas",
Name: certName
}
}
});
this.logger.info(`部署站点[${siteId}]证书成功:${JSON.stringify(res)}`);
} catch (e) {
if (e.message.includes("Certificate.Duplicated")) {
this.logger.info(`站点[${siteId}]证书已存在,无需重复部署`);
} else {
throw e;
}
} finally {
try {
await this.clearSiteExpiredCert(client, siteId);
} catch (e) {
this.logger.error(`清理站点[${siteId}]过期证书失败`, e)
}
}
2025-05-22 23:21:32 +08:00
}
}
2025-10-22 23:07:33 +08:00
2025-05-22 23:21:32 +08:00
async getClient(access: AliyunAccess) {
const endpoint = `esa.${this.regionId}.aliyuncs.com`;
return access.getClient(endpoint);
}
async onGetSiteList(data: any) {
if (!this.accessId) {
throw new Error("请选择Access授权");
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getClient(access);
const res = await client.doRequest({
action: "ListSites",
version: "2024-09-10",
method: "GET",
data: {}
});
const list = res?.Sites;
if (!list || list.length === 0) {
throw new Error("没有找到站点,请先创建站点");
}
const options = list.map((item: any) => {
return {
label: item.SiteName,
value: item.SiteId,
domain: item.SiteName
};
});
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
}
async clearSiteExpiredCert(client: AliyunClientV2, siteId: string) {
2025-05-22 23:21:32 +08:00
this.logger.info(`开始清理站点[${siteId}]过期证书`);
const certListRes = await client.doRequest({
action: "ListCertificates",
version: "2024-09-10",
method: "GET",
data: {
query: {
SiteId: siteId,
PageSize: 100
}
2025-05-22 23:21:32 +08:00
}
});
const list = certListRes.Result;
for (const item of list) {
this.logger.info(`证书${item.Name}状态:${item.Status}`);
if (item.Status === "Expired") {
this.logger.info(`证书${item.Name}已过期,执行删除`);
try {
2025-10-22 23:07:33 +08:00
await client.doRequest({
action: "DeleteCertificate",
version: "2024-09-10",
// 接口 HTTP 方法
method: "GET",
data: {
2025-10-22 23:07:33 +08:00
query: {
SiteId: siteId,
Id: item.id
}
}
});
this.logger.info(`证书${item.Name}已删除`);
} catch (e) {
this.logger.error(`过期证书${item.Name}删除失败:`, e.message)
2025-10-22 23:07:33 +08:00
}
}
}
}
2025-10-22 23:07:33 +08:00
async clearSiteLimitCert(client: AliyunClientV2, siteId: string) {
//删除最旧的证书
2026-02-01 15:43:19 +08:00
const certLimit = this.certLimit || 2;
this.logger.info(`站点[${siteId}]证书数量检查,当前限制${certLimit}`);
const certListRes = await client.doRequest({
action: "ListCertificates",
version: "2024-09-10",
method: "GET",
data: {
query: {
SiteId: siteId,
PageSize: 100
}
}
});
const list = certListRes.Result;
if (!list || list.length === 0) {
this.logger.info(`站点[${siteId}]没有证书, 无需删除`);
return
}
2026-02-01 15:43:19 +08:00
if (list.length < certLimit) {
this.logger.info(`站点[${siteId}]证书数量(${list.length})未超限制, 无需删除`);
return;
}
this.logger.info(`站点[${siteId}]证书数量(${list.length})已超限制, 开始删除最旧的证书`);
let oldly:any = null;
for (const item of list) {
if (!oldly) {
oldly = item;
continue;
}
if (dayjs(item.CreateTime).valueOf() < (dayjs(oldly.CreateTime)).valueOf()){
oldly = item;
}
}
this.logger.info(`最旧的证书${oldly.Name}创建时间:${oldly.CreateTime}`);
await client.doRequest({
action: "DeleteCertificate",
version: "2024-09-10",
method: "GET",
data: {
query: {
SiteId: siteId,
2026-02-01 15:40:35 +08:00
Id: oldly.Id
}
}
});
this.logger.info(`最旧证书${oldly.Name}已删除`);
2025-05-22 23:21:32 +08:00
}
}
new AliyunDeployCertToESA();