mirror of https://github.com/certd/certd
pref(plugin-volcengine): 新增火山引擎 CDN部署功能
parent
41e23fb6a8
commit
5d6f0d8546
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './plugin-deploy-to-cdn.js'
|
|
@ -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();
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue