mirror of
https://github.com/certd/certd.git
synced 2026-04-16 14:00:51 +08:00
pref: 调整插件目录,增加一些帮助说明
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
import { Config, Configuration, Logger } from '@midwayjs/decorator';
|
||||
import { ILogger } from '@midwayjs/logger';
|
||||
import { IMidwayContainer } from '@midwayjs/core';
|
||||
import { Cron } from './cron';
|
||||
|
||||
// ... (see below) ...
|
||||
@Configuration({
|
||||
namespace: 'cron',
|
||||
//importConfigs: [join(__dirname, './config')],
|
||||
})
|
||||
export class CronConfiguration {
|
||||
@Config()
|
||||
config;
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
cron: Cron;
|
||||
async onReady(container: IMidwayContainer) {
|
||||
this.logger.info('cron start');
|
||||
this.cron = new Cron({
|
||||
logger: this.logger,
|
||||
...this.config,
|
||||
});
|
||||
container.registerObject('cron', this.cron);
|
||||
this.logger.info('cron started');
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import cron from 'node-cron';
|
||||
export type CronTask = {
|
||||
/**
|
||||
* 为空则为单次执行
|
||||
*/
|
||||
cron: string;
|
||||
job: () => Promise<void>;
|
||||
name: string;
|
||||
};
|
||||
export class Cron {
|
||||
logger;
|
||||
constructor(opts) {
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
register(task: CronTask) {
|
||||
if (!task.cron) {
|
||||
this.logger.info(`[cron] register once : [${task.name}]`);
|
||||
task.job();
|
||||
return;
|
||||
}
|
||||
this.logger.info(`[cron] register cron : [${task.name}] ,${task.cron}`);
|
||||
cron.schedule(task.cron, task.job, {
|
||||
name: task.name,
|
||||
});
|
||||
this.logger.info('当前定时任务数量:', this.getListSize());
|
||||
}
|
||||
|
||||
remove(taskName: string) {
|
||||
this.logger.info(`[cron] remove : [${taskName}]`);
|
||||
const tasks = cron.getTasks() as Map<any, any>;
|
||||
const node = tasks.get(taskName);
|
||||
if (node) {
|
||||
node.stop();
|
||||
tasks.delete(taskName);
|
||||
}
|
||||
}
|
||||
|
||||
getListSize() {
|
||||
const tasks = cron.getTasks();
|
||||
return tasks.size;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// src/index.ts
|
||||
export { CronConfiguration as Configuration } from './configuration';
|
||||
// export * from './controller/user';
|
||||
// export * from './controller/api';
|
||||
// export * from './service/user';
|
||||
6
packages/ui/certd-server/src/plugins/index.ts
Normal file
6
packages/ui/certd-server/src/plugins/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from '@certd/plugin-cert';
|
||||
export * from './plugin-aliyun';
|
||||
export * from './plugin-tencent';
|
||||
export * from './plugin-host';
|
||||
export * from './plugin-huawei';
|
||||
export * from './plugin-demo';
|
||||
@@ -1,6 +0,0 @@
|
||||
// src/index.ts
|
||||
import '@certd/plugin-all';
|
||||
// export { PipelineConfiguration as Configuration } from '@certd/pipeline';
|
||||
// export * from './controller/user';
|
||||
// export * from './controller/api';
|
||||
// export * from './service/user';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IsAccess, AccessInput } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'aliyun',
|
||||
title: '阿里云授权',
|
||||
desc: '',
|
||||
})
|
||||
export class AliyunAccess {
|
||||
@AccessInput({
|
||||
title: 'accessKeyId',
|
||||
component: {
|
||||
placeholder: 'accessKeyId',
|
||||
},
|
||||
helper:
|
||||
'注意:证书申请,需要dns解析权限;其他阿里云插件,也需要对应的权限,比如证书上传需要证书管理权限',
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = '';
|
||||
@AccessInput({
|
||||
title: 'accessKeySecret',
|
||||
component: {
|
||||
placeholder: 'accessKeySecret',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessKeySecret = '';
|
||||
}
|
||||
|
||||
new AliyunAccess();
|
||||
@@ -0,0 +1 @@
|
||||
export * from './aliyun-access';
|
||||
@@ -0,0 +1,150 @@
|
||||
import Core from '@alicloud/pop-core';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
CreateRecordOptions,
|
||||
IDnsProvider,
|
||||
IsDnsProvider,
|
||||
RemoveRecordOptions,
|
||||
} from '@certd/plugin-cert';
|
||||
import { Autowire, ILogger } from '@certd/pipeline';
|
||||
import { AliyunAccess } from '../access';
|
||||
|
||||
@IsDnsProvider({
|
||||
name: 'aliyun',
|
||||
title: '阿里云',
|
||||
desc: '阿里云DNS解析提供商',
|
||||
accessType: 'aliyun',
|
||||
})
|
||||
export class AliyunDnsProvider implements IDnsProvider {
|
||||
client: any;
|
||||
@Autowire()
|
||||
access!: AliyunAccess;
|
||||
@Autowire()
|
||||
logger!: ILogger;
|
||||
async onInstance() {
|
||||
const access: any = this.access;
|
||||
this.client = new Core({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: 'https://alidns.aliyuncs.com',
|
||||
apiVersion: '2015-01-09',
|
||||
});
|
||||
}
|
||||
|
||||
async getDomainList() {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
PageSize: 100,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
const ret = await this.client.request(
|
||||
'DescribeDomains',
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
return ret.Domains.Domain;
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: string) {
|
||||
const list = await this.getDomainList();
|
||||
let domain = null;
|
||||
const domainList = [];
|
||||
for (const item of list) {
|
||||
domainList.push(item.DomainName);
|
||||
if (_.endsWith(dnsRecord, item.DomainName)) {
|
||||
domain = item.DomainName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error(
|
||||
`can not find Domain :${dnsRecord} ,list: ${JSON.stringify(domainList)}`
|
||||
);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
async getRecords(domain: string, rr: string, value: string) {
|
||||
const params: any = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domain,
|
||||
RRKeyWord: rr,
|
||||
ValueKeyWord: undefined,
|
||||
};
|
||||
if (value) {
|
||||
params.ValueKeyWord = value;
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
const ret = await this.client.request(
|
||||
'DescribeDomainRecords',
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
return ret.DomainRecords.Record;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info('添加域名解析:', fullRecord, value);
|
||||
const domain = await this.matchDomain(fullRecord);
|
||||
const rr = fullRecord.replace('.' + domain, '');
|
||||
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domain,
|
||||
RR: rr,
|
||||
Type: type,
|
||||
Value: value,
|
||||
// Line: 'oversea' // 海外
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
try {
|
||||
const ret = await this.client.request(
|
||||
'AddDomainRecord',
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
this.logger.info('添加域名解析成功:', value, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
} catch (e: any) {
|
||||
if (e.code === 'DomainRecordDuplicate') {
|
||||
return;
|
||||
}
|
||||
this.logger.info('添加域名解析出错', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async removeRecord(options: RemoveRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, record } = options;
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
RecordId: record,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
const ret = await this.client.request(
|
||||
'DeleteDomainRecord',
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
}
|
||||
}
|
||||
|
||||
new AliyunDnsProvider();
|
||||
@@ -0,0 +1 @@
|
||||
import './aliyun-dns-provider';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './access/index';
|
||||
export * from './dns-provider/index';
|
||||
export * from './plugin/index';
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
utils,
|
||||
} from '@certd/pipeline';
|
||||
// @ts-ignore
|
||||
import { ROAClient } from '@alicloud/pop-core';
|
||||
import { AliyunAccess } from '../../access';
|
||||
import { K8sClient } from '@certd/plugin-util';
|
||||
import { appendTimeSuffix } from '../../utils';
|
||||
import { CertInfo } from '@certd/plugin-cert';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunAckIngress',
|
||||
title: '部署到阿里云AckIngress',
|
||||
input: {},
|
||||
output: {},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '集群id',
|
||||
component: {
|
||||
placeholder: '集群id',
|
||||
},
|
||||
})
|
||||
clusterId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '保密字典Id',
|
||||
component: {
|
||||
placeholder: '保密字典Id',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
secretName!: string | string[];
|
||||
|
||||
@TaskInput({
|
||||
title: '大区',
|
||||
value: 'cn-shanghai',
|
||||
component: {
|
||||
placeholder: '集群所属大区',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
regionId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '命名空间',
|
||||
value: 'default',
|
||||
component: {
|
||||
placeholder: '命名空间',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
namespace!: string;
|
||||
@TaskInput({
|
||||
title: 'ingress名称',
|
||||
value: '',
|
||||
component: {
|
||||
placeholder: 'ingress名称',
|
||||
},
|
||||
required: true,
|
||||
helper: '可以传入一个数组',
|
||||
})
|
||||
ingressName!: string;
|
||||
@TaskInput({
|
||||
title: 'ingress类型',
|
||||
value: 'nginx',
|
||||
component: {
|
||||
placeholder: '暂时只支持nginx类型',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
ingressClass!: string;
|
||||
@TaskInput({
|
||||
title: '是否私网ip',
|
||||
value: false,
|
||||
component: {
|
||||
placeholder: '集群连接端点是否是私网ip',
|
||||
},
|
||||
helper: '如果您当前certd运行在同一个私网下,可以选择是。',
|
||||
required: true,
|
||||
})
|
||||
isPrivateIpAddress!: boolean;
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
@TaskInput({
|
||||
title: 'Access授权',
|
||||
helper: '阿里云授权AccessKeyId、AccessKeySecret',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'aliyun',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance(): Promise<void> {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
console.log('开始部署证书到阿里云cdn');
|
||||
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } =
|
||||
this;
|
||||
const access = (await this.accessService.getById(
|
||||
this.accessId
|
||||
)) as AliyunAccess;
|
||||
const client = this.getClient(access, regionId);
|
||||
const kubeConfigStr = await this.getKubeConfig(
|
||||
client,
|
||||
clusterId,
|
||||
isPrivateIpAddress
|
||||
);
|
||||
|
||||
this.logger.info('kubeconfig已成功获取');
|
||||
const k8sClient = new K8sClient(kubeConfigStr);
|
||||
const ingressType = ingressClass || 'qcloud';
|
||||
if (ingressType === 'qcloud') {
|
||||
throw new Error('暂未实现');
|
||||
// await this.patchQcloudCertSecret({ k8sClient, props, context })
|
||||
} else {
|
||||
await this.patchNginxCertSecret({ cert, k8sClient });
|
||||
}
|
||||
|
||||
await utils.sleep(3000); // 停留2秒,等待secret部署完成
|
||||
// await this.restartIngress({ k8sClient, props })
|
||||
}
|
||||
|
||||
async restartIngress(options: { k8sClient: any }) {
|
||||
const { k8sClient } = options;
|
||||
const { namespace } = this;
|
||||
|
||||
const body = {
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: appendTimeSuffix('certd'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const ingressList = await k8sClient.getIngressList({ namespace });
|
||||
console.log('ingressList:', ingressList);
|
||||
if (!ingressList || !ingressList.body || !ingressList.body.items) {
|
||||
return;
|
||||
}
|
||||
const ingressNames = ingressList.body.items
|
||||
.filter((item: any) => {
|
||||
if (!item.spec.tls) {
|
||||
return false;
|
||||
}
|
||||
for (const tls of item.spec.tls) {
|
||||
if (tls.secretName === this.secretName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((item: any) => {
|
||||
return item.metadata.name;
|
||||
});
|
||||
for (const ingress of ingressNames) {
|
||||
await k8sClient.patchIngress({ namespace, ingressName: ingress, body });
|
||||
this.logger.info(`ingress已重启:${ingress}`);
|
||||
}
|
||||
}
|
||||
|
||||
async patchNginxCertSecret(options: { cert: any; k8sClient: any }) {
|
||||
const { cert, k8sClient } = options;
|
||||
const crt = cert.crt;
|
||||
const key = cert.key;
|
||||
const crtBase64 = Buffer.from(crt).toString('base64');
|
||||
const keyBase64 = Buffer.from(key).toString('base64');
|
||||
|
||||
const { namespace, secretName } = this;
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
'tls.crt': crtBase64,
|
||||
'tls.key': keyBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: appendTimeSuffix('certd'),
|
||||
},
|
||||
},
|
||||
};
|
||||
let secretNames: any = secretName;
|
||||
if (typeof secretName === 'string') {
|
||||
secretNames = [secretName];
|
||||
}
|
||||
for (const secret of secretNames) {
|
||||
await k8sClient.patchSecret({ namespace, secretName: secret, body });
|
||||
this.logger.info(`CertSecret已更新:${secret}`);
|
||||
}
|
||||
}
|
||||
|
||||
getClient(aliyunProvider: any, regionId: string) {
|
||||
return new ROAClient({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: `https://cs.${regionId}.aliyuncs.com`,
|
||||
apiVersion: '2015-12-15',
|
||||
});
|
||||
}
|
||||
|
||||
async getKubeConfig(
|
||||
client: any,
|
||||
clusterId: string,
|
||||
isPrivateIpAddress = false
|
||||
) {
|
||||
const httpMethod = 'GET';
|
||||
const uriPath = `/k8s/${clusterId}/user_config`;
|
||||
const queries = {
|
||||
PrivateIpAddress: isPrivateIpAddress,
|
||||
};
|
||||
const body = '{}';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const requestOption = {};
|
||||
|
||||
try {
|
||||
const res = await client.request(
|
||||
httpMethod,
|
||||
uriPath,
|
||||
queries,
|
||||
body,
|
||||
headers,
|
||||
requestOption
|
||||
);
|
||||
return res.config;
|
||||
} catch (e) {
|
||||
console.error('请求出错:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new DeployCertToAliyunAckIngressPlugin();
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
} from '@certd/pipeline';
|
||||
import dayjs from 'dayjs';
|
||||
import Core from '@alicloud/pop-core';
|
||||
import RPCClient from '@alicloud/pop-core';
|
||||
import { AliyunAccess } from '../../access';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunCDN',
|
||||
title: '部署证书至阿里云CDN',
|
||||
desc: '依赖证书申请前置任务,自动部署域名证书至阿里云CDN',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: 'CDN加速域名',
|
||||
helper: '你在阿里云上配置的CDN加速域名,比如:certd.docmirror.cn',
|
||||
required: true,
|
||||
})
|
||||
domainName!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '证书名称',
|
||||
helper: '上传后将以此名称作为前缀备注',
|
||||
})
|
||||
certName!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'Access授权',
|
||||
helper: '阿里云授权AccessKeyId、AccessKeySecret',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'aliyun',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
console.log('开始部署证书到阿里云cdn');
|
||||
const access = (await this.accessService.getById(
|
||||
this.accessId
|
||||
)) as AliyunAccess;
|
||||
const client = this.getClient(access);
|
||||
const params = await this.buildParams();
|
||||
await this.doRequest(client, params);
|
||||
console.log('部署完成');
|
||||
}
|
||||
|
||||
getClient(access: AliyunAccess) {
|
||||
return new Core({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: 'https://cdn.aliyuncs.com',
|
||||
apiVersion: '2018-05-10',
|
||||
});
|
||||
}
|
||||
|
||||
async buildParams() {
|
||||
const CertName =
|
||||
(this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss');
|
||||
const cert: any = this.cert;
|
||||
return {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: this.domainName,
|
||||
ServerCertificateStatus: 'on',
|
||||
CertName: CertName,
|
||||
CertType: 'upload',
|
||||
ServerCertificate: cert.crt,
|
||||
PrivateKey: cert.key,
|
||||
};
|
||||
}
|
||||
|
||||
async doRequest(client: RPCClient, params: any) {
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
const ret: any = await client.request(
|
||||
'SetDomainServerCertificate',
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
this.checkRet(ret);
|
||||
this.logger.info('设置cdn证书成功:', ret.RequestId);
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (ret.code != null) {
|
||||
throw new Error('执行失败:' + ret.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
new DeployCertToAliyunCDN();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './deploy-to-cdn/index';
|
||||
export * from './deploy-to-ack-ingress/index';
|
||||
export * from './upload-to-aliyun/index';
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
TaskOutput,
|
||||
} from '@certd/pipeline';
|
||||
import Core from '@alicloud/pop-core';
|
||||
import { AliyunAccess } from '../../access';
|
||||
import { appendTimeSuffix, checkRet, ZoneOptions } from '../../utils';
|
||||
import { Logger } from 'log4js';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'uploadCertToAliyun',
|
||||
title: '上传证书到阿里云',
|
||||
desc: '',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class UploadCertToAliyun extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '证书名称',
|
||||
helper: '证书上传后将以此参数作为名称前缀',
|
||||
})
|
||||
name!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '大区',
|
||||
value: 'cn-hangzhou',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
options: ZoneOptions,
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
regionId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: any;
|
||||
|
||||
@TaskInput({
|
||||
title: 'Access授权',
|
||||
helper: '阿里云授权AccessKeyId、AccessKeySecret',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'aliyun',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: '上传成功后的阿里云CertId',
|
||||
})
|
||||
aliyunCertId!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: Logger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
console.log('开始部署证书到阿里云cdn');
|
||||
const access = (await this.accessService.getById(
|
||||
this.accessId
|
||||
)) as AliyunAccess;
|
||||
const client = this.getClient(access);
|
||||
const certName = appendTimeSuffix(this.name);
|
||||
const params = {
|
||||
RegionId: this.regionId || 'cn-hangzhou',
|
||||
Name: certName,
|
||||
Cert: this.cert.crt,
|
||||
Key: this.cert.key,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
const ret = (await client.request(
|
||||
'CreateUserCertificate',
|
||||
params,
|
||||
requestOption
|
||||
)) as any;
|
||||
checkRet(ret);
|
||||
this.logger.info('证书上传成功:aliyunCertId=', ret.CertId);
|
||||
|
||||
//output
|
||||
this.aliyunCertId = ret.CertId;
|
||||
}
|
||||
|
||||
getClient(aliyunProvider: AliyunAccess) {
|
||||
return new Core({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: 'https://cas.aliyuncs.com',
|
||||
apiVersion: '2018-07-13',
|
||||
});
|
||||
}
|
||||
}
|
||||
//注册插件
|
||||
new UploadCertToAliyun();
|
||||
@@ -0,0 +1,15 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const ZoneOptions = [{ value: 'cn-hangzhou' }];
|
||||
export function appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = 'certd';
|
||||
}
|
||||
return name + '-' + dayjs().format('YYYYMMDD-HHmmss');
|
||||
}
|
||||
|
||||
export function checkRet(ret: any) {
|
||||
if (ret.code != null) {
|
||||
throw new Error('执行失败:' + ret.Message);
|
||||
}
|
||||
}
|
||||
42
packages/ui/certd-server/src/plugins/plugin-demo/access.ts
Normal file
42
packages/ui/certd-server/src/plugins/plugin-demo/access.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { IsAccess, AccessInput } from '@certd/pipeline';
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: 'plugins',
|
||||
title: '授权插件示例',
|
||||
desc: '',
|
||||
})
|
||||
export class DemoAccess {
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: '密钥Id',
|
||||
component: {
|
||||
placeholder: 'demoKeyId',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
demoKeyId = '';
|
||||
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
//标题
|
||||
title: '密钥串',
|
||||
component: {
|
||||
//input组件的placeholder
|
||||
placeholder: 'demoKeySecret',
|
||||
},
|
||||
//是否必填
|
||||
required: true,
|
||||
})
|
||||
//属性名称
|
||||
demoKeySecret = '';
|
||||
}
|
||||
|
||||
new DemoAccess();
|
||||
@@ -0,0 +1,78 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
CreateRecordOptions,
|
||||
IDnsProvider,
|
||||
IsDnsProvider,
|
||||
RemoveRecordOptions,
|
||||
} from '@certd/plugin-cert';
|
||||
import { Autowire, ILogger } from '@certd/pipeline';
|
||||
import { DemoAccess } from './access';
|
||||
|
||||
// TODO 这里注册一个dnsProvider
|
||||
@IsDnsProvider({
|
||||
name: 'plugins',
|
||||
title: 'Dns提供商Demo',
|
||||
desc: 'plugins dns provider示例',
|
||||
accessType: 'plugins',
|
||||
})
|
||||
export class DemoDnsProvider implements IDnsProvider {
|
||||
@Autowire()
|
||||
access!: DemoAccess;
|
||||
@Autowire()
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
const access: any = this.access;
|
||||
this.logger.debug('access', access);
|
||||
//初始化的操作
|
||||
//...
|
||||
}
|
||||
|
||||
async getDomainList(): Promise<any[]> {
|
||||
// TODO 这里你要实现一个获取域名列表的方法
|
||||
const access = this.access;
|
||||
this.logger.debug('access', access);
|
||||
return [];
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: string): Promise<any> {
|
||||
const domainList = await this.getDomainList();
|
||||
let domainRecord = null;
|
||||
for (const item of domainList) {
|
||||
//TODO 根据域名去匹配账户中是否有该域名, 这里不一定是item.name 具体要看你要实现的平台的接口而定
|
||||
if (_.endsWith(dnsRecord + '.', item.name)) {
|
||||
domainRecord = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!domainRecord) {
|
||||
this.logger.info('账户中域名列表:', domainList);
|
||||
this.logger.error('找不到域名,请确认账户中是否真的有此域名');
|
||||
throw new Error('can not find Domain:' + dnsRecord);
|
||||
}
|
||||
return domainRecord;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info('添加域名解析:', fullRecord, value, type);
|
||||
//先确定账户中是否有该域名
|
||||
const domainRecord = await this.matchDomain(fullRecord);
|
||||
this.logger.debug('matchDomain:', domainRecord);
|
||||
//TODO 然后调用接口,创建txt类型的dns解析记录
|
||||
// .. 这里调用对应平台的后台接口
|
||||
const access = this.access;
|
||||
this.logger.debug('access', access);
|
||||
}
|
||||
async removeRecord(options: RemoveRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, record } = options;
|
||||
this.logger.info('删除域名解析:', fullRecord, value, record);
|
||||
//TODO 这里调用删除txt dns解析记录接口
|
||||
const access = this.access;
|
||||
this.logger.debug('access', access);
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO 实例化这个provider,将其自动注册到系统中
|
||||
new DemoDnsProvider();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './dns-provider';
|
||||
export * from './plugins';
|
||||
export * from './access';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './plugin-test';
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
} from '@certd/pipeline';
|
||||
import { CertInfo, CertReader } from '@certd/plugin-cert';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'demoTest',
|
||||
title: 'Demo测试插件',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class DemoTestPlugin extends AbstractTaskPlugin {
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: '属性示例',
|
||||
component: {
|
||||
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/input-cn
|
||||
name: 'a-input',
|
||||
},
|
||||
})
|
||||
text!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: '选择框',
|
||||
component: {
|
||||
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn
|
||||
name: 'a-select',
|
||||
options: [
|
||||
{ value: '1', label: '选项1' },
|
||||
{ value: '2', label: '选项2' },
|
||||
],
|
||||
},
|
||||
})
|
||||
select!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: 'switch',
|
||||
component: {
|
||||
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/switch-cn
|
||||
name: 'a-switch',
|
||||
vModel: 'checked',
|
||||
},
|
||||
})
|
||||
switch!: boolean;
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: 'demo授权',
|
||||
helper: 'demoAccess授权',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'plugins',
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }],
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const { select, text, cert, accessId } = this;
|
||||
const certReader = new CertReader(cert);
|
||||
const access = await this.accessService.getById(accessId);
|
||||
this.logger.debug('access', access);
|
||||
this.logger.debug('certReader', certReader);
|
||||
this.logger.info('DemoTestPlugin execute');
|
||||
this.logger.info('text:', text);
|
||||
this.logger.info('select:', select);
|
||||
this.logger.info('switch:', this.switch);
|
||||
this.logger.info('授权id:', accessId);
|
||||
//TODO 这里实现你要部署的执行方法
|
||||
}
|
||||
}
|
||||
//TODO 这里实例化插件,进行注册
|
||||
new DemoTestPlugin();
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ssh-access';
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AccessInput, IAccess, IsAccess } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'ssh',
|
||||
title: '主机登录授权',
|
||||
desc: '',
|
||||
input: {},
|
||||
})
|
||||
export class SshAccess implements IAccess {
|
||||
@AccessInput({
|
||||
title: '主机地址',
|
||||
component: {
|
||||
placeholder: '主机域名或IP地址',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
host!: string;
|
||||
@AccessInput({
|
||||
title: '端口',
|
||||
value: '22',
|
||||
component: {
|
||||
placeholder: '22',
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }],
|
||||
})
|
||||
port!: string;
|
||||
@AccessInput({
|
||||
title: '用户名',
|
||||
value: 'root',
|
||||
rules: [{ required: true, message: '此项必填' }],
|
||||
})
|
||||
username!: string;
|
||||
@AccessInput({
|
||||
title: '密码',
|
||||
component: {
|
||||
name: 'a-input-password',
|
||||
vModel: 'value',
|
||||
},
|
||||
helper: '登录密码或密钥必填一项',
|
||||
})
|
||||
password!: string;
|
||||
@AccessInput({
|
||||
title: '密钥',
|
||||
helper: '密钥或密码必填一项',
|
||||
component: {
|
||||
name: 'a-textarea',
|
||||
vModel: 'value',
|
||||
},
|
||||
})
|
||||
privateKey!: string;
|
||||
}
|
||||
|
||||
new SshAccess();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './access';
|
||||
export * from './lib/ssh';
|
||||
export * from './plugin';
|
||||
162
packages/ui/certd-server/src/plugins/plugin-host/lib/ssh.ts
Normal file
162
packages/ui/certd-server/src/plugins/plugin-host/lib/ssh.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
// @ts-ignore
|
||||
import ssh2 from 'ssh2';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import { ILogger } from '@certd/pipeline';
|
||||
export class SshClient {
|
||||
logger: ILogger;
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param connectConf
|
||||
{
|
||||
host: '192.168.100.100',
|
||||
port: 22,
|
||||
username: 'frylock',
|
||||
password: 'nodejsrules'
|
||||
}
|
||||
* @param options
|
||||
*/
|
||||
uploadFiles(options: { connectConf: any; transports: any }) {
|
||||
const { connectConf, transports } = options;
|
||||
const conn = new ssh2.Client();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
conn
|
||||
.on('ready', () => {
|
||||
this.logger.info('连接服务器成功');
|
||||
conn.sftp(async (err: any, sftp: any) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const transport of transports) {
|
||||
this.logger.info('上传文件:', JSON.stringify(transport));
|
||||
await this.exec({
|
||||
connectConf,
|
||||
script: `mkdir -p ${path.dirname(transport.remotePath)} `,
|
||||
});
|
||||
await this.fastPut({ sftp, ...transport });
|
||||
}
|
||||
resolve({});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} finally {
|
||||
conn.end();
|
||||
}
|
||||
});
|
||||
})
|
||||
.connect(connectConf);
|
||||
});
|
||||
}
|
||||
|
||||
exec(options: { connectConf: any; script: string | Array<string> }) {
|
||||
let { script } = options;
|
||||
const { connectConf } = options;
|
||||
if (_.isArray(script)) {
|
||||
script = script as Array<string>;
|
||||
script = script.join('\n');
|
||||
}
|
||||
this.logger.info('执行命令:', script);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connect({
|
||||
connectConf,
|
||||
onError(err: any) {
|
||||
reject(err);
|
||||
},
|
||||
onReady: (conn: any) => {
|
||||
conn.exec(script, (err: Error, stream: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let data: any = null;
|
||||
stream
|
||||
.on('close', (code: any, signal: any) => {
|
||||
this.logger.info(`[${connectConf.host}][close]:code:${code}`);
|
||||
data = data ? data.toString() : null;
|
||||
if (code === 0) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(new Error(data));
|
||||
}
|
||||
conn.end();
|
||||
})
|
||||
.on('data', (ret: any) => {
|
||||
this.logger.info(`[${connectConf.host}][info]: ` + ret);
|
||||
data = ret;
|
||||
})
|
||||
.stderr.on('data', (err: Error) => {
|
||||
this.logger.info(`[${connectConf.host}][error]: ` + err);
|
||||
data = err;
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
shell(options: { connectConf: any; script: string }) {
|
||||
const { connectConf, script } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connect({
|
||||
connectConf,
|
||||
onError: (err: any) => {
|
||||
this.logger.error(err);
|
||||
reject(err);
|
||||
},
|
||||
onReady: (conn: any) => {
|
||||
conn.shell((err: Error, stream: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const output: any = [];
|
||||
stream
|
||||
.on('close', () => {
|
||||
this.logger.info('Stream :: close');
|
||||
conn.end();
|
||||
resolve(output);
|
||||
})
|
||||
.on('data', (data: any) => {
|
||||
this.logger.info('' + data);
|
||||
output.push('' + data);
|
||||
});
|
||||
stream.end(script + '\nexit\n');
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
connect(options: { connectConf: any; onReady: any; onError: any }) {
|
||||
const { connectConf, onReady, onError } = options;
|
||||
const conn = new ssh2.Client();
|
||||
conn
|
||||
.on('error', (err: any) => {
|
||||
onError(err);
|
||||
})
|
||||
.on('ready', () => {
|
||||
this.logger.info('Client :: ready');
|
||||
onReady(conn);
|
||||
})
|
||||
.connect(connectConf);
|
||||
return conn;
|
||||
}
|
||||
|
||||
fastPut(options: { sftp: any; localPath: string; remotePath: string }) {
|
||||
const { sftp, localPath, remotePath } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.fastPut(localPath, remotePath, (err: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AbstractTaskPlugin, IAccessService, ILogger, IsTaskPlugin, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { SshClient } from "../../lib/ssh";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "hostShellExecute",
|
||||
title: "执行远程主机脚本命令",
|
||||
input: {},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
})
|
||||
export class HostShellExecutePlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "主机登录配置",
|
||||
helper: "登录",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "ssh",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
@TaskInput({
|
||||
title: "shell脚本命令",
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
vModel: "value",
|
||||
},
|
||||
})
|
||||
script!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const { script, accessId } = this;
|
||||
const connectConf = await this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
const ret = await sshClient.exec({
|
||||
connectConf,
|
||||
script,
|
||||
});
|
||||
this.logger.info("exec res:", ret);
|
||||
}
|
||||
}
|
||||
|
||||
new HostShellExecutePlugin();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './host-shell-execute';
|
||||
export * from './upload-to-host';
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
TaskOutput,
|
||||
} from '@certd/pipeline';
|
||||
import { SshClient } from '../../lib/ssh';
|
||||
import { CertInfo, CertReader } from '@certd/plugin-cert';
|
||||
import * as fs from 'fs';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'uploadCertToHost',
|
||||
title: '上传证书到主机',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含证书文件名',
|
||||
component: {
|
||||
placeholder: '/root/deploy/nginx/cert.crt',
|
||||
},
|
||||
})
|
||||
crtPath!: string;
|
||||
@TaskInput({
|
||||
title: '私钥保存路径',
|
||||
helper: '需要有写入权限,路径要包含证书文件名',
|
||||
component: {
|
||||
placeholder: '/root/deploy/nginx/cert.crt',
|
||||
},
|
||||
})
|
||||
keyPath!: string;
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
@TaskInput({
|
||||
title: '主机登录配置',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'ssh',
|
||||
},
|
||||
rules: [{ required: true, message: '此项必填' }],
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: '证书保存路径',
|
||||
})
|
||||
hostCrtPath!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: '私钥保存路径',
|
||||
})
|
||||
hostKeyPath!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const { crtPath, keyPath, cert, accessId } = this;
|
||||
const certReader = new CertReader(cert);
|
||||
const connectConf = await this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
|
||||
const saveCrtPath = certReader.saveToFile('crt');
|
||||
const saveKeyPath = certReader.saveToFile('key');
|
||||
|
||||
await sshClient.uploadFiles({
|
||||
connectConf,
|
||||
transports: [
|
||||
{
|
||||
localPath: saveCrtPath,
|
||||
remotePath: crtPath,
|
||||
},
|
||||
{
|
||||
localPath: saveKeyPath,
|
||||
remotePath: keyPath,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.logger.info('证书上传成功:crtPath=', crtPath, ',keyPath=', keyPath);
|
||||
|
||||
//删除临时文件
|
||||
fs.unlinkSync(saveCrtPath);
|
||||
fs.unlinkSync(saveKeyPath);
|
||||
|
||||
//输出
|
||||
this.hostCrtPath = crtPath;
|
||||
this.hostKeyPath = keyPath;
|
||||
}
|
||||
}
|
||||
|
||||
new UploadCertToHostPlugin();
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IsAccess, AccessInput } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'huawei',
|
||||
title: '华为云授权',
|
||||
desc: '',
|
||||
})
|
||||
export class HuaweiAccess {
|
||||
@AccessInput({
|
||||
title: 'accessKeyId',
|
||||
component: {
|
||||
placeholder: 'accessKeyId',
|
||||
},
|
||||
helper: '证书申请需要有dns解析权限',
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = '';
|
||||
@AccessInput({
|
||||
title: 'accessKeySecret',
|
||||
component: {
|
||||
placeholder: 'accessKeySecret',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessKeySecret = '';
|
||||
}
|
||||
|
||||
new HuaweiAccess();
|
||||
@@ -0,0 +1 @@
|
||||
export * from './huawei-access';
|
||||
@@ -0,0 +1,121 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
CreateRecordOptions,
|
||||
IDnsProvider,
|
||||
IsDnsProvider,
|
||||
RemoveRecordOptions,
|
||||
} from '@certd/plugin-cert';
|
||||
import { Autowire, ILogger } from '@certd/pipeline';
|
||||
import { HuaweiAccess } from '../access';
|
||||
import { ApiRequestOptions, HuaweiYunClient } from '../lib/client';
|
||||
|
||||
export type SearchRecordOptions = {
|
||||
zoneId: string;
|
||||
} & CreateRecordOptions;
|
||||
|
||||
@IsDnsProvider({
|
||||
name: 'huawei',
|
||||
title: '华为云',
|
||||
desc: '华为云DNS解析提供商',
|
||||
accessType: 'huawei',
|
||||
})
|
||||
export class HuaweiDnsProvider implements IDnsProvider {
|
||||
client!: HuaweiYunClient;
|
||||
@Autowire()
|
||||
access!: HuaweiAccess;
|
||||
@Autowire()
|
||||
logger!: ILogger;
|
||||
domainEndpoint = 'https://domains-external.myhuaweicloud.com';
|
||||
dnsEndpoint = 'https://dns.cn-south-1.myhuaweicloud.com';
|
||||
async onInstance() {
|
||||
const access: any = this.access;
|
||||
this.client = new HuaweiYunClient(access);
|
||||
}
|
||||
|
||||
async getDomainList() {
|
||||
const url = `${this.dnsEndpoint}/v2/zones`;
|
||||
const ret = await this.client.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
});
|
||||
return ret.zones;
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: string) {
|
||||
const zoneList = await this.getDomainList();
|
||||
let zoneRecord = null;
|
||||
for (const item of zoneList) {
|
||||
if (_.endsWith(dnsRecord + '.', item.name)) {
|
||||
zoneRecord = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!zoneRecord) {
|
||||
throw new Error('can not find Domain ,' + dnsRecord);
|
||||
}
|
||||
return zoneRecord;
|
||||
}
|
||||
|
||||
async searchRecord(options: SearchRecordOptions): Promise<any> {
|
||||
const req: ApiRequestOptions = {
|
||||
url: `${this.dnsEndpoint}/v2/zones/${options.zoneId}/recordsets?name=${options.fullRecord}.`,
|
||||
method: 'GET',
|
||||
};
|
||||
const ret = await this.client.request(req);
|
||||
return ret.recordsets;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info('添加域名解析:', fullRecord, value);
|
||||
const zoneRecord = await this.matchDomain(fullRecord);
|
||||
const zoneId = zoneRecord.id;
|
||||
|
||||
const records: any = await this.searchRecord({
|
||||
zoneId,
|
||||
...options,
|
||||
});
|
||||
if (records && records.length > 0) {
|
||||
for (const record of records) {
|
||||
await this.removeRecord({
|
||||
record,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const req: ApiRequestOptions = {
|
||||
url: `${this.dnsEndpoint}/v2/zones/${zoneId}/recordsets`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
name: fullRecord + '.',
|
||||
type,
|
||||
records: [`"${value}"`],
|
||||
},
|
||||
};
|
||||
const ret = await this.client.request(req);
|
||||
this.logger.info('添加域名解析成功:', value, ret);
|
||||
return ret;
|
||||
} catch (e: any) {
|
||||
if (e.code === 'DNS.0312') {
|
||||
return;
|
||||
}
|
||||
this.logger.info('添加域名解析出错', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async removeRecord(options: RemoveRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, record } = options;
|
||||
const req: ApiRequestOptions = {
|
||||
url: `${this.dnsEndpoint}/v2/zones/${record.zone_id}/recordsets/${record.id}`,
|
||||
method: 'DELETE',
|
||||
};
|
||||
|
||||
const ret = await this.client.request(req);
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
}
|
||||
}
|
||||
|
||||
new HuaweiDnsProvider();
|
||||
@@ -0,0 +1 @@
|
||||
import './huawei-dns-provider';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './access';
|
||||
export * from './dns-provider';
|
||||
@@ -0,0 +1,24 @@
|
||||
# License
|
||||
|
||||
[The MIT License (MIT)](http://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2009-2013 Jeff Mott
|
||||
Copyright (c) 2013-2016 Evan Vosberg
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
35
packages/ui/certd-server/src/plugins/plugin-huawei/lib/APIGW-javascript-sdk-2.0.5/node_demo.js
vendored
Normal file
35
packages/ui/certd-server/src/plugins/plugin-huawei/lib/APIGW-javascript-sdk-2.0.5/node_demo.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
var signer = require("./signer");
|
||||
var https = require("https");
|
||||
var sig = new signer.Signer();
|
||||
//Set the AK/SK to sign and authenticate the request.
|
||||
sig.Key = "QTWAOYTTINDUT2QVKYUC";
|
||||
sig.Secret = "MFyfvK41ba2giqM7**********KGpownRZlmVmHc";
|
||||
|
||||
//The following example shows how to set the request URL and parameters to query a VPC list.
|
||||
//Specify a request method, such as GET, PUT, POST, DELETE, HEAD, and PATCH.
|
||||
//Set request host.
|
||||
//Set request URI.
|
||||
//Set parameters for the request URL.
|
||||
var r = new signer.HttpRequest("GET", "endpoint.example.com/v1/77b6a44cba5143ab91d13ab9a8ff44fd/vpcs?limie=1");
|
||||
//Add header parameters, for example, x-domain-id for invoking a global service and x-project-id for invoking a project-level service.
|
||||
r.headers = { "Content-Type": "application/json" };
|
||||
//Add a body if you have specified the PUT or POST method. Special characters, such as the double quotation mark ("), contained in the body must be escaped.
|
||||
r.body = "";
|
||||
|
||||
var opt = sig.Sign(r);
|
||||
console.log(opt.headers["X-Sdk-Date"]);
|
||||
console.log(opt.headers["Authorization"]);
|
||||
|
||||
var req = https.request(opt, function (res) {
|
||||
console.log(res.statusCode);
|
||||
console.log("headers:", JSON.stringify(res.headers));
|
||||
res.on("data", function (chunk) {
|
||||
console.log(chunk.toString());
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", function (err) {
|
||||
console.log(err.message);
|
||||
});
|
||||
req.write(r.body);
|
||||
req.end();
|
||||
501
packages/ui/certd-server/src/plugins/plugin-huawei/lib/APIGW-javascript-sdk-2.0.5/signer.js
vendored
Normal file
501
packages/ui/certd-server/src/plugins/plugin-huawei/lib/APIGW-javascript-sdk-2.0.5/signer.js
vendored
Normal file
@@ -0,0 +1,501 @@
|
||||
// HWS API Gateway Signature
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
|
||||
/*global define*/
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// AMD
|
||||
define(["CryptoJS"], function (CryptoJS) {
|
||||
var crypto_wrapper = {
|
||||
hmacsha256: function (keyByte, message) {
|
||||
return CryptoJS.HmacSHA256(message, keyByte).toString(CryptoJS.enc.Hex);
|
||||
},
|
||||
HexEncodeSHA256Hash: function (body) {
|
||||
return CryptoJS.SHA256(body);
|
||||
},
|
||||
};
|
||||
return factory(crypto_wrapper);
|
||||
});
|
||||
} else if (typeof wx === "object") {
|
||||
// wechat
|
||||
var CryptoJS = require("./js/hmac-sha256.js");
|
||||
var crypto_wrapper = {
|
||||
hmacsha256: function (keyByte, message) {
|
||||
return CryptoJS.HmacSHA256(message, keyByte).toString(CryptoJS.enc.Hex);
|
||||
},
|
||||
HexEncodeSHA256Hash: function (body) {
|
||||
return CryptoJS.SHA256(body);
|
||||
},
|
||||
};
|
||||
module.exports = factory(crypto_wrapper);
|
||||
} else if (typeof module === "object" && module.exports) {
|
||||
// Node
|
||||
var crypto = require("crypto");
|
||||
var crypto_wrapper = {
|
||||
hmacsha256: function (keyByte, message) {
|
||||
return crypto.createHmac("SHA256", keyByte).update(message).digest().toString("hex");
|
||||
},
|
||||
HexEncodeSHA256Hash: function (body) {
|
||||
return crypto.createHash("SHA256").update(body).digest().toString("hex");
|
||||
},
|
||||
};
|
||||
module.exports = factory(crypto_wrapper);
|
||||
} else {
|
||||
// Browser
|
||||
var CryptoJS = root.CryptoJS;
|
||||
var crypto_wrapper = {
|
||||
hmacsha256: function (keyByte, message) {
|
||||
return CryptoJS.HmacSHA256(message, keyByte).toString(CryptoJS.enc.Hex);
|
||||
},
|
||||
HexEncodeSHA256Hash: function (body) {
|
||||
return CryptoJS.SHA256(body);
|
||||
},
|
||||
};
|
||||
root.signer = factory(crypto_wrapper);
|
||||
}
|
||||
})(this, function (crypto_wrapper) {
|
||||
"use strict";
|
||||
|
||||
var Algorithm = "SDK-HMAC-SHA256";
|
||||
var HeaderXDate = "X-Sdk-Date";
|
||||
var HeaderAuthorization = "Authorization";
|
||||
var HeaderContentSha256 = "x-sdk-content-sha256";
|
||||
|
||||
const hexTable = new Array(256);
|
||||
for (var i = 0; i < 256; ++i) hexTable[i] = "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase();
|
||||
|
||||
const noEscape = [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 0 - 15
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 16 - 31
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0, // 32 - 47
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 48 - 63
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1, // 64 - 79
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1, // 80 - 95
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1, // 96 - 111
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0, // 112 - 127
|
||||
];
|
||||
|
||||
// function urlEncode is based on https://github.com/nodejs/node/blob/master/lib/querystring.js
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
function urlEncode(str) {
|
||||
if (typeof str !== "string") {
|
||||
if (typeof str === "object") str = String(str);
|
||||
else str += "";
|
||||
}
|
||||
var out = "";
|
||||
var lastPos = 0;
|
||||
|
||||
for (var i = 0; i < str.length; ++i) {
|
||||
var c = str.charCodeAt(i);
|
||||
|
||||
// ASCII
|
||||
if (c < 0x80) {
|
||||
if (noEscape[c] === 1) continue;
|
||||
if (lastPos < i) out += str.slice(lastPos, i);
|
||||
lastPos = i + 1;
|
||||
out += hexTable[c];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastPos < i) out += str.slice(lastPos, i);
|
||||
|
||||
// Multi-byte characters ...
|
||||
if (c < 0x800) {
|
||||
lastPos = i + 1;
|
||||
out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)];
|
||||
continue;
|
||||
}
|
||||
if (c < 0xd800 || c >= 0xe000) {
|
||||
lastPos = i + 1;
|
||||
out += hexTable[0xe0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3f)] + hexTable[0x80 | (c & 0x3f)];
|
||||
continue;
|
||||
}
|
||||
// Surrogate pair
|
||||
++i;
|
||||
|
||||
if (i >= str.length) throw new errors.URIError("ERR_INVALID_URI");
|
||||
|
||||
var c2 = str.charCodeAt(i) & 0x3ff;
|
||||
|
||||
lastPos = i + 1;
|
||||
c = 0x10000 + (((c & 0x3ff) << 10) | c2);
|
||||
out += hexTable[0xf0 | (c >> 18)] + hexTable[0x80 | ((c >> 12) & 0x3f)] + hexTable[0x80 | ((c >> 6) & 0x3f)] + hexTable[0x80 | (c & 0x3f)];
|
||||
}
|
||||
if (lastPos === 0) return str;
|
||||
if (lastPos < str.length) return out + str.slice(lastPos);
|
||||
return out;
|
||||
}
|
||||
|
||||
function HttpRequest(method, url, headers, body) {
|
||||
if (method === undefined) {
|
||||
this.method = "";
|
||||
} else {
|
||||
this.method = method;
|
||||
}
|
||||
if (url === undefined) {
|
||||
this.host = "";
|
||||
this.uri = "";
|
||||
this.query = {};
|
||||
} else {
|
||||
this.query = {};
|
||||
var host, path;
|
||||
var i = url.indexOf("://");
|
||||
if (i !== -1) {
|
||||
url = url.substr(i + 3);
|
||||
}
|
||||
var i = url.indexOf("?");
|
||||
if (i !== -1) {
|
||||
var query_str = url.substr(i + 1);
|
||||
url = url.substr(0, i);
|
||||
var spl = query_str.split("&");
|
||||
for (var i in spl) {
|
||||
var kv = spl[i];
|
||||
var index = kv.indexOf("=");
|
||||
var key, value;
|
||||
if (index >= 0) {
|
||||
key = kv.substr(0, index);
|
||||
value = kv.substr(index + 1);
|
||||
} else {
|
||||
key = kv;
|
||||
value = "";
|
||||
}
|
||||
if (key !== "") {
|
||||
key = decodeURI(key);
|
||||
value = decodeURI(value);
|
||||
if (this.query[key] === undefined) {
|
||||
this.query[key] = [value];
|
||||
} else {
|
||||
this.query[key].push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var i = url.indexOf("/");
|
||||
if (i === -1) {
|
||||
host = url;
|
||||
path = "/";
|
||||
} else {
|
||||
host = url.substr(0, i);
|
||||
path = url.substr(i);
|
||||
}
|
||||
this.host = host;
|
||||
this.uri = decodeURI(path);
|
||||
}
|
||||
if (headers === undefined) {
|
||||
this.headers = {};
|
||||
} else {
|
||||
this.headers = headers;
|
||||
}
|
||||
if (body === undefined) {
|
||||
this.body = "";
|
||||
} else {
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
function findHeader(r, header) {
|
||||
for (var k in r.headers) {
|
||||
if (k.toLowerCase() === header.toLowerCase()) {
|
||||
return r.headers[k];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build a CanonicalRequest from a regular request string
|
||||
//
|
||||
// CanonicalRequest =
|
||||
// HTTPRequestMethod + '\n' +
|
||||
// CanonicalURI + '\n' +
|
||||
// CanonicalQueryString + '\n' +
|
||||
// CanonicalHeaders + '\n' +
|
||||
// SignedHeaders + '\n' +
|
||||
// HexEncode(Hash(RequestPayload))
|
||||
function CanonicalRequest(r, signedHeaders) {
|
||||
var hexencode = findHeader(r, HeaderContentSha256);
|
||||
if (hexencode === null) {
|
||||
var data = RequestPayload(r);
|
||||
hexencode = crypto_wrapper.HexEncodeSHA256Hash(data);
|
||||
}
|
||||
return (
|
||||
r.method +
|
||||
"\n" +
|
||||
CanonicalURI(r) +
|
||||
"\n" +
|
||||
CanonicalQueryString(r) +
|
||||
"\n" +
|
||||
CanonicalHeaders(r, signedHeaders) +
|
||||
"\n" +
|
||||
signedHeaders.join(";") +
|
||||
"\n" +
|
||||
hexencode
|
||||
);
|
||||
}
|
||||
|
||||
function CanonicalURI(r) {
|
||||
var pattens = r.uri.split("/");
|
||||
var uri = [];
|
||||
for (var k in pattens) {
|
||||
var v = pattens[k];
|
||||
uri.push(urlEncode(v));
|
||||
}
|
||||
var urlpath = uri.join("/");
|
||||
if (urlpath[urlpath.length - 1] !== "/") {
|
||||
urlpath = urlpath + "/";
|
||||
}
|
||||
//r.uri = urlpath
|
||||
return urlpath;
|
||||
}
|
||||
|
||||
function CanonicalQueryString(r) {
|
||||
var keys = [];
|
||||
for (var key in r.query) {
|
||||
keys.push(key);
|
||||
}
|
||||
keys.sort();
|
||||
var a = [];
|
||||
for (var i in keys) {
|
||||
var key = urlEncode(keys[i]);
|
||||
var value = r.query[keys[i]];
|
||||
if (Array.isArray(value)) {
|
||||
value.sort();
|
||||
for (var iv in value) {
|
||||
a.push(key + "=" + urlEncode(value[iv]));
|
||||
}
|
||||
} else {
|
||||
a.push(key + "=" + urlEncode(value));
|
||||
}
|
||||
}
|
||||
return a.join("&");
|
||||
}
|
||||
|
||||
function CanonicalHeaders(r, signedHeaders) {
|
||||
var headers = {};
|
||||
for (var key in r.headers) {
|
||||
headers[key.toLowerCase()] = r.headers[key];
|
||||
}
|
||||
var a = [];
|
||||
for (var i in signedHeaders) {
|
||||
var value = headers[signedHeaders[i]];
|
||||
a.push(signedHeaders[i] + ":" + value.trim());
|
||||
}
|
||||
return a.join("\n") + "\n";
|
||||
}
|
||||
|
||||
function SignedHeaders(r) {
|
||||
var a = [];
|
||||
for (var key in r.headers) {
|
||||
a.push(key.toLowerCase());
|
||||
}
|
||||
a.sort();
|
||||
return a;
|
||||
}
|
||||
|
||||
function RequestPayload(r) {
|
||||
return r.body;
|
||||
}
|
||||
|
||||
// Create a "String to Sign".
|
||||
function StringToSign(canonicalRequest, t) {
|
||||
var bytes = crypto_wrapper.HexEncodeSHA256Hash(canonicalRequest);
|
||||
return Algorithm + "\n" + t + "\n" + bytes;
|
||||
}
|
||||
|
||||
// Create the HWS Signature.
|
||||
function SignStringToSign(stringToSign, signingKey) {
|
||||
return crypto_wrapper.hmacsha256(signingKey, stringToSign);
|
||||
}
|
||||
|
||||
// Get the finalized value for the "Authorization" header. The signature
|
||||
// parameter is the output from SignStringToSign
|
||||
function AuthHeaderValue(signature, Key, signedHeaders) {
|
||||
return Algorithm + " Access=" + Key + ", SignedHeaders=" + signedHeaders.join(";") + ", Signature=" + signature;
|
||||
}
|
||||
|
||||
function twoChar(s) {
|
||||
if (s >= 10) {
|
||||
return "" + s;
|
||||
} else {
|
||||
return "0" + s;
|
||||
}
|
||||
}
|
||||
|
||||
function getTime() {
|
||||
var date = new Date();
|
||||
return (
|
||||
"" +
|
||||
date.getUTCFullYear() +
|
||||
twoChar(date.getUTCMonth() + 1) +
|
||||
twoChar(date.getUTCDate()) +
|
||||
"T" +
|
||||
twoChar(date.getUTCHours()) +
|
||||
twoChar(date.getUTCMinutes()) +
|
||||
twoChar(date.getUTCSeconds()) +
|
||||
"Z"
|
||||
);
|
||||
}
|
||||
|
||||
function Signer() {
|
||||
this.Key = "";
|
||||
this.Secret = "";
|
||||
}
|
||||
|
||||
Signer.prototype.Sign = function (r) {
|
||||
var headerTime = findHeader(r, HeaderXDate);
|
||||
if (headerTime === null) {
|
||||
headerTime = getTime();
|
||||
r.headers[HeaderXDate] = headerTime;
|
||||
}
|
||||
if (r.method !== "PUT" && r.method !== "PATCH" && r.method !== "POST") {
|
||||
r.body = "";
|
||||
}
|
||||
var queryString = CanonicalQueryString(r);
|
||||
if (queryString !== "") {
|
||||
queryString = "?" + queryString;
|
||||
}
|
||||
var options = {
|
||||
hostname: r.host,
|
||||
path: encodeURI(r.uri) + queryString,
|
||||
method: r.method,
|
||||
headers: r.headers,
|
||||
};
|
||||
if (findHeader(r, "host") === null) {
|
||||
r.headers.host = r.host;
|
||||
}
|
||||
var signedHeaders = SignedHeaders(r);
|
||||
var canonicalRequest = CanonicalRequest(r, signedHeaders);
|
||||
var stringToSign = StringToSign(canonicalRequest, headerTime);
|
||||
var signature = SignStringToSign(stringToSign, this.Secret);
|
||||
options.headers[HeaderAuthorization] = AuthHeaderValue(signature, this.Key, signedHeaders);
|
||||
return options;
|
||||
};
|
||||
return {
|
||||
HttpRequest: HttpRequest,
|
||||
Signer: Signer,
|
||||
urlEncode: urlEncode,
|
||||
findHeader: findHeader,
|
||||
SignedHeaders: SignedHeaders,
|
||||
CanonicalRequest: CanonicalRequest,
|
||||
StringToSign: StringToSign,
|
||||
};
|
||||
});
|
||||
10
packages/ui/certd-server/src/plugins/plugin-huawei/lib/APIGW-javascript-sdk-2.0.5/test.js
vendored
Normal file
10
packages/ui/certd-server/src/plugins/plugin-huawei/lib/APIGW-javascript-sdk-2.0.5/test.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
var assert = require("assert");
|
||||
var signer = require('./signer')
|
||||
|
||||
var s = ""
|
||||
for (i = 0; i < 0x80; i++) {
|
||||
s = s + signer.urlEncode(String.fromCharCode(i))
|
||||
}
|
||||
console.log(s)
|
||||
assert.equal(s, "%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F")
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Signer, SigHttpRequest } from './signer';
|
||||
import { HuaweiAccess } from '../access';
|
||||
import axios from 'axios';
|
||||
import { logger } from '@certd/pipeline';
|
||||
|
||||
export type ApiRequestOptions = {
|
||||
method: string;
|
||||
url: string;
|
||||
headers?: any;
|
||||
data?: any;
|
||||
};
|
||||
export class HuaweiYunClient {
|
||||
access: HuaweiAccess;
|
||||
constructor(access: HuaweiAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
async request(options: ApiRequestOptions) {
|
||||
const sig = new Signer(
|
||||
this.access.accessKeyId,
|
||||
this.access.accessKeySecret
|
||||
);
|
||||
|
||||
//The following example shows how to set the request URL and parameters to query a VPC list.
|
||||
//Specify a request method, such as GET, PUT, POST, DELETE, HEAD, and PATCH.
|
||||
//Set request host.
|
||||
//Set request URI.
|
||||
//Set parameters for the request URL.
|
||||
let body = undefined;
|
||||
if (options.data) {
|
||||
body = JSON.stringify(options.data);
|
||||
}
|
||||
const r = new SigHttpRequest(
|
||||
options.method,
|
||||
options.url,
|
||||
options.headers,
|
||||
body
|
||||
);
|
||||
//Add header parameters, for example, x-domain-id for invoking a global service and x-project-id for invoking a project-level service.
|
||||
r.headers = { 'Content-Type': 'application/json' };
|
||||
//Add a body if you have specified the PUT or POST method. Special characters, such as the double quotation mark ("), contained in the body must be escaped.
|
||||
// r.body = option;
|
||||
const opt = sig.Sign(r);
|
||||
try {
|
||||
const res = await axios.request({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: opt.headers,
|
||||
data: body,
|
||||
});
|
||||
return res.data;
|
||||
} catch (e: any) {
|
||||
logger.error('华为云接口请求出错:', e?.response?.data);
|
||||
const error: any = new Error(e?.response?.data.message);
|
||||
error.code = e?.response?.code;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
476
packages/ui/certd-server/src/plugins/plugin-huawei/lib/signer.ts
Normal file
476
packages/ui/certd-server/src/plugins/plugin-huawei/lib/signer.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
import crypto from 'crypto';
|
||||
function hmacsha256(keyByte: any, message: any) {
|
||||
return crypto
|
||||
.createHmac('SHA256', keyByte)
|
||||
.update(message)
|
||||
.digest()
|
||||
.toString('hex');
|
||||
}
|
||||
function HexEncodeSHA256Hash(body: any) {
|
||||
return crypto.createHash('SHA256').update(body).digest().toString('hex');
|
||||
}
|
||||
const Algorithm = 'SDK-HMAC-SHA256';
|
||||
const HeaderXDate = 'X-Sdk-Date';
|
||||
const HeaderAuthorization = 'Authorization';
|
||||
const HeaderContentSha256 = 'x-sdk-content-sha256';
|
||||
|
||||
const hexTable = new Array(256);
|
||||
for (let i = 0; i < 256; ++i)
|
||||
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
|
||||
|
||||
const noEscape = [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 0 - 15
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 16 - 31
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0, // 32 - 47
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 48 - 63
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1, // 64 - 79
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1, // 80 - 95
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1, // 96 - 111
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0, // 112 - 127
|
||||
];
|
||||
|
||||
// function urlEncode is based on https://github.com/nodejs/node/blob/master/lib/querystring.js
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
function urlEncode(str: any) {
|
||||
if (typeof str !== 'string') {
|
||||
if (typeof str === 'object') str = String(str);
|
||||
else str += '';
|
||||
}
|
||||
let out = '';
|
||||
let lastPos = 0;
|
||||
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
let c = str.charCodeAt(i);
|
||||
|
||||
// ASCII
|
||||
if (c < 0x80) {
|
||||
if (noEscape[c] === 1) continue;
|
||||
if (lastPos < i) out += str.slice(lastPos, i);
|
||||
lastPos = i + 1;
|
||||
out += hexTable[c];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastPos < i) out += str.slice(lastPos, i);
|
||||
|
||||
// Multi-byte characters ...
|
||||
if (c < 0x800) {
|
||||
lastPos = i + 1;
|
||||
out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)];
|
||||
continue;
|
||||
}
|
||||
if (c < 0xd800 || c >= 0xe000) {
|
||||
lastPos = i + 1;
|
||||
out +=
|
||||
hexTable[0xe0 | (c >> 12)] +
|
||||
hexTable[0x80 | ((c >> 6) & 0x3f)] +
|
||||
hexTable[0x80 | (c & 0x3f)];
|
||||
continue;
|
||||
}
|
||||
// Surrogate pair
|
||||
++i;
|
||||
|
||||
if (i >= str.length) throw new Error('ERR_INVALID_URI');
|
||||
|
||||
const c2 = str.charCodeAt(i) & 0x3ff;
|
||||
|
||||
lastPos = i + 1;
|
||||
c = 0x10000 + (((c & 0x3ff) << 10) | c2);
|
||||
out +=
|
||||
hexTable[0xf0 | (c >> 18)] +
|
||||
hexTable[0x80 | ((c >> 12) & 0x3f)] +
|
||||
hexTable[0x80 | ((c >> 6) & 0x3f)] +
|
||||
hexTable[0x80 | (c & 0x3f)];
|
||||
}
|
||||
if (lastPos === 0) return str;
|
||||
if (lastPos < str.length) return out + str.slice(lastPos);
|
||||
return out;
|
||||
}
|
||||
|
||||
function findHeader(r: any, header: any) {
|
||||
for (const k in r.headers) {
|
||||
if (k.toLowerCase() === header.toLowerCase()) {
|
||||
return r.headers[k];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build a CanonicalRequest from a regular request string
|
||||
//
|
||||
// CanonicalRequest =
|
||||
// HTTPRequestMethod + '\n' +
|
||||
// CanonicalURI + '\n' +
|
||||
// CanonicalQueryString + '\n' +
|
||||
// CanonicalHeaders + '\n' +
|
||||
// SignedHeaders + '\n' +
|
||||
// HexEncode(Hash(RequestPayload))
|
||||
function CanonicalRequest(r: any, signedHeaders: any) {
|
||||
let hexencode = findHeader(r, HeaderContentSha256);
|
||||
if (hexencode === null) {
|
||||
const data = RequestPayload(r);
|
||||
hexencode = HexEncodeSHA256Hash(data);
|
||||
}
|
||||
return (
|
||||
r.method +
|
||||
'\n' +
|
||||
CanonicalURI(r) +
|
||||
'\n' +
|
||||
CanonicalQueryString(r) +
|
||||
'\n' +
|
||||
CanonicalHeaders(r, signedHeaders) +
|
||||
'\n' +
|
||||
signedHeaders.join(';') +
|
||||
'\n' +
|
||||
hexencode
|
||||
);
|
||||
}
|
||||
|
||||
function CanonicalURI(r: any) {
|
||||
const pattens = r.uri.split('/');
|
||||
const uri = [];
|
||||
for (const k in pattens) {
|
||||
const v = pattens[k];
|
||||
uri.push(urlEncode(v));
|
||||
}
|
||||
let urlpath = uri.join('/');
|
||||
if (urlpath[urlpath.length - 1] !== '/') {
|
||||
urlpath = urlpath + '/';
|
||||
}
|
||||
//r.uri = urlpath
|
||||
return urlpath;
|
||||
}
|
||||
|
||||
function CanonicalQueryString(r: any) {
|
||||
const keys = [];
|
||||
for (const key in r.query) {
|
||||
keys.push(key);
|
||||
}
|
||||
keys.sort();
|
||||
const a = [];
|
||||
for (const i in keys) {
|
||||
const key = urlEncode(keys[i]);
|
||||
const value = r.query[keys[i]];
|
||||
if (Array.isArray(value)) {
|
||||
value.sort();
|
||||
for (const iv in value) {
|
||||
a.push(key + '=' + urlEncode(value[iv]));
|
||||
}
|
||||
} else {
|
||||
a.push(key + '=' + urlEncode(value));
|
||||
}
|
||||
}
|
||||
return a.join('&');
|
||||
}
|
||||
|
||||
function CanonicalHeaders(r: any, signedHeaders: any) {
|
||||
const headers: any = {};
|
||||
for (const key in r.headers) {
|
||||
headers[key.toLowerCase()] = r.headers[key];
|
||||
}
|
||||
const a = [];
|
||||
for (const i in signedHeaders) {
|
||||
const value = headers[signedHeaders[i]];
|
||||
a.push(signedHeaders[i] + ':' + value.trim());
|
||||
}
|
||||
return a.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function SignedHeaders(r: any) {
|
||||
const a = [];
|
||||
for (const key in r.headers) {
|
||||
a.push(key.toLowerCase());
|
||||
}
|
||||
a.sort();
|
||||
return a;
|
||||
}
|
||||
|
||||
function RequestPayload(r: any) {
|
||||
return r.body;
|
||||
}
|
||||
|
||||
// Create a "String to Sign".
|
||||
function StringToSign(canonicalRequest: any, t: any) {
|
||||
const bytes = HexEncodeSHA256Hash(canonicalRequest);
|
||||
return Algorithm + '\n' + t + '\n' + bytes;
|
||||
}
|
||||
|
||||
// Create the HWS Signature.
|
||||
function SignStringToSign(stringToSign: any, signingKey: any) {
|
||||
return hmacsha256(signingKey, stringToSign);
|
||||
}
|
||||
|
||||
// Get the finalized value for the "Authorization" header. The signature
|
||||
// parameter is the output from SignStringToSign
|
||||
function AuthHeaderValue(signature: any, Key: any, signedHeaders: any) {
|
||||
return (
|
||||
Algorithm +
|
||||
' Access=' +
|
||||
Key +
|
||||
', SignedHeaders=' +
|
||||
signedHeaders.join(';') +
|
||||
', Signature=' +
|
||||
signature
|
||||
);
|
||||
}
|
||||
|
||||
function twoChar(s: any) {
|
||||
if (s >= 10) {
|
||||
return '' + s;
|
||||
} else {
|
||||
return '0' + s;
|
||||
}
|
||||
}
|
||||
|
||||
function getTime() {
|
||||
const date = new Date();
|
||||
return (
|
||||
'' +
|
||||
date.getUTCFullYear() +
|
||||
twoChar(date.getUTCMonth() + 1) +
|
||||
twoChar(date.getUTCDate()) +
|
||||
'T' +
|
||||
twoChar(date.getUTCHours()) +
|
||||
twoChar(date.getUTCMinutes()) +
|
||||
twoChar(date.getUTCSeconds()) +
|
||||
'Z'
|
||||
);
|
||||
}
|
||||
|
||||
export class SigHttpRequest {
|
||||
method = '';
|
||||
host = '';
|
||||
uri = '';
|
||||
query: any = {};
|
||||
headers: any = {};
|
||||
body = '';
|
||||
|
||||
constructor(method: any, url: any, headers: any, body: any) {
|
||||
if (method === undefined) {
|
||||
this.method = '';
|
||||
} else {
|
||||
this.method = method;
|
||||
}
|
||||
if (url === undefined) {
|
||||
this.host = '';
|
||||
this.uri = '';
|
||||
this.query = {};
|
||||
} else {
|
||||
this.query = {};
|
||||
let host, path;
|
||||
let i = url.indexOf('://');
|
||||
if (i !== -1) {
|
||||
url = url.substr(i + 3);
|
||||
}
|
||||
i = url.indexOf('?');
|
||||
if (i !== -1) {
|
||||
const query_str = url.substr(i + 1);
|
||||
url = url.substr(0, i);
|
||||
const spl = query_str.split('&');
|
||||
for (const i in spl) {
|
||||
const kv = spl[i];
|
||||
const index = kv.indexOf('=');
|
||||
let key, value;
|
||||
if (index >= 0) {
|
||||
key = kv.substr(0, index);
|
||||
value = kv.substr(index + 1);
|
||||
} else {
|
||||
key = kv;
|
||||
value = '';
|
||||
}
|
||||
if (key !== '') {
|
||||
key = decodeURI(key);
|
||||
value = decodeURI(value);
|
||||
if (this.query[key] === undefined) {
|
||||
this.query[key] = [value];
|
||||
} else {
|
||||
this.query[key].push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i = url.indexOf('/');
|
||||
if (i === -1) {
|
||||
host = url;
|
||||
path = '/';
|
||||
} else {
|
||||
host = url.substr(0, i);
|
||||
path = url.substr(i);
|
||||
}
|
||||
this.host = host;
|
||||
this.uri = decodeURI(path);
|
||||
}
|
||||
if (headers === undefined) {
|
||||
this.headers = {};
|
||||
} else {
|
||||
this.headers = headers;
|
||||
}
|
||||
if (body === undefined) {
|
||||
this.body = '';
|
||||
} else {
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
}
|
||||
export class Signer {
|
||||
Key = '';
|
||||
Secret = '';
|
||||
constructor(Key: any, Secret: any) {
|
||||
this.Key = Key;
|
||||
this.Secret = Secret;
|
||||
}
|
||||
|
||||
Sign(r: any) {
|
||||
let headerTime = findHeader(r, HeaderXDate);
|
||||
if (headerTime === null) {
|
||||
headerTime = getTime();
|
||||
r.headers[HeaderXDate] = headerTime;
|
||||
}
|
||||
if (r.method !== 'PUT' && r.method !== 'PATCH' && r.method !== 'POST') {
|
||||
r.body = '';
|
||||
}
|
||||
let queryString = CanonicalQueryString(r);
|
||||
if (queryString !== '') {
|
||||
queryString = '?' + queryString;
|
||||
}
|
||||
const options = {
|
||||
hostname: r.host,
|
||||
path: encodeURI(r.uri) + queryString,
|
||||
method: r.method,
|
||||
headers: r.headers,
|
||||
};
|
||||
if (findHeader(r, 'host') === null) {
|
||||
r.headers.host = r.host;
|
||||
}
|
||||
const signedHeaders = SignedHeaders(r);
|
||||
const canonicalRequest = CanonicalRequest(r, signedHeaders);
|
||||
const stringToSign = StringToSign(canonicalRequest, headerTime);
|
||||
const signature = SignStringToSign(stringToSign, this.Secret);
|
||||
options.headers[HeaderAuthorization] = AuthHeaderValue(
|
||||
signature,
|
||||
this.Key,
|
||||
signedHeaders
|
||||
);
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IsAccess, AccessInput } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'dnspod',
|
||||
title: 'dnspod',
|
||||
desc: '腾讯云的域名解析接口已迁移到dnspod',
|
||||
})
|
||||
export class DnspodAccess {
|
||||
@AccessInput({
|
||||
title: '端点',
|
||||
component: {
|
||||
placeholder: 'endpoint',
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
options: [
|
||||
{ value: 'https://dnsapi.cn', label: '中国站' },
|
||||
{ value: 'https://api.dnspod.com', label: '国际站' },
|
||||
],
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
endpoint = '';
|
||||
|
||||
@AccessInput({
|
||||
title: '账户id',
|
||||
component: {
|
||||
placeholder: 'dnspod接口账户id',
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
id = '';
|
||||
|
||||
@AccessInput({
|
||||
title: 'token',
|
||||
component: {
|
||||
placeholder: '开放接口token',
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
token = '';
|
||||
}
|
||||
|
||||
new DnspodAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './dnspod-access';
|
||||
export * from './tencent-access';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IsAccess, AccessInput } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'tencent',
|
||||
title: '腾讯云',
|
||||
})
|
||||
export class TencentAccess {
|
||||
@AccessInput({
|
||||
title: 'secretId',
|
||||
helper:
|
||||
'使用对应的插件需要有对应的权限,比如上传证书,需要证书管理权限;部署到clb需要clb相关权限',
|
||||
component: {
|
||||
placeholder: 'secretId',
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
secretId = '';
|
||||
@AccessInput({
|
||||
title: 'secretKey',
|
||||
component: {
|
||||
placeholder: 'secretKey',
|
||||
},
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
secretKey = '';
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
|
||||
import {
|
||||
CreateRecordOptions,
|
||||
IDnsProvider,
|
||||
IsDnsProvider,
|
||||
RemoveRecordOptions,
|
||||
} from '@certd/plugin-cert';
|
||||
import _ from 'lodash';
|
||||
import { DnspodAccess } from '../access';
|
||||
|
||||
@IsDnsProvider({
|
||||
name: 'dnspod',
|
||||
title: 'dnspod(腾讯云)',
|
||||
desc: '腾讯云的域名解析接口已迁移到dnspod',
|
||||
accessType: 'dnspod',
|
||||
})
|
||||
export class DnspodDnsProvider implements IDnsProvider {
|
||||
@Autowire()
|
||||
http!: HttpClient;
|
||||
|
||||
@Autowire()
|
||||
access!: DnspodAccess;
|
||||
@Autowire()
|
||||
logger!: ILogger;
|
||||
|
||||
loginToken: any;
|
||||
|
||||
endpoint = '';
|
||||
async onInstance() {
|
||||
const access: DnspodAccess = this.access as DnspodAccess;
|
||||
this.loginToken = access.id + ',' + access.token;
|
||||
this.endpoint = access.endpoint || 'https://dnsapi.cn';
|
||||
}
|
||||
|
||||
async doRequest(options: any, successCodes: string[] = []) {
|
||||
const config: any = {
|
||||
// @ts-ignore
|
||||
method: 'post',
|
||||
formData: {
|
||||
login_token: this.loginToken,
|
||||
format: 'json',
|
||||
lang: 'cn',
|
||||
error_on_empty: 'no',
|
||||
},
|
||||
timeout: 5000,
|
||||
};
|
||||
_.merge(config, options);
|
||||
|
||||
const ret: any = await this.http.request(config);
|
||||
if (!ret || !ret.status) {
|
||||
const code = ret.status.code;
|
||||
if (code !== '1' || !successCodes.includes(code)) {
|
||||
throw new Error(
|
||||
'请求失败:' + ret.status.message + ',api=' + config.url
|
||||
);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getDomainList() {
|
||||
const ret = await this.doRequest({
|
||||
url: this.access.endpoint + '/Domain.List',
|
||||
});
|
||||
this.logger.info('dnspod 域名列表:', ret.domains);
|
||||
return ret.domains;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info('添加域名解析:', fullRecord, value);
|
||||
const domainItem = await this.matchDomain(fullRecord);
|
||||
const domain = domainItem.name;
|
||||
const rr = fullRecord.replace('.' + domain, '');
|
||||
|
||||
const ret = await this.doRequest(
|
||||
{
|
||||
url: this.access.endpoint + '/Record.Create',
|
||||
formData: {
|
||||
domain,
|
||||
sub_domain: rr,
|
||||
record_type: type,
|
||||
record_line: '默认',
|
||||
value: value,
|
||||
mx: 1,
|
||||
},
|
||||
},
|
||||
['104']
|
||||
); // 104错误码为记录已存在,无需再次添加
|
||||
this.logger.info(
|
||||
'添加域名解析成功:',
|
||||
fullRecord,
|
||||
value,
|
||||
JSON.stringify(ret.record)
|
||||
);
|
||||
return ret.record;
|
||||
}
|
||||
|
||||
async removeRecord(options: RemoveRecordOptions) {
|
||||
const { fullRecord, value, record } = options;
|
||||
const domain = await this.matchDomain(fullRecord);
|
||||
|
||||
const ret = await this.doRequest({
|
||||
url: this.access.endpoint + '/Record.Remove',
|
||||
formData: {
|
||||
domain,
|
||||
record_id: record.id,
|
||||
},
|
||||
});
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value);
|
||||
return ret.RecordId;
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: any) {
|
||||
const list = await this.getDomainList();
|
||||
if (list == null) {
|
||||
throw new Error('域名列表不能为空');
|
||||
}
|
||||
let domain = null;
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, '.' + item.name)) {
|
||||
domain = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error('找不到域名,请检查域名是否正确:' + dnsRecord);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
new DnspodDnsProvider();
|
||||
@@ -0,0 +1 @@
|
||||
import './dnspod-dns-provider';
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
} from '@certd/pipeline';
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs/index';
|
||||
import { TencentAccess } from '../../access';
|
||||
import { CertInfo } from '@certd/plugin-cert';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToTencentCDN',
|
||||
title: '部署到腾讯云CDN',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class DeployToCdnPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
from: 'CertApply',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: 'Access提供者',
|
||||
helper: 'access 授权',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'tencent',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '证书名称',
|
||||
helper: '证书上传后将以此参数作为名称前缀',
|
||||
})
|
||||
certName!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'cdn加速域名',
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
domainName!: string;
|
||||
|
||||
// @TaskInput({
|
||||
// title: "CDN接口",
|
||||
// helper: "CDN接口端点",
|
||||
// component: {
|
||||
// name: "a-select",
|
||||
// type: "tencent",
|
||||
// },
|
||||
// required: true,
|
||||
// })
|
||||
// endpoint!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const accessProvider: TencentAccess = (await this.accessService.getById(
|
||||
this.accessId
|
||||
)) as TencentAccess;
|
||||
const client = this.getClient(accessProvider);
|
||||
const params = this.buildParams();
|
||||
await this.doRequest(client, params);
|
||||
}
|
||||
|
||||
getClient(accessProvider: TencentAccess) {
|
||||
const CdnClient = tencentcloud.cdn.v20180606.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region: '',
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'cdn.tencentcloudapi.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new CdnClient(clientConfig);
|
||||
}
|
||||
|
||||
buildParams() {
|
||||
return {
|
||||
Https: {
|
||||
Switch: 'on',
|
||||
CertInfo: {
|
||||
Certificate: this.cert.crt,
|
||||
PrivateKey: this.cert.key,
|
||||
},
|
||||
},
|
||||
Domain: this.domainName,
|
||||
};
|
||||
}
|
||||
|
||||
async doRequest(client: any, params: any) {
|
||||
const ret = await client.UpdateDomainConfig(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info('设置腾讯云CDN证书成功:', ret.RequestId);
|
||||
return ret.RequestId;
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
utils,
|
||||
} from '@certd/pipeline';
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs/index';
|
||||
import { TencentAccess } from '../../access';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToTencentCLB',
|
||||
title: '部署到腾讯云CLB',
|
||||
desc: '暂时只支持单向认证证书,暂时只支持通用负载均衡',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class DeployToClbPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '大区',
|
||||
value: 'ap-guangzhou',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
options: [{ value: 'ap-guangzhou' }],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
region!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '证书名称前缀',
|
||||
})
|
||||
certName!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '负载均衡ID',
|
||||
helper:
|
||||
'如果没有配置,则根据域名匹配负载均衡下的监听器(根据域名匹配时暂时只支持前100个)',
|
||||
required: true,
|
||||
})
|
||||
loadBalancerId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '监听器ID',
|
||||
helper: '如果没有配置,则根据域名或负载均衡id匹配监听器',
|
||||
})
|
||||
listenerId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名',
|
||||
required: true,
|
||||
helper: '要更新的支持https的负载均衡的域名',
|
||||
})
|
||||
domain!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: any;
|
||||
|
||||
@TaskInput({
|
||||
title: 'Access提供者',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'tencent',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const accessProvider = (await this.accessService.getById(
|
||||
this.accessId
|
||||
)) as TencentAccess;
|
||||
const client = this.getClient(accessProvider, this.region);
|
||||
|
||||
const lastCertId = await this.getCertIdFromProps(client);
|
||||
if (!this.domain) {
|
||||
await this.updateListener(client);
|
||||
} else {
|
||||
await this.updateByDomainAttr(client);
|
||||
}
|
||||
|
||||
try {
|
||||
await utils.sleep(2000);
|
||||
let newCertId = await this.getCertIdFromProps(client);
|
||||
if (
|
||||
(lastCertId && newCertId === lastCertId) ||
|
||||
(!lastCertId && !newCertId)
|
||||
) {
|
||||
await utils.sleep(2000);
|
||||
newCertId = await this.getCertIdFromProps(client);
|
||||
}
|
||||
if (newCertId === lastCertId) {
|
||||
return;
|
||||
}
|
||||
this.logger.info('腾讯云证书ID:', newCertId);
|
||||
} catch (e) {
|
||||
this.logger.warn('查询腾讯云证书失败', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async getCertIdFromProps(client: any) {
|
||||
const listenerRet = await this.getListenerList(
|
||||
client,
|
||||
this.loadBalancerId,
|
||||
[this.listenerId]
|
||||
);
|
||||
return this.getCertIdFromListener(listenerRet[0], this.domain);
|
||||
}
|
||||
|
||||
getCertIdFromListener(listener: any, domain: string) {
|
||||
let certId;
|
||||
if (!domain) {
|
||||
certId = listener.Certificate.CertId;
|
||||
} else {
|
||||
if (listener.Rules && listener.Rules.length > 0) {
|
||||
for (const rule of listener.Rules) {
|
||||
if (rule.Domain === domain) {
|
||||
if (rule.Certificate != null) {
|
||||
certId = rule.Certificate.CertId;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return certId;
|
||||
}
|
||||
|
||||
async updateListener(client: any) {
|
||||
const params = this.buildProps();
|
||||
const ret = await client.ModifyListener(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info(
|
||||
'设置腾讯云CLB证书成功:',
|
||||
ret.RequestId,
|
||||
'->loadBalancerId:',
|
||||
this.loadBalancerId,
|
||||
'listenerId',
|
||||
this.listenerId
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
async updateByDomainAttr(client: any) {
|
||||
const params: any = this.buildProps();
|
||||
params.Domain = this.domain;
|
||||
const ret = await client.ModifyDomainAttributes(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info(
|
||||
'设置腾讯云CLB证书(sni)成功:',
|
||||
ret.RequestId,
|
||||
'->loadBalancerId:',
|
||||
this.loadBalancerId,
|
||||
'listenerId',
|
||||
this.listenerId,
|
||||
'domain:',
|
||||
this.domain
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = 'certd';
|
||||
}
|
||||
return name + '-' + dayjs().format('YYYYMMDD-HHmmss');
|
||||
}
|
||||
buildProps() {
|
||||
return {
|
||||
Certificate: {
|
||||
SSLMode: 'UNIDIRECTIONAL', // 单向认证
|
||||
CertName: this.appendTimeSuffix(this.certName || this.cert.domain),
|
||||
CertKey: this.cert.key,
|
||||
CertContent: this.cert.crt,
|
||||
},
|
||||
LoadBalancerId: this.loadBalancerId,
|
||||
ListenerId: this.listenerId,
|
||||
};
|
||||
}
|
||||
|
||||
async getCLBList(client: any) {
|
||||
const params = {
|
||||
Limit: 100, // 最大暂时只支持100个,暂时没做翻页
|
||||
OrderBy: 'CreateTime',
|
||||
OrderType: 0,
|
||||
// ...this.DescribeLoadBalancers,
|
||||
};
|
||||
const ret = await client.DescribeLoadBalancers(params);
|
||||
this.checkRet(ret);
|
||||
return ret.LoadBalancerSet;
|
||||
}
|
||||
|
||||
async getListenerList(client: any, balancerId: any, listenerIds: any) {
|
||||
// HTTPS
|
||||
const params = {
|
||||
LoadBalancerId: balancerId,
|
||||
Protocol: 'HTTPS',
|
||||
ListenerIds: listenerIds,
|
||||
};
|
||||
const ret = await client.DescribeListeners(params);
|
||||
this.checkRet(ret);
|
||||
return ret.Listeners;
|
||||
}
|
||||
|
||||
getClient(accessProvider: TencentAccess, region: string) {
|
||||
const ClbClient = tencentcloud.clb.v20180317.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region: region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'clb.tencentcloudapi.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new ClbClient(clientConfig);
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
utils,
|
||||
} from '@certd/pipeline';
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs/index';
|
||||
import { K8sClient } from '@certd/plugin-util';
|
||||
import dayjs from 'dayjs';
|
||||
import { Logger } from 'log4js';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToTencentTKEIngress',
|
||||
title: '部署到腾讯云TKE-ingress',
|
||||
desc: '需要【上传到腾讯云】作为前置任务',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({ title: '大区', value: 'ap-guangzhou', required: true })
|
||||
region!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '集群ID',
|
||||
required: true,
|
||||
desc: '例如:cls-6lbj1vee',
|
||||
request: true,
|
||||
})
|
||||
clusterId!: string;
|
||||
|
||||
@TaskInput({ title: '集群namespace', value: 'default', required: true })
|
||||
namespace!: string;
|
||||
|
||||
@TaskInput({ title: '证书的secret名称', required: true })
|
||||
secretName!: string | string[];
|
||||
|
||||
@TaskInput({ title: 'ingress名称', required: true })
|
||||
ingressName!: string | string[];
|
||||
|
||||
@TaskInput({
|
||||
title: 'ingress类型',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
options: [{ value: 'qcloud' }, { value: 'nginx' }],
|
||||
},
|
||||
helper: '可选 qcloud / nginx',
|
||||
})
|
||||
ingressClass!: string;
|
||||
|
||||
@TaskInput({ title: '集群内网ip', helper: '如果开启了外网的话,无需设置' })
|
||||
clusterIp!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '集群域名',
|
||||
helper: '可不填,默认为:[clusterId].ccs.tencent-cloud.com',
|
||||
})
|
||||
clusterDomain!: string;
|
||||
@TaskInput({
|
||||
title: '腾讯云证书id',
|
||||
helper: '请选择“上传证书到腾讯云”前置任务的输出',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
from: 'UploadCertToTencent',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
tencentCertId!: string;
|
||||
|
||||
/**
|
||||
* AccessProvider的key,或者一个包含access的具体的对象
|
||||
*/
|
||||
@TaskInput({
|
||||
title: 'Access授权',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'tencent',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: any;
|
||||
|
||||
logger!: Logger;
|
||||
accessService!: IAccessService;
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const accessProvider = await this.accessService.getById(this.accessId);
|
||||
const tkeClient = this.getTkeClient(accessProvider, this.region);
|
||||
const kubeConfigStr = await this.getTkeKubeConfig(
|
||||
tkeClient,
|
||||
this.clusterId
|
||||
);
|
||||
|
||||
this.logger.info('kubeconfig已成功获取');
|
||||
const k8sClient = new K8sClient(kubeConfigStr);
|
||||
if (this.clusterIp != null) {
|
||||
if (!this.clusterDomain) {
|
||||
this.clusterDomain = `${this.clusterId}.ccs.tencent-cloud.com`;
|
||||
}
|
||||
// 修改内网解析ip地址
|
||||
k8sClient.setLookup({ [this.clusterDomain]: { ip: this.clusterIp } });
|
||||
}
|
||||
const ingressType = this.ingressClass || 'qcloud';
|
||||
if (ingressType === 'qcloud') {
|
||||
await this.patchQcloudCertSecret({ k8sClient });
|
||||
} else {
|
||||
await this.patchNginxCertSecret({ k8sClient });
|
||||
}
|
||||
|
||||
await utils.sleep(2000); // 停留2秒,等待secret部署完成
|
||||
await this.restartIngress({ k8sClient });
|
||||
}
|
||||
|
||||
getTkeClient(accessProvider: any, region = 'ap-guangzhou') {
|
||||
const TkeClient = tencentcloud.tke.v20180525.Client;
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'tke.tencentcloudapi.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new TkeClient(clientConfig);
|
||||
}
|
||||
|
||||
async getTkeKubeConfig(client: any, clusterId: string) {
|
||||
// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
|
||||
const params = {
|
||||
ClusterId: clusterId,
|
||||
};
|
||||
const ret = await client.DescribeClusterKubeconfig(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info(
|
||||
'注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster'
|
||||
);
|
||||
return ret.Kubeconfig;
|
||||
}
|
||||
|
||||
appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = 'certd';
|
||||
}
|
||||
return name + '-' + dayjs().format('YYYYMMDD-HHmmss');
|
||||
}
|
||||
|
||||
async patchQcloudCertSecret(options: { k8sClient: any }) {
|
||||
if (this.tencentCertId == null) {
|
||||
throw new Error('请先将【上传证书到腾讯云】作为前置任务');
|
||||
}
|
||||
this.logger.info('腾讯云证书ID:', this.tencentCertId);
|
||||
const certIdBase64 = Buffer.from(this.tencentCertId).toString('base64');
|
||||
|
||||
const { namespace, secretName } = this;
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
qcloud_cert_id: certIdBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix('certd'),
|
||||
},
|
||||
},
|
||||
};
|
||||
let secretNames: any = secretName;
|
||||
if (typeof secretName === 'string') {
|
||||
secretNames = [secretName];
|
||||
}
|
||||
for (const secret of secretNames) {
|
||||
await options.k8sClient.patchSecret({
|
||||
namespace,
|
||||
secretName: secret,
|
||||
body,
|
||||
});
|
||||
this.logger.info(`CertSecret已更新:${secret}`);
|
||||
}
|
||||
}
|
||||
|
||||
async patchNginxCertSecret(options: { k8sClient: any }) {
|
||||
const { k8sClient } = options;
|
||||
const { cert } = this;
|
||||
const crt = cert.crt;
|
||||
const key = cert.key;
|
||||
const crtBase64 = Buffer.from(crt).toString('base64');
|
||||
const keyBase64 = Buffer.from(key).toString('base64');
|
||||
|
||||
const { namespace, secretName } = this;
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
'tls.crt': crtBase64,
|
||||
'tls.key': keyBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix('certd'),
|
||||
},
|
||||
},
|
||||
};
|
||||
let secretNames = secretName;
|
||||
if (typeof secretName === 'string') {
|
||||
secretNames = [secretName];
|
||||
}
|
||||
for (const secret of secretNames) {
|
||||
await k8sClient.patchSecret({ namespace, secretName: secret, body });
|
||||
this.logger.info(`CertSecret已更新:${secret}`);
|
||||
}
|
||||
}
|
||||
|
||||
async restartIngress(options: { k8sClient: any }) {
|
||||
const { k8sClient } = options;
|
||||
const { namespace, ingressName } = this;
|
||||
|
||||
const body = {
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix('certd'),
|
||||
},
|
||||
},
|
||||
};
|
||||
let ingressNames = this.ingressName;
|
||||
if (typeof ingressName === 'string') {
|
||||
ingressNames = [ingressName];
|
||||
}
|
||||
for (const ingress of ingressNames) {
|
||||
await k8sClient.patchIngress({ namespace, ingressName: ingress, body });
|
||||
this.logger.info(`ingress已重启:${ingress}`);
|
||||
}
|
||||
}
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './deploy-to-clb';
|
||||
export * from './deploy-to-tke-ingress';
|
||||
export * from './deploy-to-cdn';
|
||||
export * from './upload-to-tencent';
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
AbstractTaskPlugin,
|
||||
IAccessService,
|
||||
ILogger,
|
||||
IsTaskPlugin,
|
||||
RunStrategy,
|
||||
TaskInput,
|
||||
TaskOutput,
|
||||
} from '@certd/pipeline';
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs/index';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'UploadCertToTencent',
|
||||
title: '上传证书到腾讯云',
|
||||
desc: '上传成功后输出:tencentCertId',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class UploadToTencentPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({ title: '证书名称' })
|
||||
name!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'Access授权',
|
||||
helper: 'access授权',
|
||||
component: {
|
||||
name: 'pi-access-selector',
|
||||
type: 'tencent',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'pi-output-selector',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: any;
|
||||
|
||||
@TaskOutput({
|
||||
title: '上传成功后的腾讯云CertId',
|
||||
})
|
||||
tencentCertId?: string;
|
||||
|
||||
accessService!: IAccessService;
|
||||
logger!: ILogger;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const { accessId, name, cert } = this;
|
||||
const accessProvider = await this.accessService.getById(accessId);
|
||||
const certName = this.appendTimeSuffix(name || cert.domain);
|
||||
const client = this.getClient(accessProvider);
|
||||
|
||||
const params = {
|
||||
CertificatePublicKey: cert.crt,
|
||||
CertificatePrivateKey: cert.key,
|
||||
Alias: certName,
|
||||
};
|
||||
const ret = await client.UploadCertificate(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info('证书上传成功:tencentCertId=', ret.CertificateId);
|
||||
|
||||
this.tencentCertId = ret.CertificateId;
|
||||
}
|
||||
|
||||
appendTimeSuffix(name: string) {
|
||||
if (name == null) {
|
||||
name = 'certd';
|
||||
}
|
||||
return name + '-' + dayjs().format('YYYYMMDD-HHmmss');
|
||||
}
|
||||
|
||||
getClient(accessProvider: any) {
|
||||
const SslClient = tencentcloud.ssl.v20191205.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey,
|
||||
},
|
||||
region: '',
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'ssl.tencentcloudapi.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new SslClient(clientConfig);
|
||||
}
|
||||
|
||||
// async rollback({ input }) {
|
||||
// const { accessId } = input;
|
||||
// const accessProvider = await this.accessService.getById(accessId);
|
||||
// const client = this.getClient(accessProvider);
|
||||
//
|
||||
// const { tencentCertId } = context;
|
||||
// const params = {
|
||||
// CertificateId: tencentCertId,
|
||||
// };
|
||||
// const ret = await client.DeleteCertificate(params);
|
||||
// this.checkRet(ret);
|
||||
// this.logger.info("证书删除成功:DeleteResult=", ret.DeleteResult);
|
||||
// delete context.tencentCertId;
|
||||
// }
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user