pref(plugin-volcengine): 新增火山引擎 CDN部署功能

pull/361/head
xiaojunnuo 2025-03-30 00:30:42 +08:00
parent 41e23fb6a8
commit 5d6f0d8546
6 changed files with 314 additions and 185 deletions

View File

@ -0,0 +1,60 @@
import { VolcengineOpts } from "./dns-client.js";
import { CertInfo } from "@certd/plugin-cert";
export class VolcengineCdnClient {
opts: VolcengineOpts;
service: any;
constructor(opts: VolcengineOpts) {
this.opts = opts;
}
async getCdnClient() {
if (this.service) {
return this.service;
}
const { cdn } = await import("@volcengine/openapi");
const service = new cdn.CdnService();
// 设置ak、sk
service.setAccessKeyId(this.opts.access.accessKeyId);
service.setSecretKey(this.opts.access.secretAccessKey);
this.service = service;
return service;
}
async uploadCert(cert: CertInfo, certName: string) {
const service = await this.getCdnClient();
const res = await service.Generic("AddCertificate", {
Source: "volc_cert_center",
CertType: "server_cert",
Certificate: cert.crt,
PrivateKey: cert.key,
EncryType: "inter_cert",
Repeatable: false,
Desc: certName
});
if (res.ResponseMetadata?.Error) {
if (res.ResponseMetadata?.Error?.Code?.includes("Duplicated")) {
// 证书已存在ID为 cert-16293a8524844a3e8e30ed62f8e5bc94。
const message = res.ResponseMetadata?.Error?.Message
const reg = /ID为 (\S+)。/;
const certId = message.match(reg)?.[1]
if (certId) {
this.opts.logger.info(`证书已存在ID为 ${certId}`);
return certId;
}
}
throw new Error(JSON.stringify(res.ResponseMetadata?.Error));
}
const certId = res.Result.CertId
this.opts.logger.info(`上传证书成功:${certId}`)
return certId;
}
}

View File

