Files
certd/packages/core/pipeline/src/plugin/api.ts
T

429 lines
11 KiB
TypeScript
Raw Normal View History

2026-03-31 23:57:25 +08:00
import { domainUtils, HttpClient, HttpRequestConfig, ILogger, logger, utils } from "@certd/basic";
2024-09-19 14:23:15 +08:00
import dayjs from "dayjs";
import { cloneDeep, upperFirst } from "lodash-es";
2026-03-31 23:57:25 +08:00
import { accessRegistry, IAccessService } from "../access/index.js";
import { PageSearch } from "../context/index.js";
import { FileStore } from "../core/file-store.js";
import { CancelError, IContext, RunHistory, RunnableCollection } from "../core/index.js";
import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.js";
2025-01-15 01:05:34 +08:00
import { INotificationService } from "../notification/index.js";
2026-03-31 23:57:25 +08:00
import { Registrable } from "../registry/index.js";
import { IPluginConfigService } from "../service/config.js";
2025-01-15 01:05:34 +08:00
import { TaskEmitter } from "../service/emit.js";
2026-03-31 23:57:25 +08:00
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService } from "../service/index.js";
2024-11-22 17:12:39 +08:00
export type PluginRequestHandleReq<T = any> = {
typeName: string;
action: string;
input: T;
data: any;
2026-02-15 12:59:08 +08:00
record: { id: number; type: string; title: string };
2026-03-16 23:27:24 +08:00
fromType?: "sys" | "user"; // sys、user
2024-11-22 17:12:39 +08:00
};
export type UserInfo = {
role: "admin" | "user";
id: any;
};
2022-10-26 09:02:47 +08:00
export enum ContextScope {
global,
pipeline,
runtime,
}
export type TaskOutputDefine = {
title: string;
value?: any;
2024-09-05 15:36:35 +08:00
type?: string;
2022-10-26 09:02:47 +08:00
};
2023-06-25 23:25:56 +08:00
export type TaskInputDefine = {
required?: boolean;
isSys?: boolean;
} & FormItemProps;
2022-10-26 09:02:47 +08:00
export type PluginDefine = Registrable & {
2022-12-27 12:32:09 +08:00
default?: any;
group?: string;
2024-09-19 17:38:51 +08:00
icon?: string;
2023-01-07 23:22:02 +08:00
input?: {
2022-10-26 09:02:47 +08:00
[key: string]: TaskInputDefine;
};
2023-01-07 23:22:02 +08:00
output?: {
2022-10-26 09:02:47 +08:00
[key: string]: TaskOutputDefine;
};
2022-12-27 12:32:09 +08:00
shortcut?: {
[key: string]: {
title: string;
icon: string;
action: string;
form: any;
};
};
onlyAdmin?: boolean;
needPlus?: boolean;
2025-04-10 09:35:50 +08:00
showRunStrategy?: boolean;
runStrategy?: any;
pluginType?: string; //类型
type?: string; //来源
2022-10-26 09:02:47 +08:00
};
2023-05-24 15:41:35 +08:00
export type ITaskPlugin = {
2023-05-09 10:16:49 +08:00
onInstance(): Promise<void>;
execute(): Promise<void | string>;
2024-09-27 02:15:41 +08:00
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
2023-05-24 15:41:35 +08:00
[key: string]: any;
};
export type TaskResult = {
clearLastStatus?: boolean;
2023-06-25 23:25:56 +08:00
files?: FileItem[];
2024-08-04 02:35:45 +08:00
pipelineVars: Record<string, any>;
2024-10-15 12:59:40 +08:00
pipelinePrivateVars?: Record<string, any>;
2023-05-24 15:41:35 +08:00
};
export type CertTargetItem = {
value: string;
label: string;
domain: string | string[];
};
2023-06-25 23:25:56 +08:00
export type TaskInstanceContext = {
//流水线定义
2023-06-25 23:25:56 +08:00
pipeline: Pipeline;
//运行时历史
runtime: RunHistory;
//步骤定义
2023-06-25 23:25:56 +08:00
step: Step;
2025-07-22 12:22:54 +08:00
define: PluginDefine;
//日志
2024-11-06 01:17:36 +08:00
logger: ILogger;
//当前步骤输入参数跟上一次执行比较是否有变化
2024-09-09 16:01:42 +08:00
inputChanged: boolean;
//授权获取服务
2023-06-25 23:25:56 +08:00
accessService: IAccessService;
//邮件服务
2023-06-25 23:25:56 +08:00
emailService: IEmailService;
//cname记录服务
cnameProxyService: ICnameProxyService;
2024-10-12 23:51:05 +08:00
//插件配置服务
2024-10-13 01:27:08 +08:00
pluginConfigService: IPluginConfigService;
//通知服务
notificationService: INotificationService;
//url构建
urlService: IUrlService;
//流水线上下文
2023-06-25 23:25:56 +08:00
pipelineContext: IContext;
//用户上下文
2023-06-25 23:25:56 +08:00
userContext: IContext;
//http请求客户端
2024-09-09 16:01:42 +08:00
http: HttpClient;
2024-11-04 16:39:02 +08:00
//下载文件方法
download: (config: HttpRequestConfig, savePath: string) => Promise<void>;
//文件存储
2023-06-25 23:25:56 +08:00
fileStore: FileStore;
//上一次执行结果状态
2023-06-25 23:25:56 +08:00
lastStatus?: Runnable;
//用户取消信号
signal: AbortSignal;
//工具类
utils: typeof utils;
//用户信息
user: UserInfo;
2025-01-15 01:05:34 +08:00
2026-02-13 21:28:17 +08:00
//项目id
projectId?: number;
2025-01-15 01:05:34 +08:00
emitter: TaskEmitter;
2025-03-17 00:19:01 +08:00
//service 容器
2025-03-18 00:52:50 +08:00
serviceGetter?: IServiceGetter;
2023-06-25 23:25:56 +08:00
};
2023-05-24 15:41:35 +08:00
export abstract class AbstractTaskPlugin implements ITaskPlugin {
2024-10-15 12:59:40 +08:00
_result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {}, pipelinePrivateVars: {} };
2023-06-25 23:25:56 +08:00
ctx!: TaskInstanceContext;
2024-08-13 20:30:42 +08:00
logger!: ILogger;
http!: HttpClient;
2024-08-13 20:30:42 +08:00
accessService!: IAccessService;
2023-05-24 15:41:35 +08:00
clearLastStatus() {
2023-06-25 23:25:56 +08:00
this._result.clearLastStatus = true;
}
getFiles() {
return this._result.files;
}
checkSignal() {
if (this.ctx.signal && this.ctx.signal.aborted) {
2024-11-01 02:13:34 +08:00
throw new CancelError("用户取消");
}
}
2023-06-25 23:25:56 +08:00
setCtx(ctx: TaskInstanceContext) {
this.ctx = ctx;
2024-08-13 20:30:42 +08:00
this.logger = ctx.logger;
this.accessService = ctx.accessService;
this.http = ctx.http;
2025-06-03 23:52:43 +08:00
// 将证书加入secret
// @ts-ignore
if (this.cert && this.cert.crt && this.cert.key) {
//有证书
// @ts-ignore
const cert: any = this.cert;
this.registerSecret(cert.crt);
this.registerSecret(cert.key);
this.registerSecret(cert.one);
}
if (this.ctx?.define?.onlyAdmin) {
2026-03-04 23:53:19 +08:00
this.checkAdmin();
}
2023-05-24 15:41:35 +08:00
}
2023-06-25 23:25:56 +08:00
async getAccess<T = any>(accessId: string | number, isCommon = false) {
2024-09-27 02:15:41 +08:00
if (accessId == null) {
throw new Error("您还没有配置授权");
}
let res: any = null;
if (isCommon) {
res = await this.ctx.accessService.getCommonById(accessId);
} else {
res = await this.ctx.accessService.getById(accessId);
}
2024-09-27 02:15:41 +08:00
if (res == null) {
throw new Error("授权不存在,可能已被删除,请前往任务配置里面重新选择授权");
}
res.ctx.logger = this.logger;
res.ctx.http = this.http;
// @ts-ignore
if (this.logger?.addSecret) {
// 隐藏加密信息,不在日志中输出
const type = res._type;
const plugin = accessRegistry.get(type);
const define = plugin.define;
// @ts-ignore
const input = define.input;
for (const key in input) {
if (input[key].encrypt && res[key] != null) {
// @ts-ignore
this.logger.addSecret(res[key]);
}
}
}
2024-10-29 22:18:45 +08:00
return res as T;
2024-09-27 02:15:41 +08:00
}
2025-06-03 23:52:43 +08:00
registerSecret(value: string) {
// @ts-ignore
if (this.logger?.addSecret) {
// @ts-ignore
this.logger.addSecret(value);
}
}
2023-06-28 23:15:37 +08:00
randomFileId() {
return Math.random().toString(36).substring(2, 9);
}
2023-06-25 23:25:56 +08:00
saveFile(filename: string, file: Buffer) {
const filePath = this.ctx.fileStore.writeFile(filename, file);
2024-05-30 10:12:48 +08:00
logger.info(`saveFile:${filePath}`);
2024-07-15 00:30:33 +08:00
this._result.files?.push({
2023-06-28 23:15:37 +08:00
id: this.randomFileId(),
2023-06-25 23:25:56 +08:00
filename,
path: filePath,
});
}
2024-07-15 00:30:33 +08:00
extendsFiles() {
if (this._result.files == null) {
this._result.files = [];
}
this._result.files.push(...(this.ctx.lastStatus?.status?.files || []));
}
2023-06-25 23:25:56 +08:00
get pipeline() {
return this.ctx.pipeline;
}
get step() {
return this.ctx.step;
}
2023-05-24 15:41:35 +08:00
async onInstance(): Promise<void> {
return;
}
2023-06-25 23:25:56 +08:00
2024-12-23 13:27:04 +08:00
abstract execute(): Promise<void | string>;
2024-09-19 14:23:15 +08:00
appendTimeSuffix(name?: string) {
2026-01-31 19:30:20 +08:00
return utils.string.appendTimeSuffix(name);
2024-09-19 14:23:15 +08:00
}
2024-09-27 02:15:41 +08:00
2025-09-04 23:42:03 +08:00
buildCertName(domain: string, prefix = "") {
2025-05-26 22:44:56 +08:00
domain = domain.replaceAll("*", "_").replaceAll(".", "_");
2025-09-04 23:42:03 +08:00
return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
2025-05-22 23:21:32 +08:00
}
2024-09-27 02:15:41 +08:00
async onRequest(req: PluginRequestHandleReq<any>) {
if (!req.action) {
throw new Error("action is required");
}
2024-09-30 18:00:51 +08:00
let methodName = req.action;
if (!req.action.startsWith("on")) {
2024-10-13 01:27:08 +08:00
methodName = `on${upperFirst(req.action)}`;
2024-09-30 18:00:51 +08:00
}
2024-09-27 02:15:41 +08:00
// @ts-ignore
const method = this[methodName];
if (method) {
// @ts-ignore
return await this[methodName](req.data);
}
throw new Error(`action ${req.action} not found`);
}
isAdmin() {
return this.ctx.user.role === "admin";
}
2024-09-30 18:00:51 +08:00
2026-03-04 23:53:19 +08:00
checkAdmin() {
if (!this.isAdmin()) {
throw new Error("只有“管理员”或“系统级项目”才有权限运行此插件任务");
}
}
2024-09-30 18:00:51 +08:00
getStepFromPipeline(stepId: string) {
let found: any = null;
2025-03-18 00:52:50 +08:00
RunnableCollection.each(this.ctx.pipeline.stages, step => {
2024-09-30 18:00:51 +08:00
if (step.id === stepId) {
found = step;
return;
}
});
return found;
}
getStepIdFromRefInput(ref = ".") {
return ref.split(".")[1];
}
2026-01-31 19:30:20 +08:00
buildDomainGroupOptions(options: any[], domains: string[]) {
return utils.options.buildGroupOptions(options, domains);
}
2026-02-15 18:44:35 +08:00
getLastStatus(): Runnable {
return this.ctx.lastStatus || ({} as any);
}
getLastOutput(key: string) {
return this.getLastStatus().status?.output?.[key];
}
isDomainMatched(domainList: string | string[], certDomains: string[]): boolean {
const matched = domainUtils.match(domainList, certDomains);
return matched;
}
isNotChanged() {
const lastResult = this.ctx?.lastStatus?.status?.status;
return !this.ctx.inputChanged && lastResult === "success";
}
async getAutoMatchedTargets(req: {
targetName: string;
certDomains: string[];
pageSize: number;
getDeployTargetList: (req: PageSearch) => Promise<{ list: CertTargetItem[]; total: number }>;
}): Promise<CertTargetItem[]> {
const matchedDomains: CertTargetItem[] = [];
let pageNo = 1;
const { certDomains } = req;
const pageSize = req.pageSize || 100;
while (true) {
const result = await req.getDeployTargetList({
pageNo,
pageSize,
});
const pageData = result.list;
this.logger.info(`获取到 ${pageData.length}${req.targetName}`);
if (!pageData || pageData.length === 0) {
break;
}
for (const item of pageData) {
const domainName = item.domain;
if (this.isDomainMatched(domainName, certDomains)) {
matchedDomains.push(item);
}
}
const totalCount = result.total || 0;
if (pageNo * pageSize >= totalCount || matchedDomains.length == 0) {
break;
}
pageNo++;
}
return matchedDomains;
}
async autoMatchedDeploy(req: {
targetName: string;
getCertDomains: () => Promise<string[]>;
uploadCert: () => Promise<any>;
2026-03-29 02:40:26 +08:00
deployOne: (req: { target: CertTargetItem; cert: any }) => Promise<void>;
getDeployTargetList: (req: PageSearch) => Promise<{ list: CertTargetItem[]; total: number }>;
2026-03-29 02:40:26 +08:00
}): Promise<{ result: string; deployedList: string[] }> {
this.logger.info("证书匹配模式部署");
const certDomains = await req.getCertDomains();
const certTargetList = await this.getAutoMatchedTargets({
targetName: req.targetName,
pageSize: 200,
certDomains,
getDeployTargetList: req.getDeployTargetList,
});
if (certTargetList.length === 0) {
this.logger.warn(`未找到匹配的${req.targetName}`);
return { result: "skip", deployedList: [] };
}
this.logger.info(`找到 ${certTargetList.length} 个匹配的${req.targetName}`);
//开始部署,检查是否已经部署过
const deployedList = cloneDeep(this.getLastStatus()?.status?.output?.deployedList || []);
const unDeployedTargets = certTargetList.filter(item => !deployedList.includes(item.value));
const count = unDeployedTargets.length;
const deployedCount = certTargetList.length - count;
if (deployedCount > 0) {
this.logger.info(`跳过 ${deployedCount} 个已部署过的${req.targetName}`);
}
this.logger.info(`需要部署 ${count}${req.targetName}`);
if (count === 0) {
return { result: "skip", deployedList };
}
this.logger.info(`开始部署`);
const aliCrtId = await req.uploadCert();
for (const target of unDeployedTargets) {
await req.deployOne({
cert: aliCrtId,
target,
});
deployedList.push(target.value);
}
this.logger.info(`本次成功部署 ${count}${req.targetName}`);
return { result: "success", deployedList };
}
2022-10-26 09:02:47 +08:00
}
export type OutputVO = {
key: string;
title: string;
value: any;
};