mirror of https://github.com/certd/certd
				
				
				
			feat: midway注解方式编写插件
							parent
							
								
									52522f27e9
								
							
						
					
					
						commit
						dcd1023a39
					
				|  | @ -6,7 +6,7 @@ export type AccessInputDefine = FormItemProps & { | |||
|   required?: boolean; | ||||
| }; | ||||
| export type AccessDefine = Registrable & { | ||||
|   inputs?: { | ||||
|   input?: { | ||||
|     [key: string]: AccessInputDefine; | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -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); | ||||
|   }; | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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); | ||||
|   }; | ||||
| } | ||||
|  | @ -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"; | ||||
|  |  | |||
|  | @ -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, | ||||
| }; | ||||
|  | @ -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>; | ||||
| } | ||||
|  | @ -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"; | ||||
|  |  | |||
|  | @ -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); | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ export class Registry { | |||
|     this.storage[key] = value; | ||||
|   } | ||||
| 
 | ||||
|   get(name: string) { | ||||
|   get(name: string): RegistryItem { | ||||
|     if (!name) { | ||||
|       throw new Error("插件名称不能为空"); | ||||
|     } | ||||
|  |  | |||
|  | @ -32,4 +32,4 @@ export function buildLogger(write: (text: string) => void) { | |||
|   }); | ||||
|   return logger; | ||||
| } | ||||
| export type LOGGER = Logger; | ||||
| export type ILogger = Logger; | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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,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", | ||||
|  |  | |||
|  | @ -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({ | ||||
|  |  | |||
|  | @ -1,45 +1,60 @@ | |||
| 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 { | ||||
| @IsTaskPlugin({ | ||||
|   name: "DeployCertToAliyunAckIngress", | ||||
|   title: "部署到阿里云AckIngress", | ||||
|     input: { | ||||
|       clusterId: { | ||||
|   input: {}, | ||||
|   output: {}, | ||||
|   default: { | ||||
|     strategy: { | ||||
|       runStrategy: RunStrategy.SkipWhenSucceed, | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| export class DeployCertToAliyunAckIngressPlugin implements ITaskPlugin { | ||||
|   @TaskInput({ | ||||
|     title: "集群id", | ||||
|     component: { | ||||
|       placeholder: "集群id", | ||||
|     }, | ||||
|       }, | ||||
|       secretName: { | ||||
|   }) | ||||
|   clusterId!: string; | ||||
| 
 | ||||
|   @TaskInput({ | ||||
|     title: "保密字典Id", | ||||
|     component: { | ||||
|       placeholder: "保密字典Id", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       regionId: { | ||||
|   }) | ||||
|   secretName!: string | string[]; | ||||
| 
 | ||||
|   @TaskInput({ | ||||
|     title: "大区", | ||||
|     value: "cn-shanghai", | ||||
|     component: { | ||||
|       placeholder: "集群所属大区", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       namespace: { | ||||
|   }) | ||||
|   regionId!: string; | ||||
| 
 | ||||
|   @TaskInput({ | ||||
|     title: "命名空间", | ||||
|     value: "default", | ||||
|     component: { | ||||
|       placeholder: "命名空间", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       ingressName: { | ||||
|   }) | ||||
|   namespace!: string; | ||||
|   @TaskInput({ | ||||
|     title: "ingress名称", | ||||
|     value: "", | ||||
|     component: { | ||||
|  | @ -47,16 +62,18 @@ import { appendTimeSuffix } from "../../utils"; | |||
|     }, | ||||
|     required: true, | ||||
|     helper: "可以传入一个数组", | ||||
|       }, | ||||
|       ingressClass: { | ||||
|   }) | ||||
|   ingressName!: string; | ||||
|   @TaskInput({ | ||||
|     title: "ingress类型", | ||||
|     value: "nginx", | ||||
|     component: { | ||||
|       placeholder: "暂时只支持nginx类型", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       isPrivateIpAddress: { | ||||
|   }) | ||||
|   ingressClass!: string; | ||||
|   @TaskInput({ | ||||
|     title: "是否私网ip", | ||||
|     value: false, | ||||
|     component: { | ||||
|  | @ -64,16 +81,18 @@ import { appendTimeSuffix } from "../../utils"; | |||
|     }, | ||||
|     helper: "如果您当前certd运行在同一个私网下,可以选择是。", | ||||
|     required: true, | ||||
|       }, | ||||
|       cert: { | ||||
|   }) | ||||
|   isPrivateIpAddress!: boolean; | ||||
|   @TaskInput({ | ||||
|     title: "域名证书", | ||||
|     helper: "请选择前置任务输出的域名证书", | ||||
|     component: { | ||||
|       name: "pi-output-selector", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       accessId: { | ||||
|   }) | ||||
|   cert!: CertInfo; | ||||
|   @TaskInput({ | ||||
|     title: "Access授权", | ||||
|     helper: "阿里云授权AccessKeyId、AccessKeySecret", | ||||
|     component: { | ||||
|  | @ -81,21 +100,20 @@ import { appendTimeSuffix } from "../../utils"; | |||
|       type: "aliyun", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|     }, | ||||
|     output: {}, | ||||
|     default: { | ||||
|       strategy: { | ||||
|         runStrategy: RunStrategy.SkipWhenSucceed, | ||||
|       }, | ||||
|     }, | ||||
|   }; | ||||
|   }) | ||||
| export class DeployCertToAliyunAckIngressPlugin extends AbstractPlugin implements TaskPlugin { | ||||
|   async execute(input: TaskInput): Promise<TaskOutput> { | ||||
|   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]; | ||||
|     } | ||||
|  |  | |||
|  | @ -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> { | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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 | ||||
|   } | ||||
| } | ||||
|  | @ -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,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"; | ||||
|  |  | |||
|  | @ -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,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>; | ||||
| } | ||||
|  | @ -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; | ||||
|  | @ -1,4 +1,4 @@ | |||
| import { Registry } from "../registry"; | ||||
| import { Registry } from "@certd/pipeline"; | ||||
| 
 | ||||
| // @ts-ignore
 | ||||
| export const dnsProviderRegistry = new Registry(); | ||||
|  | @ -1 +1,2 @@ | |||
| export * from "./plugin"; | ||||
| export * from "./dns-provider"; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  |  | |||
|  | @ -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 }] | ||||
|   } | ||||
|  |  | |||
|  | @ -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" | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,48 +1,47 @@ | |||
| import { AbstractAccess, IsAccess } from "@certd/pipeline"; | ||||
| import { AccessInput, IAccess, IsAccess } from "@certd/pipeline"; | ||||
| 
 | ||||
| @IsAccess({ | ||||
|   name: "ssh", | ||||
|   title: "主机登录授权", | ||||
|   desc: "", | ||||
|   input: { | ||||
|     host: { | ||||
|   input: {}, | ||||
| }) | ||||
| export class SshAccess implements IAccess { | ||||
|   @AccessInput({ | ||||
|     title: "主机地址", | ||||
|     component: { | ||||
|       placeholder: "主机域名或IP地址", | ||||
|     }, | ||||
|     required: true, | ||||
|     }, | ||||
|     port: { | ||||
|   }) | ||||
|   host!: string; | ||||
|   @AccessInput({ | ||||
|     title: "端口", | ||||
|     value: "22", | ||||
|     component: { | ||||
|       placeholder: "22", | ||||
|     }, | ||||
|     rules: [{ required: true, message: "此项必填" }], | ||||
|     }, | ||||
|     username: { | ||||
|   }) | ||||
|   port!: string; | ||||
|   @AccessInput({ | ||||
|     title: "用户名", | ||||
|     value: "root", | ||||
|     rules: [{ required: true, message: "此项必填" }], | ||||
|     }, | ||||
|     password: { | ||||
|   }) | ||||
|   username!: string; | ||||
|   @AccessInput({ | ||||
|     title: "密码", | ||||
|     component: { | ||||
|       name: "a-input-password", | ||||
|       vModel: "value", | ||||
|     }, | ||||
|     helper: "登录密码或密钥必填一项", | ||||
|     }, | ||||
|     privateKey: { | ||||
|   }) | ||||
|   password!: string; | ||||
|   @AccessInput({ | ||||
|     title: "密钥", | ||||
|     helper: "密钥或密码必填一项", | ||||
|     }, | ||||
|   }, | ||||
|   }) | ||||
| export class SshAccess extends AbstractAccess { | ||||
|   host = ""; | ||||
|   port = 22; | ||||
|   username = "root"; | ||||
|   password?: string; | ||||
|   privateKey?: string; | ||||
|   privateKey!: string; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|   } | ||||
|   /** | ||||
|  |  | |||
|  | @ -1,12 +1,20 @@ | |||
| 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 { | ||||
| @IsTaskPlugin({ | ||||
|   name: "hostShellExecute", | ||||
|   title: "执行远程主机脚本命令", | ||||
|     input: { | ||||
|       accessId: { | ||||
|   input: {}, | ||||
|   default: { | ||||
|     strategy: { | ||||
|       runStrategy: RunStrategy.SkipWhenSucceed, | ||||
|     }, | ||||
|   }, | ||||
|   output: {}, | ||||
| }) | ||||
| export class HostShellExecutePlugin implements ITaskPlugin { | ||||
|   @TaskInput({ | ||||
|     title: "主机登录配置", | ||||
|     helper: "登录", | ||||
|     component: { | ||||
|  | @ -14,34 +22,35 @@ import { SshClient } from "../../lib/ssh"; | |||
|       type: "ssh", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       cert: { | ||||
|   }) | ||||
|   accessId!: string; | ||||
|   @TaskInput({ | ||||
|     title: "域名证书", | ||||
|     helper: "请选择前置任务输出的域名证书", | ||||
|     component: { | ||||
|       name: "pi-output-selector", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       script: { | ||||
|   }) | ||||
|   cert!: CertInfo; | ||||
|   @TaskInput({ | ||||
|     title: "shell脚本命令", | ||||
|     component: { | ||||
|       name: "a-textarea", | ||||
|       vModel: "value", | ||||
|     }, | ||||
|       }, | ||||
|     }, | ||||
|     default: { | ||||
|       strategy: { | ||||
|         runStrategy: RunStrategy.SkipWhenSucceed, | ||||
|       }, | ||||
|     }, | ||||
|     output: {}, | ||||
|   }; | ||||
|   }) | ||||
| export class HostShellExecutePlugin extends AbstractPlugin implements TaskPlugin { | ||||
|   async execute(input: TaskInput): Promise<TaskOutput> { | ||||
|     const { script, accessId } = input; | ||||
|   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 {}; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,26 +1,36 @@ | |||
| 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 { | ||||
| @IsTaskPlugin({ | ||||
|   name: "uploadCertToHost", | ||||
|   title: "上传证书到主机", | ||||
|     input: { | ||||
|       crtPath: { | ||||
|   default: { | ||||
|     strategy: { | ||||
|       runStrategy: RunStrategy.SkipWhenSucceed, | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| export class UploadCertToHostPlugin implements ITaskPlugin { | ||||
|   @TaskInput({ | ||||
|     title: "证书保存路径", | ||||
|       }, | ||||
|       keyPath: { | ||||
|   }) | ||||
|   crtPath!: string; | ||||
|   @TaskInput({ | ||||
|     title: "私钥保存路径", | ||||
|       }, | ||||
|       cert: { | ||||
|   }) | ||||
|   keyPath!: string; | ||||
|   @TaskInput({ | ||||
|     title: "域名证书", | ||||
|     helper: "请选择前置任务输出的域名证书", | ||||
|     component: { | ||||
|       name: "pi-output-selector", | ||||
|     }, | ||||
|     required: true, | ||||
|       }, | ||||
|       accessId: { | ||||
|   }) | ||||
|   cert!: CertInfo; | ||||
|   @TaskInput({ | ||||
|     title: "主机登录配置", | ||||
|     helper: "access授权", | ||||
|     component: { | ||||
|  | @ -28,44 +38,50 @@ import { SshClient } from "../../lib/ssh"; | |||
|       type: "ssh", | ||||
|     }, | ||||
|     rules: [{ required: true, message: "此项必填" }], | ||||
|       }, | ||||
|       sudo: { | ||||
|   }) | ||||
|   accessId!: string; | ||||
|   @TaskInput({ | ||||
|     title: "是否sudo", | ||||
|     component: { | ||||
|       name: "a-checkbox", | ||||
|       vModel: "checked", | ||||
|     }, | ||||
|       }, | ||||
|     }, | ||||
|     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; | ||||
|   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; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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 }] | ||||
|   } | ||||
|  |  | |||
|  | @ -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": { | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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() {} | ||||
|  |  | |||
|  | @ -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() {} | ||||
|  |  | |||
|  | @ -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() {} | ||||
|  |  | |||
|  | @ -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 | ||||
		Loading…
	
		Reference in New Issue
	
	 xiaojunnuo
						xiaojunnuo