From c6a988bc925886bd7163c1270f2b7a10a57b1c5b Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 29 Mar 2026 01:57:33 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20dcdn=E8=87=AA=E5=8A=A8=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E9=83=A8=E7=BD=B2=EF=BC=8C=E6=94=AF=E6=8C=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=9F=9F=E5=90=8D=E6=84=9F=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/pipeline/src/core/executor.ts | 2 +- packages/core/pipeline/src/plugin/api.ts | 108 +++++++++++- .../plugin-lib/src/cert/cert-reader.ts | 15 ++ .../plugins/common/remote-select.vue | 1 - .../plugin/deploy-to-dcdn/index.ts | 158 ++++++++---------- .../plugin/upload-to-aliyun/index.ts | 4 +- .../plugin-lib/aliyun/lib/ssl-client.ts | 10 +- 7 files changed, 204 insertions(+), 94 deletions(-) diff --git a/packages/core/pipeline/src/core/executor.ts b/packages/core/pipeline/src/core/executor.ts index cf39ad379..67355a2f0 100644 --- a/packages/core/pipeline/src/core/executor.ts +++ b/packages/core/pipeline/src/core/executor.ts @@ -334,7 +334,7 @@ export class Executor { //参数没有变化 inputChanged = false; } - if (step.strategy?.runStrategy === RunStrategy.SkipWhenSucceed) { + if (step.strategy?.runStrategy === RunStrategy.SkipWhenSucceed && define.runStrategy !== RunStrategy.AlwaysRun) { if (lastResult != null && lastResult === ResultType.success && !inputChanged) { step.status!.output = lastNode?.status?.output; step.status!.files = lastNode?.status?.files; diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index 323ad82e2..55534361c 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -4,13 +4,14 @@ import { FileStore } from "../core/file-store.js"; import { accessRegistry, IAccessService } from "../access/index.js"; import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService } from "../service/index.js"; import { CancelError, IContext, RunHistory, RunnableCollection } from "../core/index.js"; -import { HttpRequestConfig, ILogger, logger, optionsUtils, utils } from "@certd/basic"; +import { domainUtils, HttpRequestConfig, ILogger, logger, optionsUtils, utils } from "@certd/basic"; import { HttpClient } from "@certd/basic"; import dayjs from "dayjs"; import { IPluginConfigService } from "../service/config.js"; -import { upperFirst } from "lodash-es"; +import { cloneDeep, upperFirst } from "lodash-es"; import { INotificationService } from "../notification/index.js"; import { TaskEmitter } from "../service/emit.js"; +import { PageSearch } from "../context/index.js"; export type PluginRequestHandleReq = { typeName: string; @@ -64,6 +65,7 @@ export type PluginDefine = Registrable & { onlyAdmin?: boolean; needPlus?: boolean; showRunStrategy?: boolean; + runStrategy?: any; pluginType?: string; //类型 type?: string; //来源 }; @@ -81,6 +83,12 @@ export type TaskResult = { pipelineVars: Record; pipelinePrivateVars?: Record; }; + +export type CertTargetItem = { + value: string; + label: string; + domain: string | string[]; +}; export type TaskInstanceContext = { //流水线定义 pipeline: Pipeline; @@ -316,10 +324,102 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { return this.getLastStatus().status?.output?.[key]; } - getMatchedDomains(domainList: string[], certDomains: string[]): string[] { - const { matched } = optionsUtils.groupByDomain(domainList, certDomains); + isDomainMatched(domainList: string | string[], certDomains: string[]): boolean { + const matched = domainUtils.match(domainList, certDomains); return matched; } + + isNotChanged() { + const lastResult = this.ctx?.lastStatus?.status?.status; + return !this.ctx.inputChanged && lastResult === "success"; + } + + async getAutoMatchedTargets(req: { + targetName: string; + certDomains: string[]; + pageSize: number; + getDeployTargetList: (req: PageSearch) => Promise<{ list: CertTargetItem[]; total: number }>; + }): Promise { + const matchedDomains: CertTargetItem[] = []; + let pageNo = 1; + const { certDomains } = req; + + const pageSize = req.pageSize || 100; + while (true) { + const result = await req.getDeployTargetList({ + pageNo, + pageSize, + }); + const pageData = result.list; + this.logger.info(`获取到 ${pageData.length} 个 ${req.targetName}`); + + if (!pageData || pageData.length === 0) { + break; + } + + for (const item of pageData) { + const domainName = item.domain; + if (this.isDomainMatched(domainName, certDomains)) { + matchedDomains.push(item); + } + } + + const totalCount = result.total || 0; + if (pageNo * pageSize >= totalCount || matchedDomains.length == 0) { + break; + } + + pageNo++; + } + + return matchedDomains; + } + + async autoMatchedDeploy(req: { + targetName: string; + getCertDomains: () => string[]; + uploadCert: () => Promise; + deployOne: (req: { target: any; cert: any }) => Promise; + getDeployTargetList: (req: PageSearch) => Promise<{ list: CertTargetItem[]; total: number }>; + }): Promise<{ result: any; deployedList: any[] }> { + this.logger.info("证书匹配模式部署"); + const certDomains = req.getCertDomains(); + const certTargetList = await this.getAutoMatchedTargets({ + targetName: req.targetName, + pageSize: 200, + certDomains, + getDeployTargetList: req.getDeployTargetList, + }); + if (certTargetList.length === 0) { + this.logger.warn(`未找到匹配的${req.targetName}`); + return { result: "skip", deployedList: [] }; + } + this.logger.info(`找到 ${certTargetList.length} 个匹配的${req.targetName}`); + + //开始部署,检查是否已经部署过 + const deployedList = cloneDeep(this.getLastStatus()?.status?.output?.deployedList || []); + const unDeployedTargets = certTargetList.filter(item => !deployedList.includes(item.value)); + const count = unDeployedTargets.length; + const deployedCount = certTargetList.length - count; + if (deployedCount > 0) { + this.logger.info(`跳过 ${deployedCount} 个已部署过的${req.targetName}`); + } + this.logger.info(`需要部署 ${count} 个${req.targetName}`); + if (count === 0) { + return { result: "skip", deployedList }; + } + this.logger.info(`开始部署`); + const aliCrtId = await req.uploadCert(); + for (const target of unDeployedTargets) { + await req.deployOne({ + cert: aliCrtId, + target, + }); + deployedList.push(target.value); + } + this.logger.info(`本次成功部署 ${count} 个${req.targetName}`); + return { result: "success", deployedList }; + } } export type OutputVO = { diff --git a/packages/plugins/plugin-lib/src/cert/cert-reader.ts b/packages/plugins/plugin-lib/src/cert/cert-reader.ts index c4071ca8f..62132d5d4 100644 --- a/packages/plugins/plugin-lib/src/cert/cert-reader.ts +++ b/packages/plugins/plugin-lib/src/cert/cert-reader.ts @@ -43,6 +43,12 @@ const formats = { jks: ["jks"], p7b: ["p7b", "key"], }; + +export type SimpleCertDetail = { + notBefore: Date; + notAfter: Date; + domains: string[]; +}; export class CertReader { cert: CertInfo; @@ -116,6 +122,15 @@ export class CertReader { return CertReader.readCertDetail(crt); } + getSimpleDetail() { + const { detail } = this.getCrtDetail(); + return { + notBefore: detail.notBefore, + notAfter: detail.notAfter, + domains: this.getAllDomains(), + }; + } + static readCertDetail(crt: string) { const detail = crypto.readCertificateInfo(crt.toString()); const effective = detail.notBefore; diff --git a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue index 9c5952cfb..ccf1a1c44 100644 --- a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue +++ b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue @@ -235,7 +235,6 @@ watch( const { form } = value; const oldForm: any = oldValue?.form; let changed = oldForm == null || optionsRef.value.length == 0; - debugger; if (props.watches && props.watches.length > 0) { for (const key of props.watches) { if (oldForm && JSON.stringify(form[key]) != JSON.stringify(oldForm[key])) { diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts index b1732cfd3..1858255f9 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts @@ -1,6 +1,7 @@ -import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; +import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline'; import dayjs from 'dayjs'; import { + CertReader, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; @@ -9,18 +10,19 @@ import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; import { CertInfo } from '@certd/plugin-cert'; import { CertApplyPluginNames } from '@certd/plugin-cert'; import { optionsUtils } from "@certd/basic"; -import { AliyunClient, CasCertId } from "../../../plugin-lib/aliyun/lib/index.js"; +import { AliyunClient, AliyunSslClient, CasCertId } from "../../../plugin-lib/aliyun/lib/index.js"; @IsTaskPlugin({ name: 'DeployCertToAliyunDCDN', title: '阿里云-部署证书至DCDN', icon: 'svg:icon-aliyun', group: pluginGroups.aliyun.key, desc: '依赖证书申请前置任务,自动部署域名证书至阿里云DCDN', - default: { - strategy: { - runStrategy: RunStrategy.SkipWhenSucceed, - }, - }, + runStrategy: RunStrategy.AlwaysRun, + // default: { + // strategy: { + // runStrategy: RunStrategy.SkipWhenSucceed, + // }, + // }, }) export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { @TaskInput({ @@ -57,15 +59,15 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { @TaskInput({ title: '域名匹配模式', - helper: '选择域名匹配方式', + helper: '根据证书匹配:根据证书域名自动匹配DCDN加速域名自动部署,新增加速域名自动感知,自动新增部署', component: { - name: 'select', + name: 'a-select', options: [ { label: '手动选择', value: 'manual' }, { label: '根据证书匹配', value: 'auto' }, ], }, - default: 'manual', + value: 'manual', }) domainMatchMode!: 'manual' | 'auto'; @@ -79,7 +81,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { mergeScript: ` return { show: ctx.compute(({form})=>{ - return domainMatchMode === "manual" + return form.domainMatchMode === "manual" }) } `, @@ -87,44 +89,82 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { ) domainName!: string | string[]; + @TaskOutput({ + title: '已部署过的DCDN加速域名', + }) + deployedList!: string[]; + async onInstance() { } - async execute(): Promise { + async execute(): Promise { this.logger.info('开始部署证书到阿里云DCDN'); const access = (await this.getAccess(this.accessId)) as AliyunAccess; const client = await this.getClient(access); - - let domains: string[] = []; + const sslClient = new AliyunSslClient({ access, logger: this.logger }); + if (this.domainMatchMode === 'auto') { - this.logger.info('使用根据证书匹配模式'); - if (!this.certDomains || this.certDomains.length === 0) { - throw new Error('未获取到证书域名信息'); - } - domains = await this.getAutoMatchedDomains(this.certDomains); - if (domains.length === 0) { - this.logger.warn('未找到匹配的DCDN域名'); - return; - } - this.logger.info(`找到 ${domains.length} 个匹配的DCDN域名`); + const { result, deployedList } = await this.autoMatchedDeploy({ + targetName: 'DCDN加速域名', + uploadCert: async () => { + return await sslClient.uploadCertOrGet(this.cert); + }, + deployOne: async (req:{target:any,cert:any})=>{ + return await this.deployOne(client, req.target.value, req.cert); + }, + getCertDomains: ()=>{ + return this.getCertDomains(); + }, + getDeployTargetList: async (req: PageSearch)=>{ + return await this.onGetDomainList(req); + }, + }); + this.deployedList = deployedList; + return result; + } else { + if (this.isNotChanged()) { + this.logger.info('输入参数未变更,跳过'); + return "skip"; + } if (!this.domainName) { throw new Error('您还未选择DCDN域名'); } + let domains: string[] = []; domains = typeof this.domainName === 'string' ? [this.domainName] : this.domainName; + const aliCrtId = await sslClient.uploadCertOrGet(this.cert); + for (const domainName of domains) { + await this.deployOne(client, domainName, aliCrtId); + } } - for (const domainName of domains) { - this.logger.info(`[${domainName}]开始部署`) - const params = await this.buildParams(domainName); - await this.doRequest(client, params); - await this.ctx.utils.sleep(1000); - this.logger.info(`[${domainName}]部署成功`) - } this.logger.info('部署完成'); } + getCertDomains(): string[]{ + const casCert = this.cert as CasCertId; + const certInfo = this.cert as CertInfo; + if (casCert.certId) { + if (!casCert.detail){ + throw new Error('未获取到证书域名列表,请尝试强制重新运行一下流水线'); + } + return casCert.detail?.domains || []; + }else if (certInfo.crt){ + return new CertReader(certInfo).getSimpleDetail().domains || []; + }else{ + throw new Error('未获取到证书域名列表,请尝试强制重新运行一下流水线'); + } + } + + async deployOne(client: any, domainName: string, aliCrtId: CasCertId){ + this.logger.info(`[${domainName}]开始部署`) + const params = await this.buildParams(domainName, aliCrtId); + await this.doRequest(client, params); + await this.ctx.utils.sleep(1000); + this.logger.info(`[${domainName}]部署成功`) + } + async getClient(access: AliyunAccess) { const client = new AliyunClient({ logger: this.logger }); await client.init({ @@ -136,30 +176,9 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { return client; } - async buildParams(domainName: string) { + async buildParams(domainName: string, aliCrtId: CasCertId) { const CertName = (this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss'); - - let certId: any = this.cert - if (typeof this.cert === 'object') { - const certInfo = this.cert as CertInfo; - const casCertId = this.cert as CasCertId; - if (certInfo.crt) { - this.logger.info('上传证书:', CertName); - const cert: any = this.cert; - return { - DomainName: domainName, - SSLProtocol: 'on', - CertName: CertName, - CertType: 'upload', - SSLPub: cert.crt, - SSLPri: cert.key, - }; - }else if (casCertId.certId){ - certId = casCertId.certId; - }else{ - throw new Error('证书格式错误'+JSON.stringify(this.cert)); - } - } + const certId = aliCrtId.certId; this.logger.info('使用已上传的证书:', certId); return { DomainName: domainName, @@ -187,36 +206,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { } - async getAutoMatchedDomains(certDomains: string[]): Promise { - const matchedDomains: string[] = []; - let pageNumber = 1; - - while (true) { - const result = await this.onGetDomainList({ pageNo: pageNumber }); - const pageData = result.list; - this.logger.info(`获取到 ${pageData.length} 个DCDN域名`); - - if (!pageData || pageData.length === 0) { - break; - } - - const matched = this.getMatchedDomains(pageData, certDomains); - matchedDomains.push(...matched); - - const totalCount = result.total || 0; - if (pageNumber * 500 >= totalCount) { - break; - } - - pageNumber++; - } - - return matchedDomains; - } - - - - async onGetDomainList(data: PageSearch) { + async onGetDomainList(data: PageSearch): Promise<{list: CertTargetItem[], total: number}> { if (!this.accessId) { throw new Error('请选择Access授权'); } diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/upload-to-aliyun/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/upload-to-aliyun/index.ts index 98d4017e3..1ae805f98 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/upload-to-aliyun/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/upload-to-aliyun/index.ts @@ -99,11 +99,13 @@ export class UploadCertToAliyun extends AbstractTaskPlugin { endpoint, }); let certName = "" + const certReader = new CertReader(this.cert); if (this.name){ certName = this.appendTimeSuffix(this.name) }else { - certName = this.buildCertName(CertReader.getMainDomain(this.cert.crt)) + certName = this.buildCertName(certReader.getMainDomain()) } + const certIdRes = await client.uploadCertificate({ name: certName, cert: this.cert, diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts index d93aecc4b..acc06fc51 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts @@ -1,7 +1,7 @@ import { ILogger, utils } from "@certd/basic"; import { AliyunAccess } from "../access/index.js"; import { AliyunClient } from "./index.js"; -import { CertInfo, CertReader } from "@certd/plugin-lib"; +import { CertInfo, CertReader, SimpleCertDetail } from "@certd/plugin-lib"; export type AliyunCertInfo = { crt: string; //fullchain证书 @@ -37,6 +37,7 @@ export type CasCertId = { certId: number; certIdentifier: string; certName: string; + detail?: SimpleCertDetail; } export class AliyunSslClient { opts: AliyunSslClientOpts; @@ -119,10 +120,12 @@ export class AliyunSslClient { this.checkRet(ret); this.opts.logger.info("证书上传成功:aliyunCertId=", ret.CertId); //output + const certReader = new CertReader(req.cert as any); return { certId: ret.CertId, certName: req.name, certIdentifier: this.getCertIdentifier(ret.CertId), + detail:certReader.getSimpleDetail(), } } @@ -136,7 +139,8 @@ export class AliyunSslClient { const certInfo = cert as CertInfo; // 上传证书到阿里云 this.logger.info(`开始上传证书`); - const certName = CertReader.buildCertName(certInfo); + const certReader = new CertReader(certInfo); + const certName = certReader.buildCertName(); const res = await this.uploadCertificate({ name: certName, cert: certInfo @@ -151,7 +155,7 @@ export class AliyunSslClient { return { certId, certIdentifier, - certName + certName, } }