perf: 支持部署证书到火山dcdn

pull/409/head
xiaojunnuo 2025-04-29 18:40:13 +08:00
parent a63d687f1c
commit 5f85219495
5 changed files with 248 additions and 15 deletions

View File

@ -1,4 +1,4 @@
import { domainUtils } from './util.domain.js'; import { domainUtils } from "./util.domain.js";
function groupByDomain(options: any[], inDomains: string[]) { function groupByDomain(options: any[], inDomains: string[]) {
const matched = []; const matched = [];
@ -19,16 +19,16 @@ function groupByDomain(options: any[], inDomains: string[]) {
function buildGroupOptions(options: any[], inDomains: string[]) { function buildGroupOptions(options: any[], inDomains: string[]) {
const grouped = groupByDomain(options, inDomains); const grouped = groupByDomain(options, inDomains);
const groupOptions = []; const groupOptions = [];
groupOptions.push({ value: 'matched', disabled: true, label: '----已匹配----' }); groupOptions.push({ value: "matched", disabled: true, label: "----已匹配----" });
if (grouped.matched.length === 0) { if (grouped.matched.length === 0) {
options.push({ value: '', disabled: true, label: '没有可以匹配的域名' }); options.push({ value: "", disabled: true, label: "没有可以匹配的域名" });
} else { } else {
for (const matched of grouped.matched) { for (const matched of grouped.matched) {
groupOptions.push(matched); groupOptions.push(matched);
} }
} }
if (grouped.notMatched.length > 0) { if (grouped.notMatched.length > 0) {
groupOptions.push({ value: 'unmatched', disabled: true, label: '----未匹配----' }); groupOptions.push({ value: "unmatched", disabled: true, label: "----未匹配----" });
for (const notMatched of grouped.notMatched) { for (const notMatched of grouped.notMatched) {
groupOptions.push(notMatched); groupOptions.push(notMatched);
} }

View File

@ -37,6 +37,7 @@ export function createRemoteSelectInputDefine(opts?: {
multi?: boolean; multi?: boolean;
required?: boolean; required?: boolean;
rules?: any; rules?: any;
mergeScript?: string;
}) { }) {
const title = opts?.title || "请选择"; const title = opts?.title || "请选择";
const certDomainsInputKey = opts?.certDomainsInputKey || "certDomains"; const certDomainsInputKey = opts?.certDomainsInputKey || "certDomains";
@ -66,7 +67,9 @@ export function createRemoteSelectInputDefine(opts?: {
}, },
rules: opts?.rules, rules: opts?.rules,
required: opts.required ?? true, required: opts.required ?? true,
mergeScript: ` mergeScript:
opts.mergeScript ??
`
return { return {
component:{ component:{
form: ctx.compute(({form})=>{ form: ctx.compute(({form})=>{
@ -80,3 +83,5 @@ export function createRemoteSelectInputDefine(opts?: {
return merge(item, opts?.formItem); return merge(item, opts?.formItem);
} }

View File

@ -53,11 +53,15 @@ const getOptions = async () => {
if (!define) { if (!define) {
return; return;
} }
const pluginType = getPluginType();
const { form } = getScope();
const input = pluginType === "plugin" ? form.input : form;
for (let key in define.input) { for (let key in define.input) {
const inWatches = props.watches.includes(key); const inWatches = props.watches.includes(key);
const inputDefine = define.input[key]; const inputDefine = define.input[key];
if (inWatches && inputDefine.required) { if (inWatches && inputDefine.required) {
const value = props.form[key]; const value = input[key];
if (value == null || value === "") { if (value == null || value === "") {
console.log("remote-select required", key); console.log("remote-select required", key);
return; return;
@ -69,8 +73,6 @@ const getOptions = async () => {
hasError.value = false; hasError.value = false;
loading.value = true; loading.value = true;
optionsRef.value = []; optionsRef.value = [];
const { form } = getScope();
const pluginType = getPluginType();
try { try {
const res = await doRequest( const res = await doRequest(
@ -78,7 +80,7 @@ const getOptions = async () => {
type: pluginType, type: pluginType,
typeName: form.type, typeName: form.type,
action: props.action, action: props.action,
input: pluginType === "plugin" ? form.input : form, input,
}, },
{ {
onError(err: any) { onError(err: any) {
@ -115,11 +117,16 @@ async function refreshOptions() {
watch( watch(
() => { () => {
const values = []; const values = [];
const pluginType = getPluginType();
const { form } = getScope();
const input = pluginType === "plugin" ? form.input : form;
for (const item of props.watches) { for (const item of props.watches) {
values.push(props.form[item]); values.push(input[item]);
} }
return { return {
form: props.form, form: input,
watched: values, watched: values,
}; };
}, },

View File

@ -0,0 +1,204 @@
import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from "@certd/pipeline";
import {createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
import {CertApplyPluginNames, CertInfo, CertReader} from "@certd/plugin-cert";
import {VolcengineAccess} from "../access.js";
import {VolcengineClient} from "../ve-client.js";
@IsTaskPlugin({
name: "VolcengineDeployToDCDN",
title: "火山引擎-部署证书至DCDN",
icon: "svg:icon-volcengine",
group: pluginGroups.volcengine.key,
desc: "部署至火山引擎全站加速",
// showRunStrategy: true,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
export class VolcengineDeployToDCDN extends AbstractTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
},
required: true
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({props: {required: false}}))
certDomains!: string[];
@TaskInput({
title: "自动匹配",
helper: "是否根据证书自动匹配合适的DCDN域名进行部署",
value: false,
component: {
name: "a-switch",
type: "checked"
},
required: true
})
autoMatch!: boolean;
@TaskInput({
title: "Access授权",
helper: "火山引擎AccessKeyId、AccessKeySecret",
component: {
name: "access-selector",
type: "volcengine"
},
required: true
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: "DCDN域名",
helper: "选择要部署证书的DCDN域名",
action: VolcengineDeployToDCDN.prototype.onGetDomainList.name,
watches: ["certDomains", "accessId"],
required: true,
mergeScript: `
return {
show: ctx.compute(({form})=>{
return !form.autoMatch
})
}
`
})
)
domainList!: string | string[];
async onInstance() {
}
async uploadCert(client: VolcengineClient) {
const certService = await client.getCertCenterService();
let certId = this.cert;
if (typeof certId !== "string") {
const certInfo = this.cert as CertInfo;
this.logger.info(`开始上传证书`);
certId = await certService.ImportCertificate({
certName: this.appendTimeSuffix("certd"),
cert: certInfo
});
this.logger.info(`上传证书成功:${certId}`);
} else {
this.logger.info(`使用已有证书ID:${certId}`);
}
return certId
}
async execute(): Promise<void> {
this.logger.info("开始部署证书到火山引擎DCDN");
const client = await this.getClient();
let certId = await this.uploadCert(client);
const service = await client.getDCDNService();
this.certDomains = new CertReader(this.cert).getAllDomains()
let domainList = this.domainList
if (!this.autoMatch) {
//手动根据域名部署
if (!this.domainList || this.domainList.length === 0) {
throw new Error("域名列表不能为空");
}
} else {
//自动匹配
const options = await this.getDomainOptions(service);
const grouped = this.ctx.utils.options.groupByDomain(options, this.certDomains);
const matched = grouped.matched
domainList = matched.map(item => item.domain)
if (domainList.length === 0) {
this.logger.warn("没有匹配到域名,跳过部署")
this.logger.info("当前证书域名:", this.certDomains)
this.logger.info("当前DCDN域名", grouped.notMatched.map(item => item.domain))
return
}
}
//域名十个十个的分割
for (let i = 0; i < domainList.length; i += 10) {
const batch = domainList.slice(i, i + 10);
this.logger.info(`开始部署证书到域名:${batch}`);
const res = await service.request({
action: "CreateCertBind",
method: "POST",
body: {
"DomainNames": batch,
"CertSource": "volc",
"CertId": certId
},
version: "2021-04-01"
});
this.logger.info(`部署证书到域名成功:`,JSON.stringify(res));
}
this.logger.info("部署完成");
}
async getClient() {
const access = await this.getAccess<VolcengineAccess>(this.accessId);
return new VolcengineClient({
logger: this.logger,
access,
http: this.http
})
}
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error("请选择Access授权");
}
const client = await this.getClient();
const service = await client.getDCDNService();
const options = await this.getDomainOptions(service);
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
}
private async getDomainOptions(service: any) {
const res = await service.request({
method: "POST",
action: "DescribeUserDomains",
body: {
"PageSize": 1000
}
})
const list = res.Result?.Domains;
if (!list || list.length === 0) {
throw new Error("找不到DCDN域名您也可以手动输入域名");
}
const options = list.map((item: any) => {
return {
value: item.Domain,
label: `${item.Domain}<${item.Scope}>`,
domain: item.Domain
};
});
return options;
}
}
new VolcengineDeployToDCDN();

View File

@ -100,6 +100,19 @@ export class VolcengineClient {
return service; return service;
} }
async getDCDNService( opts?: { }) {
const CommonService = await this.getServiceCls();
const service = new CommonService({
serviceName: "dcdn",
defaultVersion: "2023-01-01"
});
service.setAccessKeyId(this.opts.access.accessKeyId);
service.setSecretKey(this.opts.access.secretAccessKey);
service.setRegion("cn-north-1");
return service;
}
async getServiceCls() { async getServiceCls() {
if (this.CommonService) { if (this.CommonService) {
return this.CommonService; return this.CommonService;
@ -114,11 +127,11 @@ export class VolcengineClient {
defaultVersion: string; defaultVersion: string;
}) { }) {
super(Object.assign({ host: "open.volcengineapi.com" }, options)); super(Object.assign({ host: "open.volcengineapi.com" }, options));
this.Generic = async (req: { action: string, body?: any, method?: string, query?: any }) => { this.Generic = async (req: { action: string, body?: any, method?: string, query?: any ,version?:string}) => {
const { action, method, body, query } = req; const { action, method, body, query,version } = req;
return await this.fetchOpenAPI({ return await this.fetchOpenAPI({
Action: action, Action: action,
Version: options.defaultVersion, Version: version||options.defaultVersion,
method: method as any, method: method as any,
headers: { headers: {
"content-type": "application/json" "content-type": "application/json"
@ -129,8 +142,11 @@ export class VolcengineClient {
}; };
} }
async request(req: { action: string, body?: any, method?: string, query?: any }) { async request(req: { action: string, body?: any, method?: string, query?: any,version?:string }) {
const res = await this.Generic(req); const res = await this.Generic(req);
if (res ==="Not Found"){
throw new Error(`${res} (检查method)`);
}
if (res.errorcode) { if (res.errorcode) {
throw new Error(`${res.errorcode}:${res.message}`); throw new Error(`${res.errorcode}:${res.message}`);
} }
@ -146,4 +162,5 @@ export class VolcengineClient {
} }
} }