mirror of
https://github.com/certd/certd.git
synced 2026-04-09 01:30:57 +08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d65d94b784 | ||
|
|
00f1e0da59 | ||
|
|
65ef685729 | ||
|
|
6e344140c6 | ||
|
|
97a01b6f6d | ||
|
|
c49ccbde93 | ||
|
|
fc73d9d615 | ||
|
|
1133d6b0f7 | ||
|
|
b80210f24b | ||
|
|
3bad0b2685 | ||
|
|
af388ec39f | ||
|
|
8d7c2c8e29 | ||
|
|
8088cd6d58 | ||
|
|
590ce9642e | ||
|
|
99302b8ff2 | ||
|
|
14b108f09e | ||
|
|
0669835d4e | ||
|
|
fbeaed2035 | ||
|
|
ecad7f58c1 | ||
|
|
1dd9a8d4d3 | ||
|
|
bd73a163cd | ||
|
|
1e9b5638aa | ||
|
|
71ac8aae4a | ||
|
|
d5bfcdb6de | ||
|
|
1480efb43d | ||
|
|
1c17b41e16 | ||
|
|
192d9dc7e3 | ||
|
|
d0d3c2b588 | ||
|
|
b8a8f20448 | ||
|
|
28a32aed7d | ||
|
|
ff46771d8d | ||
|
|
87a2673e8c | ||
|
|
c59cab1aae | ||
|
|
6314e8d7eb | ||
|
|
5ade12d700 | ||
|
|
ceb210b1b7 | ||
|
|
5e084db038 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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/
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -18,7 +18,8 @@ https://afdian.com/a/greper
|
||||
专业版特权
|
||||
1. 证书流水线条数无限制(免费版限制10条)
|
||||
2. 免配置发邮件功能
|
||||
3. 更多功能增加中...
|
||||
3. FTP上传、cdnfly、宝塔等部署插件
|
||||
4. 更多功能增加中...
|
||||
************************
|
||||
|
||||
## 一、特性
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
22:35
|
||||
|
||||
@@ -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
|
||||
@@ -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
20
init.sh
Normal 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配置完成"
|
||||
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.24.1"
|
||||
"version": "1.24.3"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ exports.directory = {
|
||||
*/
|
||||
|
||||
exports.crypto = require('./crypto');
|
||||
exports.forge = require('./crypto/forge');
|
||||
// exports.forge = require('./crypto/forge');
|
||||
|
||||
/**
|
||||
* Axios
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
23:56
|
||||
23:19
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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("此为专业版功能,请升级到专业版");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -118,7 +118,8 @@ export type HistoryResultGroup = {
|
||||
};
|
||||
};
|
||||
export type HistoryResult = {
|
||||
input: any;
|
||||
// input: any;
|
||||
inputHash?: string;
|
||||
output: any;
|
||||
files?: FileItem[];
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ export enum ContextScope {
|
||||
export type TaskOutputDefine = {
|
||||
title: string;
|
||||
value?: any;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type TaskInputDefine = FormItemProps;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
9
packages/core/pipeline/src/utils/util.hash.ts
Normal file
9
packages/core/pipeline/src/utils/util.hash.ts
Normal 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,
|
||||
};
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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},测试访问成功`);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {}
|
||||
};
|
||||
|
||||
@@ -70,6 +70,7 @@ const onError = (error: any) => {
|
||||
}
|
||||
.vcron-select-input {
|
||||
min-height: 22px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.vcron-select-container {
|
||||
display: flex;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
40
packages/ui/certd-client/src/use/use-refrence.tsx
Normal file
40
packages/ui/certd-client/src/use/use-refrence.tsx
Normal 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>;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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-ACME:基于Lego实现,支持海量DNS提供商</li>
|
||||
<li>JS-ACME:如果你的域名DNS属于阿里云、腾讯云、Cloudflare可以选择用它来申请</li>
|
||||
<li>JS-ACME:如果你的域名DNS属于阿里云、腾讯云、Cloudflare、西部数码可以选择用它来申请</li>
|
||||
<li>Lego-ACME:基于Lego实现,支持海量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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '此项必填' }]
|
||||
}"
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
@@ -1 +0,0 @@
|
||||
export * from './aliyun-access.js';
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './dns-provider/index.js';
|
||||
export * from './plugin/index.js';
|
||||
export * from './access/index.js';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
//注册插件
|
||||
|
||||
@@ -34,6 +34,7 @@ export class DemoAccess implements IAccess {
|
||||
},
|
||||
//是否必填
|
||||
required: true,
|
||||
//改属性是否需要加密
|
||||
encrypt: true,
|
||||
})
|
||||
//属性名称
|
||||
|
||||
39
packages/ui/certd-server/src/plugins/plugin-doge/access.ts
Normal file
39
packages/ui/certd-server/src/plugins/plugin-doge/access.ts
Normal 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();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './access.js';
|
||||
export * from './lib/index.js';
|
||||
export * from './plugins/index.js';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -0,0 +1 @@
|
||||
export * from './deploy-to-cdn/index.js';
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
|
||||
lang: 'cn',
|
||||
error_on_empty: 'no',
|
||||
},
|
||||
timeout: 5000,
|
||||
timeout: 10000,
|
||||
};
|
||||
_.merge(config, options);
|
||||
|
||||
|
||||
92
packages/ui/certd-server/src/plugins/plugin-west/access.ts
Normal file
92
packages/ui/certd-server/src/plugins/plugin-west/access.ts
Normal 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();
|
||||
126
packages/ui/certd-server/src/plugins/plugin-west/dns-provider.ts
Normal file
126
packages/ui/certd-server/src/plugins/plugin-west/dns-provider.ts
Normal 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();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './dns-provider.js';
|
||||
export * from './access.js';
|
||||
@@ -11,12 +11,15 @@ git clone https://github.com/certd/certd
|
||||
#进入项目目录
|
||||
cd certd
|
||||
|
||||
# 切换到最新的版本tag,v2分支可能不稳定
|
||||
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
3
test/docker/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache openssl
|
||||
WORKDIR /app/
|
||||
14
test/docker/docker-compose.yaml
Normal file
14
test/docker/docker-compose.yaml
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user