@ -1,182 +0,0 @@
import { VolcengineAccess } from "./access.js";
import { http, HttpClient, ILogger } from "@certd/basic";
import querystring from "querystring";
export type VolcengineOpts = {
access: VolcengineAccess
logger: ILogger
http: HttpClient
}
export type VolcengineReq = {
method?: string;
path?: string;
headers?: any;
body?: any;
query?: any;
service?: string, // 替换为实际服务名称
region?: string, // 替换为实际区域名称
}
export class VolcengineClient {
opts: VolcengineOpts;
constructor(opts: VolcengineOpts) {
this.opts = opts;
}
// // 生成签名函数
// async createSignedRequest(req: VolcengineReq) {
// if (!req.body) {
// req.body = {};
// }
// const bodyStr = JSON.stringify(req.body);
// const { method, path, body, query } = req;
// const crypto = await import("crypto");
// const config = {
// accessKeyId: this.opts.access.accessKeyId,
// secretKey: this.opts.access.secretAccessKey,
// service: req.service || "dns", // 默认服务名称为 dns
// region: req.region || "cn-beijing", // 默认区域名称为 cn-beijing
// endpoint: "https://open.volcengineapi.com"
// };
//
// // 1. 生成时间戳
// const now = new Date();
// // 20201103T104027Z
// const timestamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
//
// // 2. 处理查询参数
// const sortedQuery = Object.keys(query || {})
// .sort()
// .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`)
// .join("&");
//
// // 3. 构造规范请求
// const canonicalRequest = [
// method.toUpperCase(),
// path || "/",
// sortedQuery,
// `content-type:application/json\nhost:${new URL(config.endpoint).host}`,
// "content-type;host",
// crypto.createHash("sha256").update(bodyStr).digest("hex")
// ].join("\n");
//
// // 4. 生成签名字符串
// const date = now.toISOString().substring(0, 10).replace(/-/g, "");
// const credentialScope = `${date}/${config.region}/${config.service}/request`;
//
// const stringToSign = [
// "HMAC-SHA256",
// timestamp,
// credentialScope,
// crypto.createHash("sha256").update(canonicalRequest).digest("hex")
// ].join("\n");
//
// // 5. 计算签名
// const sign = (key: Buffer, msg: string) => crypto.createHmac("sha256", key).update(msg).digest();
//
// const kDate = sign(Buffer.from(`HMAC${config.secretKey}`, "utf8"), date);
// const kRegion = sign(kDate, config.region);
// const kService = sign(kRegion, config.service);
// const kSigning = sign(kService, "request");
// const signature = crypto.createHmac("sha256", kSigning)
// .update(stringToSign)
// .digest("hex");
//
// // 6. 构造请求头
// const headers = {
// "Content-Type": "application/json",
// Host: new URL(config.endpoint).host,
// "X-Date": timestamp,
// Authorization: `HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=content-type;host, Signature=${signature}`
// };
//
// return {
// method,
// url: `${config.endpoint}${path || ""}${sortedQuery ? `?${sortedQuery}` : ""}`,
// headers,
// data: body
// };
// }
//
// async doRequest(req: VolcengineReq) {
// const requestConfig = await this.createSignedRequest(req);
// try {
// const res = await this.opts.http.request(requestConfig);
// if (res?.ResponseMetadata?.Error) {
// throw new Error(JSON.stringify(res.ResponseMetadata.Error));
// }
// return res;
// } catch (e) {
// if (e?.response?.ResponseMetadata.Error) {
// throw new Error(JSON.stringify(e.response.ResponseMetadata.Error));
// }
// throw e;
// }
// }
async doRequest(req: VolcengineReq) {
const {Signer} =await import('@volcengine/openapi') ;
// http request data
const openApiRequestData: any = {
region: req.region,
method: req.method,
// [optional] http request url query
params: {
...req.query,
},
// http request headers
headers: {
"Content-Type": "application/json",
},
// [optional] http request body
body: req.body,
}
const signer = new Signer(openApiRequestData, req.service);
// sign
signer.addAuthorization({accessKeyId:this.opts.access.accessKeyId, secretKey:this.opts.access.secretAccessKey});
// Print signed headers
console.log(openApiRequestData.headers);
const url = `https://open.volcengineapi.com/?${querystring.stringify(req.query)}`
const res = await http.request({
url: url,
method: req.method,
headers: openApiRequestData.headers,
data:req.body
});
if (res?.ResponseMetadata?.Error) {
throw new Error(JSON.stringify(res.ResponseMetadata.Error));
}
return res
}
// 列出域名解析记录
async findDomain(domain: string) {
const req: VolcengineReq = {
method: "POST",
region: "cn-beijing",
service: "dns",
query: {
Action: "ListZones",
Version: "2018-08-01",
},
body:{
Key: domain,
SearchMode: "exact"
}
};
return this.doRequest(req);
}
}

View File

@ -0,0 +1,106 @@
import { VolcengineAccess } from "./access.js";
import { http, HttpClient, ILogger } from "@certd/basic";
import querystring from "querystring";
export type VolcengineOpts = {
access: VolcengineAccess
logger: ILogger
http: HttpClient
}
export type VolcengineReq = {
method?: string;
path?: string;
headers?: any;
body?: any;
query?: any;
service?: string, // 替换为实际服务名称
region?: string, // 替换为实际区域名称
}
export class VolcengineDnsClient {
opts: VolcengineOpts;
constructor(opts: VolcengineOpts) {
this.opts = opts;
}
async doRequest(req: VolcengineReq) {
const {Signer} =await import('@volcengine/openapi') ;
// http request data
const openApiRequestData: any = {
region: req.region,
method: req.method,
// [optional] http request url query
params: {
...req.query,
},
// http request headers
headers: {
"Content-Type": "application/json",
},
// [optional] http request body
body: req.body,
}
const signer = new Signer(openApiRequestData, req.service);
// sign
signer.addAuthorization({accessKeyId:this.opts.access.accessKeyId, secretKey:this.opts.access.secretAccessKey});
// Print signed headers
console.log(openApiRequestData.headers);
const url = `https://open.volcengineapi.com/?${querystring.stringify(req.query)}`
try{
const res = await http.request({
url: url,
method: req.method,
headers: openApiRequestData.headers,
data:req.body
});
if (res?.ResponseMetadata?.Error) {
const err = new Error(JSON.stringify(res.ResponseMetadata.Error));
// @ts-ignore
err.detail = res.ResponseMetadata.Error;
throw err
}
return res
}catch (e) {
if(e.response){
const err = new Error(JSON.stringify(e.response.data.ResponseMetadata.Error));
// @ts-ignore
err.detail = e.response.data.ResponseMetadata.Error;
throw err
}
}
}
// 列出域名解析记录
async findDomain(domain: string) {
const req: VolcengineReq = {
method: "POST",
region: "cn-beijing",
service: "dns",
query: {
Action: "ListZones",
Version: "2018-08-01",
},
body:{
Key: domain,
SearchMode: "exact"
}
};
return this.doRequest(req);
}
}

