Compare commits

...

37 Commits

Author SHA1 Message Date
xiaojunnuo
d65d94b784 v1.24.3 2024-09-06 23:21:11 +08:00
xiaojunnuo
00f1e0da59 build: prepare to build 2024-09-06 23:19:58 +08:00
xiaojunnuo
65ef685729 perf: 支持多吉云cdn证书部署 2024-09-06 23:19:34 +08:00
xiaojunnuo
6e344140c6 chore: 1 2024-09-06 22:45:08 +08:00
xiaojunnuo
97a01b6f6d build: trigger build image 2024-09-06 22:35:27 +08:00
xiaojunnuo
c49ccbde93 v1.24.2 2024-09-06 22:34:49 +08:00
xiaojunnuo
fc73d9d615 build: prepare to build 2024-09-06 22:33:30 +08:00
xiaojunnuo
1133d6b0f7 chore: 2024-09-06 22:32:29 +08:00
xiaojunnuo
b80210f24b perf: 优化跳过处理逻辑 2024-09-06 10:19:03 +08:00
xiaojunnuo
3bad0b2685 chore: 1 2024-09-06 00:13:21 +08:00
xiaojunnuo
af388ec39f Merge remote-tracking branch 'origin/v2' into v2 2024-09-05 18:01:04 +08:00
xiaojunnuo
8d7c2c8e29 Merge branch 'v2' of https://github.com/certd/certd into v2 2024-09-05 18:01:05 +08:00
xiaojunnuo
8088cd6d58 1 2024-09-05 18:00:51 +08:00
xiaojunnuo
590ce9642e 1 2024-09-05 18:00:45 +08:00
xiaojunnuo
99302b8ff2 chore: 2024-09-05 16:19:00 +08:00
xiaojunnuo
14b108f09e chore: 2024-09-05 16:18:42 +08:00
xiaojunnuo
0669835d4e chore: 2024-09-05 16:11:03 +08:00
xiaojunnuo
fbeaed2035 perf: 支持pfx、der 2024-09-05 15:36:35 +08:00
xiaojunnuo
ecad7f58c1 chore: 2024-09-05 14:33:45 +08:00
xiaojunnuo
1dd9a8d4d3 docs: 2024-09-05 11:00:21 +08:00
xiaojunnuo
bd73a163cd perf: 阶段、任务、步骤全面支持拖动排序 2024-09-05 10:47:03 +08:00
xiaojunnuo
1e9b5638aa perf: 任务支持拖动排序 2024-09-05 01:39:46 +08:00
xiaojunnuo
71ac8aae4a fix: 修复windows下无法执行第二条命令的bug 2024-09-05 00:04:31 +08:00
xiaojunnuo
d5bfcdb6de perf: 修复windows下无法执行第二条命令的bug 2024-09-04 18:29:39 +08:00
xiaojunnuo
1480efb43d pref: 支持https启动 2024-09-04 16:15:42 +08:00
xiaojunnuo
1c17b41e16 perf: 西部数据支持用户级的apikey 2024-09-04 15:49:15 +08:00
xiaojunnuo
192d9dc7e3 perf: 任务配置不需要的字段可以自动隐藏 2024-09-04 15:49:00 +08:00
xiaojunnuo
d0d3c2b588 Merge remote-tracking branch 'origin/v2' into v2 2024-09-04 11:28:22 +08:00
Greper
b8a8f20448 perf: 支持西部数码DNS
perf: 支持西部数码DNS
2024-09-04 11:28:07 +08:00
xiaojunnuo
28a32aed7d chore: 2024-09-04 11:26:56 +08:00
xiaojunnuo
ff46771d8d perf: client 请求超时时间延长为10s 2024-09-03 22:09:48 +08:00
xiaojunnuo
87a2673e8c perf: 支持阿里云oss 2024-09-03 18:21:02 +08:00
Moeyuuko
c59cab1aae perf: 支持西部数码DNS 2024-09-03 15:40:45 +08:00
xiaojunnuo
6314e8d7eb fix: 修复复制流水线出现的各种问题 2024-09-03 11:42:05 +08:00
xiaojunnuo
5ade12d700 chore: 2024-09-03 00:26:35 +08:00
xiaojunnuo
ceb210b1b7 chore: 2024-09-03 00:06:13 +08:00
xiaojunnuo
5e084db038 build: trigger build image 2024-09-02 23:58:23 +08:00
89 changed files with 1833 additions and 668 deletions

13
.gitignore vendored
View File

