From 93e9498b410353f504e11e264db62468895d7290 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Tue, 15 Jul 2025 15:05:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=81=E6=B0=B4?= =?UTF-8?q?=E7=BA=BF=E9=A1=B5=E9=9D=A2=E7=8A=B6=E6=80=81=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/plugin-lib/src/oss/impls/ssh.ts | 4 + .../views/certd/pipeline/pipeline/index.vue | 8 +- .../src/plugins/plugin-host/plugin/index.ts | 1 + .../plugin/plugin-upload-to-oss.ts | 274 ++++++++++++++++++ .../plugins/plugin-deploy-to-mail.ts | 85 +++++- 5 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-host/plugin/plugin-upload-to-oss.ts diff --git a/packages/plugins/plugin-lib/src/oss/impls/ssh.ts b/packages/plugins/plugin-lib/src/oss/impls/ssh.ts index c2f23981..0e2d076f 100644 --- a/packages/plugins/plugin-lib/src/oss/impls/ssh.ts +++ b/packages/plugins/plugin-lib/src/oss/impls/ssh.ts @@ -16,6 +16,10 @@ export default class SshOssClientImpl extends BaseOssClient { throw new Error("Method not implemented."); } async upload(filePath: string, fileContent: Buffer) { + if (!filePath) { + filePath = ""; + } + filePath = filePath.trim(); const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath); // Write file to temp path diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue index 485e25d1..a0326e65 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue @@ -374,6 +374,7 @@ export default defineComponent({ const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id }); currentHistory.value.logs = detail.logs; currentHistory.value.pipeline = detail.pipeline; + currentHistory.value.status = detail.pipeline.status.result; }; const changeCurrentHistory = async (history?: RunHistory) => { if (!history) { @@ -385,7 +386,7 @@ export default defineComponent({ currentHistory.value = history; await loadCurrentHistoryDetail(); pipeline.value = currentHistory.value.pipeline; - currentPipeline.value = cloneDeep(pipeline.value); + currentPipeline.value = currentHistory.value.pipeline; }; async function loadHistoryList(reload = false) { @@ -441,6 +442,11 @@ export default defineComponent({ if (currentHistory.value != null) { if (currentHistory.value.pipeline?.status?.status === "start") { await loadCurrentHistoryDetail(); + pipeline.value = currentHistory.value.pipeline; + // if (currentHistory.value.pipeline?.status?.status !== "start") { + // 不传true好像不会刷新 + // await loadHistoryList(true); + // } } } } catch (e) { diff --git a/packages/ui/certd-server/src/plugins/plugin-host/plugin/index.ts b/packages/ui/certd-server/src/plugins/plugin-host/plugin/index.ts index 6bb828b9..bb1e4d36 100644 --- a/packages/ui/certd-server/src/plugins/plugin-host/plugin/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-host/plugin/index.ts @@ -1,3 +1,4 @@ export * from './host-shell-execute/index.js'; export * from './upload-to-host/index.js'; export * from './copy-to-local/index.js' +export * from './plugin-upload-to-oss.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-host/plugin/plugin-upload-to-oss.ts b/packages/ui/certd-server/src/plugins/plugin-host/plugin/plugin-upload-to-oss.ts new file mode 100644 index 00000000..0fc92151 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-host/plugin/plugin-upload-to-oss.ts @@ -0,0 +1,274 @@ +import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from '@certd/pipeline'; +import {CertInfo} from "@certd/plugin-cert"; +import {ossClientFactory} from "@certd/plugin-lib"; +import {utils} from "@certd/basic"; + +@IsTaskPlugin({ + name: 'UploadCertToOss', + title: '上传证书到对象存储OSS', + icon: 'ri:rest-time-line', + desc: '支持阿里云OSS、腾讯云COS、七牛云KODO、S3、MinIO、FTP、SFTP', + group: pluginGroups.other.key, + showRunStrategy:false, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class UploadCertToOssPlugin extends AbstractTaskPlugin { + + @TaskInput({ + title: '域名证书', + helper: '请选择前置任务输出的域名证书', + component: { + name: 'output-selector', + from: [":cert:"], + }, + required: true, + }) + cert!: CertInfo; + + + @TaskInput({ + title: 'OSS类型', + component: { + name: 'a-select', + vModel:"value", + options: [ + { label: "阿里云OSS", value: "alioss" }, + { label: "腾讯云COS", value: "tencentcos" }, + { label: "七牛OSS", value: "qiniuoss" }, + { label: "S3/Minio", value: "s3" }, + { label: "SFTP", value: "sftp" }, + { label: "FTP", value: "ftp" }, + ] + }, + required: true, + }) + uploaderType!: string; + + + @TaskInput({ + title: 'OSS授权', + component: { + name: 'access-selector', + }, + required: true, + mergeScript: ` + return { + component: { + type: ctx.compute(({form})=>{ + return form.uploaderType; + }) + } + } + `, + }) + accessId!: string; + + + @TaskInput({ + title: '证书格式', + helper: '要部署的证书格式,支持pem、pfx、der、jks', + component: { + name: 'a-select', + options: [ + { value: 'pem', label: 'pem(crt),Nginx等大部分应用' }, + { value: 'pfx', label: 'pfx,一般用于IIS' }, + { value: 'der', label: 'der,一般用于Apache' }, + { value: 'jks', label: 'jks,一般用于JAVA应用' }, + { value: 'one', label: '证书私钥一体,crt+key简单合并为一个pem文件' }, + ], + }, + required: true, + }) + certType!: string; + + @TaskInput({ + title: '证书保存路径', + helper: '路径要包含证书文件名,例如:/tmp/cert.pem', + component: { + placeholder: '/root/deploy/nginx/full_chain.pem', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'pem'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + crtPath!: string; + @TaskInput({ + title: '私钥保存路径', + helper: '原本的私钥保存路径,需要有写入权限,路径要包含私钥文件名,例如:/tmp/cert.key', + component: { + placeholder: '/root/deploy/nginx/cert.key', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'pem'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + keyPath!: string; + + @TaskInput({ + title: '中间证书保存路径', + helper: '路径要包含文件名,一般情况传上面两个文件即可,极少数情况需要这个中间证书', + component: { + placeholder: '/root/deploy/nginx/intermediate.pem', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'pem'; + }) + } + `, + rules: [{ type: 'filepath' }], + }) + icPath!: string; + + @TaskInput({ + title: 'PFX证书保存路径', + helper: '路径要包含证书文件名,例如:D:\\iis\\cert.pfx', + component: { + placeholder: 'D:\\iis\\cert.pfx', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'pfx'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + pfxPath!: string; + + @TaskInput({ + title: 'DER证书保存路径', + helper: '路径要包含证书文件名,例如:/tmp/cert.der', + component: { + placeholder: '/root/deploy/apache/cert.der', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'der'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + derPath!: string; + + @TaskInput({ + title: 'jks证书保存路径', + helper: '路径要包含证书文件名,例如:/tmp/cert.jks', + component: { + placeholder: '/root/deploy/java_app/cert.jks', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'jks'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + jksPath!: string; + + @TaskInput({ + title: '一体证书保存路径', + helper: '路径要包含证书文件名,例如:/tmp/crt_key.pem', + component: { + placeholder: '/app/crt_key.pem', + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.certType === 'one'; + }) + } + `, + required: true, + rules: [{ type: 'filepath' }], + }) + onePath!: string; + + + async onInstance() {} + async execute(): Promise { + const { accessId } = this; + let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath, onePath } = this; + if (!accessId) { + throw new Error('OSS授权配置不能为空'); + } + + const uploaderType = this.uploaderType + const uploaderAccess = this.accessId + + const httpUploaderContext = { + accessService: this.ctx.accessService, + logger: this.logger, + utils, + }; + + const access = await this.getAccess(uploaderAccess); + this.logger.info("上传方式", uploaderType); + const httpUploader = await ossClientFactory.createOssClientByType(uploaderType, { + access, + rootDir: "", + ctx: httpUploaderContext, + }); + + + this.logger.info('准备上传文件到OSS'); + + if (crtPath) { + await httpUploader.upload(crtPath, Buffer.from(this.cert.crt)) + this.logger.info(`上传证书:${crtPath}`); + } + if (keyPath) { + await httpUploader.upload(keyPath, Buffer.from(this.cert.key)) + this.logger.info(`上传私钥:${keyPath}`); + } + if (icPath) { + await httpUploader.upload(icPath, Buffer.from(this.cert.ic)) + this.logger.info(`上传中间证书:${icPath}`); + } + if (pfxPath) { + await httpUploader.upload(pfxPath, Buffer.from(this.cert.pfx, "base64")) + this.logger.info(`上传PFX证书:${pfxPath}`); + } + if (derPath) { + await httpUploader.upload(derPath, Buffer.from(this.cert.der, "base64")) + this.logger.info(`上传DER证书:${derPath}`); + } + if (this.jksPath) { + await httpUploader.upload(jksPath,Buffer.from(this.cert.jks, "base64")) + this.logger.info(`上传jks证书:${jksPath}`); + } + + if (onePath) { + await httpUploader.upload(onePath, Buffer.from(this.cert.one)) + this.logger.info(`上传一体证书:${onePath}`); + } + + this.logger.info('上传文件成功'); + } +} +new UploadCertToOssPlugin(); diff --git a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts index 73a2a85a..5c4e962d 100644 --- a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts +++ b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts @@ -50,6 +50,60 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin { }) email!: string[]; + + /** + * title: + * title: 邮件标题 + * helper: |- + * 请输入邮件标题否则将使用默认标题 + * 域名:${certDomains} + * component: + * name: a-input + * required: false + * template: + * title: 邮件模版 + * helper: |- + * 请输入模版内容否则将使用默认模版 + * 域名:${certDomains} + * value: |- + * 尊敬的用户你好: + * 以下是域名(${certDomains})证书文件 + * component: + * name: a-textarea + * autosize: + * minRows: 6 + * maxRows: 10 + * required: false + */ + + + @TaskInput({ + title: '邮件标题', + component: { + name: 'a-input', + vModel: 'value', + }, + helper: '请输入邮件标题否则将使用默认标题\n模板变量在标题中也可以使用', + required: false, + }) + title!: string; + + @TaskInput({ + title: '邮件模版', + component: { + name: 'a-textarea', + vModel: 'value', + autosize: { + minRows: 6, + maxRows: 10, + }, + }, + helper: `请输入模版内容否则将使用默认模版 +变量:主域名=$\{mainDomain}、全部域名=$\{domains}、过期时间=$\{expiresTime}、备注=$\{remark}`, + required: false, + }) + template!: string; + @TaskInput({ title: '备注', component: { @@ -67,15 +121,33 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin { const certReader = new CertReader(this.cert) const mainDomain = certReader.getMainDomain(); const domains = certReader.getAllDomains().join(','); - const title = `证书申请成功【${mainDomain}】`; - const html = ` + + + const data = { + mainDomain, + domains, + expiresTime: dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss"), + remark:this.remark + } + + let title = `证书申请成功【${mainDomain}】`; + let html = `

证书申请成功

域名:${domains}

-

证书有效期:${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")}

+

证书有效期:${data.expiresTime}

备注:${this.remark||""}

`; + + if (this.title) { + const compile = this.compile(this.title); + title = compile(data); + } + if (this.template) { + const compile = this.compile(this.template); + html = compile(data); + } const file = this.certZip if (!file) { throw new Error('证书压缩文件还未生成,重新运行证书任务'); @@ -91,8 +163,13 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin { }, ], }) + } - + compile(templateString:string) { + return new Function('data', ` with(data || {}) { + return \`${templateString}\`; + } + `); } } new DeployCertToMailPlugin();