View File

@ -0,0 +1 @@
export * from './plugin-deploy-to-cdn.js'

View File

@ -0,0 +1,144 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { VolcengineAccess } from "../access.js";
import { VolcengineCdnClient } from "../cdn-client.js";
@IsTaskPlugin({
name: 'VolcengineDeployToCDN',
title: '火山引擎-部署证书至CDN',
icon: 'svg:icon-volcengine',
group: pluginGroups.volcengine.key,
desc: '支持网页,文件下载,音视频点播',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class VolcengineDeployToCDN extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames, 'VolcengineUploadCert'],
},
required: true,
})
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '火山引擎AccessKeyId、AccessKeySecret',
component: {
name: 'access-selector',
type: 'volcengine',
},
required: true,
})
accessId!: string;
@TaskInput({
title: '服务类型',
helper: '网页,文件下载,音视频点播',
component: {
name: 'a-select',
options:[
{ label: "网页", value: "web" },
{ label: "文件下载", value: "download" },
{ label: "音视频点播", value: 'video'},
]
},
value: 'web',
required: true,
})
serviceType:string = "web"
@TaskInput(
createRemoteSelectInputDefine({
title: 'CDN加速域名',
helper: '你在火山引擎上配置的CDN加速域名比如:certd.docmirror.cn',
action: VolcengineDeployToCDN.prototype.onGetDomainList.name,
watches: ['certDomains', 'accessId', 'serviceType'],
required: true,
})
)
domainName!: string | string[];
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到火山引擎CDN');
const access = await this.accessService.getById<VolcengineAccess>(this.accessId);
const client = await this.getClient(access)
const service = await client.getCdnClient()
let certId = this.cert
if (typeof certId !== 'string') {
const certInfo = this.cert as CertInfo
this.logger.info(`开始上传证书`)
certId = await client.uploadCert(certInfo, this.appendTimeSuffix('certd'))
}
for (const domain of this.domainName) {
this.logger.info(`开始部署域名${domain}证书`)
await service.UpdateCdnConfig({
Domain: domain,
HTTPS: {
CertInfo: { CertId: certId },
}
})
this.logger.info(`部署域名${domain}证书成功`);
await this.ctx.utils.sleep(1000)
}
this.logger.info('部署完成');
}
async getClient(access: VolcengineAccess) {
return new VolcengineCdnClient({
logger: this.logger,
access,
http:this.http
})
}
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
const access = await this.accessService.getById<VolcengineAccess>(this.accessId);
const client = await this.getClient(access);
const service = await client.getCdnClient()
const res = await service.ListCdnDomains({
ServiceType: this.serviceType,
PageNum: 1,
PageSize: 100,
})
// @ts-ignore
const list = res?.Result?.Data
if (!list || list.length === 0) {
throw new Error('找不到加速域名,您可以手动输入');
}
const options = list.map((item: any) => {
return {
value: item.Domain,
label: item.Domain,
domain: item.Domain,
};
});
return optionsUtils.buildGroupOptions(options, this.certDomains);
}
}
new VolcengineDeployToCDN();

View File

@ -1,7 +1,7 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { Autowire } from "@certd/pipeline";
import { VolcengineClient } from "./client.js";
import { VolcengineDnsClient } from "./dns-client.js";
import { VolcengineAccess } from "./access.js";
@IsDnsProvider({
@ -12,13 +12,13 @@ import { VolcengineAccess } from "./access.js";
icon: "svg:icon-volcengine"
})
export class VolcengineDnsProvider extends AbstractDnsProvider {
client: VolcengineClient;
client: VolcengineDnsClient;
@Autowire()
access!: VolcengineAccess;
async onInstance() {
this.client = new VolcengineClient({
this.client = new VolcengineDnsClient({
access: this.access,
logger: this.logger,
http: this.http