@@ -19,24 +19,13 @@ gen
/*.log
/packages/ui/*/.idea
/packages/ui/*/node_modules
/packages/*/node_modules
/packages/ui/certd-server/tmp/
/packages/ui/certd-ui/dist/
/other
/dev-sidecar-test
/packages/core/certd/yarn.lock
/packages/test
/test/own
/pnpm-lock.yaml
docker/image/workspace
/packages/core/lego
tsconfig.tsbuildinfo
test/**/*.js
/packages/ui/certd-server/data/db.sqlite
/packages/ui/certd-server/data/keys.yaml
/packages/pro/
/packages/pro/

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
### Performance Improvements
* 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde))
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
### Bug Fixes
* 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577))
* 修复windows下无法执行第二条命令的bug ([71ac8aa](https://github.com/certd/certd/commit/71ac8aae4aa694e1a23761e9761c9fba30b43a21))
### Performance Improvements
* 阶段、任务、步骤全面支持拖动排序 ([bd73a16](https://github.com/certd/certd/commit/bd73a163cd0497f062bd424ddc6bc9bbc95f81ea))
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
* 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327))
* 西部数据支持用户级的apikey ([1c17b41](https://github.com/certd/certd/commit/1c17b41e160944b073e1849e6f9467c3659a4bfc))
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
* 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda))
* 支持阿里云oss ([87a2673](https://github.com/certd/certd/commit/87a2673e8c33dff6eda1b836d92ecc121564ed78))
* 支持西部数码DNS ([c59cab1](https://github.com/certd/certd/commit/c59cab1aaeb19f86df8e3e0d8127cbd0a9ef77f3))
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
* client 请求超时时间延长为10s ([ff46771](https://github.com/certd/certd/commit/ff46771d8dd43e71c1ca70e3ba783945750342cc))
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Bug Fixes

View File

@@ -18,7 +18,8 @@ https://afdian.com/a/greper
专业版特权
1. 证书流水线条数无限制免费版限制10条
2. 免配置发邮件功能
3. 更多功能增加中...
3. FTP上传、cdnfly、宝塔等部署插件
4. 更多功能增加中...
************************
## 一、特性

View File

@@ -1 +1 @@
1
22:35

View File

@@ -1,14 +0,0 @@
version: '3.3'
services:
ftp:
# 镜像 # ↓↓↓↓↓ --- 1、 镜像版本号,建议改成固定版本号【可选】
image: gists/pure-ftpd
container_name: ftp # 容器名
restart: unless-stopped # 自动重启
volumes:
- /data/ftp2/:/home/ftpuser
ports: # 端口映射
- "21:21"
- "30000-30009:30000-30009"
environment: # 环境变量
- TZ=Asia/Shanghai

View File

@@ -1,31 +1,38 @@
version: '3.3'
services:
certd:
# 镜像 # ↓↓↓↓↓ --- 1、 镜像版本号,建议改成固定版本号【可选】
# 镜像 # ↓↓↓↓↓ --- 镜像版本号,建议改成固定版本号【可选】
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
container_name: certd # 容器名
restart: unless-stopped # 自动重启
volumes:
# ↓↓↓↓↓ ------------------------------------------------------- 2、 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下【可选】
# ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下【可选】
- /data/certd:/app/data
ports: # 端口映射
# ↓↓↓↓ ----------------------------------------------------------3、如果端口有冲突可以修改第一个7001为其他不冲突的端口号【可选】
# ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突可以修改第一个7001为其他不冲突的端口号【可选】
- "7001:7001"
dns:
# 如果出现getaddrinfo ENOTFOUND等错误可以尝试修改或注释dns配置
- 223.5.5.5
- 223.6.6.6
# ↓↓↓↓ ----------------------------------------------------------如果你服务器部署在国外可以用8.8.8.8替换上面的dns【可选】
# ↓↓↓↓ ---------------------------------------------------------- 如果你服务器部署在国外可以用8.8.8.8替换上面的dns【可选】
# - 8.8.8.8
# - 8.8.4.4
environment: # 环境变量
- TZ=Asia/Shanghai
#- HTTPS_PROXY=http://xxxxxx:xx
#- HTTP_PROXY=http://xxxxxx:xx
# ↑↑↑↑↑ ------------------------------------- 这里可以设置http代理【可选】
- certd_system_resetAdminPasswd=false
# ↑↑↑↑↑---------------------------4、如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false【可选】
# ↑↑↑↑↑--------------------------- 如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false【可选】
- certd_cron_immediateTriggerOnce=false
# ↑↑↑↑↑---------------------------5、如果设置为true启动后所有配置了cron的流水线任务都将被立即触发一次【可选】
# ↑↑↑↑↑--------------------------- 如果设置为true启动后所有配置了cron的流水线任务都将被立即触发一次【可选】
- VITE_APP_ICP_NO=
# ↑↑↑↑↑ -----------------------------------------6、这里可以设置备案号【可选】
# ↑↑↑↑↑ ----------------------------------------- 这里可以设置备案号【可选】
#- certd_koa_key=./data/ssl/cert.key
#- certd_koa_cert=./data/ssl/cert.crt
# ↑↑↑↑↑ ----------------------------------------- 配置证书和key则表示https方式启动访问网址要使用 https://your.domain:7001【可选】
# 设置环境变量即可自定义certd配置
# 服务端配置项见: packages/ui/certd-server/src/config/config.default.ts
# 服务端配置规则: certd_ + 配置项, 点号用_代替

20
init.sh Normal file
View File

@@ -0,0 +1,20 @@
current_pwd=$(pwd)
echo "开始设置git配置"
read -p "请输入username" username
git config user.name $username
read -p "请输入email" email
git config user.email $email
git config credential.helper "store --file=$current_pwd/.git/credential.store"
echo "已设置记住git账号密码"
git config core.autocrlf input
echo "已设置auto crlf = input"
git config core.filemode false
echo "已设置忽略文件模式变化"
echo "git配置完成"

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.24.1"
"version": "1.24.3"
}

View File

@@ -24,9 +24,9 @@
"license": "AGPL-3.0",
"dependencies": {
"axios": "^1.7.2",
"lodash": "^4.17.21"
"lodash-es": "^4.17.21"
},
"workspaces": [
"packages/**"
]
}
}

Submodule packages/certd-pro deleted from 0dec0d461f

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/publishlab/node-acme-client/compare/v1.24.2...v1.24.3) (2024-09-06)
**Note:** Version bump only for package @certd/acme-client
## [1.24.2](https://github.com/publishlab/node-acme-client/compare/v1.24.1...v1.24.2) (2024-09-06)
### Performance Improvements
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/publishlab/node-acme-client/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
## [1.24.1](https://github.com/publishlab/node-acme-client/compare/v1.24.0...v1.24.1) (2024-09-02)
### Bug Fixes

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.24.1",
"version": "1.24.3",
"main": "src/index.js",
"types": "types/index.d.ts",
"license": "MIT",
@@ -59,5 +59,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
}

View File

@@ -32,7 +32,7 @@ exports.directory = {
*/
exports.crypto = require('./crypto');
exports.forge = require('./crypto/forge');
// exports.forge = require('./crypto/forge');
/**
* Axios

View File

@@ -111,7 +111,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
log(`DNS query finished successfully, found ${recordValues.length} TXT records`);
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
throw new Error(`Authorization not found in DNS TXT record: ${recordName}`);
throw new Error(`Authorization not found in DNS TXT record: ${recordName}need:${keyAuthorization},found:${recordValues}`);
}
log(`Key authorization match for ${challenge.type}/${recordName}, ACME challenge verified`);

View File

@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
### Performance Improvements
* 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde))
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
### Performance Improvements
* 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda))
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Performance Improvements

View File

@@ -1 +1 @@
23:56
23:19

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.24.1",
"version": "1.24.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -57,5 +57,5 @@
"vite": "^4.3.8",
"vue-tsc": "^1.6.5"
},
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
}

View File

@@ -12,6 +12,7 @@ import { RegistryItem } from "../registry/index.js";
import { Decorator } from "../decorator/index.js";
import { IEmailService } from "../service/index.js";
import { FileStore } from "./file-store.js";
import { hashUtils } from "../utils/index.js";
// import { TimeoutPromise } from "../utils/util.promise.js";
export type ExecutorOptions = {
@@ -69,6 +70,7 @@ export class Executor {
}
async run(runtimeId: any = 0, triggerType: string) {
let intervalFlushLogId: any = undefined;
try {
await this.init();
const trigger = { type: triggerType };
@@ -76,8 +78,14 @@ export class Executor {
this.runtime = new RunHistory(runtimeId, trigger, this.pipeline);
this.logger.info(`pipeline.${this.pipeline.id} start`);
await this.notification("start");
this.runtime.start(this.pipeline);
intervalFlushLogId = setInterval(async () => {
await this.onChanged(this.runtime);
}, 5000);
await this.runWithHistory(this.pipeline, "pipeline", async () => {
await this.runStages(this.pipeline);
return await this.runStages(this.pipeline);
});
if (this.lastRuntime && this.lastRuntime.pipeline.status?.status === ResultType.error) {
await this.notification("turnToSuccess");
@@ -87,53 +95,33 @@ export class Executor {
await this.notification("error", e);
this.logger.error("pipeline 执行失败", e.stack);
} finally {
clearInterval(intervalFlushLogId);
await this.onChanged(this.runtime);
await this.pipelineContext.setObj("lastRuntime", this.runtime);
this.logger.info(`pipeline.${this.pipeline.id} end`);
}
}
async runWithHistory(runnable: Runnable, runnableType: string, run: () => Promise<void>) {
async runWithHistory(runnable: Runnable, runnableType: string, run: () => Promise<ResultType | void>) {
runnable.runnableType = runnableType;
this.runtime.start(runnable);
// const timeout = runnable.timeout ?? 20 * 60 * 1000;
await this.onChanged(this.runtime);
if (runnable.strategy?.runStrategy === RunStrategy.SkipWhenSucceed) {
//如果是成功后跳过策略
const lastNode = this.lastStatusMap.get(runnable.id);
const lastResult = lastNode?.status?.status;
const lastInput = JSON.stringify(lastNode?.status?.input);
let inputChanged = false;
if (runnableType === "step") {
const step = runnable as Step;
const input = JSON.stringify(step.input);
if (input != null && lastInput !== input) {
//参数有变化
inputChanged = true;
}
}
if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
runnable.status!.output = lastNode?.status?.output;
runnable.status!.files = lastNode?.status?.files;
this.runtime.skip(runnable);
await this.onChanged(this.runtime);
return ResultType.skip;
}
}
const intervalFlushLogId = setInterval(async () => {
await this.onChanged(this.runtime);
}, 5000);
// const timeout = runnable.timeout ?? 20 * 60 * 1000;
try {
if (this.abort.signal.aborted) {
this.runtime.cancel(runnable);
return ResultType.canceled;
}
await run();
const resultType = await run();
if (this.abort.signal.aborted) {
this.runtime.cancel(runnable);
return ResultType.canceled;
}
if (resultType == ResultType.skip) {
this.runtime.skip(runnable);
return resultType;
}
this.runtime.success(runnable);
return ResultType.success;
} catch (e: any) {
@@ -141,7 +129,6 @@ export class Executor {
throw e;
} finally {
this.runtime.finally(runnable);
clearInterval(intervalFlushLogId);
await this.onChanged(this.runtime);
}
}
@@ -150,7 +137,7 @@ export class Executor {
const resList: ResultType[] = [];
for (const stage of pipeline.stages) {
const res: ResultType = await this.runWithHistory(stage, "stage", async () => {
await this.runStage(stage);
return await this.runStage(stage);
});
resList.push(res);
}
@@ -163,6 +150,7 @@ export class Executor {
const runner = async () => {
return this.runWithHistory(task, "task", async () => {
await this.runTask(task);
return ResultType.success;
});
};
runnerList.push(runner);
@@ -205,7 +193,7 @@ export class Executor {
for (const step of task.steps) {
step.runnableType = "step";
const res: ResultType = await this.runWithHistory(step, "step", async () => {
await this.runStep(step);
return await this.runStep(step);
});
resList.push(res);
}
@@ -224,19 +212,40 @@ export class Executor {
// @ts-ignore
const define: PluginDefine = plugin.define;
//从outputContext读取输入参数
Decorator.inject(define.input, instance, step.input, (item, key) => {
const input = _.cloneDeep(step.input);
Decorator.inject(define.input, instance, input, (item, key) => {
if (item.component?.name === "pi-output-selector") {
const contextKey = step.input[key];
const contextKey = input[key];
if (contextKey != null) {
// "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert",
const arr = contextKey.split(".");
const id = arr[1];
const outputKey = arr[2];
step.input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
}
}
});
const newInputHash = hashUtils.md5(JSON.stringify(input));
step.status!.inputHash = newInputHash;
//判断是否需要跳过
const lastNode = this.lastStatusMap.get(step.id);
const lastResult = lastNode?.status?.status;
if (step.strategy?.runStrategy === RunStrategy.SkipWhenSucceed) {
//如果是成功后跳过策略
let inputChanged = true;
const lastInputHash = lastNode?.status?.inputHash;
if (lastInputHash && newInputHash && lastInputHash === newInputHash) {
//参数有变化
inputChanged = false;
}
if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
step.status!.output = lastNode?.status?.output;
step.status!.files = lastNode?.status?.files;
return ResultType.skip;
}
}
const http = createAxiosService({ logger: currentLogger });
const taskCtx: TaskInstanceContext = {
pipeline: this.pipeline,
@@ -271,7 +280,6 @@ export class Executor {
// this.runtime.context[stepOutputKey] = instance[key];
});
step.status!.files = instance.getFiles();
//更新pipeline vars
if (Object.keys(instance._result.pipelineVars).length > 0) {
// 判断 pipelineVars 有值时更新
@@ -288,17 +296,17 @@ export class Executor {
let subject = "";
let content = "";
if (when === "start") {
subject = `【CertD】开始执行${this.pipeline.title}, buildId:${this.runtime.id}`;
content = subject;
subject = `【CertD】开始执行${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}`;
} else if (when === "success") {
subject = `【CertD】执行成功${this.pipeline.title}, buildId:${this.runtime.id}`;
content = subject;
subject = `【CertD】执行成功${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}`;
} else if (when === "turnToSuccess") {
subject = `【CertD】执行成功错误转成功${this.pipeline.title}, buildId:${this.runtime.id}`;
content = subject;
subject = `【CertD】执行成功错误转成功${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}`;
} else if (when === "error") {
subject = `【CertD】执行失败${this.pipeline.title}, buildId:${this.runtime.id}`;
content = `<pre>${error.message}</pre>`;
subject = `【CertD】执行失败${this.pipeline.id}${this.pipeline.title}`;
content = `buildId:${this.runtime.id}\nerror:${error.message}`;
} else {
return;
}

View File

@@ -1,4 +1,10 @@
import { logger } from "../utils/index.js";
import { setLogger } from "@certd/plus";
import { setLogger, isPlus } from "@certd/plus";
setLogger(logger);
export * from "@certd/plus";
export function checkPlus() {
if (!isPlus()) {
throw new Error("此为专业版功能,请升级到专业版");
}
}

View File

@@ -41,10 +41,8 @@ export class RunHistory {
this._loggers[runnable.id] = buildLogger((text) => {
this.logs[runnable.id].push(text);
});
const input = (runnable as Step).input;
const status: HistoryResult = {
output: {},
input: _.cloneDeep(input),
status: ResultType.start,
startTime: now,
result: ResultType.start,

View File

@@ -118,7 +118,8 @@ export type HistoryResultGroup = {
};
};
export type HistoryResult = {
input: any;
// input: any;
inputHash?: string;
output: any;
files?: FileItem[];
/**

View File

@@ -17,6 +17,7 @@ export enum ContextScope {
export type TaskOutputDefine = {
title: string;
value?: any;
type?: string;
};
export type TaskInputDefine = FormItemProps;

View File

@@ -21,5 +21,6 @@ export const pluginGroups = {
huawei: new PluginGroup("huawei", "华为云", 3),
tencent: new PluginGroup("tencent", "腾讯云", 4),
host: new PluginGroup("host", "主机", 5),
cdn: new PluginGroup("cdn", "CDN", 6),
other: new PluginGroup("other", "其他", 7),
};

View File

@@ -4,6 +4,7 @@ export * from "./util.log.js";
export * from "./util.file.js";
export * from "./util.sp.js";
export * as promises from "./util.promise.js";
export * from "./util.hash.js";
export const utils = {
sleep,
http: request,

View File

@@ -0,0 +1,9 @@
import crypto from "crypto";
function md5(data: string) {
return crypto.createHash("md5").update(data).digest("hex");
}
export const hashUtils = {
md5,
};

View File

@@ -51,7 +51,7 @@ export type SpawnOption = {
cmd: string | string[];
onStdout?: (data: string) => void;
onStderr?: (data: string) => void;
env: any;
env?: any;
logger?: ILogger;
options?: any;
};
@@ -66,6 +66,8 @@ async function spawn(opts: SpawnOption): Promise<string> {
cmd = item;
}
}
} else {
cmd = opts.cmd;
}
log.info(`执行命令: ${cmd}`);
let stdout = "";

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
**Note:** Version bump only for package @certd/lib-huawei
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
**Note:** Version bump only for package @certd/lib-huawei
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.24.1",
"version": "1.24.3",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -16,5 +16,5 @@
"axios": "^1.7.2",
"rollup": "^3.7.4"
},
"gitHead": "47fe3d5826661f678d081ab53e67c847a3239d88"
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
**Note:** Version bump only for package @certd/lib-k8s
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
**Note:** Version bump only for package @certd/lib-k8s
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.24.1",
"version": "1.24.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,7 +16,7 @@
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
"@certd/pipeline": "^1.24.1",
"@certd/pipeline": "^1.24.3",
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
@@ -37,5 +37,5 @@
"tslib": "^2.5.2",
"typescript": "^4.8.4"
},
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
}

View File

@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
**Note:** Version bump only for package @certd/plugin-cert
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
### Performance Improvements
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
* 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda))
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.24.1",
"version": "1.24.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,8 +13,8 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.24.1",
"@certd/pipeline": "^1.24.1",
"@certd/acme-client": "^1.24.3",
"@certd/pipeline": "^1.24.3",
"jszip": "^3.10.1",
"node-forge": "^0.10.0",
"psl": "^1.9.0"
@@ -53,5 +53,5 @@
"vite": "^3.1.0",
"vue-tsc": "^0.38.9"
},
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
}

View File

@@ -12,6 +12,8 @@ export type CertInfo = {
crt: string;
key: string;
csr: string;
pfx?: string;
der?: string;
};
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
@@ -81,6 +83,7 @@ export class AcmeService {
if (conf.key == null) {
conf.key = await this.createNewKey();
await this.saveAccountConfig(email, conf);
this.logger.info(`创建新的Accountkey:${email}`);
}
let directoryUrl = "";
if (isTest) {
@@ -124,7 +127,7 @@ export class AcmeService {
}
async createNewKey() {
const key = await acme.forge.createPrivateKey();
const key = await acme.crypto.createPrivateKey(2048);
return key.toString();
}
@@ -236,7 +239,10 @@ export class AcmeService {
const privateKeyType = options.privateKeyType || "rsa_2048";
const privateKeyArr = privateKeyType.split("_");
const type = privateKeyArr[0];
const size = parseInt(privateKeyArr[1]);
let size = 2048;
if (privateKeyArr.length > 1) {
size = parseInt(privateKeyArr[1]);
}
if (type == "ec") {
const name: any = "P-" + size;
privateKey = await acme.crypto.createPrivateEcdsaKey(name);
@@ -308,10 +314,10 @@ export class AcmeService {
await utils.http({
url: directoryUrl,
method: "GET",
timeout: 5000,
timeout: 10000,
});
} catch (e) {
this.logger.error(`${directoryUrl},测试访问失败`, e);
this.logger.error(`${directoryUrl},测试访问失败`, e.stack);
return false;
}
this.logger.info(`${directoryUrl},测试访问成功`);

View File

@@ -3,6 +3,8 @@ import dayjs from "dayjs";
import type { CertInfo } from "./acme.js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
import { CertConverter } from "./convert.js";
import fs from "fs";
export { CertReader };
export type { CertInfo };
@@ -15,6 +17,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
vModel: "value",
mode: "tags",
open: false,
tokenSeparators: [",", " ", "", "、", "|"],
},
required: true,
col: {
@@ -25,7 +28,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
"1、支持通配符域名例如 *.foo.com、foo.com、*.test.handsfree.work\n" +
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
"3、多级子域名要分成多个域名输入*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
"4、输入一个回车之后,再输入下一个",
"4、输入一个空格之后,再输入下一个",
})
domains!: string[];
@@ -41,6 +44,18 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
})
email!: string;
@TaskInput({
title: "PFX密码",
component: {
name: "a-input-password",
vModel: "value",
},
required: false,
order: 100,
helper: "PFX格式证书是否需要加密",
})
pfxPassword!: string;
@TaskInput({
title: "更新天数",
value: 20,
@@ -77,13 +92,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
})
successNotify = true;
@TaskInput({
title: "配置说明",
order: 9999,
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
})
intro!: string;
// @TaskInput({
// title: "CsrInfo",
// helper: "暂时没有用",
@@ -135,22 +143,44 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (cert.pfx == null || cert.der == null) {
try {
const converter = new CertConverter({ logger: this.logger });
const res = await converter.convert({
cert,
pfxPassword: this.pfxPassword,
});
const pfxBuffer = fs.readFileSync(res.pfxPath);
cert.pfx = pfxBuffer.toString("base64");
const derBuffer = fs.readFileSync(res.derPath);
cert.der = derBuffer.toString("base64");
this.logger.info("转换证书格式成功");
isNew = true;
} catch (e) {
this.logger.error("转换证书格式失败", e);
}
}
if (isNew) {
const applyTime = dayjs(certReader.detail.notBefore).format("YYYYMMDD_HHmmss");
await this.zipCert(cert, applyTime);
const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore);
await this.zipCert(cert, zipFileName);
} else {
this.extendsFiles();
}
// thi
// s.logger.info(JSON.stringify(certReader.detail));
}
async zipCert(cert: CertInfo, applyTime: string) {
async zipCert(cert: CertInfo, filename: string) {
const zip = new JSZip();
zip.file("cert.crt", cert.crt);
zip.file("cert.key", cert.key);
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
const filename = `cert_${domain_name}_${applyTime}.zip`;
if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
}
if (cert.der) {
zip.file("cert.der", Buffer.from(cert.der, "base64"));
}
const content = await zip.generateAsync({ type: "nodebuffer" });
this.saveFile(filename, content);
this.logger.info(`已保存文件:${filename}`);

View File

@@ -3,7 +3,14 @@ import fs from "fs";
import os from "os";
import path from "path";
import { crypto } from "@certd/acme-client";
export class CertReader implements CertInfo {
import { ILogger } from "@certd/pipeline";
import dayjs from "dayjs";
export type CertReaderHandleContext = { reader: CertReader; tmpCrtPath: string; tmpKeyPath: string; tmpPfxPath?: string; tmpDerPath?: string };
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
export class CertReader {
cert: CertInfo;
crt: string;
key: string;
csr: string;
@@ -11,30 +18,31 @@ export class CertReader implements CertInfo {
detail: any;
expires: number;
constructor(certInfo: CertInfo) {
this.cert = certInfo;
this.crt = certInfo.crt;
this.key = certInfo.key;
this.csr = certInfo.csr;
const { detail, expires } = this.getCrtDetail(this.crt);
const { detail, expires } = this.getCrtDetail(this.cert.crt);
this.detail = detail;
this.expires = expires.getTime();
}
toCertInfo(): CertInfo {
return {
crt: this.crt,
key: this.key,
csr: this.csr,
};
return this.cert;
}
getCrtDetail(crt: string) {
getCrtDetail(crt: string = this.cert.crt) {
const detail = crypto.readCertificateInfo(crt.toString());
const expires = detail.notAfter;
return { detail, expires };
}
saveToFile(type: "crt" | "key", filepath?: string) {
saveToFile(type: "crt" | "key" | "pfx" | "der", filepath?: string) {
if (!this.cert[type]) {
return;
}
if (filepath == null) {
//写入临时目录
filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.${type}`);
@@ -44,8 +52,50 @@ export class CertReader implements CertInfo {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filepath, this[type]);
if (type === "crt" || type === "key") {
fs.writeFileSync(filepath, this.cert[type]);
} else {
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
}
return filepath;
}
async readCertFile(opts: HandleOpts) {
const logger = opts.logger;
logger.info("将证书写入本地缓存文件");
const tmpCrtPath = this.saveToFile("crt");
const tmpKeyPath = this.saveToFile("key");
const tmpPfxPath = this.saveToFile("pfx");
const tmpDerPath = this.saveToFile("der");
logger.info("本地文件写入成功");
try {
await opts.handle({
reader: this,
tmpCrtPath: tmpCrtPath,
tmpKeyPath: tmpKeyPath,
tmpPfxPath: tmpPfxPath,
tmpDerPath: tmpDerPath,
});
} finally {
//删除临时文件
logger.info("删除临时文件");
function removeFile(filepath?: string) {
if (filepath) {
fs.unlinkSync(filepath);
}
}
removeFile(tmpCrtPath);
removeFile(tmpKeyPath);
removeFile(tmpPfxPath);
removeFile(tmpDerPath);
}
}
buildCertFileName(suffix: string, applyTime: number, prefix = "cert") {
const detail = this.getCrtDetail();
let domain = detail.detail.domains.commonName;
domain = domain.replace(".", "_").replace("*", "_");
const timeStr = dayjs(applyTime).format("YYYYMMDDHHmmss");
return `${prefix}_${domain}_${timeStr}.${suffix}`;
}
}

View File

@@ -0,0 +1,91 @@
import { ILogger, sp } from "@certd/pipeline";
import type { CertInfo } from "../cert-plugin/acme.js";
import { CertReader, CertReaderHandleContext } from "../cert-plugin/cert-reader.js";
import path from "path";
import os from "os";
import fs from "fs";
export { CertReader };
export type { CertInfo };
export class CertConverter {
logger: ILogger;
constructor(opts: { logger: ILogger }) {
this.logger = opts.logger;
}
async convert(opts: { cert: CertInfo; pfxPassword: string }): Promise<{
pfxPath: string;
derPath: string;
}> {
const certReader = new CertReader(opts.cert);
let pfxPath: string;
let derPath: string;
const handle = async (opts: CertReaderHandleContext) => {
// 调用openssl 转pfx
pfxPath = await this.convertPfx(opts);
// 转der
derPath = await this.convertDer(opts);
};
await certReader.readCertFile({ logger: this.logger, handle });
return {
pfxPath,
derPath,
};
}
async exec(cmd: string) {
await sp.spawn({
cmd: cmd,
logger: this.logger,
});
}
private async convertPfx(opts: CertReaderHandleContext, pfxPassword?: string) {
const { tmpCrtPath, tmpKeyPath } = opts;
const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", "cert.pfx");
const dir = path.dirname(pfxPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
let passwordArg = "-passout pass:";
if (pfxPassword) {
passwordArg = `-password pass:${pfxPassword}`;
}
await this.exec(`openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`);
return pfxPath;
// const fileBuffer = fs.readFileSync(pfxPath);
// this.pfxCert = fileBuffer.toString("base64");
//
// const applyTime = new Date().getTime();
// const filename = reader.buildCertFileName("pfx", applyTime);
// this.saveFile(filename, fileBuffer);
}
private async convertDer(opts: CertReaderHandleContext) {
const { tmpCrtPath } = opts;
const derPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.der`);
const dir = path.dirname(derPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`);
return derPath;
// const fileBuffer = fs.readFileSync(derPath);
// this.derCert = fileBuffer.toString("base64");
//
// const applyTime = new Date().getTime();
// const filename = reader.buildCertFileName("der", applyTime);
// this.saveFile(filename, fileBuffer);
}
}

View File

@@ -6,8 +6,8 @@ import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../.
import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
export { CertReader };
export type { CertInfo };
export * from "./cert-reader.js";
@IsTaskPlugin({
name: "CertApply",
@@ -37,11 +37,31 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
{ value: "zerossl", label: "ZeroSSL" },
],
},
helper: "如果letsencrypt.org或dv.acme-v02.api.pki.goog无法访问请尝试开启代理选项\n如果使用ZeroSSL、google证书需要提供EAB授权",
helper: "Let's Encrypt最简单如果使用ZeroSSL、google证书需要提供EAB授权",
required: true,
})
sslProvider!: SSLProvider;
@TaskInput({
title: "EAB授权",
component: {
name: "pi-access-selector",
type: "eab",
},
maybeNeed: true,
required: true,
helper:
"需要提供EAB授权\nZeroSSL请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials' \n Google请查看[google获取eab帮助文档](https://github.com/certd/certd/blob/v2/doc/google/google.md)",
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.sslProvider === 'zerossl' || form.sslProvider === 'google'
})
}
`,
})
eabAccessId!: number;
@TaskInput({
title: "加密算法",
value: "rsa_2048",
@@ -62,18 +82,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
})
privateKeyType!: PrivateKeyType;
@TaskInput({
title: "EAB授权",
component: {
name: "pi-access-selector",
type: "eab",
},
maybeNeed: true,
helper:
"如果使用ZeroSSL或者google证书需要提供EAB授权\nZeroSSL请前往 https://app.zerossl.com/developer 生成 'EAB Credentials' \n Google请前往https://github.com/certd/certd/blob/v2/doc/google/google.md",
})
eabAccessId!: number;
@TaskInput({
title: "DNS提供商",
component: {
@@ -81,7 +89,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
},
required: true,
helper:
"请选择dns解析提供商您的域名是在哪里注册的或者域名的dns解析服务器属于哪个平台\n如果这里没有您的dns解析提供商可以将域名解析服务器设置成上面的任意一个提供商",
"请选择dns解析提供商您的域名是在哪里注册的或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商需要将域名解析服务器设置成上面的任意一个提供商",
})
dnsProviderType!: string;
@@ -92,13 +100,14 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
},
required: true,
helper: "请选择dns解析提供商授权",
reference: [
{
src: "form.dnsProviderType",
dest: "component.type",
type: "computed",
},
],
mergeScript: `return {
component:{
type: ctx.compute(({form})=>{
return form.dnsProviderType
})
}
}
`,
})
dnsProviderAccess!: string;

View File

@@ -10,6 +10,7 @@ RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
FROM node:20-alpine
RUN apk add --no-cache openssl
WORKDIR /app/
COPY --from=builder /workspace/certd-server/ /app/
RUN chmod +x /app/tools/linux/*

View File

@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
**Note:** Version bump only for package @certd/ui-client
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
### Bug Fixes
* 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577))
### Performance Improvements
* 阶段、任务、步骤全面支持拖动排序 ([bd73a16](https://github.com/certd/certd/commit/bd73a163cd0497f062bd424ddc6bc9bbc95f81ea))
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
* 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327))
* client 请求超时时间延长为10s ([ff46771](https://github.com/certd/certd/commit/ff46771d8dd43e71c1ca70e3ba783945750342cc))
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.24.1",
"version": "1.24.3",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -55,10 +55,10 @@
"vue-cropperjs": "^5.0.0",
"vue-i18n": "^9.10.2",
"vue-router": "^4.3.0",
"vuedraggable": "^2.24.3"
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@certd/pipeline": "^1.24.1",
"@certd/pipeline": "^1.24.3",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -112,7 +112,7 @@ function createRequestFunction(service: any) {
headers: {
"Content-Type": get(config, "headers.Content-Type", "application/json")
},
timeout: 5000,
timeout: 10000,
baseURL: env.API,
data: {}
};

View File

@@ -70,6 +70,7 @@ const onError = (error: any) => {
}
.vcron-select-input {
min-height: 22px;
background-color: #fff;
}
.vcron-select-container {
display: flex;

View File

@@ -3,7 +3,7 @@ import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
import PiDnsProviderSelector from "./dns-provider-selector/index.vue";
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
import PiEditable from "./editable.vue";
import VipButton from "./vip-button/index.vue";
import vip from "./vip-button/install.js";
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
import CronEditor from "./cron-editor/index.vue";
import { CronLight } from "@vue-js-cron/light";
@@ -15,12 +15,14 @@ export default {
app.component("PiEditable", PiEditable);
app.component("PiOutputSelector", PiOutputSelector);
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
app.component("VipButton", VipButton);
app.component("CronLight", CronLight);
app.component("CronEditor", CronEditor);
app.component("CheckCircleOutlined", CheckCircleOutlined);
app.component("InfoCircleOutlined", InfoCircleOutlined);
app.component("UndoOutlined", UndoOutlined);
app.use(vip);
}
};

View File

@@ -0,0 +1,23 @@
import { message, notification } from "ant-design-vue";
import { useUserStore } from "/@/store/modules/user";
export default {
mounted(el: any, binding: any, vnode: any) {
const { value } = binding;
const userStore = useUserStore();
el.className = el.className + " need-plus";
if (!userStore.isPlus) {
function checkPlus() {
// 事件处理代码
notification.warn({
message: "此为专业版功能,请升级到专业版"
});
}
el.addEventListener("click", function (event: any) {
checkPlus();
});
el.addEventListener("move", function (event: any) {
checkPlus();
});
}
}
};

View File

@@ -129,8 +129,10 @@ function openUpgrade() {
<div>
<h3 class="block-header">专业版特权</h3>
<ul>
<li>证书流水线数量无限制</li>
<li>可加VIP群需求优先实现</li>
<li>证书流水线数量无限制免费版限制10条</li>
<li>免配置发邮件功能</li>
<li>FTP上传cdnfly宝塔易盾等部署插件</li>
<li>更多特权敬请期待</li>
</ul>
</div>

View File

@@ -0,0 +1,6 @@
import VipButton from "./index.vue";
import plus from "./directive.js";
export default function (app: any) {
app.component("VipButton", VipButton);
app.directive("plus", plus);
}

View File

@@ -7,7 +7,7 @@ import * as UserApi from "/src/api/modules/api.user";
import { RegisterReq } from "/src/api/modules/api.user";
// @ts-ignore
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
import { Modal, notification } from "ant-design-vue";
import { message, Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { mitter } from "/src/utils/util.mitt";
@@ -67,7 +67,14 @@ export const useUserStore = defineStore({
LocalStorage.remove(TOKEN_KEY);
LocalStorage.remove(USER_INFO_KEY);
},
checkPlus() {
if (!this.isPlus) {
notification.warn({
message: "此为专业版功能,请先升级到专业版"
});
throw new Error("此为专业版功能,请升级到专业版");
}
},
async register(user: RegisterReq) {
await UserApi.register(user);
notification.success({

View File

@@ -22,15 +22,14 @@ h1, h2, h3, h4, h5, h6 {
margin-bottom: 0;
}
.fs-desc{
.fs-desc {
font-size: 12px;
color:#888888;
color: #888888;
margin-left: 5px;
margin-right: 5px;
}
.ant-btn-link {
height: 24px;
}
@@ -45,69 +44,88 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important;
}
.pointer{
cursor: pointer;
.pointer {
cursor: pointer;
}
.flex-center{
display: flex;
justify-content: center;
align-items: center;
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-o{
.flex-o {
display: flex !important;
align-items: center;
}
.flex{
.flex {
display: flex;
align-items: center;
}
.flex-1{
.flex-1 {
flex: 1;
}
.mb-2{
margin-bottom:2px;
}
.ml-5{
margin-left:5px;
}
.ml-20{
margin-left:20px;
}
.ml-15{
margin-left:15px;
.mb-2 {
margin-bottom: 2px;
}
.mr-5{
.ml-5 {
margin-left: 5px;
}
.ml-10 {
margin-left: 10px;
}
.ml-20 {
margin-left: 20px;
}
.ml-15 {
margin-left: 15px;
}
.mr-5 {
margin-right: 5px;
}
.mr-20{
margin-right: 20px;
}
.mr-15{
margin-right: 15px;
}
.mt-5{
margin-top:5px;
}
.mt-10{
margin-top:10px;
}
.mb-10{
margin-bottom: 10px;
}
.m-10{
margin:10px;
.mr-10 {
margin-right: 10px;
}
.p-5{
padding:5px;
.mr-20 {
margin-right: 20px;
}
.p-10{
padding:10px;
.mr-15 {
margin-right: 15px;
}
.mt-5 {
margin-top: 5px;
}
.mt-10 {
margin-top: 10px;
}
.mb-10 {
margin-bottom: 10px;
}
.m-10 {
margin: 10px;
}
.p-5 {
padding: 5px;
}
.p-10 {
padding: 10px;
}
.ellipsis {
@@ -116,18 +134,35 @@ h1, h2, h3, h4, h5, h6 {
text-overflow: ellipsis;
}
.w-100{
width: 100%;
.w-100 {
width: 100%;
}
.block-header{
margin:3px;
.block-header {
margin: 3px;
padding-top: 15px;
padding-bottom:3px;
padding-bottom: 3px;
border-bottom: 1px solid #dedede;
}
.color-blue{
color: #1890ff;
}
.color-blue {
color: #1890ff;
}
.icon-box {
display: inline-flex;
align-items: center;
justify-content: center;
.fs-icon {
display: flex;
align-items: center;
justify-content: center;
}
}
.need-plus {
color: #c5913f !important;
}

View File

@@ -1,17 +0,0 @@
import _ from "lodash-es";
import { compute } from "@fast-crud/fast-crud";
export function useReference(form: any) {
if (!form.reference) {
return;
}
for (const reference of form.reference) {
_.set(
form,
reference.dest,
compute<any>((scope) => {
return _.get(scope, reference.src);
})
);
}
}

View File

@@ -0,0 +1,40 @@
import _ from "lodash-es";
import { compute } from "@fast-crud/fast-crud";
export function useReference(formItem: any) {
if (formItem.reference) {
for (const reference of formItem.reference) {
_.set(
formItem,
reference.dest,
compute<any>((scope) => {
return _.get(scope, reference.src);
})
);
}
delete formItem.reference;
}
if (formItem.mergeScript) {
const ctx = {
compute
};
const script = formItem.mergeScript;
const func = new Function("ctx", script);
const merged = func(ctx);
_.merge(formItem, merged);
delete formItem.mergeScript;
}
//helper
if (formItem.helper && typeof formItem.helper === "string") {
//正则表达式替换 [name](url) 成 <a href="url" >
let helper = formItem.helper.replace(/\[(.*)\]\((.*)\)/g, '<a href="$2" target="_blank">$1</a>');
helper = helper.replace(/\n/g, "<br/>");
formItem.helper = {
render: () => {
return <div innerHTML={helper}></div>;
}
};
}
}

View File

@@ -1,4 +1,4 @@
import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud";
import { ColumnCompositionProps, dict, compute } from "@fast-crud/fast-crud";
// @ts-ignore
import * as api from "./api";
// @ts-ignore
@@ -32,11 +32,25 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
...value,
key
};
const column = _.merge({ title: key }, defaultPluginConfig, field);
let column = _.merge({ title: key }, defaultPluginConfig, field);
//eval
if (column.mergeScript) {
const ctx = {
compute
};
const script = column.mergeScript;
delete column.mergeScript;
const func = new Function("ctx", script);
const merged = func(ctx);
column = _.merge(column, merged);
}
//设置默认值
if (column.value != null && _.get(form, key) == null) {
//设置默认值
_.set(form, key, column.value);
}
//字段配置赋值
columnsRef.value[key] = column;
console.log("form", columnsRef.value);
});
@@ -55,7 +69,12 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
},
form: {
component: {
disabled: false
disabled: false,
showSearch: true,
filterOption: (input: string, option: any) => {
input = input?.toLowerCase();
return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0;
}
},
rules: [{ required: true, message: "请选择类型" }],
valueChange: {

View File

@@ -22,12 +22,17 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
form: {
...inputDefine,
show: compute((ctx) => {
console.log(formWrapperRef);
const form = formWrapperRef.value.getFormData();
if (!form) {
return false;
}
return form?.certApplyPlugin === plugin.name;
let inputDefineShow = true;
if (inputDefine.show != null) {
const computeShow = inputDefine.show as any;
inputDefineShow = computeShow.computeFn({ form });
}
return form?.certApplyPlugin === plugin.name && inputDefineShow;
})
}
};
@@ -60,8 +65,8 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
render: () => {
return (
<ul>
<li>Lego-ACMELego实现DNS提供商</li>
<li>JS-ACMEDNS属于阿里云Cloudflare可以选择用它来申请</li>
<li>JS-ACMEDNS属于阿里云Cloudflare西</li>
<li>Lego-ACMELego实现DNS提供商LEGO的用户可以使用</li>
</ul>
);
}
@@ -78,7 +83,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
vModel: "modelValue",
placeholder: "0 0 4 * * *"
},
helper: "请输入cron表达式, 例如0 0 4 * * *每天凌晨4点触发",
helper: "点击上面的按钮,选择每天几点几分定时执行, 例如0 0 4 * * *每天凌晨4点0分0秒触发",
order: 100
}
},

View File

@@ -10,11 +10,52 @@ import { env } from "/@/utils/util.env";
import { useUserStore } from "/@/store/modules/user";
import dayjs from "dayjs";
import { useSettingStore } from "/@/store/modules/settings";
import _ from "lodash-es";
export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const lastResRef = ref();
function setRunnableIds(pipeline: any) {
const idMap: any = {};
function createId(oldId: any) {
if (oldId == null) {
return nanoid();
}
const newId = nanoid();
idMap[oldId] = newId;
return newId;
}
if (pipeline.stages) {
for (const stage of pipeline.stages) {
stage.id = createId(stage.id);
if (stage.tasks) {
for (const task of stage.tasks) {
task.id = createId(task.id);
if (task.steps) {
for (const step of task.steps) {
step.id = createId(step.id);
}
}
}
}
}
}
for (const trigger of pipeline.triggers) {
trigger.id = nanoid();
}
for (const notification of pipeline.notifications) {
notification.id = nanoid();
}
let content = JSON.stringify(pipeline);
for (const key in idMap) {
content = content.replaceAll(key, idMap[key]);
}
return JSON.parse(content);
}
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
@@ -34,9 +75,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
title: form.title
});
} else {
const content = JSON.parse(form.content);
content.title = form.title;
form.content = JSON.stringify(content);
//复制的流水线
delete form.status;
delete form.lastHistoryTime;
delete form.lastVars;
delete form.createTime;
delete form.id;
let pipeline = JSON.parse(form.content);
pipeline.title = form.title;
pipeline = setRunnableIds(pipeline);
form.content = JSON.stringify(pipeline);
}
const res = await api.AddObj(form);
@@ -48,12 +96,11 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
// 添加certd pipeline
const triggers = [];
if (form.triggerCron) {
triggers.push({ id: nanoid(), title: "定时触发", type: "timer", props: { cron: form.triggerCron } });
triggers.push({ title: "定时触发", type: "timer", props: { cron: form.triggerCron } });
}
const notifications = [];
if (form.emailNotify) {
notifications.push({
id: nanoid(),
type: "email",
when: ["error", "turnToSuccess"],
options: {
@@ -61,19 +108,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}
});
}
const pipeline = {
let pipeline = {
title: form.domains[0] + "证书自动化",
stages: [
{
id: nanoid(),
title: "证书申请阶段",
tasks: [
{
id: nanoid(),
title: "证书申请任务",
steps: [
{
id: nanoid(),
title: "申请证书",
input: {
renewDays: 20,
@@ -92,8 +136,10 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
triggers,
notifications
};
pipeline = setRunnableIds(pipeline);
const id = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30
});
@@ -145,15 +191,18 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
},
copy: {
click: async (context) => {
userStore.checkPlus();
const { ui } = useUi();
// @ts-ignore
const row = context[ui.tableColumn.row];
let row = context[ui.tableColumn.row];
row = _.cloneDeep(row);
row.title = row.title + "_copy";
await crudExpose.openCopy({
row: row,
index: context.index
});
}
},
class: "need-plus"
},
config: {
order: 1,

View File

@@ -1,5 +1,5 @@
<template>
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="700px" @after-open-change="stepDrawerOnAfterVisibleChange">
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="700px">
<template #title>
编辑步骤
<a-button v-if="editMode" @click="stepDelete()">
@@ -59,7 +59,7 @@
:get-context-fn="blankFn"
/>
<template v-for="(item, key) in currentPlugin.input" :key="key">
<fs-form-item v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
<fs-form-item v-if="item.show !== false" v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
</template>
<fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
@@ -83,9 +83,12 @@ import { nanoid } from "nanoid";
import { CopyOutlined } from "@ant-design/icons-vue";
import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
import { useUserStore } from "/@/store/modules/user";
import { compute, useCompute } from "@fast-crud/fast-crud";
import { useReference } from "/@/use/use-refrence";
export default {
name: "PiStepForm",
// eslint-disable-next-line vue/no-unused-components
components: { CopyOutlined },
props: {
editMode: {
@@ -106,7 +109,6 @@ export default {
const mode: Ref = ref("add");
const callback: Ref = ref();
const currentStep: Ref = ref({ title: undefined, input: {} });
const currentPlugin: Ref = ref({});
const stepFormRef: Ref = ref(null);
const stepDrawerVisible: Ref = ref(false);
const rules: Ref = ref({
@@ -150,15 +152,10 @@ export default {
stepDrawerVisible.value = false;
};
const stepDrawerOnAfterVisibleChange = (val: any) => {
console.log("stepDrawerOnAfterVisibleChange", val);
};
const stepOpen = (step: any, emit: any) => {
callback.value = emit;
currentStep.value = _.merge({ input: {}, strategy: {} }, step);
console.log("currentStepOpen", currentStep.value);
if (step.type) {
changeCurrentPlugin(currentStep.value);
}
@@ -189,33 +186,41 @@ export default {
stepOpen(step, emit);
};
const currentPluginDefine = ref();
function getContext() {
return {
form: currentStep.value.input
};
}
const { doComputed } = useCompute();
const currentPlugin = doComputed(() => {
return currentPluginDefine.value;
}, getContext);
const changeCurrentPlugin = (step: any) => {
const stepType = step.type;
const pluginDefine = pluginGroups.get(stepType);
if (pluginDefine) {
step.type = stepType;
step._isAdd = false;
currentPlugin.value = _.cloneDeep(pluginDefine);
for (let key in currentPlugin.value.input) {
const input = currentPlugin.value.input[key];
if (input?.reference) {
for (const reference of input.reference) {
_.set(
input,
reference.dest,
computed<any>(() => {
const scope = {
form: currentStep.value.input
};
return _.get(scope, reference.src);
})
);
}
}
//设置初始值
if ((input.default != null || input.value != null) && currentStep.value.input[key] == null) {
currentStep.value.input[key] = input.default ?? input.value;
}
step.type = stepType;
step._isAdd = false;
let pluginDefine = pluginGroups.get(stepType);
if (pluginDefine == null) {
console.log("插件未找到", stepType);
return;
}
pluginDefine = _.cloneDeep(pluginDefine);
const columns = pluginDefine.input;
for (let key in columns) {
const column = columns[key];
useReference(column);
}
currentPluginDefine.value = pluginDefine;
for (let key in pluginDefine.input) {
const column = pluginDefine.input[key];
//设置初始值
if ((column.default != null || column.value != null) && currentStep.value.input[key] == null) {
currentStep.value.input[key] = column.default ?? column.value;
}
}
@@ -269,7 +274,6 @@ export default {
stepView,
stepDrawerShow,
stepDrawerVisible,
stepDrawerOnAfterVisibleChange,
currentStep,
currentPlugin,
stepSave,

View File

@@ -43,25 +43,22 @@
<a-button type="primary" @click="stepAdd(currentTask)">添加步骤</a-button>
</template>
</a-descriptions>
<a-list class="step-list" item-layout="horizontal" :data-source="currentTask.steps">
<template #renderItem="{ item, index }">
<a-list-item>
<template #actions>
<a key="edit" @click="stepEdit(currentTask, item, index)">编辑</a>
<a key="edit" @click="stepCopy(currentTask, item, index)">复制</a>
<v-draggable v-model="currentTask.steps" class="step-list" handle=".handle" item-key="id" :disabled="!userStore.isPlus">
<template #item="{ element, index }">
<div class="step-row">
<div class="text">
<fs-icon icon="ion:flash"></fs-icon>
<h4 class="title">{{ element.title }}</h4>
</div>
<div class="action">
<a key="edit" @click="stepEdit(currentTask, element, index)">编辑</a>
<a key="edit" @click="stepCopy(currentTask, element, index)">复制</a>
<a key="remove" @click="stepDelete(currentTask, index)">删除</a>
</template>
<a-list-item-meta>
<template #title>
{{ item.title }}
</template>
<template #avatar>
<fs-icon icon="ion:flash"></fs-icon>
</template>
</a-list-item-meta>
</a-list-item>
<fs-icon v-plus class="icon-button handle" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div>
</div>
</template>
</a-list>
</v-draggable>
</a-form-item>
</div>
</a-form>
@@ -85,10 +82,11 @@ import { nanoid } from "nanoid";
import PiStepForm from "../step-form/index.vue";
import { Modal } from "ant-design-vue";
import { CopyOutlined } from "@ant-design/icons-vue";
import VDraggable from "vuedraggable";
import { useUserStore } from "/@/store/modules/user";
export default {
name: "PiTaskForm",
components: { CopyOutlined, PiStepForm },
components: { CopyOutlined, PiStepForm, VDraggable },
props: {
editMode: {
type: Boolean,
@@ -97,6 +95,7 @@ export default {
},
emits: ["update"],
setup(props: any, ctx: any) {
const userStore = useUserStore();
function useStep() {
const stepFormRef: Ref<any> = ref(null);
const currentStepIndex = ref(0);
@@ -251,6 +250,7 @@ export default {
};
}
return {
userStore,
labelCol: { span: 6 },
wrapperCol: { span: 16 },
...useTaskForm(),
@@ -268,5 +268,42 @@ export default {
.ant-list .ant-list-item .ant-list-item-meta .ant-list-item-meta-title {
margin: 0;
}
.ant-list .ant-list-item .ant-list-item-action {
display: flex;
> li {
display: flex;
align-items: center;
}
}
.step-list {
padding: 10px;
.icon-button {
font-size: 18px;
color: #1677ff;
cursor: pointer;
}
.step-row {
padding: 10px;
display: flex;
align-items: center;
justify-content: space-between;
.text {
display: flex;
> * {
margin: 0px;
margin-right: 15px;
}
}
.action {
display: flex;
align-items: center;
> * {
margin-right: 15px;
font-size: 14px;
}
}
}
}
}
</style>

View File

@@ -56,7 +56,7 @@
name: 'cron-editor',
vModel: 'modelValue'
},
helper: 'cron表达式例如: 0 0 3 * * * 表示每天凌晨3点触发',
helper: '点击上面的按钮,选择每天几点几分定时执行, 例如0 0 4 * * *每天凌晨4点0分0秒触发',
rules: [{ required: true, message: '此项必填' }]
}"
/>

View File

@@ -20,177 +20,204 @@
<div class="layout-left">
<div class="pipeline-container">
<div class="pipeline">
<div class="stages">
<div class="stage first-stage">
<div class="title">
<pi-editable model-value="触发源" :disabled="true" />
</div>
<div class="tasks">
<div class="task-container first-task">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="primary" @click="run">
<fs-icon icon="ion:play"></fs-icon>
手动触发
</a-button>
</div>
<v-draggable v-model="pipeline.stages" class="stages" item-key="id" handle=".stage-move-handle" :disabled="!userStore.isPlus">
<template #header>
<div class="stage first-stage">
<div class="title stage-move-handle">
<pi-editable model-value="触发源" :disabled="true" />
</div>
<div v-for="(trigger, index) of pipeline.triggers" :key="trigger.id" class="task-container">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="triggerEdit(trigger, index)">
<fs-icon icon="ion:time"></fs-icon>
{{ trigger.title }}
</a-button>
</div>
</div>
<div v-if="editMode" class="task-container is-add">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="triggerAdd">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
触发源定时
</a-button>
</div>
</div>
</div>
</div>
<div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
<div class="title">
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
</div>
<div class="tasks">
<div
v-for="(task, taskIndex) of stage.tasks"
:key="task.id"
class="task-container"
:class="{
'first-task': taskIndex === 0
}"
>
<div class="line">
<div class="flow-line"></div>
<fs-icon v-if="editMode" class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd(index)"></fs-icon>
</div>
<div class="task">
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
<a-popover title="步骤">
<!-- :open="true"-->
<template #content>
<div v-for="(item, index) of task.steps" class="flex-o w-100">
<span class="ellipsis flex-1">{{ index + 1 }}. {{ item.title }} </span>
<pi-status-show :status="item.status?.result"></pi-status-show>
<fs-icon class="pointer color-blue" title="重新运行此步骤" icon="SyncOutlined" @click="run(item.id)"></fs-icon>
</div>
</template>
<span class="flex-o w-100">
<span class="ellipsis flex-1" :class="{ 'mr-15': editMode }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
</span>
</a-popover>
</a-button>
<fs-icon v-if="editMode" class="copy" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
</div>
</div>
<div v-if="editMode" class="task-container is-add">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-tooltip>
<a-button type="dashed" shape="round" @click="taskAdd(stage, index)">
<fs-icon class="font-20" icon="ion:add-circle-outline"></fs-icon>
并行任务
<div class="tasks">
<div class="task-container first-task">
<div class="line line-right">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="primary" @click="run()">
<fs-icon icon="ion:play"></fs-icon>
手动触发
</a-button>
</a-tooltip>
</div>
</div>
<div v-for="(trigger, index) of pipeline.triggers" :key="trigger.id" class="task-container">
<div class="line line-right">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="triggerEdit(trigger, index)">
<fs-icon icon="ion:time"></fs-icon>
{{ trigger.title }}
</a-button>
</div>
</div>
</div>
</div>
</div>
<div v-if="editMode" class="stage last-stage">
<div class="title">
<pi-editable model-value="新阶段" :disabled="true" />
</div>
<div class="tasks">
<div class="task-container first-task">
<div class="line">
<div class="flow-line"></div>
<fs-icon class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd()"></fs-icon>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="stageAdd()">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
添加任务
</a-button>
<div v-if="editMode" class="task-container is-add">
<div class="line line-right">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="triggerAdd">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
触发源定时
</a-button>
</div>
</div>
</div>
<div class="task-container">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="notificationAdd()">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
</div>
</template>
添加通知
</a-button>
<template #item="{ element: stage, index }">
<div :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
<div class="title">
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
<div v-plus class="icon-box stage-move-handle">
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div>
</div>
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
<div class="line">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, ii as number)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.type }}
</a-button>
</div>
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!userStore.isPlus">
<template #item="{ element: task, index: taskIndex }">
<div
class="task-container"
:class="{
'first-task': taskIndex === 0
}"
>
<div class="line line-left">
<div class="flow-line"></div>
<fs-icon v-if="editMode" class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd(index)"></fs-icon>
</div>
<div class="line line-right">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
<a-popover title="步骤" :trigger="editMode ? 'none' : 'hover'">
<!-- :open="true"-->
<template #content>
<div v-for="(item, index) of task.steps" class="flex-o w-100">
<span class="ellipsis flex-1">{{ index + 1 }}. {{ item.title }} </span>
<pi-status-show v-if="!editMode" :status="item.status?.result"></pi-status-show>
<fs-icon
v-if="!editMode"
class="pointer color-blue ml-2"
title="重新运行此步骤"
icon="SyncOutlined"
@click="run(item.id)"
></fs-icon>
</div>
</template>
<span class="flex-o w-100">
<span class="ellipsis flex-1 task-title" :class="{ 'in-edit': editMode }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
</span>
</a-popover>
</a-button>
<div class="icon-box action copy">
<fs-icon v-if="editMode" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
</div>
<div v-plus class="icon-box task-move-handle action drag">
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div>
</div>
</div>
</template>
<template #footer>
<div v-if="editMode" class="task-container is-add">
<div class="line line-left">
<div class="flow-line"></div>
</div>
<div class="line line-right">
<div class="flow-line"></div>
</div>
<div class="task">
<a-tooltip>
<a-button type="dashed" shape="round" @click="taskAdd(stage, index)">
<fs-icon class="font-20" icon="ion:add-circle-outline"></fs-icon>
并行任务
</a-button>
</a-tooltip>
</div>
</div>
</template>
</v-draggable>
</div>
</template>
<template #footer>
<div v-if="editMode" class="stage last-stage">
<div class="title">
<pi-editable model-value="新阶段" :disabled="true" />
</div>
</div>
</div>
<div v-else class="stage last-stage">
<div class="title">
<pi-editable model-value="结束" :disabled="true" />
</div>
<div v-if="pipeline.notifications?.length > 0" class="tasks">
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
<div class="line">
<div class="flow-line"></div>
<div class="tasks">
<div class="task-container first-task">
<div class="line line-left">
<div class="flow-line"></div>
<fs-icon class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd()"></fs-icon>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="stageAdd()">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
添加任务
</a-button>
</div>
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, index)">
<fs-icon icon="ion:notifications"></fs-icon>
<div class="task-container">
<div class="line line-left">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="dashed" @click="notificationAdd()">
<fs-icon icon="ion:add-circle-outline"></fs-icon>
通知 {{ item.type }}
</a-button>
添加通知
</a-button>
</div>
</div>
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
<div class="line line-left">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, ii as number)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.type }}
</a-button>
</div>
</div>
</div>
</div>
<div v-else class="tasks">
<div class="task-container first-task">
<div class="line">
<div class="flow-line"></div>
<div v-else class="stage last-stage">
<div class="title">
<pi-editable model-value="结束" :disabled="true" />
</div>
<div v-if="pipeline.notifications?.length > 0" class="tasks">
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
<div class="line line-left">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" @click="notificationEdit(item, index)">
<fs-icon icon="ion:notifications"></fs-icon>
通知 {{ item.type }}
</a-button>
</div>
</div>
<div class="task">
<a-button shape="round" type="dashed">
<fs-icon icon="ion:notifications"></fs-icon>
通知未设置
</a-button>
</div>
<div v-else class="tasks">
<div class="task-container first-task">
<div class="line line-left">
<div class="flow-line"></div>
</div>
<div class="task">
<a-button shape="round" type="dashed">
<fs-icon icon="ion:notifications"></fs-icon>
通知未设置
</a-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</v-draggable>
</div>
</div>
</div>
@@ -229,18 +256,19 @@ import PiTriggerForm from "./component/trigger-form/index.vue";
import PiNotificationForm from "./component/notification-form/index.vue";
import PiTaskView from "./component/task-view/index.vue";
import PiStatusShow from "./component/status-show.vue";
import VDraggable from "vuedraggable";
import _ from "lodash-es";
import { message, Modal, notification } from "ant-design-vue";
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
import { nanoid } from "nanoid";
import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type";
import type { Runnable } from "@certd/pipeline";
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
import { FsIcon } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable },
props: {
pipelineId: {
type: [Number, String],
@@ -271,6 +299,8 @@ export default defineComponent({
router.back();
}
const userStore = useUserStore();
const loadCurrentHistoryDetail = async () => {
console.log("load history logs");
const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id });
@@ -626,6 +656,7 @@ export default defineComponent({
currentHistory,
histories,
goBack,
userStore,
...useTaskRet,
...useStageRet,
...useTrigger(),
@@ -700,39 +731,39 @@ export default defineComponent({
.title {
padding: 20px;
color: gray;
}
&.first-stage {
.line {
width: 50% !important;
.flow-line {
border-left: 0;
}
display: flex;
.stage-move-handle {
cursor: move;
margin-left: 4px;
}
}
&.last-stage {
.line {
width: 50% !important;
left: 0;
right: auto;
.flow-line {
border-right: 0;
}
.add-stage-btn {
visibility: hidden;
}
}
}
//.sortable-ghost {
// .line {
// visibility: hidden;
// }
//}
.line {
height: 50px;
position: absolute;
top: -25px;
right: 0;
width: 100%;
width: 25px;
&.line-left {
left: 25px;
.flow-line {
border-right: 0;
}
}
&.line-right {
right: 25px;
.flow-line {
border-left: 0;
}
}
.flow-line {
height: 100%;
margin-left: 28px;
margin-right: 28px;
border: 1px solid #c7c7c7;
border-top: 0;
}
@@ -751,6 +782,52 @@ export default defineComponent({
}
}
.task-container:first-child {
.line {
width: 50px;
&.line-left {
left: 0;
.flow-line {
border-right: 0;
border-left: 0;
}
}
&.line-right {
right: 0;
.flow-line {
border-left: 0;
border-right: 0;
}
}
.add-stage-btn {
visibility: visible;
}
}
}
&.first-stage {
.line {
.flow-line {
border-left: 0;
}
}
}
&.last-stage {
.line {
width: 50% !important;
right: auto;
.flow-line {
border-right: 0;
}
.add-stage-btn {
visibility: hidden;
}
}
}
.tasks {
.task-container {
width: 100%;
@@ -760,18 +837,6 @@ export default defineComponent({
justify-content: center;
align-items: center;
position: relative;
&.first-task {
.line {
.flow-line {
margin: 0;
border-left: 0;
border-right: 0;
}
.add-stage-btn {
visibility: visible;
}
}
}
.task {
display: flex;
flex-direction: column;
@@ -780,14 +845,29 @@ export default defineComponent({
height: 100%;
z-index: 2;
.copy {
.task-title {
&.in-edit {
margin-right: 28px;
}
}
.action {
position: absolute;
right: 60px;
top: 18px;
//font-size: 18px;
cursor: pointer;
z-index: 10;
&:hover {
color: #1890ff;
}
&.copy {
right: 80px;
}
&.drag {
right: 60px;
cursor: move;
}
}
.ant-btn {

View File

@@ -1,5 +1,5 @@
import type { Pipeline } from "@certd/pipeline";
import { FormItemProps } from "@fast-crud/fast-crud";
import { DynamicType, FormItemProps } from "@fast-crud/fast-crud";
export type PipelineDetail = {
pipeline: Pipeline;
};
@@ -24,7 +24,7 @@ export type PluginDefine = {
title: string;
desc?: string;
input: {
[key: string]: FormItemProps;
[key: string]: DynamicType<FormItemProps>;
};
output: {
[key: string]: any;

View File

@@ -1,6 +1,7 @@
koa:
port: 7001
# key: ./data/ssl/cert.key
# cert: ./data/ssl/cert.crt
#plus:
# server:
# baseUrl: 'http://127.0.0.1:11007'

View File

@@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
### Performance Improvements
* 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde))
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
### Bug Fixes
* 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577))
* 修复windows下无法执行第二条命令的bug ([71ac8aa](https://github.com/certd/certd/commit/71ac8aae4aa694e1a23761e9761c9fba30b43a21))
### Performance Improvements
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
* 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327))
* 西部数据支持用户级的apikey ([1c17b41](https://github.com/certd/certd/commit/1c17b41e160944b073e1849e6f9467c3659a4bfc))
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
* 支持阿里云oss ([87a2673](https://github.com/certd/certd/commit/87a2673e8c33dff6eda1b836d92ecc121564ed78))
* 支持西部数码DNS ([c59cab1](https://github.com/certd/certd/commit/c59cab1aaeb19f86df8e3e0d8127cbd0a9ef77f3))
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Performance Improvements

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.24.1",
"version": "1.24.3",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -21,13 +21,13 @@
"dependencies": {
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/pop-core": "^1.7.10",
"@certd/acme-client": "^1.24.1",
"@certd/lib-huawei": "^1.24.1",
"@certd/lib-k8s": "^1.24.1",
"@certd/acme-client": "^1.24.3",
"@certd/lib-huawei": "^1.24.3",
"@certd/lib-k8s": "^1.24.3",
"@certd/midway-flyway-js": "^1.22.6",
"@certd/pipeline": "^1.24.1",
"@certd/plugin-cert": "^1.24.1",
"@certd/plugin-plus": "^1.24.1",
"@certd/pipeline": "^1.24.3",
"@certd/plugin-cert": "^1.24.3",
"@certd/plugin-plus": "^1.24.3",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "^3.16.2",
"@midwayjs/cache": "^3.14.0",
@@ -39,6 +39,7 @@
"@midwayjs/static-file": "^3.16.4",
"@midwayjs/typeorm": "^3.16.4",
"@midwayjs/validate": "^3.16.4",
"ali-oss": "^6.21.0",
"axios": "^1.7.2",
"basic-ftp": "^5.0.5",
"bcryptjs": "^2.4.3",
@@ -47,7 +48,7 @@
"cron-parser": "^4.9.0",
"dayjs": "^1.11.7",
"glob": "^10.4.5",
"https-proxy-agent": "^7.0.4",
"https-proxy-agent": "^7.0.5",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
@@ -61,14 +62,17 @@
"nanoid": "^4.0.0",
"nodemailer": "^6.9.3",
"pg": "^8.12.0",
"querystring": "^0.2.1",
"reflect-metadata": "^0.1.13",
"ssh2": "^1.15.0",
"strip-ansi": "^7.1.0",
"svg-captcha": "^1.4.0",
"tencentcloud-sdk-nodejs": "^4.0.44",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@midwayjs/mock": "^3.16.4",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^3.4.3",
"@types/jest": "^26.0.24",
"@types/koa": "2.13.4",

View File

@@ -49,13 +49,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async add(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= freeCount) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await super.add(bean);
await this.save(bean);
return bean;
}
@@ -105,19 +99,37 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async save(bean: PipelineEntity) {
let old = null;
if (bean.id > 0) {
//修改
old = await this.info(bean.id);
}
const isUpdate = bean.id > 0 && old != null;
if (!isPlus()) {
const count = await this.repository.count();
let count = await this.repository.count();
if (isUpdate) {
count -= 1;
}
if (count >= freeCount) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
if (!isUpdate) {
//如果是添加先保存一下获取到id更新pipeline.id
await this.addOrUpdate(bean);
}
await this.clearTriggers(bean.id);
if (bean.content) {
const pipeline = JSON.parse(bean.content);
bean.title = pipeline.title;
if (pipeline.title) {
bean.title = pipeline.title;
}
pipeline.id = bean.id;
bean.content = JSON.stringify(pipeline);
}
await this.addOrUpdate(bean);
await this.registerTriggerById(bean.id);
return bean;
}
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {

View File

@@ -24,6 +24,7 @@ export class CronTask {
this.job = req.job;
this.name = req.name;
this.logger = logger;
this.logger.info(`[cron] CronTask created [${this.name}], cron:${this.cron}`);
this.genNextTime();
}

View File

@@ -6,3 +6,5 @@ export * from './plugin-host/index.js';
export * from './plugin-huawei/index.js';
export * from './plugin-demo/index.js';
export * from './plugin-other/index.js';
export * from './plugin-west/index.js';
export * from './plugin-doge/index.js';

View File

@@ -1,30 +0,0 @@
import { IsAccess, AccessInput } from '@certd/pipeline';
@IsAccess({
name: 'aliyun',
title: '阿里云授权',
desc: '',
})
export class AliyunAccess {
@AccessInput({
title: 'accessKeyId',
component: {
placeholder: 'accessKeyId',
},
helper: '登录阿里云控制台->AccessKey管理页面获取。',
required: true,
})
accessKeyId = '';
@AccessInput({
title: 'accessKeySecret',
component: {
placeholder: 'accessKeySecret',
},
required: true,
encrypt: true,
helper: '注意证书申请需要dns解析权限其他阿里云插件需要对应的权限比如证书上传需要证书管理权限嫌麻烦就用主账号的全量权限的accessKey',
})
accessKeySecret = '';
}
new AliyunAccess();

View File

@@ -1 +0,0 @@
export * from './aliyun-access.js';

View File

@@ -1,6 +1,6 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { AliyunAccess } from '../access/index.js';
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
@IsDnsProvider({
name: 'aliyun',
@@ -16,13 +16,14 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
logger!: ILogger;
async onInstance() {
const access: any = this.access;
const Core = await import('@alicloud/pop-core');
this.client = new Core.default({
this.client = new AliyunClient({logger:this.logger})
await this.client.init({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09',
});
})
}
//
// async getDomainList() {
@@ -100,6 +101,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
// Line: 'oversea' // 海外
};
const requestOption = {
method: 'POST',
};

View File

@@ -1,3 +1,2 @@
export * from './dns-provider/index.js';
export * from './plugin/index.js';
export * from './access/index.js';

View File

@@ -1,5 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
import { AliyunAccess } from '../../access/index.js';
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
import { appendTimeSuffix } from '../../utils/index.js';
import { CertInfo } from '@certd/plugin-cert';
@@ -51,7 +51,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
},
required: true,
})
namespace!: string;
namespace: string = 'default';
@TaskInput({
title: 'ingress名称',
value: '',
@@ -109,7 +109,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
this.K8sClient = sdk.K8sClient;
}
async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn');
this.logger.info('开始部署证书到阿里云cdn');
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = await this.getClient(access, regionId);
@@ -144,7 +144,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
},
};
const ingressList = await k8sClient.getIngressList({ namespace });
console.log('ingressList:', ingressList);
this.logger.info('ingressList:', ingressList);
if (!ingressList || !ingressList.items) {
return;
}
@@ -200,14 +200,15 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
}
async getClient(aliyunProvider: any, regionId: string) {
const Core = await import('@alicloud/pop-core');
return new Core.default({
const client = new AliyunClient({logger:this.logger})
await client.init({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: `https://cs.${regionId}.aliyuncs.com`,
apiVersion: '2015-12-15',
});
})
return client
}
async getKubeConfig(client: any, clusterId: string, isPrivateIpAddress = false) {

View File

@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs';
import { AliyunAccess } from '../../access/index.js';
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
@IsTaskPlugin({
name: 'DeployCertToAliyunCDN',
title: '部署证书至阿里云CDN',
@@ -50,23 +50,23 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
async onInstance() {}
async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn');
this.logger.info('开始部署证书到阿里云cdn');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = await this.getClient(access);
const params = await this.buildParams();
await this.doRequest(client, params);
console.log('部署完成');
this.logger.info('部署完成');
}
async getClient(access: AliyunAccess) {
const Core = await import('@alicloud/pop-core');
return new Core.default({
const client = new AliyunClient({logger:this.logger})
await client.init({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://cdn.aliyuncs.com',
apiVersion: '2018-05-10',
});
})
return client
}
async buildParams() {

View File

@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs';
import { AliyunAccess } from '../../access/index.js';
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
@IsTaskPlugin({
name: 'DeployCertToAliyunDCDN',
title: '部署证书至阿里云DCDN',
@@ -59,13 +59,14 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
}
async getClient(access: AliyunAccess) {
const sdk = await import('@alicloud/pop-core');
return new sdk.default({
const client = new AliyunClient({logger:this.logger})
await client.init({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://dcdn.aliyuncs.com',
apiVersion: '2018-01-15',
});
})
return client
}
async buildParams() {

View File

@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { appendTimeSuffix, checkRet } from '../../utils/index.js';
import { AliyunAccess } from '../../access/index.js';
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
@IsTaskPlugin({
name: 'uploadCertToAliyun',
@@ -86,13 +86,14 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
}
async getClient(aliyunProvider: AliyunAccess) {
const Core = await import('@alicloud/pop-core');
return new Core.default({
const client = new AliyunClient({logger:this.logger})
await client.init({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: 'https://cas.aliyuncs.com',
apiVersion: '2018-07-13',
});
})
return client
}
}
//注册插件

View File

@@ -34,6 +34,7 @@ export class DemoAccess implements IAccess {
},
//是否必填
required: true,
//改属性是否需要加密
encrypt: true,
})
//属性名称

View File

@@ -0,0 +1,39 @@
import { IsAccess, AccessInput } from '@certd/pipeline';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'dogecloud',
title: '多吉云',
desc: '',
})
export class DogeCloudAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: 'AccessKey',
component: {
placeholder: 'AccessKey',
},
helper: '请前往[多吉云-密钥管理](https://console.dogecloud.com/user/keys)获取',
required: true,
encrypt: false,
})
accessKey = '';
@AccessInput({
title: 'SecretKey',
component: {
placeholder: 'SecretKey',
},
helper: '请前往[多吉云-密钥管理](https://console.dogecloud.com/user/keys)获取',
required: true,
encrypt: true,
})
secretKey = '';
}
new DogeCloudAccess();

View File

@@ -0,0 +1,3 @@
export * from './access.js';
export * from './lib/index.js';
export * from './plugins/index.js';

View File

@@ -0,0 +1,42 @@
import crypto from 'crypto';
import querystring from 'querystring';
import { DogeCloudAccess } from '../access.js';
import { AxiosInstance } from 'axios';
export class DogeClient {
accessKey: string;
secretKey: string;
http: AxiosInstance;
constructor(access: DogeCloudAccess, http: AxiosInstance) {
this.accessKey = access.accessKey;
this.secretKey = access.secretKey;
this.http = http;
}
async request(apiPath: string, data: any = {}, jsonMode = false) {
// 这里替换为你的多吉云永久 AccessKey 和 SecretKey可在用户中心 - 密钥管理中查看
// 请勿在客户端暴露 AccessKey 和 SecretKey那样恶意用户将获得账号完全控制权
const body = jsonMode ? JSON.stringify(data) : querystring.encode(data);
const sign = crypto
.createHmac('sha1', this.secretKey)
.update(Buffer.from(apiPath + '\n' + body, 'utf8'))
.digest('hex');
const authorization = 'TOKEN ' + this.accessKey + ':' + sign;
const res: any = await this.http.request({
url: 'https://api.dogecloud.com' + apiPath,
method: 'POST',
data: body,
responseType: 'json',
headers: {
'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded',
Authorization: authorization,
},
});
if (res.code !== 200) {
throw new Error('API Error: ' + res.msg);
}
return res.data;
}
}

View File

@@ -0,0 +1,75 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { DogeClient } from '../../lib/index.js';
import dayjs from 'dayjs';
@IsTaskPlugin({
name: 'DogeCloudDeployToCDN',
title: '部署证书到多吉云CDN',
group: pluginGroups.cdn.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DogeCloudDeployToCDNPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '域名',
helper: 'CDN域名',
required: true,
})
domain!: string;
//证书选择,此项必须要有
@TaskInput({
title: '证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
from: 'CertApply',
},
required: true,
})
cert!: CertInfo;
//授权选择框
@TaskInput({
title: '多吉云授权',
helper: '多吉云AccessKey',
component: {
name: 'pi-access-selector',
type: 'dogecloud',
},
rules: [{ required: true, message: '此项必填' }],
})
accessId!: string;
dogeClient!: DogeClient;
async onInstance() {
const access = await this.accessService.getById(this.accessId);
this.dogeClient = new DogeClient(access, this.ctx.http);
}
async execute(): Promise<void> {
const certId: number = await this.updateCert();
await this.bindCert(certId);
}
async updateCert() {
const certReader = new CertReader(this.cert);
const data = await this.dogeClient.request('/cdn/cert/upload.json', {
note: 'certd-' + dayjs().format('YYYYMMDDHHmmss'),
cert: certReader.crt,
private: certReader.key,
});
return data.id;
}
async bindCert(certId: number) {
await this.dogeClient.request('/cdn/cert/bind.json', {
id: certId,
domain: this.domain,
});
}
}
new DogeCloudDeployToCDNPlugin();

View File

@@ -0,0 +1 @@
export * from './deploy-to-cdn/index.js';

View File

@@ -3,8 +3,8 @@ import ssh2, { ConnectConfig } from 'ssh2';
import path from 'path';
import * as _ from 'lodash-es';
import { ILogger } from '@certd/pipeline';
import iconv from 'iconv-lite';
import { SshAccess } from '../access/index.js';
import stripAnsi from 'strip-ansi';
export class AsyncSsh2Client {
conn: ssh2.Client;
logger: ILogger;
@@ -18,7 +18,7 @@ export class AsyncSsh2Client {
this.encoding = connConf.encoding;
}
convert(buffer: Buffer) {
convert(iconv: any, buffer: Buffer) {
if (this.encoding) {
return iconv.decode(buffer, this.encoding);
}
@@ -79,6 +79,8 @@ export class AsyncSsh2Client {
this.logger.info('script 为空,取消执行');
return;
}
let iconv: any = await import('iconv-lite');
iconv = iconv.default;
return new Promise((resolve, reject) => {
this.logger.info(`执行命令:[${this.connConf.host}][exec]: ` + script);
this.conn.exec(script, (err: Error, stream: any) => {
@@ -97,7 +99,7 @@ export class AsyncSsh2Client {
}
})
.on('data', (ret: Buffer) => {
const out = this.convert(ret);
const out = this.convert(iconv, ret);
data += out;
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
})
@@ -106,7 +108,7 @@ export class AsyncSsh2Client {
this.logger.error(err);
})
.stderr.on('data', (ret: Buffer) => {
const err = this.convert(ret);
const err = this.convert(iconv, ret);
data += err;
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
});
@@ -123,22 +125,33 @@ export class AsyncSsh2Client {
return;
}
const output: string[] = [];
function ansiHandle(data: string) {
data = data.replace(/\[[0-9]+;1H/g, '\n');
data = stripAnsi(data);
return data;
}
stream
.on('close', () => {
this.logger.info('Stream :: close');
resolve(output);
})
.on('data', (ret: Buffer) => {
const data = this.convert(ret);
this.logger.info('' + data);
const data = ansiHandle(ret.toString());
this.logger.info(data);
output.push(data);
})
.on('error', (err: any) => {
reject(err);
this.logger.error(err);
})
.stderr.on('data', (ret: Buffer) => {
const data = this.convert(ret);
const data = ansiHandle(ret.toString());
output.push(data);
this.logger.info(`[${this.connConf.host}][error]: ` + data);
});
stream.end(script + '\nexit\n');
//保证windows下正常退出
const exit = '\r\nexit\r\n';
stream.end(script + exit);
});
});
}
@@ -182,14 +195,14 @@ export class SshClient {
this.logger.info('请注意windows下文件目录分隔应该写成\\而不是/');
this.logger.info('--------------------------');
}
const spec = await conn.exec('echo %COMSPEC%');
if (spec.toString().trim() === '%COMSPEC%') {
const isCmd = await this.isCmd(conn);
if (!isCmd) {
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
} else {
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
}
}
await conn.exec(mkdirCmd);
await conn.shell(mkdirCmd);
}
await conn.fastPut({ sftp, ...transport });
@@ -199,24 +212,69 @@ export class SshClient {
});
}
async isCmd(conn: AsyncSsh2Client) {
const spec = await conn.exec('echo %COMSPEC%');
if (spec.toString().trim() === '%COMSPEC%') {
return false;
} else {
return true;
}
}
/**
*
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
* Start-Service sshd
*
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
* @param options
*/
async exec(options: { connectConf: SshAccess; script: string | Array<string> }) {
let { script } = options;
const { connectConf } = options;
if (_.isArray(script)) {
script = script as Array<string>;
script = script.join('\n');
}
this.logger.info('执行命令:', script);
this.logger.info('命令:', script);
return await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {
return await conn.exec(script as string);
let isWinCmd = false;
if (connectConf.windows) {
isWinCmd = await this.isCmd(conn);
}
if (isWinCmd) {
//组合成&&的形式
if (typeof script === 'string') {
script = script.split('\n');
}
script = script as Array<string>;
script = script.join('&& ');
} else {
if (_.isArray(script)) {
script = script as Array<string>;
script = script.join('\n');
}
}
await conn.exec(script);
},
});
}
async shell(options: { connectConf: SshAccess; script: string }): Promise<string[]> {
const { connectConf, script } = options;
//废弃
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string[]> {
let { script } = options;
const { connectConf } = options;
if (_.isArray(script)) {
script = script as Array<string>;
if (connectConf.windows) {
script = script.join('\r\n');
} else {
script = script.join('\n');
}
} else {
if (connectConf.windows) {
script = script.replaceAll('\n', '\r\n');
}
}
return await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {

View File

@@ -17,7 +17,7 @@ import path from 'path';
export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '证书保存路径',
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.pem',
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.pem',
component: {
placeholder: './tmp/cert.pem',
},
@@ -25,12 +25,32 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
crtPath!: string;
@TaskInput({
title: '私钥保存路径',
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.key',
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.key',
component: {
placeholder: './tmp/cert.key',
},
})
keyPath!: string;
@TaskInput({
title: 'PFX证书保存路径',
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.pfx',
component: {
placeholder: './tmp/cert.pfx',
},
})
pfxPath!: string;
@TaskInput({
title: 'DER证书保存路径',
helper:
'需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.der\n.der和.cer是相同的东西改个后缀名即可',
component: {
placeholder: './tmp/cert.der 或 ./tmp/cert.cer',
},
})
derPath!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
@@ -44,14 +64,28 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
@TaskOutput({
title: '证书保存路径',
type: 'HostCrtPath',
})
hostCrtPath!: string;
@TaskOutput({
title: '私钥保存路径',
type: 'HostKeyPath',
})
hostKeyPath!: string;
@TaskOutput({
title: 'PFX保存路径',
type: 'HostPfxPath',
})
hostPfxPath!: string;
@TaskOutput({
title: 'DER保存路径',
type: 'HostDerPath',
})
hostDerPath!: string;
async onInstance() {}
copyFile(srcFile: string, destFile: string) {
@@ -63,37 +97,38 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
fs.copyFileSync(srcFile, destFile);
}
async execute(): Promise<void> {
let { crtPath, keyPath } = this;
let { crtPath, keyPath, pfxPath, derPath } = this;
const certReader = new CertReader(this.cert);
this.logger.info('将证书写入本地缓存文件');
const saveCrtPath = certReader.saveToFile('crt');
const saveKeyPath = certReader.saveToFile('key');
this.logger.info('本地文件写入成功');
try {
this.logger.info('复制到目标路径');
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
// crtPath = path.resolve(crtPath);
// keyPath = path.resolve(keyPath);
this.copyFile(saveCrtPath, crtPath);
this.copyFile(saveKeyPath, keyPath);
this.logger.info('证书复制成功crtPath=', crtPath, ',keyPath=', keyPath);
const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath }) => {
this.logger.info('复制到目标路径');
if (crtPath) {
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
this.copyFile(tmpCrtPath, crtPath);
this.hostCrtPath = crtPath;
}
if (keyPath) {
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
this.copyFile(tmpKeyPath, keyPath);
this.hostKeyPath = keyPath;
}
if (pfxPath) {
pfxPath = pfxPath.startsWith('/') ? pfxPath : path.join(Constants.dataDir, pfxPath);
this.copyFile(tmpPfxPath, pfxPath);
this.hostPfxPath = pfxPath;
}
if (derPath) {
derPath = derPath.startsWith('/') ? derPath : path.join(Constants.dataDir, derPath);
this.copyFile(tmpDerPath, derPath);
this.hostDerPath = derPath;
}
this.logger.info('请注意,如果使用的是相对路径,那么文件就在你的数据库同级目录下,默认是/data/certd/下面');
this.logger.info('请注意,如果使用的是绝对路径,文件在容器内的目录下,你需要给容器做目录映射才能复制到宿主机');
} catch (e) {
this.logger.error(`复制失败:${e.message}`);
throw e;
} finally {
//删除临时文件
this.logger.info('删除临时文件');
fs.unlinkSync(saveCrtPath);
fs.unlinkSync(saveKeyPath);
}
};
await certReader.readCertFile({ logger: this.logger, handle });
this.logger.info('执行完成');
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
}
}

View File

@@ -31,6 +31,7 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
vModel: 'value',
rows: 6,
},
helper: '注意如果目标主机是windows且终端是cmd系统会自动将多行命令通过“&&”连接成一行',
required: true,
})
script!: string;
@@ -40,11 +41,13 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
const { script, accessId } = this;
const connectConf = await this.accessService.getById(accessId);
const sshClient = new SshClient(this.logger);
const ret = await sshClient.exec({
const scripts = script.split('\n');
await sshClient.exec({
connectConf,
script,
script: scripts,
});
this.logger.info('exec res:', ret);
// this.logger.info('exec res:', ret);
}
}

