feat: midway注解方式编写插件

pull/9/head^2
xiaojunnuo 2023-01-11 20:39:48 +08:00
parent 52522f27e9
commit dcd1023a39
47 changed files with 484 additions and 714 deletions

View File

@ -6,7 +6,7 @@ export type AccessInputDefine = FormItemProps & {
required?: boolean;
};
export type AccessDefine = Registrable & {
inputs?: {
input?: {
[key: string]: AccessInputDefine;
};
};

View File

@ -20,7 +20,7 @@ export function IsAccess(define: AccessDefine): ClassDecorator {
inputs[property] = input;
}
}
_.merge(define, { inputs });
_.merge(define, { input: inputs });
Reflect.defineMetadata(ACCESS_CLASS_KEY, define, target);
target.define = define;
accessRegistry.register(define.name, {
@ -30,9 +30,9 @@ export function IsAccess(define: AccessDefine): ClassDecorator {
};
}
export function IsAccessInput(input?: AccessInputDefine): PropertyDecorator {
export function AccessInput(input?: AccessInputDefine): PropertyDecorator {
return (target, propertyKey) => {
target = Decorator.target(target);
target = Decorator.target(target, propertyKey);
// const _type = Reflect.getMetadata("design:type", target, propertyKey);
Reflect.defineMetadata(ACCESS_INPUT_KEY, input, target, propertyKey);
};

View File

@ -9,6 +9,7 @@ import { Logger } from "log4js";
import { request } from "../utils/util.request";
import { IAccessService } from "../access";
import { RegistryItem } from "../registry";
import { Decorator } from "../decorator";
export class Executor {
userId: any;
@ -149,21 +150,17 @@ export class Executor {
// @ts-ignore
const define: PluginDefine = plugin.define;
//从outputContext读取输入参数
_.forEach(define.input, (item, key) => {
Decorator.inject(define.input, instance, step.input, (item, key) => {
if (item.component?.name === "pi-output-selector") {
const contextKey = step.input[key];
if (contextKey != null) {
step.input[key] = this.runtime.context[contextKey];
}
} else {
instance[key] = step.input[key];
}
});
_.forEach(define.autowire, (item, key: string) => {
instance[key] = context[key];
});
Decorator.inject(define.autowire, instance, context);
await instance.onInit();
await instance.execute();
//输出到output context

View File

@ -0,0 +1,17 @@
import { Decorator } from "./index";
export type AutowireProp = {
name?: string;
type?: any;
};
export const AUTOWIRE_KEY = "pipeline:autowire";
export function Autowire(props?: AutowireProp): PropertyDecorator {
return (target, propertyKey) => {
const _type = Reflect.getMetadata("design:type", target, propertyKey);
target = Decorator.target(target, propertyKey);
props = props || {};
props.type = _type;
Reflect.defineMetadata(AUTOWIRE_KEY, props || {}, target, propertyKey);
};
}

View File

@ -1,28 +1,2 @@
const propertyMap: any = {};
function attachProperty(target: any, propertyKey: string | symbol) {
let props = propertyMap[target];
if (props == null) {
props = {};
propertyMap[target] = props;
}
props[propertyKey] = true;
}
function getClassProperties(target: any) {
return propertyMap[target] || {};
}
function target(target: any, propertyKey?: string | symbol) {
if (typeof target === "object" && target.constructor) {
target = target.constructor;
}
if (propertyKey != null) {
attachProperty(target, propertyKey);
}
return target;
}
export const Decorator = {
target,
attachProperty,
getClassProperties,
};
export * from "./utils";
export * from "./common";

View File

@ -0,0 +1,42 @@
import _ from "lodash";
const propertyMap: any = {};
function attachProperty(target: any, propertyKey: string | symbol) {
let props = propertyMap[target];
if (props == null) {
props = {};
propertyMap[target] = props;
}
props[propertyKey] = true;
}
function getClassProperties(target: any) {
return propertyMap[target] || {};
}
function target(target: any, propertyKey?: string | symbol) {
if (typeof target === "object" && target.constructor) {
target = target.constructor;
}
if (propertyKey != null) {
attachProperty(target, propertyKey);
}
return target;
}
function inject(define: any, instance: any, context: any, preHandler?: (item: any, key: string, instance: any, context: any) => void) {
_.forEach(define, (item, key) => {
if (preHandler) {
preHandler(item, key, instance, context);
}
if (context[key] != undefined) {
instance[key] = context[key];
}
});
}
export const Decorator = {
target,
attachProperty,
getClassProperties,
inject,
};

View File

@ -1,22 +0,0 @@
import { AbstractRegistrable } from "../registry";
import { CreateRecordOptions, IDnsProvider, DnsProviderDefine, RemoveRecordOptions } from "./api";
import { AbstractAccess } from "../access";
import { Logger } from "log4js";
import { AxiosInstance } from "axios";
export abstract class AbstractDnsProvider extends AbstractRegistrable<DnsProviderDefine> implements IDnsProvider {
access!: AbstractAccess;
logger!: Logger;
http!: AxiosInstance;
doInit(options: { access: AbstractAccess; logger: Logger; http: AxiosInstance }) {
this.access = options.access;
this.logger = options.logger;
this.http = options.http;
this.onInit();
}
protected abstract onInit(): void;
abstract createRecord(options: CreateRecordOptions): Promise<any>;
abstract removeRecord(options: RemoveRecordOptions): Promise<any>;
}

View File

@ -2,8 +2,8 @@ export * from "./core";
export * from "./d.ts";
export * from "./access";
export * from "./registry";
export * from "./dns-provider";
export * from "./plugin";
export * from "./utils";
export * from "./midway";
export * from "./context";
export * from "./decorator";

View File

@ -2,6 +2,7 @@ import _ from "lodash";
import { pluginRegistry } from "./registry";
import { PluginDefine, TaskInputDefine, TaskOutputDefine } from "./api";
import { Decorator } from "../decorator";
import { AUTOWIRE_KEY } from "../decorator";
// 提供一个唯一 key
export const PLUGIN_CLASS_KEY = "pipeline:plugin";
@ -20,7 +21,7 @@ export function IsTaskPlugin(define: PluginDefine): ClassDecorator {
inputs[property] = input;
}
const autowire = Reflect.getMetadata(PLUGIN_AUTOWIRE_KEY, target, property);
const autowire = Reflect.getMetadata(AUTOWIRE_KEY, target, property);
if (autowire) {
autowires[property] = autowire;
}
@ -59,19 +60,3 @@ export function TaskOutput(output?: TaskOutputDefine): PropertyDecorator {
Reflect.defineMetadata(PLUGIN_OUTPUT_KEY, output, target, propertyKey);
};
}
export type AutowireProp = {
name?: string;
type?: any;
};
export const PLUGIN_AUTOWIRE_KEY = "pipeline:plugin:autowire";
export function Autowire(props?: AutowireProp): PropertyDecorator {
return (target, propertyKey) => {
const _type = Reflect.getMetadata("design:type", target, propertyKey);
target = Decorator.target(target, propertyKey);
props = props || {};
props.type = _type;
Reflect.defineMetadata(PLUGIN_AUTOWIRE_KEY, props || {}, target, propertyKey);
};
}

View File

@ -20,7 +20,7 @@ export class Registry {
this.storage[key] = value;
}
get(name: string) {
get(name: string): RegistryItem {
if (!name) {
throw new Error("插件名称不能为空");
}

View File

@ -32,4 +32,4 @@ export function buildLogger(write: (text: string) => void) {
});
return logger;
}
export type LOGGER = Logger;
export type ILogger = Logger;

View File

@ -1,4 +1,4 @@
import { IsTaskPlugin, TaskInput, ITaskPlugin, LOGGER, Autowire, TaskOutput } from "../src";
import { IsTaskPlugin, TaskInput, ITaskPlugin, ILogger, Autowire, TaskOutput } from "../src";
@IsTaskPlugin({
name: "EchoPlugin",
@ -15,7 +15,7 @@ export class EchoPlugin implements ITaskPlugin {
cert!: any;
@Autowire()
logger!: LOGGER;
logger!: ILogger;
@TaskOutput({
title: "cert info",

View File

@ -15,6 +15,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}

View File

@ -1,4 +1,4 @@
import { IsAccess, IsAccessInput } from "@certd/pipeline";
import { IsAccess, AccessInput } from "@certd/pipeline";
@IsAccess({
name: "aliyun",
@ -6,7 +6,7 @@ import { IsAccess, IsAccessInput } from "@certd/pipeline";
desc: "",
})
export class AliyunAccess {
@IsAccessInput({
@AccessInput({
title: "accessKeyId",
component: {
placeholder: "accessKeyId",
@ -14,7 +14,7 @@ export class AliyunAccess {
required: true,
})
accessKeyId = "";
@IsAccessInput({
@AccessInput({
title: "accessKeySecret",
component: {
placeholder: "accessKeySecret",

View File

@ -1,7 +1,8 @@
import Core from "@alicloud/pop-core";
import _ from "lodash";
import { CreateRecordOptions, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "@certd/pipeline";
import { Logger } from "log4js";
import { CreateRecordOptions, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { Autowire, ILogger } from "@certd/pipeline";
import { AliyunAccess } from "../access";
@IsDnsProvider({
name: "aliyun",
@ -11,8 +12,10 @@ import { Logger } from "log4js";
})
export class AliyunDnsProvider implements IDnsProvider {
client: any;
access: any;
logger!: Logger;
@Autowire()
access!: AliyunAccess;
@Autowire()
logger!: ILogger;
async onInit() {
const access: any = this.access;
this.client = new Core({

View File

@ -1,101 +1,119 @@
import { AbstractPlugin, IsTask, RunStrategy, TaskInput, TaskOutput, TaskPlugin, utils } from "@certd/pipeline";
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, ILogger, 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";
@IsTask(() => {
return {
name: "DeployCertToAliyunAckIngress",
title: "部署到阿里云AckIngress",
input: {
clusterId: {
title: "集群id",
component: {
placeholder: "集群id",
},
},
secretName: {
title: "保密字典Id",
component: {
placeholder: "保密字典Id",
},
required: true,
},
regionId: {
title: "大区",
value: "cn-shanghai",
component: {
placeholder: "集群所属大区",
},
required: true,
},
namespace: {
title: "命名空间",
value: "default",
component: {
placeholder: "命名空间",
},
required: true,
},
ingressName: {
title: "ingress名称",
value: "",
component: {
placeholder: "ingress名称",
},
required: true,
helper: "可以传入一个数组",
},
ingressClass: {
title: "ingress类型",
value: "nginx",
component: {
placeholder: "暂时只支持nginx类型",
},
required: true,
},
isPrivateIpAddress: {
title: "是否私网ip",
value: false,
component: {
placeholder: "集群连接端点是否是私网ip",
},
helper: "如果您当前certd运行在同一个私网下可以选择是。",
required: true,
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
},
accessId: {
title: "Access授权",
helper: "阿里云授权AccessKeyId、AccessKeySecret",
component: {
name: "pi-access-selector",
type: "aliyun",
},
required: true,
},
@IsTaskPlugin({
name: "DeployCertToAliyunAckIngress",
title: "部署到阿里云AckIngress",
input: {},
output: {},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
output: {},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
};
},
})
export class DeployCertToAliyunAckIngressPlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
export class DeployCertToAliyunAckIngressPlugin implements ITaskPlugin {
@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;
@Autowire()
accessService!: IAccessService;
@Autowire()
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit(): Promise<void> {}
async execute(): Promise<void> {
console.log("开始部署证书到阿里云cdn");
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = input;
const access = (await this.accessService.getById(input.accessId)) as AliyunAccess;
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);
@ -106,17 +124,16 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractPlugin implement
throw new Error("暂未实现");
// await this.patchQcloudCertSecret({ k8sClient, props, context })
} else {
await this.patchNginxCertSecret({ cert, k8sClient, input });
await this.patchNginxCertSecret({ cert, k8sClient });
}
await utils.sleep(3000); // 停留2秒等待secret部署完成
// await this.restartIngress({ k8sClient, props })
return {};
}
async restartIngress(options: { k8sClient: any; input: TaskInput }) {
const { k8sClient, input } = options;
const { namespace } = input;
async restartIngress(options: { k8sClient: any }) {
const { k8sClient } = options;
const { namespace } = this;
const body = {
metadata: {
@ -136,7 +153,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractPlugin implement
return false;
}
for (const tls of item.spec.tls) {
if (tls.secretName === input.secretName) {
if (tls.secretName === this.secretName) {
return true;
}
}
@ -151,14 +168,14 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractPlugin implement
}
}
async patchNginxCertSecret(options: { cert: any; k8sClient: any; input: TaskInput }) {
const { cert, k8sClient, input } = options;
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 } = input;
const { namespace, secretName } = this;
const body = {
data: {
@ -171,7 +188,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractPlugin implement
},
},
};
let secretNames = secretName;
let secretNames: any = secretName;
if (typeof secretName === "string") {
secretNames = [secretName];
}

View File

@ -1,9 +1,8 @@
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, LOGGER, RunStrategy, TaskInput } from "@certd/pipeline";
import { Autowire, IAccessService, ILogger, IsTaskPlugin, ITaskPlugin, 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";
import { Inject } from "@midwayjs/core";
@IsTaskPlugin({
name: "DeployCertToAliyunCDN",
@ -50,11 +49,11 @@ export class DeployCertToAliyunCDN implements ITaskPlugin {
})
accessId!: string;
@Inject()
@Autowire()
accessService!: IAccessService;
@Autowire()
logger!: LOGGER;
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit() {}
async execute(): Promise<void> {

View File

@ -17,7 +17,6 @@ import { Logger } from "log4js";
export class UploadCertToAliyun implements ITaskPlugin {
@TaskInput({
title: "证书名称",
helper: "证书上传后将以此参数作为名称前缀",
})
name!: string;
@ -66,6 +65,9 @@ export class UploadCertToAliyun implements ITaskPlugin {
@Autowire()
logger!: Logger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit() {}
async execute(): Promise<void> {
console.log("开始部署证书到阿里云cdn");
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;

View File

@ -1,199 +0,0 @@
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
import Core from '@alicloud/pop-core'
import { K8sClient } from '@certd/plugin-common'
const ROAClient = Core.ROAClient
const define = {
name: 'deployCertToAliyunAckIngress',
title: '部署到阿里云AckIngress',
input: {
clusterId: {
title: '集群id',
component: {
placeholder: '集群id'
}
},
secretName: {
title: '保密字典Id',
component: {
placeholder: '保密字典Id'
},
required: true
},
regionId: {
title: '大区',
value: 'cn-shanghai',
component: {
placeholder: '集群所属大区'
},
required: true
},
namespace: {
title: '命名空间',
value: 'default',
component: {
placeholder: '命名空间'
},
required: true
},
ingressName: {
title: 'ingress名称',
value: '',
component: {
placeholder: 'ingress名称'
},
required: true,
helper: '可以传入一个数组'
},
ingressClass: {
title: 'ingress类型',
value: 'nginx',
component: {
placeholder: '暂时只支持nginx类型'
},
required: true
},
isPrivateIpAddress: {
title: '是否私网ip',
value: false,
component: {
placeholder: '集群连接端点是否是私网ip'
},
helper: '如果您当前certd运行在同一个私网下可以选择是。',
required: true
},
accessProvider: {
title: 'Access授权',
type: [String, Object],
helper: 'AccessKey、AccessSecret',
component: {
name: 'access-selector',
type: 'aliyun'
},
required: true
}
},
output: {
}
}
export class DeployCertToAliyunAckIngress extends AbstractAliyunPlugin {
static define () {
return define
}
async execute ({ cert, props, context }) {
const accessProvider = this.getAccessProvider(props.accessProvider)
const client = this.getClient(accessProvider, props.regionId)
const kubeConfigStr = await this.getKubeConfig(client, props.clusterId, props.isPrivateIpAddress)
this.logger.info('kubeconfig已成功获取')
const k8sClient = new K8sClient(kubeConfigStr)
const ingressType = props.ingressClass || 'qcloud'
if (ingressType === 'qcloud') {
throw new Error('暂未实现')
// await this.patchQcloudCertSecret({ k8sClient, props, context })
} else {
await this.patchNginxCertSecret({ cert, k8sClient, props, context })
}
await this.sleep(3000) // 停留2秒等待secret部署完成
// await this.restartIngress({ k8sClient, props })
return true
}
async restartIngress ({ k8sClient, props }) {
const { namespace } = props
const body = {
metadata: {
labels: {
certd: this.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 => {
if (!item.spec.tls) {
return false
}
for (const tls of item.spec.tls) {
if (tls.secretName === props.secretName) {
return true
}
}
return false
}).map(item => {
return item.metadata.name
})
for (const ingress of ingressNames) {
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
this.logger.info(`ingress已重启:${ingress}`)
}
}
async patchNginxCertSecret ({ cert, k8sClient, props, context }) {
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 } = props
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}`)
}
}
getClient (aliyunProvider, regionId) {
return new ROAClient({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: `https://cs.${regionId}.aliyuncs.com`,
apiVersion: '2015-12-15'
})
}
async getKubeConfig (client, clusterId, 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
}
}
}

View File

@ -1,106 +0,0 @@
import Core from '@alicloud/pop-core'
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
import { ZoneOptions } from '../../utils/index.js'
const define = {
name: 'uploadCertToAliyun',
title: '上传证书到阿里云',
desc: '',
input: {
name: {
title: '证书名称',
helper: '证书上传后将以此参数作为名称前缀'
},
regionId: {
title: '大区',
value: 'cn-hangzhou',
component: {
name: 'a-select',
vModel: 'value',
options: ZoneOptions
},
required: true
},
accessProvider: {
title: 'Access授权',
helper: 'Access授权',
component: {
name: 'access-selector',
type: 'aliyun'
},
required: true
}
},
output: {
aliyunCertId: {
type: String,
desc: '上传成功后的阿里云CertId'
}
}
}
export class UploadCertToAliyun extends AbstractAliyunPlugin {
static define () {
return define
}
getClient (aliyunProvider) {
return new Core({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: 'https://cas.aliyuncs.com',
apiVersion: '2018-07-13'
})
}
async execute ({ cert, props, context }) {
const { name, accessProvider } = props
const certName = this.appendTimeSuffix(name || cert.domain)
const params = {
RegionId: props.regionId || 'cn-hangzhou',
Name: certName,
Cert: cert.crt,
Key: cert.key
}
const requestOption = {
method: 'POST'
}
const provider = this.getAccessProvider(accessProvider)
const client = this.getClient(provider)
const ret = await client.request('CreateUserCertificate', params, requestOption)
this.checkRet(ret)
this.logger.info('证书上传成功aliyunCertId=', ret.CertId)
context.aliyunCertId = ret.CertId
}
/**
* 没用现在阿里云证书不允许删除
* @param accessProviders
* @param cert
* @param props
* @param context
* @returns {Promise<void>}
*/
async rollback ({ cert, props, context }) {
const { accessProvider } = props
const { aliyunCertId } = context
this.logger.info('准备删除阿里云证书:', aliyunCertId)
const params = {
RegionId: props.regionId || 'cn-hangzhou',
CertId: aliyunCertId
}
const requestOption = {
method: 'POST'
}
const provider = this.getAccessProvider(accessProvider)
const client = this.getClient(provider)
const ret = await client.request('DeleteUserCertificate', params, requestOption)
this.checkRet(ret)
this.logger.info('证书删除成功:', aliyunCertId)
delete context.aliyunCertId
}
}

View File

@ -15,6 +15,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}

View File

@ -1,4 +1,4 @@
// export * from "@certd/plugin-aliyun";
export * from "@certd/plugin-cert";
export * from "@certd/plugin-aliyun";
export * from "@certd/plugin-tencent";
// export * from "@certd/plugin-host";
export * from "@certd/plugin-host";

View File

@ -15,6 +15,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}

View File

@ -1,7 +1,10 @@
import { Registrable } from "../registry";
import { Registrable } from "@certd/pipeline";
export type DnsProviderDefine = Registrable & {
accessType: string;
autowire?: {
[key: string]: any;
};
};
export type CreateRecordOptions = {
@ -14,6 +17,7 @@ export type RemoveRecordOptions = CreateRecordOptions & {
};
export interface IDnsProvider {
onInit(): Promise<void>;
createRecord(options: CreateRecordOptions): Promise<any>;
removeRecord(options: RemoveRecordOptions): Promise<any>;
}

View File

@ -1,7 +1,8 @@
// src/decorator/memoryCache.decorator.ts
import { dnsProviderRegistry } from "./registry";
import { DnsProviderDefine } from "./api";
import { Decorator } from "../decorator";
import { Decorator, AUTOWIRE_KEY } from "@certd/pipeline";
import _ from "lodash";
// 提供一个唯一 key
export const DNS_PROVIDER_CLASS_KEY = "pipeline:dns-provider";
@ -9,6 +10,16 @@ export const DNS_PROVIDER_CLASS_KEY = "pipeline:dns-provider";
export function IsDnsProvider(define: DnsProviderDefine): ClassDecorator {
return (target: any) => {
target = Decorator.target(target);
const autowires: any = {};
const properties = Decorator.getClassProperties(target);
for (const property in properties) {
const autowire = Reflect.getMetadata(AUTOWIRE_KEY, target, property);
if (autowire) {
autowires[property] = autowire;
}
}
_.merge(define, { autowire: autowires });
Reflect.defineMetadata(DNS_PROVIDER_CLASS_KEY, define, target);
target.define = define;

View File

@ -1,4 +1,4 @@
import { Registry } from "../registry";
import { Registry } from "@certd/pipeline";
// @ts-ignore
export const dnsProviderRegistry = new Registry();

View File

@ -1 +1,2 @@
export * from "./plugin";
export * from "./dns-provider";

View File

@ -1,10 +1,10 @@
// @ts-ignore
import * as acme from "@certd/acme-client";
import _ from "lodash";
import { IDnsProvider } from "@certd/pipeline";
import { Challenge } from "@certd/acme-client/types/rfc8555";
import { Logger } from "log4js";
import { IContext } from "@certd/pipeline/src/core/context";
import { IDnsProvider } from "../../dns-provider";
export class AcmeService {
userContext: IContext;

View File

@ -1,26 +1,46 @@
import {
Autowire,
dnsProviderRegistry,
HttpClient,
IAccessService,
IContext,
IsTaskPlugin,
ITaskPlugin,
RunStrategy,
TaskInput,
TaskOutput,
} from "@certd/pipeline";
import { Autowire, HttpClient, IAccessService, IContext, IsTaskPlugin, ITaskPlugin, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
import forge from "node-forge";
import dayjs from "dayjs";
import { AcmeService } from "./acme";
import _ from "lodash";
import { Logger } from "log4js";
export type CertInfo = {
import { Decorator } from "@certd/pipeline/src/decorator";
import { DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider";
import fs from "fs";
import os from "os";
export class CertInfo {
crt: string;
key: string;
csr: string;
};
detail: any;
expires: number;
constructor(opts: { crt: string; key: string; csr: string }) {
this.crt = opts.crt;
this.key = opts.key;
this.csr = opts.csr;
const { detail, expires } = this.getCrtDetail(this.crt);
this.detail = detail;
this.expires = expires.getTime();
}
getCrtDetail(crt: string) {
const pki = forge.pki;
const detail = pki.certificateFromPem(crt.toString());
const expires = detail.validity.notAfter;
return { detail, expires };
}
saveToFile(type: "crt" | "key", path?: string) {
if (path == null) {
//写入临时目录
path = `${os.tmpdir()}/certd/tmp/${Math.floor(Math.random() * 1000000)}/cert.${type}`;
}
fs.writeFileSync(path, this[type]);
return path;
}
}
@IsTaskPlugin({
name: "CertApply",
title: "证书申请",
@ -199,11 +219,16 @@ export class CertApplyPlugin implements ITaskPlugin {
);
this.logger.info("开始申请证书,", email, domains);
const dnsProviderClass = dnsProviderRegistry.get(dnsProviderType);
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);
const DnsProviderClass = dnsProviderPlugin.target;
const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine;
const access = await this.accessService.getById(dnsProviderAccessId);
// @ts-ignore
const dnsProvider: AbstractDnsProvider = new dnsProviderClass();
dnsProvider.doInit({ access, logger: this.logger, http: this.http });
const dnsProvider: IDnsProvider = new DnsProviderClass();
const context = { access, logger: this.logger, http: this.http };
Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context);
await dnsProvider.onInit();
const cert = await this.acme.order({
email,
@ -239,23 +264,11 @@ export class CertApplyPlugin implements ITaskPlugin {
}
async readCurrentCert() {
const cert: CertInfo = await this.pipelineContext.get("cert");
const cert: any = await this.pipelineContext.get("cert");
if (cert == null) {
return undefined;
}
const { detail, expires } = this.getCrtDetail(cert.crt);
return {
...cert,
detail,
expires: expires.getTime(),
};
}
getCrtDetail(crt: string) {
const pki = forge.pki;
const detail = pki.certificateFromPem(crt.toString());
const expires = detail.validity.notAfter;
return { detail, expires };
return new CertInfo(cert);
}
/**

View File

@ -15,6 +15,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}

View File

@ -15,31 +15,31 @@
"ssh2": "^0.8.9"
},
"devDependencies": {
"log4js": "^6.7.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"@types/lodash": "^4.14.186",
"vue-tsc": "^0.38.9",
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0",
"@alicloud/pop-core": "^1.7.10",
"@midwayjs/core": "^3.0.0",
"@midwayjs/decorator": "^3.0.0",
"@types/chai": "^4.3.3",
"@types/lodash": "^4.14.186",
"@types/mocha": "^10.0.0",
"@types/node-forge": "^1.3.0",
"@types/ssh2": "^1.11.6",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"chai": "^4.3.6",
"dayjs": "^1.9.7",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"lodash-es": "^4.17.20",
"log4js": "^6.3.0",
"mocha": "^10.1.0",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"vite": "^3.1.0"
"vite": "^3.1.0",
"vue-tsc": "^0.38.9"
}
}

View File

@ -1,48 +1,47 @@
import { AbstractAccess, IsAccess } from "@certd/pipeline";
import { AccessInput, IAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "ssh",
title: "主机登录授权",
desc: "",
input: {
host: {
title: "主机地址",
component: {
placeholder: "主机域名或IP地址",
},
required: true,
},
port: {
title: "端口",
value: "22",
component: {
placeholder: "22",
},
rules: [{ required: true, message: "此项必填" }],
},
username: {
title: "用户名",
value: "root",
rules: [{ required: true, message: "此项必填" }],
},
password: {
title: "密码",
component: {
name: "a-input-password",
vModel: "value",
},
helper: "登录密码或密钥必填一项",
},
privateKey: {
title: "密钥",
helper: "密钥或密码必填一项",
},
},
input: {},
})
export class SshAccess extends AbstractAccess {
host = "";
port = 22;
username = "root";
password?: string;
privateKey?: string;
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: "密钥或密码必填一项",
})
privateKey!: string;
}

View File

@ -1,10 +1,11 @@
// @ts-ignore
import ssh2 from "ssh2";
import path from "path";
import _ from "lodash";
import { Logger } from "log4js";
import { ILogger } from "@certd/pipeline";
export class SshClient {
logger: Logger;
constructor(logger: Logger) {
logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
/**

View File

@ -1,47 +1,56 @@
import { IsTask, TaskInput, TaskOutput, TaskPlugin, AbstractPlugin, RunStrategy } from "@certd/pipeline";
import { Autowire, IAccessService, IsTaskPlugin, ILogger, RunStrategy, TaskInput, ITaskPlugin } from "@certd/pipeline";
import { SshClient } from "../../lib/ssh";
import { CertInfo } from "@certd/plugin-cert";
@IsTask(() => {
return {
name: "hostShellExecute",
title: "执行远程主机脚本命令",
input: {
accessId: {
title: "主机登录配置",
helper: "登录",
component: {
name: "pi-access-selector",
type: "ssh",
},
required: true,
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
},
script: {
title: "shell脚本命令",
component: {
name: "a-textarea",
vModel: "value",
},
},
@IsTaskPlugin({
name: "hostShellExecute",
title: "执行远程主机脚本命令",
input: {},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
output: {},
};
},
output: {},
})
export class HostShellExecutePlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
const { script, accessId } = input;
export class HostShellExecutePlugin implements ITaskPlugin {
@TaskInput({
title: "主机登录配置",
helper: "登录",
component: {
name: "pi-access-selector",
type: "ssh",
},
required: true,
})
accessId!: string;
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
})
cert!: CertInfo;
@TaskInput({
title: "shell脚本命令",
component: {
name: "a-textarea",
vModel: "value",
},
})
script!: string;
@Autowire()
accessService!: IAccessService;
@Autowire()
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit() {}
async execute(): Promise<void> {
const { script, accessId } = this;
const connectConf = this.accessService.getById(accessId);
const sshClient = new SshClient(this.logger);
const ret = await sshClient.exec({
@ -49,6 +58,5 @@ export class HostShellExecutePlugin extends AbstractPlugin implements TaskPlugin
script,
});
this.logger.info("exec res:", ret);
return {};
}
}

View File

@ -1,71 +1,87 @@
import { IsTask, TaskInput, TaskOutput, TaskPlugin, AbstractPlugin, RunStrategy } from "@certd/pipeline";
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, ILogger, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
import { SshClient } from "../../lib/ssh";
import { CertInfo } from "@certd/plugin-cert";
import * as fs from "fs";
@IsTask(() => {
return {
name: "uploadCertToHost",
title: "上传证书到主机",
input: {
crtPath: {
title: "证书保存路径",
},
keyPath: {
title: "私钥保存路径",
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
},
accessId: {
title: "主机登录配置",
helper: "access授权",
component: {
name: "pi-access-selector",
type: "ssh",
},
rules: [{ required: true, message: "此项必填" }],
},
sudo: {
title: "是否sudo",
component: {
name: "a-checkbox",
vModel: "checked",
},
},
@IsTaskPlugin({
name: "uploadCertToHost",
title: "上传证书到主机",
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
output: {
hostCrtPath: {
title: "上传成功后的证书路径",
},
hostKeyPath: {
title: "上传成功后的私钥路径",
},
},
};
},
})
export class UploadCertToHostPlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
const { crtPath, keyPath, cert, accessId, sudo } = input;
export class UploadCertToHostPlugin implements ITaskPlugin {
@TaskInput({
title: "证书保存路径",
})
crtPath!: string;
@TaskInput({
title: "私钥保存路径",
})
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;
@TaskInput({
title: "是否sudo",
component: {
name: "a-checkbox",
vModel: "checked",
},
})
sudo!: boolean;
@Autowire()
accessService!: IAccessService;
@Autowire()
logger!: ILogger;
@TaskOutput({
title: "证书保存路径",
})
hostCrtPath!: string;
@TaskOutput({
title: "私钥保存路径",
})
hostKeyPath!: string;
async onInit() {}
async execute(): Promise<void> {
const { crtPath, keyPath, cert, accessId, sudo } = this;
const connectConf = this.accessService.getById(accessId);
const sshClient = new SshClient(this.logger);
const saveCrtPath = cert.saveToFile("crt");
const saveKeyPath = cert.saveToFile("key");
await sshClient.uploadFiles({
connectConf,
transports: [
{
localPath: cert.crtPath,
localPath: saveCrtPath,
remotePath: crtPath,
},
{
localPath: cert.keyPath,
localPath: saveKeyPath,
remotePath: keyPath,
},
],
@ -73,9 +89,12 @@ export class UploadCertToHostPlugin extends AbstractPlugin implements TaskPlugin
});
this.logger.info("证书上传成功crtPath=", crtPath, ",keyPath=", keyPath);
return {
hostCrtPath: crtPath,
hostKeyPath: keyPath,
};
//删除临时文件
fs.unlinkSync(saveCrtPath);
fs.unlinkSync(saveKeyPath);
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
}
}

View File

@ -15,6 +15,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}

View File

@ -13,6 +13,7 @@
"dependencies": {
"@certd/pipeline": "^0.3.0",
"@certd/plugin-util": "^0.3.0",
"@certd/plugin-cert": "^0.3.0",
"tencentcloud-sdk-nodejs": "^4.0.44"
},
"devDependencies": {

View File

@ -1,4 +1,4 @@
import { IsAccess, IsAccessInput } from "@certd/pipeline";
import { IsAccess, AccessInput } from "@certd/pipeline";
@IsAccess({
name: "dnspod",
@ -6,7 +6,7 @@ import { IsAccess, IsAccessInput } from "@certd/pipeline";
desc: "腾讯云的域名解析接口已迁移到dnspod",
})
export class DnspodAccess {
@IsAccessInput({
@AccessInput({
title: "token",
component: {
placeholder: "开放接口token",
@ -14,7 +14,7 @@ export class DnspodAccess {
rules: [{ required: true, message: "该项必填" }],
})
token = "";
@IsAccessInput({
@AccessInput({
title: "账户id",
component: {
placeholder: "dnspod接口账户id",

View File

@ -1,11 +1,11 @@
import { IsAccess, IsAccessInput } from "@certd/pipeline";
import { IsAccess, AccessInput } from "@certd/pipeline";
@IsAccess({
name: "tencent",
title: "腾讯云",
})
export class TencentAccess {
@IsAccessInput({
@AccessInput({
title: "secretId",
component: {
placeholder: "secretId",
@ -13,7 +13,7 @@ export class TencentAccess {
rules: [{ required: true, message: "该项必填" }],
})
secretId = "";
@IsAccessInput({
@AccessInput({
title: "secretKey",
component: {
placeholder: "secretKey",

View File

@ -1,8 +1,7 @@
import { CreateRecordOptions, HttpClient, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "@certd/pipeline";
import { Autowire, HttpClient, ILogger } from "@certd/pipeline";
import { CreateRecordOptions, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import _ from "lodash";
import { DnspodAccess } from "../access";
import { Inject } from "@midwayjs/decorator";
import { ILogger } from "@midwayjs/core";
@IsDnsProvider({
name: "dnspod",
@ -11,12 +10,12 @@ import { ILogger } from "@midwayjs/core";
accessType: "dnspod",
})
export class DnspodDnsProvider implements IDnsProvider {
@Inject()
@Autowire()
http!: HttpClient;
@Inject()
@Autowire()
access!: DnspodAccess;
@Inject()
@Autowire()
logger!: ILogger;
loginToken: any;

View File

@ -1,7 +1,7 @@
import { IAccessService, IsTaskPlugin, RunStrategy, TaskInput, ITaskPlugin, LOGGER } from "@certd/pipeline";
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, ILogger, RunStrategy, TaskInput } from "@certd/pipeline";
import tencentcloud from "tencentcloud-sdk-nodejs/index";
import { TencentAccess } from "../../access";
import { Inject } from "@midwayjs/decorator";
import { CertInfo } from "@certd/plugin-cert";
@IsTaskPlugin({
name: "DeployCertToTencentCDN",
@ -22,7 +22,7 @@ export class DeployToCdnPlugin implements ITaskPlugin {
},
required: true,
})
cert!: any;
cert!: CertInfo;
@TaskInput({
title: "Access提供者",
@ -47,11 +47,11 @@ export class DeployToCdnPlugin implements ITaskPlugin {
})
domainName!: string;
@Inject()
@Autowire()
accessService!: IAccessService;
@Inject()
logger!: LOGGER;
@Autowire()
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit() {}

View File

@ -1,8 +1,7 @@
import { IAccessService, IsTaskPlugin, ITaskPlugin, LOGGER, RunStrategy, TaskInput, utils } from "@certd/pipeline";
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, ILogger, RunStrategy, TaskInput, utils } from "@certd/pipeline";
import tencentcloud from "tencentcloud-sdk-nodejs/index";
import { TencentAccess } from "../../access";
import dayjs from "dayjs";
import { Inject } from "@midwayjs/decorator";
@IsTaskPlugin({
name: "DeployCertToTencentCLB",
@ -72,11 +71,11 @@ export class DeployToClbPlugin implements ITaskPlugin {
})
accessId!: string;
@Inject()
@Autowire()
accessService!: IAccessService;
@Inject()
logger!: LOGGER;
@Autowire()
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit() {}

View File

@ -1,4 +1,4 @@
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, RunStrategy, TaskInput, TaskOutput, LOGGER } from "@certd/pipeline";
import { Autowire, IAccessService, IsTaskPlugin, ITaskPlugin, RunStrategy, TaskInput, TaskOutput, ILogger } from "@certd/pipeline";
import tencentcloud from "tencentcloud-sdk-nodejs/index";
import dayjs from "dayjs";
@ -46,7 +46,7 @@ export class UploadToTencentPlugin implements ITaskPlugin {
accessService!: IAccessService;
@Autowire()
logger!: LOGGER;
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInit() {}

View File

@ -15,6 +15,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}

@ -1 +1 @@
Subproject commit b7422ab48ef81f5c98129bacb69d0eefdfe50645
Subproject commit 8c7b3853be27df392dff765d39c6c53de09418bd

@ -1 +1 @@
Subproject commit 5901fb5a440a7cebe3a2b6dfaec1b014e30b3a0c
Subproject commit a5681f154468fc5c4aac7e4a0ce090cfffc00e9e