mirror of https://github.com/certd/certd
refactor: pipeline edit view
parent
af919c2f6e
commit
370a28c10e
|
@ -1 +1 @@
|
|||
Subproject commit 23f1e36aa82d5d7837033a555c5ea04614cfcbcc
|
||||
Subproject commit a78bb43f6b65877b4f0aad25995d7cbf3215a3bc
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
test/user.secret.ts
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "test/**/*.test.ts",
|
||||
"require": "ts-node/register"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"printWidth": 160
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"main": "./dist/pipeline.umd.js",
|
||||
"module": "./dist/fast-crud.mjs",
|
||||
"types": "./dist/es/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^0.3.0",
|
||||
"@fast-crud/fast-crud": "^1.5.0",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"dayjs": "^1.11.6",
|
||||
"lodash": "^4.17.21",
|
||||
"node-forge": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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/mocha": "^10.0.0",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"chai": "^4.3.6",
|
||||
"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",
|
||||
"log4js": "^6.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { AbstractRegistrable } from "../registry";
|
||||
|
||||
export abstract class AbstractAccess extends AbstractRegistrable {}
|
|
@ -0,0 +1,5 @@
|
|||
import { AbstractAccess } from "./abstract-access";
|
||||
|
||||
export interface IAccessService {
|
||||
getById(id: any): AbstractAccess;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { Registrable } from "../registry";
|
||||
import { FormItemProps } from "@fast-crud/fast-crud";
|
||||
|
||||
export type AccessDefine = Registrable & {
|
||||
input: {
|
||||
[key: string]: FormItemProps;
|
||||
};
|
||||
};
|
||||
export function IsAccess(define: AccessDefine) {
|
||||
return function (target: any) {
|
||||
target.define = define;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { IsAccess } from "../api";
|
||||
import { AbstractAccess } from "../abstract-access";
|
||||
|
||||
@IsAccess({
|
||||
name: "aliyun",
|
||||
title: "阿里云授权",
|
||||
desc: "",
|
||||
input: {
|
||||
accessKeyId: {
|
||||
component: {
|
||||
placeholder: "accessKeyId",
|
||||
},
|
||||
//required: true,
|
||||
//rules: [{ required: true, message: "必填项" }],
|
||||
},
|
||||
},
|
||||
})
|
||||
export class AliyunAccess extends AbstractAccess {
|
||||
accessKeyId = "";
|
||||
accessKeySecret = "";
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./aliyun-access";
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./api";
|
||||
export * from "./impl";
|
||||
export * from "./abstract-access";
|
|
@ -0,0 +1,41 @@
|
|||
import { IStorage } from "./storage";
|
||||
|
||||
export interface IContext {
|
||||
get(key: string): Promise<any>;
|
||||
set(key: string, value: any): Promise<void>;
|
||||
}
|
||||
|
||||
export class ContextFactory {
|
||||
storage: IStorage;
|
||||
|
||||
constructor(storage: IStorage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
getContext(scope: string, namespace: string): IContext {
|
||||
return new StorageContext(scope, namespace, this.storage);
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageContext implements IContext {
|
||||
storage: IStorage;
|
||||
namespace: string;
|
||||
scope: string;
|
||||
constructor(scope: string, namespace: string, storage: IStorage) {
|
||||
this.storage = storage;
|
||||
this.scope = scope;
|
||||
this.namespace = namespace;
|
||||
}
|
||||
async get(key: string): Promise<any> {
|
||||
const str = await this.storage.get(this.scope, this.namespace, key);
|
||||
if (str) {
|
||||
const store = JSON.parse(str);
|
||||
return store.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async set(key: string, value: any) {
|
||||
await this.storage.set(this.scope, this.namespace, key, JSON.stringify({ value }));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import { ConcurrencyStrategy, Pipeline, Runnable, Stage, Step, Task } from "../d.ts/pipeline";
|
||||
import _ from "lodash";
|
||||
import { RunHistory } from "./run-history";
|
||||
import { pluginRegistry, TaskPlugin } from "../plugin";
|
||||
import { IAccessService } from "../access/access-service";
|
||||
import { ContextFactory, StorageContext } from "./context";
|
||||
import { IStorage, MemoryStorage } from "./storage";
|
||||
import { logger } from "../utils/util.log";
|
||||
import { use } from "chai";
|
||||
|
||||
export class Executor {
|
||||
userId: any;
|
||||
pipeline: Pipeline;
|
||||
runtime: RunHistory = new RunHistory();
|
||||
lastSuccessHistory: RunHistory;
|
||||
accessService: IAccessService;
|
||||
contextFactory: ContextFactory;
|
||||
onChanged: (history: RunHistory) => void;
|
||||
constructor(options: { userId: any; pipeline: Pipeline; storage: IStorage; onChanged: (history: RunHistory) => void; lastSuccessHistory?: RunHistory; accessService: IAccessService }) {
|
||||
this.pipeline = options.pipeline;
|
||||
this.lastSuccessHistory = options.lastSuccessHistory ?? new RunHistory();
|
||||
this.onChanged = options.onChanged;
|
||||
this.accessService = options.accessService;
|
||||
this.userId = options.userId;
|
||||
|
||||
this.contextFactory = new ContextFactory(options.storage);
|
||||
}
|
||||
|
||||
async run() {
|
||||
await this.runWithHistory(this.pipeline, async () => {
|
||||
return await this.runStages();
|
||||
});
|
||||
}
|
||||
|
||||
async runWithHistory(runnable: Runnable, run: () => Promise<any>) {
|
||||
this.runtime.start(runnable);
|
||||
this.onChanged(this.runtime);
|
||||
try {
|
||||
await run();
|
||||
this.runtime.success(runnable);
|
||||
this.onChanged(this.runtime);
|
||||
} catch (e: any) {
|
||||
logger.error(e);
|
||||
this.runtime.error(runnable, e);
|
||||
this.onChanged(this.runtime);
|
||||
}
|
||||
}
|
||||
|
||||
private async runStages() {
|
||||
for (const stage of this.pipeline.stages) {
|
||||
await this.runWithHistory(stage, async () => {
|
||||
return await this.runStage(stage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async runStage(stage: Stage) {
|
||||
const runnerList = [];
|
||||
for (const task of stage.tasks) {
|
||||
const runner = this.runWithHistory(task, async () => {
|
||||
return await this.runTask(task);
|
||||
});
|
||||
runnerList.push(runner);
|
||||
}
|
||||
if (stage.concurrency === ConcurrencyStrategy.Parallel) {
|
||||
await Promise.all(runnerList);
|
||||
} else {
|
||||
for (const runner of runnerList) {
|
||||
await runner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async runTask(task: Task) {
|
||||
for (const step of task.steps) {
|
||||
await this.runWithHistory(step, async () => {
|
||||
return await this.runStep(step);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async runStep(step: Step) {
|
||||
//执行任务
|
||||
const taskPlugin: TaskPlugin = await this.getPlugin(step.type);
|
||||
const res = await taskPlugin.execute(step.input);
|
||||
_.merge(this.runtime.context, res);
|
||||
}
|
||||
|
||||
private async getPlugin(type: string): Promise<TaskPlugin> {
|
||||
const pluginClass = pluginRegistry.get(type);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const plugin = new pluginClass();
|
||||
await plugin.doInit({
|
||||
accessService: this.accessService,
|
||||
pipelineContext: this.contextFactory.getContext("pipeline", this.pipeline.id),
|
||||
userContext: this.contextFactory.getContext("user", this.userId),
|
||||
});
|
||||
return plugin;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./executor";
|
||||
export * from "./run-history";
|
|
@ -0,0 +1,62 @@
|
|||
import { Context, HistoryResult, Log, Runnable } from "../d.ts/pipeline";
|
||||
import _ from "lodash";
|
||||
|
||||
export class RunHistory {
|
||||
id: any;
|
||||
logs: Log[] = [];
|
||||
context: Context = {};
|
||||
results: {
|
||||
[key: string]: HistoryResult;
|
||||
} = {};
|
||||
|
||||
start(runnable: Runnable) {
|
||||
const status = "ing";
|
||||
const now = new Date().getTime();
|
||||
_.merge(runnable, { status, lastTime: now });
|
||||
this.results[runnable.id] = {
|
||||
startTime: new Date().getTime(),
|
||||
title: runnable.title,
|
||||
status,
|
||||
};
|
||||
this.log(runnable, `${runnable.title}<${runnable.id}> 开始执行`);
|
||||
}
|
||||
|
||||
success(runnable: Runnable, result?: any) {
|
||||
const status = "success";
|
||||
const now = new Date().getTime();
|
||||
_.merge(runnable, { status, lastTime: now });
|
||||
_.merge(this.results[runnable.id], { status, endTime: now }, result);
|
||||
this.log(
|
||||
runnable,
|
||||
`${this.results[runnable.id].title}<${runnable.id}> 执行成功`
|
||||
);
|
||||
}
|
||||
|
||||
error(runnable: Runnable, e: Error) {
|
||||
const status = "error";
|
||||
const now = new Date().getTime();
|
||||
_.merge(runnable, { status, lastTime: now });
|
||||
_.merge(this.results[runnable.id], {
|
||||
status,
|
||||
endTime: now,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
|
||||
this.log(
|
||||
runnable,
|
||||
`${this.results[runnable.id].title}<${runnable.id}> 执行异常:${
|
||||
e.message
|
||||
}`,
|
||||
status
|
||||
);
|
||||
}
|
||||
|
||||
log(runnable: Runnable, text: string, level = "info") {
|
||||
this.logs.push({
|
||||
time: new Date().getTime(),
|
||||
level,
|
||||
title: runnable.title,
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export interface IStorage {
|
||||
get(scope: string, namespace: string, key: string): Promise<string | null>;
|
||||
set(scope: string, namespace: string, key: string, value: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class FileStorage implements IStorage {
|
||||
/**
|
||||
* 范围: user / pipeline / runtime / task
|
||||
*/
|
||||
scope: any;
|
||||
namespace: any;
|
||||
root: string;
|
||||
constructor(rootDir?: string) {
|
||||
if (rootDir == null) {
|
||||
const userHome = process.env.HOME || process.env.USERPROFILE;
|
||||
rootDir = userHome + "/.certd/storage/";
|
||||
}
|
||||
this.root = rootDir;
|
||||
|
||||
if (!fs.existsSync(this.root)) {
|
||||
fs.mkdirSync(this.root, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filePath: string, value: string) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, value);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
readFile(filePath: string) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return fs.readFileSync(filePath).toString();
|
||||
}
|
||||
|
||||
async get(scope: string, namespace: string, key: string): Promise<string | null> {
|
||||
const path = `${this.root}/${this.scope}/${namespace}/${key}`;
|
||||
return this.readFile(path);
|
||||
}
|
||||
|
||||
async set(scope: string, namespace: string, key: string, value: string): Promise<void> {
|
||||
const path = this.buildPath(namespace, key);
|
||||
this.writeFile(path, value);
|
||||
}
|
||||
|
||||
private buildPath(namespace: string, key: string) {
|
||||
return `${this.root}/${this.scope}/${namespace}/${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryStorage implements IStorage {
|
||||
/**
|
||||
* 范围: user / pipeline / runtime / task
|
||||
*/
|
||||
scope: any;
|
||||
namespace: any;
|
||||
context: {
|
||||
[scope: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
} = {};
|
||||
|
||||
async get(scope: string, namespace: string, key: string): Promise<string | null> {
|
||||
const context = this.context[scope];
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
return context[namespace + "." + key];
|
||||
}
|
||||
|
||||
async set(scope: string, namespace: string, key: string, value: string): Promise<void> {
|
||||
let context = this.context[scope];
|
||||
if (context == null) {
|
||||
context = context[scope];
|
||||
}
|
||||
context[namespace + "." + key] = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./pipeline";
|
|
@ -0,0 +1,95 @@
|
|||
export enum RunStrategy {
|
||||
AlwaysRun,
|
||||
SkipWhenSucceed,
|
||||
}
|
||||
|
||||
export enum ConcurrencyStrategy {
|
||||
Serial,
|
||||
Parallel,
|
||||
}
|
||||
|
||||
export enum NextStrategy {
|
||||
AllSuccess,
|
||||
OneSuccess,
|
||||
}
|
||||
|
||||
export enum HandlerType {
|
||||
//清空后续任务的状态
|
||||
ClearFollowStatus,
|
||||
SendEmail,
|
||||
}
|
||||
|
||||
export type EventHandler = {
|
||||
type: HandlerType;
|
||||
params: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
export type RunnableStrategy = {
|
||||
runStrategy: RunStrategy;
|
||||
onSuccess: EventHandler[];
|
||||
onError: EventHandler[];
|
||||
};
|
||||
|
||||
export type Step = Runnable & {
|
||||
type: string; //插件类型
|
||||
input: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
export type Task = Runnable & {
|
||||
steps: Step[];
|
||||
};
|
||||
|
||||
export type Stage = Runnable & {
|
||||
tasks: Task[];
|
||||
concurrency: ConcurrencyStrategy;
|
||||
next: NextStrategy;
|
||||
};
|
||||
|
||||
export type Trigger = {
|
||||
id: string;
|
||||
title: string;
|
||||
cron: string;
|
||||
};
|
||||
|
||||
export type Runnable = {
|
||||
id: string;
|
||||
title: string;
|
||||
status?: string;
|
||||
lastTime?: number;
|
||||
strategy?: RunnableStrategy;
|
||||
};
|
||||
|
||||
export type Pipeline = Runnable & {
|
||||
version: number;
|
||||
stages: Stage[];
|
||||
triggers: Trigger[];
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type Log = {
|
||||
title: string;
|
||||
time: number;
|
||||
level: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type HistoryResult = {
|
||||
title: string;
|
||||
/**
|
||||
* 任务状态
|
||||
*/
|
||||
status: string;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
/**
|
||||
* 处理结果
|
||||
*/
|
||||
result?: string;
|
||||
errorMessage?: string;
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { AbstractRegistrable } from "../registry";
|
||||
import {
|
||||
CreateRecordOptions,
|
||||
IDnsProvider,
|
||||
DnsProviderDefine,
|
||||
RemoveRecordOptions,
|
||||
} from "./api";
|
||||
import { AbstractAccess } from "../access";
|
||||
export abstract class AbstractDnsProvider
|
||||
extends AbstractRegistrable
|
||||
implements IDnsProvider
|
||||
{
|
||||
static define: DnsProviderDefine;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
access: AbstractAccess;
|
||||
|
||||
doInit(options: { access: AbstractAccess }) {
|
||||
this.access = options.access;
|
||||
this.onInit();
|
||||
}
|
||||
|
||||
protected abstract onInit(): void;
|
||||
|
||||
abstract createRecord(options: CreateRecordOptions): Promise<any>;
|
||||
|
||||
abstract removeRecord(options: RemoveRecordOptions): Promise<any>;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Registrable } from "../registry";
|
||||
import { dnsProviderRegistry } from "./registry";
|
||||
|
||||
export type DnsProviderDefine = Registrable & {
|
||||
accessType: string;
|
||||
};
|
||||
|
||||
export type CreateRecordOptions = {
|
||||
fullRecord: string;
|
||||
type: string;
|
||||
value: any;
|
||||
};
|
||||
export type RemoveRecordOptions = CreateRecordOptions & {
|
||||
record: any;
|
||||
};
|
||||
|
||||
export interface IDnsProvider {
|
||||
createRecord(options: CreateRecordOptions): Promise<any>;
|
||||
|
||||
removeRecord(options: RemoveRecordOptions): Promise<any>;
|
||||
}
|
||||
|
||||
export function IsDnsProvider(define: DnsProviderDefine) {
|
||||
return function (target: any) {
|
||||
target.define = define;
|
||||
dnsProviderRegistry.install(target);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import "./providers";
|
||||
export * from "./api";
|
||||
export * from "./registry";
|
|
@ -0,0 +1,145 @@
|
|||
import { AbstractDnsProvider } from "../abstract-dns-provider";
|
||||
import Core from "@alicloud/pop-core";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
CreateRecordOptions,
|
||||
IDnsProvider,
|
||||
IsDnsProvider,
|
||||
RemoveRecordOptions,
|
||||
} from "../api";
|
||||
|
||||
@IsDnsProvider({
|
||||
name: "aliyun",
|
||||
title: "阿里云",
|
||||
desc: "阿里云DNS解析提供商",
|
||||
accessType: "aliyun",
|
||||
})
|
||||
export class AliyunDnsProvider
|
||||
extends AbstractDnsProvider
|
||||
implements IDnsProvider
|
||||
{
|
||||
client: any;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
const access: any = this.access;
|
||||
this.client = new Core({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: "https://alidns.aliyuncs.com",
|
||||
apiVersion: "2015-01-09",
|
||||
});
|
||||
}
|
||||
|
||||
async getDomainList() {
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request(
|
||||
"DescribeDomains",
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
return ret.Domains.Domain;
|
||||
}
|
||||
|
||||
async matchDomain(dnsRecord: string) {
|
||||
const list = await this.getDomainList();
|
||||
let domain = null;
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, item.DomainName)) {
|
||||
domain = item.DomainName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error("can not find Domain ," + dnsRecord);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
async getRecords(domain: string, rr: string, value: string) {
|
||||
const params: any = {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domain,
|
||||
RRKeyWord: rr,
|
||||
ValueKeyWord: undefined,
|
||||
};
|
||||
if (value) {
|
||||
params.ValueKeyWord = value;
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request(
|
||||
"DescribeDomainRecords",
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
return ret.DomainRecords.Record;
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type } = options;
|
||||
this.logger.info("添加域名解析:", fullRecord, value);
|
||||
const domain = await this.matchDomain(fullRecord);
|
||||
const rr = fullRecord.replace("." + domain, "");
|
||||
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domain,
|
||||
RR: rr,
|
||||
Type: type,
|
||||
Value: value,
|
||||
// Line: 'oversea' // 海外
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
try {
|
||||
const ret = await this.client.request(
|
||||
"AddDomainRecord",
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
this.logger.info("添加域名解析成功:", value, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
} catch (e: any) {
|
||||
if (e.code === "DomainRecordDuplicate") {
|
||||
return;
|
||||
}
|
||||
this.logger.info("添加域名解析出错", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async removeRecord(options: RemoveRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type, record } = options;
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
RecordId: record,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request(
|
||||
"DeleteDomainRecord",
|
||||
params,
|
||||
requestOption
|
||||
);
|
||||
this.logger.info("删除域名解析成功:", fullRecord, value, ret.RecordId);
|
||||
return ret.RecordId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import "./aliyun-dns-provider";
|
|
@ -0,0 +1,4 @@
|
|||
import { Registry } from "../registry";
|
||||
import { AbstractDnsProvider } from "./abstract-dns-provider";
|
||||
|
||||
export const dnsProviderRegistry = new Registry<typeof AbstractDnsProvider>();
|
|
@ -0,0 +1,6 @@
|
|||
export * from "./core";
|
||||
export * from "./d.ts";
|
||||
export * from "./access";
|
||||
export * from "./registry";
|
||||
export * from "./dns-provider";
|
||||
export * from "./plugin";
|
|
@ -0,0 +1,28 @@
|
|||
import { AbstractRegistrable } from "../registry";
|
||||
import { PluginDefine } from "./api";
|
||||
import { Logger } from "log4js";
|
||||
import { logger } from "../utils/util.log";
|
||||
import { IAccessService } from "../access/access-service";
|
||||
import { IContext } from "../core/context";
|
||||
|
||||
export abstract class AbstractPlugin extends AbstractRegistrable {
|
||||
static define: PluginDefine;
|
||||
logger: Logger = logger;
|
||||
// @ts-ignore
|
||||
accessService: IAccessService;
|
||||
// @ts-ignore
|
||||
pipelineContext: IContext;
|
||||
// @ts-ignore
|
||||
userContext: IContext;
|
||||
|
||||
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext }) {
|
||||
this.accessService = options.accessService;
|
||||
this.pipelineContext = options.pipelineContext;
|
||||
this.userContext = options.userContext;
|
||||
await this.onInit();
|
||||
}
|
||||
|
||||
protected async onInit(): Promise<void> {
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { FormItemProps } from "@fast-crud/fast-crud";
|
||||
import { Registrable } from "../registry";
|
||||
import { pluginRegistry } from "./registry";
|
||||
export type TaskInput = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type TaskOutput = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export enum ContextScope {
|
||||
global,
|
||||
pipeline,
|
||||
runtime,
|
||||
}
|
||||
|
||||
export type Storage = {
|
||||
scope: ContextScope;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type TaskOutputDefine = {
|
||||
title: string;
|
||||
key: string;
|
||||
value?: any;
|
||||
storage?: Storage;
|
||||
};
|
||||
export type TaskInputDefine = FormItemProps;
|
||||
|
||||
export type PluginDefine = Registrable & {
|
||||
input: {
|
||||
[key: string]: TaskInputDefine;
|
||||
};
|
||||
output: {
|
||||
[key: string]: TaskOutputDefine;
|
||||
};
|
||||
};
|
||||
|
||||
export interface TaskPlugin {
|
||||
execute(input: TaskInput): Promise<TaskOutput>;
|
||||
}
|
||||
|
||||
export type OutputVO = {
|
||||
key: string;
|
||||
title: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export function IsTask(define: (() => PluginDefine) | PluginDefine) {
|
||||
return function (target: any) {
|
||||
if (define instanceof Function) {
|
||||
target.define = define();
|
||||
} else {
|
||||
target.define = define;
|
||||
}
|
||||
|
||||
pluginRegistry.install(target);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import "./plugins";
|
||||
export * from "./api";
|
||||
export * from "./registry";
|
|
@ -0,0 +1,198 @@
|
|||
// @ts-ignore
|
||||
import acme, { Authorization } from "@certd/acme-client";
|
||||
import _ from "lodash";
|
||||
import { logger } from "../../../utils/util.log";
|
||||
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
|
||||
import { IContext } from "../../../core/context";
|
||||
import { IDnsProvider } from "../../../dns-provider";
|
||||
import { Challenge } from "@certd/acme-client/types/rfc8555";
|
||||
|
||||
export class AcmeService {
|
||||
userContext: IContext;
|
||||
constructor(options: { userContext: IContext }) {
|
||||
this.userContext = options.userContext;
|
||||
acme.setLogger((text: string) => {
|
||||
logger.info(text);
|
||||
});
|
||||
}
|
||||
|
||||
async getAccountConfig(email: string) {
|
||||
return (await this.userContext.get(this.buildAccountKey(email))) || {};
|
||||
}
|
||||
|
||||
buildAccountKey(email: string) {
|
||||
return "acme.config." + email;
|
||||
}
|
||||
|
||||
async saveAccountConfig(email: string, conf: any) {
|
||||
await this.userContext.set(this.buildAccountKey(email), conf);
|
||||
}
|
||||
|
||||
async getAcmeClient(email: string, isTest = false): Promise<acme.Client> {
|
||||
const conf = await this.getAccountConfig(email);
|
||||
if (conf.key == null) {
|
||||
conf.key = await this.createNewKey();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
}
|
||||
if (isTest == null) {
|
||||
isTest = process.env.CERTD_MODE === "test";
|
||||
}
|
||||
const client = new acme.Client({
|
||||
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
|
||||
accountKey: conf.key,
|
||||
accountUrl: conf.accountUrl,
|
||||
backoffAttempts: 20,
|
||||
backoffMin: 5000,
|
||||
backoffMax: 10000,
|
||||
});
|
||||
|
||||
if (conf.accountUrl == null) {
|
||||
const accountPayload = {
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${email}`],
|
||||
};
|
||||
await client.createAccount(accountPayload);
|
||||
conf.accountUrl = client.getAccountUrl();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
async createNewKey() {
|
||||
const key = await acme.forge.createPrivateKey();
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
|
||||
logger.info("Triggered challengeCreateFn()");
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
|
||||
const fileContents = keyAuthorization;
|
||||
|
||||
logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would write "${fileContents}" to path "${filePath}"`);
|
||||
// await fs.writeFileAsync(filePath, fileContents);
|
||||
} else if (challenge.type === "dns-01") {
|
||||
/* dns-01 */
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
|
||||
const recordValue = keyAuthorization;
|
||||
|
||||
logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||
|
||||
return await dnsProvider.createRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: "TXT",
|
||||
value: recordValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to remove an ACME challenge response
|
||||
*
|
||||
* @param {object} authz Authorization object
|
||||
* @param {object} challenge Selected challenge
|
||||
* @param {string} keyAuthorization Authorization key
|
||||
* @param recordItem challengeCreateFn create record item
|
||||
* @param dnsProvider dnsProvider
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) {
|
||||
logger.info("Triggered challengeRemoveFn()");
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
|
||||
|
||||
logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would remove file on path "${filePath}"`);
|
||||
// await fs.unlinkAsync(filePath);
|
||||
} else if (challenge.type === "dns-01") {
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
|
||||
const recordValue = keyAuthorization;
|
||||
|
||||
logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||
await dnsProvider.removeRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: "TXT",
|
||||
value: keyAuthorization,
|
||||
record: recordItem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async order(options: { email: string; domains: string | string[]; dnsProvider: AbstractDnsProvider; csrInfo: any; isTest?: boolean }) {
|
||||
const { email, isTest, domains, csrInfo, dnsProvider } = options;
|
||||
const client: acme.Client = await this.getAcmeClient(email, isTest);
|
||||
|
||||
/* Create CSR */
|
||||
const { commonName, altNames } = this.buildCommonNameByDomains(domains);
|
||||
|
||||
const [key, csr] = await acme.forge.createCsr({
|
||||
commonName,
|
||||
...csrInfo,
|
||||
altNames,
|
||||
});
|
||||
if (dnsProvider == null) {
|
||||
throw new Error("dnsProvider 不能为空");
|
||||
}
|
||||
/* 自动申请证书 */
|
||||
const crt = await client.auto({
|
||||
csr,
|
||||
email: email,
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ["dns-01"],
|
||||
challengeCreateFn: async (authz: Authorization, challenge: Challenge, keyAuthorization: string): Promise<any> => {
|
||||
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider);
|
||||
},
|
||||
challengeRemoveFn: async (authz: Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
|
||||
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
|
||||
},
|
||||
});
|
||||
|
||||
const cert = {
|
||||
crt: crt.toString(),
|
||||
key: key.toString(),
|
||||
csr: csr.toString(),
|
||||
};
|
||||
/* Done */
|
||||
logger.debug(`CSR:\n${cert.csr}`);
|
||||
logger.debug(`Certificate:\n${cert.crt}`);
|
||||
logger.info("证书申请成功");
|
||||
return cert;
|
||||
}
|
||||
|
||||
buildCommonNameByDomains(domains: string | string[]): {
|
||||
commonName: string;
|
||||
altNames: string[] | undefined;
|
||||
} {
|
||||
if (typeof domains === "string") {
|
||||
domains = domains.split(",");
|
||||
}
|
||||
if (domains.length === 0) {
|
||||
throw new Error("domain can not be empty");
|
||||
}
|
||||
const commonName = domains[0];
|
||||
let altNames: undefined | string[] = undefined;
|
||||
if (domains.length > 1) {
|
||||
altNames = _.slice(domains, 1);
|
||||
}
|
||||
return {
|
||||
commonName,
|
||||
altNames,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
import { AbstractPlugin } from "../../abstract-plugin";
|
||||
import forge from "node-forge";
|
||||
import { ContextScope, IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
|
||||
import dayjs from "dayjs";
|
||||
import { dnsProviderRegistry } from "../../../dns-provider";
|
||||
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
|
||||
import { AcmeService } from "./acme";
|
||||
|
||||
export type CertInfo = {
|
||||
crt: string;
|
||||
key: string;
|
||||
csr: string;
|
||||
};
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "CertApply",
|
||||
title: "证书申请",
|
||||
input: {
|
||||
domains: {
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
},
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
helper: "请输入域名",
|
||||
},
|
||||
email: {
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "请输入邮箱",
|
||||
},
|
||||
dnsProviderType: {
|
||||
component: {
|
||||
name: "a-select",
|
||||
},
|
||||
helper: "请选择dns解析提供商",
|
||||
},
|
||||
dnsProviderAccess: {
|
||||
component: {
|
||||
name: "access-selector",
|
||||
},
|
||||
helper: "请选择dns解析提供商授权",
|
||||
},
|
||||
renewDays: {
|
||||
title: "更新天数",
|
||||
component: {
|
||||
name: "a-number",
|
||||
value: 20,
|
||||
},
|
||||
helper: "到期前多少天后更新证书",
|
||||
},
|
||||
forceUpdate: {
|
||||
title: "强制更新",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
value: false,
|
||||
},
|
||||
helper: "强制重新申请证书",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
cert: {
|
||||
key: "cert",
|
||||
type: "CertInfo",
|
||||
title: "证书",
|
||||
scope: ContextScope.pipeline,
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
export class CertPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
// @ts-ignore
|
||||
acme: AcmeService;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
protected async onInit() {
|
||||
this.acme = new AcmeService({ userContext: this.userContext });
|
||||
}
|
||||
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const oldCert = await this.condition(input);
|
||||
if (oldCert != null) {
|
||||
return {
|
||||
cert: oldCert,
|
||||
};
|
||||
}
|
||||
const cert = await this.doCertApply(input);
|
||||
return { cert };
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否更新证书
|
||||
* @param input
|
||||
*/
|
||||
async condition(input: TaskInput) {
|
||||
if (input.forceUpdate) {
|
||||
return null;
|
||||
}
|
||||
let oldCert;
|
||||
try {
|
||||
oldCert = await this.readCurrentCert();
|
||||
} catch (e) {
|
||||
this.logger.warn("读取cert失败:", e);
|
||||
}
|
||||
if (oldCert == null) {
|
||||
this.logger.info("还未申请过,准备申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
const ret = this.isWillExpire(oldCert.expires, input.renewDays);
|
||||
if (!ret.isWillExpire) {
|
||||
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}天`);
|
||||
return oldCert;
|
||||
}
|
||||
this.logger.info("即将过期,开始更新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
async doCertApply(input: TaskInput) {
|
||||
const email = input["email"];
|
||||
const domains = input["domains"];
|
||||
const dnsProviderType = input["dnsProviderType"];
|
||||
const dnsProviderAccessId = input["dnsProviderAccess"];
|
||||
const csrInfo = input["csrInfo"];
|
||||
this.logger.info("开始申请证书,", email, domains);
|
||||
|
||||
const dnsProviderClass = dnsProviderRegistry.get(dnsProviderType);
|
||||
const access = await this.accessService.getById(dnsProviderAccessId);
|
||||
// @ts-ignore
|
||||
const dnsProvider: AbstractDnsProvider = new dnsProviderClass();
|
||||
dnsProvider.doInit({ access });
|
||||
|
||||
const cert = await this.acme.order({
|
||||
email,
|
||||
domains,
|
||||
dnsProvider,
|
||||
csrInfo,
|
||||
isTest: false,
|
||||
});
|
||||
|
||||
await this.writeCert(cert);
|
||||
const ret = await this.readCurrentCert();
|
||||
|
||||
return {
|
||||
...ret,
|
||||
isNew: true,
|
||||
};
|
||||
}
|
||||
|
||||
formatCert(pem: string) {
|
||||
pem = pem.replace(/\r/g, "");
|
||||
pem = pem.replace(/\n\n/g, "\n");
|
||||
pem = pem.replace(/\n$/g, "");
|
||||
return pem;
|
||||
}
|
||||
|
||||
async writeCert(cert: { crt: string; key: string; csr: string }) {
|
||||
const newCert = {
|
||||
crt: this.formatCert(cert.crt),
|
||||
key: this.formatCert(cert.key),
|
||||
csr: this.formatCert(cert.csr),
|
||||
};
|
||||
await this.pipelineContext.set("cert", newCert);
|
||||
}
|
||||
|
||||
async readCurrentCert() {
|
||||
const cert: CertInfo = 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期,默认提前20天
|
||||
* @param expires
|
||||
* @param maxDays
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isWillExpire(expires: number, maxDays = 20) {
|
||||
if (expires == null) {
|
||||
throw new Error("过期时间不能为空");
|
||||
}
|
||||
// 检查有效期
|
||||
const leftDays = dayjs(expires).diff(dayjs(), "day");
|
||||
return {
|
||||
isWillExpire: leftDays < maxDays,
|
||||
leftDays,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import { AbstractPlugin } from "../../abstract-plugin";
|
||||
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
|
||||
import dayjs from "dayjs";
|
||||
import Core from "@alicloud/pop-core";
|
||||
import RPCClient from "@alicloud/pop-core";
|
||||
import { AliyunAccess } from "../../../access";
|
||||
import { CertInfo } from "../cert-plugin";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "DeployCertToAliyunCDN",
|
||||
title: "部署证书至阿里云CDN",
|
||||
input: {
|
||||
domainName: {
|
||||
title: "cdn加速域名",
|
||||
component: {
|
||||
placeholder: "cdn加速域名",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
certName: {
|
||||
title: "证书名称",
|
||||
component: {
|
||||
placeholder: "上传后将以此名称作为前缀",
|
||||
},
|
||||
},
|
||||
cert: {
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
accessId: {
|
||||
title: "Access提供者",
|
||||
helper: "access授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "aliyun",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
};
|
||||
})
|
||||
export class DeployCertToAliyunCDN extends AbstractPlugin implements TaskPlugin {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
console.log("开始部署证书到阿里云cdn");
|
||||
const access = this.accessService.getById(input.accessId) as AliyunAccess;
|
||||
const client = this.getClient(access);
|
||||
const params = await this.buildParams(input);
|
||||
await this.doRequest(client, params);
|
||||
return {};
|
||||
}
|
||||
|
||||
getClient(access: AliyunAccess) {
|
||||
return new Core({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: "https://cdn.aliyuncs.com",
|
||||
apiVersion: "2018-05-10",
|
||||
});
|
||||
}
|
||||
|
||||
async buildParams(input: TaskInput) {
|
||||
const { certName, domainName, cert } = input;
|
||||
const CertName = certName + "-" + dayjs().format("YYYYMMDDHHmmss");
|
||||
|
||||
const newCert = (await this.pipelineContext.get(cert)) as CertInfo;
|
||||
return {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domainName,
|
||||
ServerCertificateStatus: "on",
|
||||
CertName: CertName,
|
||||
CertType: "upload",
|
||||
ServerCertificate: newCert.crt,
|
||||
PrivateKey: newCert.key,
|
||||
};
|
||||
}
|
||||
|
||||
async doRequest(client: RPCClient, params: any) {
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
const ret: any = await client.request("SetDomainServerCertificate", params, requestOption);
|
||||
this.checkRet(ret);
|
||||
this.logger.info("设置cdn证书成功:", ret.RequestId);
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (ret.code != null) {
|
||||
throw new Error("执行失败:", ret.Message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { AbstractPlugin } from "../abstract-plugin";
|
||||
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../api";
|
||||
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "EchoPlugin",
|
||||
title: "测试插件回声",
|
||||
input: {
|
||||
cert: {
|
||||
component: {
|
||||
name: "output-selector",
|
||||
},
|
||||
helper: "输出选择",
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
};
|
||||
})
|
||||
export class EchoPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
for (const key in input) {
|
||||
console.log("input :", key, input[key]);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./cert-plugin/index";
|
||||
export * from "./echo-plugin";
|
||||
export * from "./deploy-to-cdn/index";
|
|
@ -0,0 +1,4 @@
|
|||
import { Registry } from "../registry";
|
||||
import { AbstractPlugin } from "./abstract-plugin";
|
||||
|
||||
export const pluginRegistry = new Registry<typeof AbstractPlugin>();
|
|
@ -0,0 +1 @@
|
|||
export * from "./registry";
|
|
@ -0,0 +1,53 @@
|
|||
import { Logger } from "log4js";
|
||||
import { logger } from "../utils/util.log";
|
||||
|
||||
export type Registrable = {
|
||||
name: string;
|
||||
title: string;
|
||||
desc?: string;
|
||||
};
|
||||
|
||||
export abstract class AbstractRegistrable {
|
||||
static define: Registrable;
|
||||
logger: Logger = logger;
|
||||
}
|
||||
export class Registry<T extends typeof AbstractRegistrable> {
|
||||
storage: {
|
||||
[key: string]: T;
|
||||
} = {};
|
||||
|
||||
install(target: T) {
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
let defineName = target.define.name;
|
||||
if (defineName == null) {
|
||||
defineName = target.name;
|
||||
}
|
||||
|
||||
this.register(defineName, target);
|
||||
}
|
||||
|
||||
register(key: string, value: T) {
|
||||
if (!key || value == null) {
|
||||
return;
|
||||
}
|
||||
this.storage[key] = value;
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
if (!name) {
|
||||
throw new Error("插件名称不能为空");
|
||||
}
|
||||
|
||||
const plugin = this.storage[name];
|
||||
if (!plugin) {
|
||||
throw new Error(`插件${name}还未注册`);
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
getStorage() {
|
||||
return this.storage;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export interface ServiceContext {
|
||||
get(name: string): any;
|
||||
register(name: string, service: any): void;
|
||||
}
|
||||
|
||||
export class ServiceContextImpl implements ServiceContext {
|
||||
register(name: string, service: any): void {}
|
||||
storage: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
get(name: string): any {
|
||||
return this.storage[name];
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceContext = new ServiceContextImpl();
|
|
@ -0,0 +1,6 @@
|
|||
import log4js from "log4js";
|
||||
log4js.configure({
|
||||
appenders: { std: { type: "stdout" } },
|
||||
categories: { default: { appenders: ["std"], level: "info" } },
|
||||
});
|
||||
export const logger = log4js.getLogger("pipeline");
|
|
@ -0,0 +1,10 @@
|
|||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { EchoPlugin } from "../src/plugin/plugins";
|
||||
describe("task_plugin", function () {
|
||||
it("#taskplugin", function () {
|
||||
const define = EchoPlugin.define;
|
||||
new EchoPlugin().execute({ context: {}, props: { test: 111 } });
|
||||
expect(define.name).eq("EchoPlugin");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { IAccessService } from "../../src/access/access-service";
|
||||
import { AbstractAccess, AliyunAccess } from "../../src";
|
||||
import { aliyunSecret } from "../user.secret";
|
||||
export class AccessServiceTest implements IAccessService {
|
||||
getById(id: any): AbstractAccess {
|
||||
return {
|
||||
...aliyunSecret,
|
||||
} as AliyunAccess;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { ConcurrencyStrategy, NextStrategy, Pipeline } from "../../src";
|
||||
|
||||
let idIndex = 0;
|
||||
function generateId() {
|
||||
idIndex++;
|
||||
return idIndex + "";
|
||||
}
|
||||
export const pipeline: Pipeline = {
|
||||
version: 1,
|
||||
id: generateId(),
|
||||
title: "测试管道",
|
||||
triggers: [],
|
||||
stages: [
|
||||
{
|
||||
id: generateId(),
|
||||
title: "证书申请阶段",
|
||||
concurrency: ConcurrencyStrategy.Serial,
|
||||
next: NextStrategy.AllSuccess,
|
||||
tasks: [
|
||||
{
|
||||
id: generateId(),
|
||||
title: "申请证书任务",
|
||||
steps: [
|
||||
{
|
||||
id: generateId(),
|
||||
title: "申请证书",
|
||||
type: "CertApply",
|
||||
input: {
|
||||
domains: ["*.docmirror.cn"],
|
||||
email: "xiaojunnuo@qq.com",
|
||||
dnsProviderType: "aliyun",
|
||||
accessId: "111",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: generateId(),
|
||||
title: "证书部署阶段",
|
||||
concurrency: ConcurrencyStrategy.Serial,
|
||||
next: NextStrategy.AllSuccess,
|
||||
tasks: [
|
||||
{
|
||||
id: generateId(),
|
||||
title: "测试输出参数",
|
||||
steps: [
|
||||
{
|
||||
id: generateId(),
|
||||
title: "输出参数",
|
||||
type: "EchoPlugin",
|
||||
input: {
|
||||
cert: "cert",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { Executor, RunHistory } from "../../src";
|
||||
import { pipeline } from "./pipeline.define";
|
||||
import { AccessServiceTest } from "./access-service-test";
|
||||
import { FileStorage } from "../../src/core/storage";
|
||||
describe("pipeline", function () {
|
||||
it("#pipeline", async function () {
|
||||
this.timeout(120000);
|
||||
function onChanged(history: RunHistory) {
|
||||
console.log("changed:");
|
||||
}
|
||||
|
||||
const executor = new Executor({ userId: 1, pipeline, onChanged, accessService: new AccessServiceTest(), storage: new FileStorage() });
|
||||
await executor.run();
|
||||
// expect(define.name).eq("EchoPlugin");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","test/**/*.ts"],
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from "vite";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: "src/index.ts",
|
||||
name: "pipeline",
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1 +1 @@
|
|||
Subproject commit 9aa73cf3b85f615018e7b9b903e0cbfe84f05fbb
|
||||
Subproject commit c9bae7552e25828d9a29d5be71b518b082c71eed
|
|
@ -1 +1 @@
|
|||
Subproject commit 06c64b9bbf0f55f7687cc654cbb5cdcd5debd140
|
||||
Subproject commit cb01f4fddf910d81b44556c907acea54dc298d5d
|
Loading…
Reference in New Issue