View File

@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { SshClient } from '../../lib/ssh.js';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { CertInfo, CertReader, CertReaderHandleContext } from '@certd/plugin-cert';
import * as fs from 'fs';
import { SshAccess } from '../../access/index.js';
@@ -17,8 +17,8 @@ import { SshAccess } from '../../access/index.js';
})
export class UploadCertToHostPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '证书保存路径',
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号',
title: 'PEM证书保存路径',
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.pem',
component: {
placeholder: '/root/deploy/nginx/cert.pem',
},
@@ -26,12 +26,31 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
crtPath!: string;
@TaskInput({
title: '私钥保存路径',
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号',
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.key',
component: {
placeholder: '/root/deploy/nginx/cert.key',
},
})
keyPath!: string;
@TaskInput({
title: 'PFX证书保存路径',
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.pfx',
component: {
placeholder: '/root/deploy/nginx/cert.pfx',
},
})
pfxPath!: string;
@TaskInput({
title: 'DER证书保存路径',
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.der',
component: {
placeholder: '/root/deploy/nginx/cert.der',
},
})
derPath!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
@@ -87,9 +106,23 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
})
hostKeyPath!: string;
@TaskOutput({
title: 'PFX保存路径',
})
hostPfxPath!: string;
@TaskOutput({
title: 'DER保存路径',
})
hostDerPath!: string;
async onInstance() {}
copyFile(srcFile: string, destFile: string) {
if (!srcFile || !destFile) {
this.logger.warn(`srcFile:${srcFile} 或 destFile:${destFile} 为空,不复制`);
return;
}
const dir = destFile.substring(0, destFile.lastIndexOf('/'));
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
@@ -99,16 +132,17 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
async execute(): Promise<void> {
const { crtPath, keyPath, cert, accessId } = this;
const certReader = new CertReader(cert);
this.logger.info('将证书写入本地缓存文件');
const saveCrtPath = certReader.saveToFile('crt');
const saveKeyPath = certReader.saveToFile('key');
this.logger.info('本地文件写入成功');
try {
const handle = async (opts: CertReaderHandleContext) => {
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath } = opts;
if (this.copyToThisHost) {
this.logger.info('复制到目标路径');
this.copyFile(saveCrtPath, crtPath);
this.copyFile(saveKeyPath, keyPath);
this.logger.info('证书复制成功crtPath=', crtPath, ',keyPath=', keyPath);
this.copyFile(tmpCrtPath, crtPath);
this.copyFile(tmpKeyPath, keyPath);
this.copyFile(tmpPfxPath, this.pfxPath);
this.copyFile(tmpDerPath, this.derPath);
this.logger.info(`证书复制成功crtPath=${crtPath},keyPath=${keyPath},pfxPath=${this.pfxPath},derPath=${this.derPath}`);
} else {
if (!accessId) {
throw new Error('主机登录授权配置不能为空');
@@ -116,35 +150,49 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
this.logger.info('准备上传文件到服务器');
const connectConf: SshAccess = await this.accessService.getById(accessId);
const sshClient = new SshClient(this.logger);
const transports: any = [];
if (crtPath) {
transports.push({
localPath: tmpCrtPath,
remotePath: crtPath,
});
}
if (keyPath) {
transports.push({
localPath: tmpKeyPath,
remotePath: keyPath,
});
}
if (this.pfxPath) {
transports.push({
localPath: tmpPfxPath,
remotePath: this.pfxPath,
});
}
if (this.derPath) {
transports.push({
localPath: tmpDerPath,
remotePath: this.derPath,
});
}
await sshClient.uploadFiles({
connectConf,
transports: [
{
localPath: saveCrtPath,
remotePath: crtPath,
},
{
localPath: saveKeyPath,
remotePath: keyPath,
},
],
transports,
mkdirs: this.mkdirs,
});
this.logger.info('证书上传成功crtPath=', crtPath, ',keyPath=', keyPath);
this.logger.info(`证书上传成功crtPath=${crtPath},keyPath=${keyPath},pfxPath=${this.pfxPath},derPath=${this.derPath}`);
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
this.hostPfxPath = this.pfxPath;
this.hostDerPath = this.derPath;
}
} catch (e) {
this.logger.error(`上传失败:${e.message}`);
throw e;
} finally {
//删除临时文件
this.logger.info('删除临时文件');
fs.unlinkSync(saveCrtPath);
fs.unlinkSync(saveKeyPath);
}
this.logger.info('执行完成');
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
};
await certReader.readCertFile({
logger: this.logger,
handle,
});
}
}

