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 { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||||
import { Autowire } from "@certd/pipeline";
|
import { Autowire } from "@certd/pipeline";
|
||||||
|
|
||||||
import { VolcengineClient } from "./client.js";
|
import { VolcengineDnsClient } from "./dns-client.js";
|
||||||
import { VolcengineAccess } from "./access.js";
|
import { VolcengineAccess } from "./access.js";
|
||||||
|
|
||||||
@IsDnsProvider({
|
@IsDnsProvider({
|
||||||
|
@ -12,13 +12,13 @@ import { VolcengineAccess } from "./access.js";
|
||||||
icon: "svg:icon-volcengine"
|
icon: "svg:icon-volcengine"
|
||||||
})
|
})
|
||||||
export class VolcengineDnsProvider extends AbstractDnsProvider {
|
export class VolcengineDnsProvider extends AbstractDnsProvider {
|
||||||
client: VolcengineClient;
|
client: VolcengineDnsClient;
|
||||||
@Autowire()
|
@Autowire()
|
||||||
access!: VolcengineAccess;
|
access!: VolcengineAccess;
|
||||||
|
|
||||||
|
|
||||||
async onInstance() {
|
async onInstance() {
|
||||||
this.client = new VolcengineClient({
|
this.client = new VolcengineDnsClient({
|
||||||
access: this.access,
|
access: this.access,
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
http: this.http
|
http: this.http
|
||||||
|
|
Loading…
Reference in New Issue