View File

@@ -37,7 +37,7 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
lang: 'cn',
error_on_empty: 'no',
},
timeout: 5000,
timeout: 10000,
};
_.merge(config, options);

View File

@@ -0,0 +1,92 @@
import { IsAccess, AccessInput } from '@certd/pipeline';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'west',
title: '西部数码授权',
desc: '',
})
export class WestAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: '权限范围',
component: {
name: 'a-select',
vModel: 'value',
options: [
{ value: 'account', label: '账户级别,对所有域名都有权限管理' },
{ value: 'domain', label: '域名级别,仅能管理单个域名' },
],
},
helper: '选择权限范围',
required: true,
})
scope = '';
/**
* 授权属性配置
*/
@AccessInput({
title: '账号',
helper: '你的登录账号',
encrypt: false,
required: false,
mergeScript: `
return {
show:ctx.compute(({form})=>{
return form.access.scope === 'account'
})
}
`,
})
username = '';
/**
* 授权属性配置
*/
@AccessInput({
title: 'ApiKey',
component: {
placeholder: '账户级别的key对整个账户都有管理权限',
},
helper: '账户级别的key对整个账户都有管理权限\n前往https://www.west.cn/manager/API/APIconfig.asp手动设置“api连接密码”',
encrypt: true,
required: false,
mergeScript: `
return {
show:ctx.compute(({form})=>{
return form.access.scope === 'account'
})
}
`,
})
apikey = '';
/**
* 授权属性配置
*/
@AccessInput({
title: 'apidomainkey',
component: {
placeholder: '域名级别的key仅对单个域名有权限',
},
helper: '域名级别的key仅对单个域名有权限。 \n前往[西部数据域名管理](https://www.west.cn/manager/domain/)点击域名右上方点击ApiKey获取密钥',
encrypt: true,
required: false,
mergeScript: `
return {
show:ctx.compute(({form})=>{
return form.access.scope === 'domain'
})
}
`,
})
apidomainkey = '';
}
new WestAccess();

View File

@@ -0,0 +1,126 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
import { WestAccess } from './access.js';
type westRecord = {
// 这里定义Record记录的数据结构跟对应云平台接口返回值一样即可一般是拿到id就行用于删除txt解析记录清理申请痕迹
code: number;
msg: string;
body: {
record_id: number;
};
};
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: 'west',
title: '西部数码',
desc: 'west dns provider',
// 这里是对应的云平台的access类型名称
accessType: 'west',
})
export class WestDnsProvider extends AbstractDnsProvider<westRecord> {
// 通过Autowire注入工具对象
@Autowire()
access!: WestAccess;
@Autowire()
logger!: ILogger;
http!: HttpClient;
async onInstance() {
// 也可以通过ctx成员变量传递context 与Autowire效果一样
this.http = this.ctx.http;
this.logger.debug('access:', this.access);
//初始化的操作
//...
}
private async doRequestApi(url: string, data: any = null, method = 'post') {
if (this.access.scope === 'account') {
data.apikey = this.access.apikey;
data.username = this.access.username;
} else {
data.apidomainkey = this.access.apidomainkey;
}
const res = await this.http.request<any, any>({
url,
method,
data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
if (res.msg !== 'success') {
throw new Error(`${JSON.stringify(res.msg)}`);
}
return res;
}
/**
* 创建dns解析记录用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<any> {
/**
* options 参数说明
* fullRecord: '_acme-challenge.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 准备要发送到API的请求体
const requestBody = {
act: 'dnsrec.add', // API动作类型
domain: domain, // 域名
record_type: 'TXT', // DNS记录类型
hostname: fullRecord, // 完整的记录名
record_value: value, // 记录的值
record_line: '', // 记录线路
record_ttl: 60, // TTL (生存时间)设置为60秒
};
const url = 'https://api.west.cn/API/v2/domain/dns/';
const res = await this.doRequestApi(url, requestBody);
const record = res as westRecord;
this.logger.info(`添加域名解析成功:fullRecord=${fullRecord},value=${value}`);
this.logger.info(`dns解析记录:${JSON.stringify(record)}`);
// 西部数码生效较慢 增加90秒等待 提高成功率
this.logger.info('等待解析生效:wait 90s');
await new Promise(resolve => setTimeout(resolve, 90000));
return record;
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<westRecord>): Promise<void> {
const { fullRecord, value, record, domain } = options;
this.logger.info('删除域名解析:', fullRecord, value, record);
if (!record) {
this.logger.info('record不存在');
return;
}
//这里调用删除txt dns解析记录接口
// 准备要发送到API的请求体
const requestBody = {
act: 'dnsrec.remove', // API动作类型
domain: domain, // 域名
record_id: record.body.record_id,
hostname: fullRecord, // 完整的记录名
record_type: 'TXT', // DNS记录类型
record_line: '', // 记录线路
};
const url = 'https://api.west.cn/API/v2/domain/dns/';
const res = await this.doRequestApi(url, requestBody);
const result = res.result;
this.logger.info('删除域名解析成功:', fullRecord, value, JSON.stringify(result));
}
}
//TODO 实例化这个provider将其自动注册到系统中
new WestDnsProvider();

View File

@@ -0,0 +1,2 @@
export * from './dns-provider.js';
export * from './access.js';

View File

@@ -11,12 +11,15 @@ git clone https://github.com/certd/certd
#进入项目目录
cd certd
# 切换到最新的版本tagv2分支可能不稳定
checkout tags/vx.x.x # x.x.x为最新的版本号
# 安装依赖
npm install -g pnpm@8.15.7
pnpm install
# 初始化构建
lerna run build
npm run init
```
启动 server:

3
test/docker/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM node:20-alpine
RUN apk add --no-cache openssl
WORKDIR /app/

View File

@@ -0,0 +1,14 @@
version: '3.3' # 指定docker-compose 版本
services: # 要拉起的服务们
certdtest:
build:
context: ./
dockerfile: Dockerfile
image: certd-test:1
container_name: certdtest # 容器名
volumes:
- ./data:/app/data
command: ["tail", "-f", "/dev/null"]
environment:
- TZ=Asia/Shanghai