mirror of
https://github.com/certd/certd.git
synced 2026-04-14 20:40:53 +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
|
/*.log
|
||||||
|
|
||||||
/packages/ui/*/.idea
|
/packages/ui/*/.idea
|
||||||
|
|
||||||
/packages/ui/*/node_modules
|
/packages/ui/*/node_modules
|
||||||
|
|
||||||
/packages/*/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
|
/pnpm-lock.yaml
|
||||||
|
|
||||||
docker/image/workspace
|
|
||||||
/packages/core/lego
|
|
||||||
|
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
test/**/*.js
|
test/**/*.js
|
||||||
/packages/ui/certd-server/data/db.sqlite
|
/packages/ui/certd-server/data/db.sqlite
|
||||||
/packages/ui/certd-server/data/keys.yaml
|
/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.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ https://afdian.com/a/greper
|
|||||||
专业版特权
|
专业版特权
|
||||||
1. 证书流水线条数无限制(免费版限制10条)
|
1. 证书流水线条数无限制(免费版限制10条)
|
||||||
2. 免配置发邮件功能
|
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'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
certd:
|
certd:
|
||||||
# 镜像 # ↓↓↓↓↓ --- 1、 镜像版本号,建议改成固定版本号【可选】
|
# 镜像 # ↓↓↓↓↓ --- 镜像版本号,建议改成固定版本号【可选】
|
||||||
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
|
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
|
||||||
container_name: certd # 容器名
|
container_name: certd # 容器名
|
||||||
restart: unless-stopped # 自动重启
|
restart: unless-stopped # 自动重启
|
||||||
volumes:
|
volumes:
|
||||||
# ↓↓↓↓↓ ------------------------------------------------------- 2、 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下【可选】
|
# ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下【可选】
|
||||||
- /data/certd:/app/data
|
- /data/certd:/app/data
|
||||||
ports: # 端口映射
|
ports: # 端口映射
|
||||||
# ↓↓↓↓ ----------------------------------------------------------3、如果端口有冲突,可以修改第一个7001为其他不冲突的端口号【可选】
|
# ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突,可以修改第一个7001为其他不冲突的端口号【可选】
|
||||||
- "7001:7001"
|
- "7001:7001"
|
||||||
dns:
|
dns:
|
||||||
# 如果出现getaddrinfo ENOTFOUND等错误,可以尝试修改或注释dns配置
|
# 如果出现getaddrinfo ENOTFOUND等错误,可以尝试修改或注释dns配置
|
||||||
- 223.5.5.5
|
- 223.5.5.5
|
||||||
- 223.6.6.6
|
- 223.6.6.6
|
||||||
# ↓↓↓↓ ----------------------------------------------------------如果你服务器部署在国外,可以用8.8.8.8替换上面的dns【可选】
|
# ↓↓↓↓ ---------------------------------------------------------- 如果你服务器部署在国外,可以用8.8.8.8替换上面的dns【可选】
|
||||||
# - 8.8.8.8
|
# - 8.8.8.8
|
||||||
# - 8.8.4.4
|
# - 8.8.4.4
|
||||||
environment: # 环境变量
|
environment: # 环境变量
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
#- HTTPS_PROXY=http://xxxxxx:xx
|
||||||
|
#- HTTP_PROXY=http://xxxxxx:xx
|
||||||
|
# ↑↑↑↑↑ ------------------------------------- 这里可以设置http代理【可选】
|
||||||
- certd_system_resetAdminPasswd=false
|
- certd_system_resetAdminPasswd=false
|
||||||
# ↑↑↑↑↑---------------------------4、如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false【可选】
|
# ↑↑↑↑↑--------------------------- 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false【可选】
|
||||||
- certd_cron_immediateTriggerOnce=false
|
- certd_cron_immediateTriggerOnce=false
|
||||||
# ↑↑↑↑↑---------------------------5、如果设置为true,启动后所有配置了cron的流水线任务都将被立即触发一次【可选】
|
# ↑↑↑↑↑--------------------------- 如果设置为true,启动后所有配置了cron的流水线任务都将被立即触发一次【可选】
|
||||||
- VITE_APP_ICP_NO=
|
- 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配置
|
# 设置环境变量即可自定义certd配置
|
||||||
# 服务端配置项见: packages/ui/certd-server/src/config/config.default.ts
|
# 服务端配置项见: packages/ui/certd-server/src/config/config.default.ts
|
||||||
# 服务端配置规则: certd_ + 配置项, 点号用_代替
|
# 服务端配置规则: 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",
|
"npmClient": "pnpm",
|
||||||
"version": "1.24.1"
|
"version": "1.24.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"lodash": "^4.17.21"
|
"lodash-es": "^4.17.21"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/**"
|
"packages/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Submodule packages/certd-pro deleted from 0dec0d461f
@@ -3,6 +3,16 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [1.24.1](https://github.com/publishlab/node-acme-client/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"description": "Simple and unopinionated ACME client",
|
"description": "Simple and unopinionated ACME client",
|
||||||
"private": false,
|
"private": false,
|
||||||
"author": "nmorsman",
|
"author": "nmorsman",
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"types": "types/index.d.ts",
|
"types": "types/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -59,5 +59,5 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||||
},
|
},
|
||||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ exports.directory = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
exports.crypto = require('./crypto');
|
exports.crypto = require('./crypto');
|
||||||
exports.forge = require('./crypto/forge');
|
// exports.forge = require('./crypto/forge');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Axios
|
* Axios
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
|||||||
log(`DNS query finished successfully, found ${recordValues.length} TXT records`);
|
log(`DNS query finished successfully, found ${recordValues.length} TXT records`);
|
||||||
|
|
||||||
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
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`);
|
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.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||||
|
|
||||||
### Performance Improvements
|
### Performance Improvements
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
23:56
|
23:19
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/pipeline",
|
"name": "@certd/pipeline",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -57,5 +57,5 @@
|
|||||||
"vite": "^4.3.8",
|
"vite": "^4.3.8",
|
||||||
"vue-tsc": "^1.6.5"
|
"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 { Decorator } from "../decorator/index.js";
|
||||||
import { IEmailService } from "../service/index.js";
|
import { IEmailService } from "../service/index.js";
|
||||||
import { FileStore } from "./file-store.js";
|
import { FileStore } from "./file-store.js";
|
||||||
|
import { hashUtils } from "../utils/index.js";
|
||||||
// import { TimeoutPromise } from "../utils/util.promise.js";
|
// import { TimeoutPromise } from "../utils/util.promise.js";
|
||||||
|
|
||||||
export type ExecutorOptions = {
|
export type ExecutorOptions = {
|
||||||
@@ -69,6 +70,7 @@ export class Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(runtimeId: any = 0, triggerType: string) {
|
async run(runtimeId: any = 0, triggerType: string) {
|
||||||
|
let intervalFlushLogId: any = undefined;
|
||||||
try {
|
try {
|
||||||
await this.init();
|
await this.init();
|
||||||
const trigger = { type: triggerType };
|
const trigger = { type: triggerType };
|
||||||
@@ -76,8 +78,14 @@ export class Executor {
|
|||||||
this.runtime = new RunHistory(runtimeId, trigger, this.pipeline);
|
this.runtime = new RunHistory(runtimeId, trigger, this.pipeline);
|
||||||
this.logger.info(`pipeline.${this.pipeline.id} start`);
|
this.logger.info(`pipeline.${this.pipeline.id} start`);
|
||||||
await this.notification("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.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) {
|
if (this.lastRuntime && this.lastRuntime.pipeline.status?.status === ResultType.error) {
|
||||||
await this.notification("turnToSuccess");
|
await this.notification("turnToSuccess");
|
||||||
@@ -87,53 +95,33 @@ export class Executor {
|
|||||||
await this.notification("error", e);
|
await this.notification("error", e);
|
||||||
this.logger.error("pipeline 执行失败", e.stack);
|
this.logger.error("pipeline 执行失败", e.stack);
|
||||||
} finally {
|
} finally {
|
||||||
|
clearInterval(intervalFlushLogId);
|
||||||
|
await this.onChanged(this.runtime);
|
||||||
await this.pipelineContext.setObj("lastRuntime", this.runtime);
|
await this.pipelineContext.setObj("lastRuntime", this.runtime);
|
||||||
this.logger.info(`pipeline.${this.pipeline.id} end`);
|
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;
|
runnable.runnableType = runnableType;
|
||||||
this.runtime.start(runnable);
|
this.runtime.start(runnable);
|
||||||
|
// const timeout = runnable.timeout ?? 20 * 60 * 1000;
|
||||||
await this.onChanged(this.runtime);
|
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 {
|
try {
|
||||||
if (this.abort.signal.aborted) {
|
if (this.abort.signal.aborted) {
|
||||||
this.runtime.cancel(runnable);
|
this.runtime.cancel(runnable);
|
||||||
return ResultType.canceled;
|
return ResultType.canceled;
|
||||||
}
|
}
|
||||||
await run();
|
const resultType = await run();
|
||||||
if (this.abort.signal.aborted) {
|
if (this.abort.signal.aborted) {
|
||||||
this.runtime.cancel(runnable);
|
this.runtime.cancel(runnable);
|
||||||
return ResultType.canceled;
|
return ResultType.canceled;
|
||||||
}
|
}
|
||||||
|
if (resultType == ResultType.skip) {
|
||||||
|
this.runtime.skip(runnable);
|
||||||
|
return resultType;
|
||||||
|
}
|
||||||
this.runtime.success(runnable);
|
this.runtime.success(runnable);
|
||||||
return ResultType.success;
|
return ResultType.success;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -141,7 +129,6 @@ export class Executor {
|
|||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
this.runtime.finally(runnable);
|
this.runtime.finally(runnable);
|
||||||
clearInterval(intervalFlushLogId);
|
|
||||||
await this.onChanged(this.runtime);
|
await this.onChanged(this.runtime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +137,7 @@ export class Executor {
|
|||||||
const resList: ResultType[] = [];
|
const resList: ResultType[] = [];
|
||||||
for (const stage of pipeline.stages) {
|
for (const stage of pipeline.stages) {
|
||||||
const res: ResultType = await this.runWithHistory(stage, "stage", async () => {
|
const res: ResultType = await this.runWithHistory(stage, "stage", async () => {
|
||||||
await this.runStage(stage);
|
return await this.runStage(stage);
|
||||||
});
|
});
|
||||||
resList.push(res);
|
resList.push(res);
|
||||||
}
|
}
|
||||||
@@ -163,6 +150,7 @@ export class Executor {
|
|||||||
const runner = async () => {
|
const runner = async () => {
|
||||||
return this.runWithHistory(task, "task", async () => {
|
return this.runWithHistory(task, "task", async () => {
|
||||||
await this.runTask(task);
|
await this.runTask(task);
|
||||||
|
return ResultType.success;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
runnerList.push(runner);
|
runnerList.push(runner);
|
||||||
@@ -205,7 +193,7 @@ export class Executor {
|
|||||||
for (const step of task.steps) {
|
for (const step of task.steps) {
|
||||||
step.runnableType = "step";
|
step.runnableType = "step";
|
||||||
const res: ResultType = await this.runWithHistory(step, "step", async () => {
|
const res: ResultType = await this.runWithHistory(step, "step", async () => {
|
||||||
await this.runStep(step);
|
return await this.runStep(step);
|
||||||
});
|
});
|
||||||
resList.push(res);
|
resList.push(res);
|
||||||
}
|
}
|
||||||
@@ -224,19 +212,40 @@ export class Executor {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const define: PluginDefine = plugin.define;
|
const define: PluginDefine = plugin.define;
|
||||||
//从outputContext读取输入参数
|
//从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") {
|
if (item.component?.name === "pi-output-selector") {
|
||||||
const contextKey = step.input[key];
|
const contextKey = input[key];
|
||||||
if (contextKey != null) {
|
if (contextKey != null) {
|
||||||
// "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert",
|
// "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert",
|
||||||
const arr = contextKey.split(".");
|
const arr = contextKey.split(".");
|
||||||
const id = arr[1];
|
const id = arr[1];
|
||||||
const outputKey = arr[2];
|
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 http = createAxiosService({ logger: currentLogger });
|
||||||
const taskCtx: TaskInstanceContext = {
|
const taskCtx: TaskInstanceContext = {
|
||||||
pipeline: this.pipeline,
|
pipeline: this.pipeline,
|
||||||
@@ -271,7 +280,6 @@ export class Executor {
|
|||||||
// this.runtime.context[stepOutputKey] = instance[key];
|
// this.runtime.context[stepOutputKey] = instance[key];
|
||||||
});
|
});
|
||||||
step.status!.files = instance.getFiles();
|
step.status!.files = instance.getFiles();
|
||||||
|
|
||||||
//更新pipeline vars
|
//更新pipeline vars
|
||||||
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
||||||
// 判断 pipelineVars 有值时更新
|
// 判断 pipelineVars 有值时更新
|
||||||
@@ -288,17 +296,17 @@ export class Executor {
|
|||||||
let subject = "";
|
let subject = "";
|
||||||
let content = "";
|
let content = "";
|
||||||
if (when === "start") {
|
if (when === "start") {
|
||||||
subject = `【CertD】开始执行,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
subject = `【CertD】开始执行,【${this.pipeline.id}】${this.pipeline.title}`;
|
||||||
content = subject;
|
content = `buildId:${this.runtime.id}`;
|
||||||
} else if (when === "success") {
|
} else if (when === "success") {
|
||||||
subject = `【CertD】执行成功,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
subject = `【CertD】执行成功,【${this.pipeline.id}】${this.pipeline.title}`;
|
||||||
content = subject;
|
content = `buildId:${this.runtime.id}`;
|
||||||
} else if (when === "turnToSuccess") {
|
} else if (when === "turnToSuccess") {
|
||||||
subject = `【CertD】执行成功(错误转成功),${this.pipeline.title}, buildId:${this.runtime.id}`;
|
subject = `【CertD】执行成功(错误转成功),【${this.pipeline.id}】${this.pipeline.title}`;
|
||||||
content = subject;
|
content = `buildId:${this.runtime.id}`;
|
||||||
} else if (when === "error") {
|
} else if (when === "error") {
|
||||||
subject = `【CertD】执行失败,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
subject = `【CertD】执行失败,【${this.pipeline.id}】${this.pipeline.title}`;
|
||||||
content = `<pre>${error.message}</pre>`;
|
content = `buildId:${this.runtime.id}\nerror:${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { logger } from "../utils/index.js";
|
import { logger } from "../utils/index.js";
|
||||||
import { setLogger } from "@certd/plus";
|
import { setLogger, isPlus } from "@certd/plus";
|
||||||
setLogger(logger);
|
setLogger(logger);
|
||||||
export * from "@certd/plus";
|
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._loggers[runnable.id] = buildLogger((text) => {
|
||||||
this.logs[runnable.id].push(text);
|
this.logs[runnable.id].push(text);
|
||||||
});
|
});
|
||||||
const input = (runnable as Step).input;
|
|
||||||
const status: HistoryResult = {
|
const status: HistoryResult = {
|
||||||
output: {},
|
output: {},
|
||||||
input: _.cloneDeep(input),
|
|
||||||
status: ResultType.start,
|
status: ResultType.start,
|
||||||
startTime: now,
|
startTime: now,
|
||||||
result: ResultType.start,
|
result: ResultType.start,
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ export type HistoryResultGroup = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type HistoryResult = {
|
export type HistoryResult = {
|
||||||
input: any;
|
// input: any;
|
||||||
|
inputHash?: string;
|
||||||
output: any;
|
output: any;
|
||||||
files?: FileItem[];
|
files?: FileItem[];
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export enum ContextScope {
|
|||||||
export type TaskOutputDefine = {
|
export type TaskOutputDefine = {
|
||||||
title: string;
|
title: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TaskInputDefine = FormItemProps;
|
export type TaskInputDefine = FormItemProps;
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ export const pluginGroups = {
|
|||||||
huawei: new PluginGroup("huawei", "华为云", 3),
|
huawei: new PluginGroup("huawei", "华为云", 3),
|
||||||
tencent: new PluginGroup("tencent", "腾讯云", 4),
|
tencent: new PluginGroup("tencent", "腾讯云", 4),
|
||||||
host: new PluginGroup("host", "主机", 5),
|
host: new PluginGroup("host", "主机", 5),
|
||||||
|
cdn: new PluginGroup("cdn", "CDN", 6),
|
||||||
other: new PluginGroup("other", "其他", 7),
|
other: new PluginGroup("other", "其他", 7),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from "./util.log.js";
|
|||||||
export * from "./util.file.js";
|
export * from "./util.file.js";
|
||||||
export * from "./util.sp.js";
|
export * from "./util.sp.js";
|
||||||
export * as promises from "./util.promise.js";
|
export * as promises from "./util.promise.js";
|
||||||
|
export * from "./util.hash.js";
|
||||||
export const utils = {
|
export const utils = {
|
||||||
sleep,
|
sleep,
|
||||||
http: request,
|
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[];
|
cmd: string | string[];
|
||||||
onStdout?: (data: string) => void;
|
onStdout?: (data: string) => void;
|
||||||
onStderr?: (data: string) => void;
|
onStderr?: (data: string) => void;
|
||||||
env: any;
|
env?: any;
|
||||||
logger?: ILogger;
|
logger?: ILogger;
|
||||||
options?: any;
|
options?: any;
|
||||||
};
|
};
|
||||||
@@ -66,6 +66,8 @@ async function spawn(opts: SpawnOption): Promise<string> {
|
|||||||
cmd = item;
|
cmd = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
cmd = opts.cmd;
|
||||||
}
|
}
|
||||||
log.info(`执行命令: ${cmd}`);
|
log.info(`执行命令: ${cmd}`);
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @certd/lib-huawei
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/lib-huawei",
|
"name": "@certd/lib-huawei",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"main": "./dist/bundle.js",
|
"main": "./dist/bundle.js",
|
||||||
"module": "./dist/bundle.js",
|
"module": "./dist/bundle.js",
|
||||||
"types": "./dist/d/index.d.ts",
|
"types": "./dist/d/index.d.ts",
|
||||||
@@ -16,5 +16,5 @@
|
|||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"rollup": "^3.7.4"
|
"rollup": "^3.7.4"
|
||||||
},
|
},
|
||||||
"gitHead": "47fe3d5826661f678d081ab53e67c847a3239d88"
|
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @certd/lib-k8s
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/lib-k8s",
|
"name": "@certd/lib-k8s",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"@kubernetes/client-node": "0.21.0"
|
"@kubernetes/client-node": "0.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@certd/pipeline": "^1.24.1",
|
"@certd/pipeline": "^1.24.3",
|
||||||
"@rollup/plugin-commonjs": "^23.0.4",
|
"@rollup/plugin-commonjs": "^23.0.4",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
@@ -37,5 +37,5 @@
|
|||||||
"tslib": "^2.5.2",
|
"tslib": "^2.5.2",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,19 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||||
|
|
||||||
### Performance Improvements
|
### Performance Improvements
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/plugin-cert",
|
"name": "@certd/plugin-cert",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/acme-client": "^1.24.1",
|
"@certd/acme-client": "^1.24.3",
|
||||||
"@certd/pipeline": "^1.24.1",
|
"@certd/pipeline": "^1.24.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"node-forge": "^0.10.0",
|
"node-forge": "^0.10.0",
|
||||||
"psl": "^1.9.0"
|
"psl": "^1.9.0"
|
||||||
@@ -53,5 +53,5 @@
|
|||||||
"vite": "^3.1.0",
|
"vite": "^3.1.0",
|
||||||
"vue-tsc": "^0.38.9"
|
"vue-tsc": "^0.38.9"
|
||||||
},
|
},
|
||||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type CertInfo = {
|
|||||||
crt: string;
|
crt: string;
|
||||||
key: string;
|
key: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
|
pfx?: string;
|
||||||
|
der?: string;
|
||||||
};
|
};
|
||||||
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
|
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
|
||||||
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
|
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) {
|
if (conf.key == null) {
|
||||||
conf.key = await this.createNewKey();
|
conf.key = await this.createNewKey();
|
||||||
await this.saveAccountConfig(email, conf);
|
await this.saveAccountConfig(email, conf);
|
||||||
|
this.logger.info(`创建新的Accountkey:${email}`);
|
||||||
}
|
}
|
||||||
let directoryUrl = "";
|
let directoryUrl = "";
|
||||||
if (isTest) {
|
if (isTest) {
|
||||||
@@ -124,7 +127,7 @@ export class AcmeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNewKey() {
|
async createNewKey() {
|
||||||
const key = await acme.forge.createPrivateKey();
|
const key = await acme.crypto.createPrivateKey(2048);
|
||||||
return key.toString();
|
return key.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +239,10 @@ export class AcmeService {
|
|||||||
const privateKeyType = options.privateKeyType || "rsa_2048";
|
const privateKeyType = options.privateKeyType || "rsa_2048";
|
||||||
const privateKeyArr = privateKeyType.split("_");
|
const privateKeyArr = privateKeyType.split("_");
|
||||||
const type = privateKeyArr[0];
|
const type = privateKeyArr[0];
|
||||||
const size = parseInt(privateKeyArr[1]);
|
let size = 2048;
|
||||||
|
if (privateKeyArr.length > 1) {
|
||||||
|
size = parseInt(privateKeyArr[1]);
|
||||||
|
}
|
||||||
if (type == "ec") {
|
if (type == "ec") {
|
||||||
const name: any = "P-" + size;
|
const name: any = "P-" + size;
|
||||||
privateKey = await acme.crypto.createPrivateEcdsaKey(name);
|
privateKey = await acme.crypto.createPrivateEcdsaKey(name);
|
||||||
@@ -308,10 +314,10 @@ export class AcmeService {
|
|||||||
await utils.http({
|
await utils.http({
|
||||||
url: directoryUrl,
|
url: directoryUrl,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
timeout: 5000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(`${directoryUrl},测试访问失败`, e);
|
this.logger.error(`${directoryUrl},测试访问失败`, e.stack);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.logger.info(`${directoryUrl},测试访问成功`);
|
this.logger.info(`${directoryUrl},测试访问成功`);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import dayjs from "dayjs";
|
|||||||
import type { CertInfo } from "./acme.js";
|
import type { CertInfo } from "./acme.js";
|
||||||
import { CertReader } from "./cert-reader.js";
|
import { CertReader } from "./cert-reader.js";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
|
import { CertConverter } from "./convert.js";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
export { CertReader };
|
export { CertReader };
|
||||||
export type { CertInfo };
|
export type { CertInfo };
|
||||||
@@ -15,6 +17,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
|||||||
vModel: "value",
|
vModel: "value",
|
||||||
mode: "tags",
|
mode: "tags",
|
||||||
open: false,
|
open: false,
|
||||||
|
tokenSeparators: [",", " ", ",", "、", "|"],
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
col: {
|
col: {
|
||||||
@@ -25,7 +28,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
|||||||
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
|
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
|
||||||
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
||||||
"3、多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com)\n" +
|
"3、多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com)\n" +
|
||||||
"4、输入一个回车之后,再输入下一个",
|
"4、输入一个空格之后,再输入下一个",
|
||||||
})
|
})
|
||||||
domains!: string[];
|
domains!: string[];
|
||||||
|
|
||||||
@@ -41,6 +44,18 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
|||||||
})
|
})
|
||||||
email!: string;
|
email!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "PFX密码",
|
||||||
|
component: {
|
||||||
|
name: "a-input-password",
|
||||||
|
vModel: "value",
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
order: 100,
|
||||||
|
helper: "PFX格式证书是否需要加密",
|
||||||
|
})
|
||||||
|
pfxPassword!: string;
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: "更新天数",
|
title: "更新天数",
|
||||||
value: 20,
|
value: 20,
|
||||||
@@ -77,13 +92,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
|||||||
})
|
})
|
||||||
successNotify = true;
|
successNotify = true;
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "配置说明",
|
|
||||||
order: 9999,
|
|
||||||
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
|
|
||||||
})
|
|
||||||
intro!: string;
|
|
||||||
|
|
||||||
// @TaskInput({
|
// @TaskInput({
|
||||||
// title: "CsrInfo",
|
// title: "CsrInfo",
|
||||||
// helper: "暂时没有用",
|
// helper: "暂时没有用",
|
||||||
@@ -135,22 +143,44 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
|
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) {
|
if (isNew) {
|
||||||
const applyTime = dayjs(certReader.detail.notBefore).format("YYYYMMDD_HHmmss");
|
const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore);
|
||||||
await this.zipCert(cert, applyTime);
|
await this.zipCert(cert, zipFileName);
|
||||||
} else {
|
} else {
|
||||||
this.extendsFiles();
|
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();
|
const zip = new JSZip();
|
||||||
zip.file("cert.crt", cert.crt);
|
zip.file("cert.crt", cert.crt);
|
||||||
zip.file("cert.key", cert.key);
|
zip.file("cert.key", cert.key);
|
||||||
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
|
if (cert.pfx) {
|
||||||
const filename = `cert_${domain_name}_${applyTime}.zip`;
|
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" });
|
const content = await zip.generateAsync({ type: "nodebuffer" });
|
||||||
this.saveFile(filename, content);
|
this.saveFile(filename, content);
|
||||||
this.logger.info(`已保存文件:${filename}`);
|
this.logger.info(`已保存文件:${filename}`);
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import fs from "fs";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { crypto } from "@certd/acme-client";
|
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;
|
crt: string;
|
||||||
key: string;
|
key: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
@@ -11,30 +18,31 @@ export class CertReader implements CertInfo {
|
|||||||
detail: any;
|
detail: any;
|
||||||
expires: number;
|
expires: number;
|
||||||
constructor(certInfo: CertInfo) {
|
constructor(certInfo: CertInfo) {
|
||||||
|
this.cert = certInfo;
|
||||||
this.crt = certInfo.crt;
|
this.crt = certInfo.crt;
|
||||||
this.key = certInfo.key;
|
this.key = certInfo.key;
|
||||||
this.csr = certInfo.csr;
|
this.csr = certInfo.csr;
|
||||||
|
|
||||||
const { detail, expires } = this.getCrtDetail(this.crt);
|
const { detail, expires } = this.getCrtDetail(this.cert.crt);
|
||||||
this.detail = detail;
|
this.detail = detail;
|
||||||
this.expires = expires.getTime();
|
this.expires = expires.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
toCertInfo(): CertInfo {
|
toCertInfo(): CertInfo {
|
||||||
return {
|
return this.cert;
|
||||||
crt: this.crt,
|
|
||||||
key: this.key,
|
|
||||||
csr: this.csr,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrtDetail(crt: string) {
|
getCrtDetail(crt: string = this.cert.crt) {
|
||||||
const detail = crypto.readCertificateInfo(crt.toString());
|
const detail = crypto.readCertificateInfo(crt.toString());
|
||||||
const expires = detail.notAfter;
|
const expires = detail.notAfter;
|
||||||
return { detail, expires };
|
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) {
|
if (filepath == null) {
|
||||||
//写入临时目录
|
//写入临时目录
|
||||||
filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.${type}`);
|
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)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
if (type === "crt" || type === "key") {
|
||||||
fs.writeFileSync(filepath, this[type]);
|
fs.writeFileSync(filepath, this.cert[type]);
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
|
||||||
|
}
|
||||||
return filepath;
|
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 { CertReader } from "./cert-reader.js";
|
||||||
import { CertApplyBasePlugin } from "./base.js";
|
import { CertApplyBasePlugin } from "./base.js";
|
||||||
|
|
||||||
export { CertReader };
|
|
||||||
export type { CertInfo };
|
export type { CertInfo };
|
||||||
|
export * from "./cert-reader.js";
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: "CertApply",
|
name: "CertApply",
|
||||||
@@ -37,11 +37,31 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
{ value: "zerossl", label: "ZeroSSL" },
|
{ 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,
|
required: true,
|
||||||
})
|
})
|
||||||
sslProvider!: SSLProvider;
|
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({
|
@TaskInput({
|
||||||
title: "加密算法",
|
title: "加密算法",
|
||||||
value: "rsa_2048",
|
value: "rsa_2048",
|
||||||
@@ -62,18 +82,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
})
|
})
|
||||||
privateKeyType!: PrivateKeyType;
|
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({
|
@TaskInput({
|
||||||
title: "DNS提供商",
|
title: "DNS提供商",
|
||||||
component: {
|
component: {
|
||||||
@@ -81,7 +89,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
helper:
|
helper:
|
||||||
"请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您的dns解析提供商,您可以将域名解析服务器设置成上面的任意一个提供商",
|
"请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商,您需要将域名解析服务器设置成上面的任意一个提供商",
|
||||||
})
|
})
|
||||||
dnsProviderType!: string;
|
dnsProviderType!: string;
|
||||||
|
|
||||||
@@ -92,13 +100,14 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
helper: "请选择dns解析提供商授权",
|
helper: "请选择dns解析提供商授权",
|
||||||
reference: [
|
mergeScript: `return {
|
||||||
{
|
component:{
|
||||||
src: "form.dnsProviderType",
|
type: ctx.compute(({form})=>{
|
||||||
dest: "component.type",
|
return form.dnsProviderType
|
||||||
type: "computed",
|
})
|
||||||
},
|
}
|
||||||
],
|
}
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
dnsProviderAccess!: string;
|
dnsProviderAccess!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
|
|||||||
|
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
RUN apk add --no-cache openssl
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
COPY --from=builder /workspace/certd-server/ /app/
|
COPY --from=builder /workspace/certd-server/ /app/
|
||||||
RUN chmod +x /app/tools/linux/*
|
RUN chmod +x /app/tools/linux/*
|
||||||
|
|||||||
@@ -3,6 +3,23 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/ui-client",
|
"name": "@certd/ui-client",
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --open",
|
"dev": "vite --open",
|
||||||
@@ -55,10 +55,10 @@
|
|||||||
"vue-cropperjs": "^5.0.0",
|
"vue-cropperjs": "^5.0.0",
|
||||||
"vue-i18n": "^9.10.2",
|
"vue-i18n": "^9.10.2",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@certd/pipeline": "^1.24.1",
|
"@certd/pipeline": "^1.24.3",
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@types/chai": "^4.3.12",
|
"@types/chai": "^4.3.12",
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ function createRequestFunction(service: any) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
||||||
},
|
},
|
||||||
timeout: 5000,
|
timeout: 10000,
|
||||||
baseURL: env.API,
|
baseURL: env.API,
|
||||||
data: {}
|
data: {}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const onError = (error: any) => {
|
|||||||
}
|
}
|
||||||
.vcron-select-input {
|
.vcron-select-input {
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
.vcron-select-container {
|
.vcron-select-container {
|
||||||
display: flex;
|
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 PiDnsProviderSelector from "./dns-provider-selector/index.vue";
|
||||||
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
|
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
|
||||||
import PiEditable from "./editable.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 { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
|
||||||
import CronEditor from "./cron-editor/index.vue";
|
import CronEditor from "./cron-editor/index.vue";
|
||||||
import { CronLight } from "@vue-js-cron/light";
|
import { CronLight } from "@vue-js-cron/light";
|
||||||
@@ -15,12 +15,14 @@ export default {
|
|||||||
app.component("PiEditable", PiEditable);
|
app.component("PiEditable", PiEditable);
|
||||||
app.component("PiOutputSelector", PiOutputSelector);
|
app.component("PiOutputSelector", PiOutputSelector);
|
||||||
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
|
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
|
||||||
app.component("VipButton", VipButton);
|
|
||||||
app.component("CronLight", CronLight);
|
app.component("CronLight", CronLight);
|
||||||
app.component("CronEditor", CronEditor);
|
app.component("CronEditor", CronEditor);
|
||||||
|
|
||||||
app.component("CheckCircleOutlined", CheckCircleOutlined);
|
app.component("CheckCircleOutlined", CheckCircleOutlined);
|
||||||
app.component("InfoCircleOutlined", InfoCircleOutlined);
|
app.component("InfoCircleOutlined", InfoCircleOutlined);
|
||||||
app.component("UndoOutlined", UndoOutlined);
|
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>
|
<div>
|
||||||
<h3 class="block-header">专业版特权</h3>
|
<h3 class="block-header">专业版特权</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>证书流水线数量无限制</li>
|
|
||||||
<li>可加VIP群,需求优先实现</li>
|
<li>可加VIP群,需求优先实现</li>
|
||||||
|
<li>证书流水线数量无限制(免费版限制10条)</li>
|
||||||
|
<li>免配置发邮件功能</li>
|
||||||
|
<li>FTP上传、cdnfly、宝塔、易盾等部署插件</li>
|
||||||
<li>更多特权敬请期待</li>
|
<li>更多特权敬请期待</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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";
|
import { RegisterReq } from "/src/api/modules/api.user";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
|
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 { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
import { mitter } from "/src/utils/util.mitt";
|
import { mitter } from "/src/utils/util.mitt";
|
||||||
@@ -67,7 +67,14 @@ export const useUserStore = defineStore({
|
|||||||
LocalStorage.remove(TOKEN_KEY);
|
LocalStorage.remove(TOKEN_KEY);
|
||||||
LocalStorage.remove(USER_INFO_KEY);
|
LocalStorage.remove(USER_INFO_KEY);
|
||||||
},
|
},
|
||||||
|
checkPlus() {
|
||||||
|
if (!this.isPlus) {
|
||||||
|
notification.warn({
|
||||||
|
message: "此为专业版功能,请先升级到专业版"
|
||||||
|
});
|
||||||
|
throw new Error("此为专业版功能,请升级到专业版");
|
||||||
|
}
|
||||||
|
},
|
||||||
async register(user: RegisterReq) {
|
async register(user: RegisterReq) {
|
||||||
await UserApi.register(user);
|
await UserApi.register(user);
|
||||||
notification.success({
|
notification.success({
|
||||||
|
|||||||
@@ -22,15 +22,14 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fs-desc{
|
.fs-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color:#888888;
|
color: #888888;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.ant-btn-link {
|
.ant-btn-link {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
@@ -45,69 +44,88 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
vertical-align: 0 !important;
|
vertical-align: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pointer{
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-center{
|
.flex-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.flex-o{
|
|
||||||
|
.flex-o {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex{
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-1{
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-2{
|
.mb-2 {
|
||||||
margin-bottom:2px;
|
margin-bottom: 2px;
|
||||||
}
|
|
||||||
.ml-5{
|
|
||||||
margin-left:5px;
|
|
||||||
}
|
|
||||||
.ml-20{
|
|
||||||
margin-left:20px;
|
|
||||||
}
|
|
||||||
.ml-15{
|
|
||||||
margin-left:15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
.mr-20{
|
|
||||||
margin-right: 20px;
|
.mr-10 {
|
||||||
}
|
margin-right: 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{
|
.mr-20 {
|
||||||
padding:5px;
|
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 {
|
.ellipsis {
|
||||||
@@ -116,18 +134,35 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-100{
|
.w-100 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-header{
|
.block-header {
|
||||||
margin:3px;
|
margin: 3px;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-bottom:3px;
|
padding-bottom: 3px;
|
||||||
border-bottom: 1px solid #dedede;
|
border-bottom: 1px solid #dedede;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.color-blue{
|
.color-blue {
|
||||||
color: #1890ff;
|
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
|
// @ts-ignore
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -32,11 +32,25 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
|
|||||||
...value,
|
...value,
|
||||||
key
|
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) {
|
if (column.value != null && _.get(form, key) == null) {
|
||||||
//设置默认值
|
|
||||||
_.set(form, key, column.value);
|
_.set(form, key, column.value);
|
||||||
}
|
}
|
||||||
|
//字段配置赋值
|
||||||
columnsRef.value[key] = column;
|
columnsRef.value[key] = column;
|
||||||
console.log("form", columnsRef.value);
|
console.log("form", columnsRef.value);
|
||||||
});
|
});
|
||||||
@@ -55,7 +69,12 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
|
|||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
component: {
|
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: "请选择类型" }],
|
rules: [{ required: true, message: "请选择类型" }],
|
||||||
valueChange: {
|
valueChange: {
|
||||||
|
|||||||
@@ -22,12 +22,17 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
|||||||
form: {
|
form: {
|
||||||
...inputDefine,
|
...inputDefine,
|
||||||
show: compute((ctx) => {
|
show: compute((ctx) => {
|
||||||
console.log(formWrapperRef);
|
|
||||||
const form = formWrapperRef.value.getFormData();
|
const form = formWrapperRef.value.getFormData();
|
||||||
if (!form) {
|
if (!form) {
|
||||||
return false;
|
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: () => {
|
render: () => {
|
||||||
return (
|
return (
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,7 +83,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
|||||||
vModel: "modelValue",
|
vModel: "modelValue",
|
||||||
placeholder: "0 0 4 * * *"
|
placeholder: "0 0 4 * * *"
|
||||||
},
|
},
|
||||||
helper: "请输入cron表达式, 例如:0 0 4 * * *,每天凌晨4点触发",
|
helper: "点击上面的按钮,选择每天几点几分定时执行, 例如:0 0 4 * * *,每天凌晨4点0分0秒触发",
|
||||||
order: 100
|
order: 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,52 @@ import { env } from "/@/utils/util.env";
|
|||||||
import { useUserStore } from "/@/store/modules/user";
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useSettingStore } from "/@/store/modules/settings";
|
import { useSettingStore } from "/@/store/modules/settings";
|
||||||
|
import _ from "lodash-es";
|
||||||
|
|
||||||
export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const lastResRef = ref();
|
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> => {
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
return await api.GetList(query);
|
return await api.GetList(query);
|
||||||
};
|
};
|
||||||
@@ -34,9 +75,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
|||||||
title: form.title
|
title: form.title
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const content = JSON.parse(form.content);
|
//复制的流水线
|
||||||
content.title = form.title;
|
delete form.status;
|
||||||
form.content = JSON.stringify(content);
|
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);
|
const res = await api.AddObj(form);
|
||||||
@@ -48,12 +96,11 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
|||||||
// 添加certd pipeline
|
// 添加certd pipeline
|
||||||
const triggers = [];
|
const triggers = [];
|
||||||
if (form.triggerCron) {
|
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 = [];
|
const notifications = [];
|
||||||
if (form.emailNotify) {
|
if (form.emailNotify) {
|
||||||
notifications.push({
|
notifications.push({
|
||||||
id: nanoid(),
|
|
||||||
type: "email",
|
type: "email",
|
||||||
when: ["error", "turnToSuccess"],
|
when: ["error", "turnToSuccess"],
|
||||||
options: {
|
options: {
|
||||||
@@ -61,19 +108,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const pipeline = {
|
let pipeline = {
|
||||||
title: form.domains[0] + "证书自动化",
|
title: form.domains[0] + "证书自动化",
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
|
||||||
title: "证书申请阶段",
|
title: "证书申请阶段",
|
||||||
tasks: [
|
tasks: [
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
|
||||||
title: "证书申请任务",
|
title: "证书申请任务",
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
|
||||||
title: "申请证书",
|
title: "申请证书",
|
||||||
input: {
|
input: {
|
||||||
renewDays: 20,
|
renewDays: 20,
|
||||||
@@ -92,8 +136,10 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
|||||||
triggers,
|
triggers,
|
||||||
notifications
|
notifications
|
||||||
};
|
};
|
||||||
|
pipeline = setRunnableIds(pipeline);
|
||||||
|
|
||||||
const id = await api.Save({
|
const id = await api.Save({
|
||||||
|
title: pipeline.title,
|
||||||
content: JSON.stringify(pipeline),
|
content: JSON.stringify(pipeline),
|
||||||
keepHistoryCount: 30
|
keepHistoryCount: 30
|
||||||
});
|
});
|
||||||
@@ -145,15 +191,18 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
|||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
click: async (context) => {
|
click: async (context) => {
|
||||||
|
userStore.checkPlus();
|
||||||
const { ui } = useUi();
|
const { ui } = useUi();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const row = context[ui.tableColumn.row];
|
let row = context[ui.tableColumn.row];
|
||||||
|
row = _.cloneDeep(row);
|
||||||
row.title = row.title + "_copy";
|
row.title = row.title + "_copy";
|
||||||
await crudExpose.openCopy({
|
await crudExpose.openCopy({
|
||||||
row: row,
|
row: row,
|
||||||
index: context.index
|
index: context.index
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
class: "need-plus"
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
order: 1,
|
order: 1,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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>
|
<template #title>
|
||||||
编辑步骤
|
编辑步骤
|
||||||
<a-button v-if="editMode" @click="stepDelete()">
|
<a-button v-if="editMode" @click="stepDelete()">
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
:get-context-fn="blankFn"
|
:get-context-fn="blankFn"
|
||||||
/>
|
/>
|
||||||
<template v-for="(item, key) in currentPlugin.input" :key="key">
|
<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>
|
</template>
|
||||||
|
|
||||||
<fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
|
<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 { CopyOutlined } from "@ant-design/icons-vue";
|
||||||
import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
|
import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
|
||||||
import { useUserStore } from "/@/store/modules/user";
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
|
import { compute, useCompute } from "@fast-crud/fast-crud";
|
||||||
|
import { useReference } from "/@/use/use-refrence";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PiStepForm",
|
name: "PiStepForm",
|
||||||
|
// eslint-disable-next-line vue/no-unused-components
|
||||||
components: { CopyOutlined },
|
components: { CopyOutlined },
|
||||||
props: {
|
props: {
|
||||||
editMode: {
|
editMode: {
|
||||||
@@ -106,7 +109,6 @@ export default {
|
|||||||
const mode: Ref = ref("add");
|
const mode: Ref = ref("add");
|
||||||
const callback: Ref = ref();
|
const callback: Ref = ref();
|
||||||
const currentStep: Ref = ref({ title: undefined, input: {} });
|
const currentStep: Ref = ref({ title: undefined, input: {} });
|
||||||
const currentPlugin: Ref = ref({});
|
|
||||||
const stepFormRef: Ref = ref(null);
|
const stepFormRef: Ref = ref(null);
|
||||||
const stepDrawerVisible: Ref = ref(false);
|
const stepDrawerVisible: Ref = ref(false);
|
||||||
const rules: Ref = ref({
|
const rules: Ref = ref({
|
||||||
@@ -150,15 +152,10 @@ export default {
|
|||||||
stepDrawerVisible.value = false;
|
stepDrawerVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepDrawerOnAfterVisibleChange = (val: any) => {
|
|
||||||
console.log("stepDrawerOnAfterVisibleChange", val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stepOpen = (step: any, emit: any) => {
|
const stepOpen = (step: any, emit: any) => {
|
||||||
callback.value = emit;
|
callback.value = emit;
|
||||||
currentStep.value = _.merge({ input: {}, strategy: {} }, step);
|
currentStep.value = _.merge({ input: {}, strategy: {} }, step);
|
||||||
|
|
||||||
console.log("currentStepOpen", currentStep.value);
|
|
||||||
if (step.type) {
|
if (step.type) {
|
||||||
changeCurrentPlugin(currentStep.value);
|
changeCurrentPlugin(currentStep.value);
|
||||||
}
|
}
|
||||||
@@ -189,33 +186,41 @@ export default {
|
|||||||
stepOpen(step, emit);
|
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 changeCurrentPlugin = (step: any) => {
|
||||||
const stepType = step.type;
|
const stepType = step.type;
|
||||||
const pluginDefine = pluginGroups.get(stepType);
|
step.type = stepType;
|
||||||
if (pluginDefine) {
|
step._isAdd = false;
|
||||||
step.type = stepType;
|
|
||||||
step._isAdd = false;
|
let pluginDefine = pluginGroups.get(stepType);
|
||||||
currentPlugin.value = _.cloneDeep(pluginDefine);
|
if (pluginDefine == null) {
|
||||||
for (let key in currentPlugin.value.input) {
|
console.log("插件未找到", stepType);
|
||||||
const input = currentPlugin.value.input[key];
|
return;
|
||||||
if (input?.reference) {
|
}
|
||||||
for (const reference of input.reference) {
|
pluginDefine = _.cloneDeep(pluginDefine);
|
||||||
_.set(
|
const columns = pluginDefine.input;
|
||||||
input,
|
for (let key in columns) {
|
||||||
reference.dest,
|
const column = columns[key];
|
||||||
computed<any>(() => {
|
useReference(column);
|
||||||
const scope = {
|
}
|
||||||
form: currentStep.value.input
|
|
||||||
};
|
currentPluginDefine.value = pluginDefine;
|
||||||
return _.get(scope, reference.src);
|
|
||||||
})
|
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;
|
||||||
if ((input.default != null || input.value != null) && currentStep.value.input[key] == null) {
|
|
||||||
currentStep.value.input[key] = input.default ?? input.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +274,6 @@ export default {
|
|||||||
stepView,
|
stepView,
|
||||||
stepDrawerShow,
|
stepDrawerShow,
|
||||||
stepDrawerVisible,
|
stepDrawerVisible,
|
||||||
stepDrawerOnAfterVisibleChange,
|
|
||||||
currentStep,
|
currentStep,
|
||||||
currentPlugin,
|
currentPlugin,
|
||||||
stepSave,
|
stepSave,
|
||||||
|
|||||||
@@ -43,25 +43,22 @@
|
|||||||
<a-button type="primary" @click="stepAdd(currentTask)">添加步骤</a-button>
|
<a-button type="primary" @click="stepAdd(currentTask)">添加步骤</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
<a-list class="step-list" item-layout="horizontal" :data-source="currentTask.steps">
|
<v-draggable v-model="currentTask.steps" class="step-list" handle=".handle" item-key="id" :disabled="!userStore.isPlus">
|
||||||
<template #renderItem="{ item, index }">
|
<template #item="{ element, index }">
|
||||||
<a-list-item>
|
<div class="step-row">
|
||||||
<template #actions>
|
<div class="text">
|
||||||
<a key="edit" @click="stepEdit(currentTask, item, index)">编辑</a>
|
<fs-icon icon="ion:flash"></fs-icon>
|
||||||
<a key="edit" @click="stepCopy(currentTask, item, index)">复制</a>
|
<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>
|
<a key="remove" @click="stepDelete(currentTask, index)">删除</a>
|
||||||
</template>
|
<fs-icon v-plus class="icon-button handle" title="拖动排序" icon="ion:move-outline"></fs-icon>
|
||||||
<a-list-item-meta>
|
</div>
|
||||||
<template #title>
|
</div>
|
||||||
{{ item.title }}
|
|
||||||
</template>
|
|
||||||
<template #avatar>
|
|
||||||
<fs-icon icon="ion:flash"></fs-icon>
|
|
||||||
</template>
|
|
||||||
</a-list-item-meta>
|
|
||||||
</a-list-item>
|
|
||||||
</template>
|
</template>
|
||||||
</a-list>
|
</v-draggable>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
@@ -85,10 +82,11 @@ import { nanoid } from "nanoid";
|
|||||||
import PiStepForm from "../step-form/index.vue";
|
import PiStepForm from "../step-form/index.vue";
|
||||||
import { Modal } from "ant-design-vue";
|
import { Modal } from "ant-design-vue";
|
||||||
import { CopyOutlined } from "@ant-design/icons-vue";
|
import { CopyOutlined } from "@ant-design/icons-vue";
|
||||||
|
import VDraggable from "vuedraggable";
|
||||||
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
export default {
|
export default {
|
||||||
name: "PiTaskForm",
|
name: "PiTaskForm",
|
||||||
components: { CopyOutlined, PiStepForm },
|
components: { CopyOutlined, PiStepForm, VDraggable },
|
||||||
props: {
|
props: {
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -97,6 +95,7 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ["update"],
|
emits: ["update"],
|
||||||
setup(props: any, ctx: any) {
|
setup(props: any, ctx: any) {
|
||||||
|
const userStore = useUserStore();
|
||||||
function useStep() {
|
function useStep() {
|
||||||
const stepFormRef: Ref<any> = ref(null);
|
const stepFormRef: Ref<any> = ref(null);
|
||||||
const currentStepIndex = ref(0);
|
const currentStepIndex = ref(0);
|
||||||
@@ -251,6 +250,7 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
userStore,
|
||||||
labelCol: { span: 6 },
|
labelCol: { span: 6 },
|
||||||
wrapperCol: { span: 16 },
|
wrapperCol: { span: 16 },
|
||||||
...useTaskForm(),
|
...useTaskForm(),
|
||||||
@@ -268,5 +268,42 @@ export default {
|
|||||||
.ant-list .ant-list-item .ant-list-item-meta .ant-list-item-meta-title {
|
.ant-list .ant-list-item .ant-list-item-meta .ant-list-item-meta-title {
|
||||||
margin: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
name: 'cron-editor',
|
name: 'cron-editor',
|
||||||
vModel: 'modelValue'
|
vModel: 'modelValue'
|
||||||
},
|
},
|
||||||
helper: 'cron表达式,例如: 0 0 3 * * * ,表示每天凌晨3点触发',
|
helper: '点击上面的按钮,选择每天几点几分定时执行, 例如:0 0 4 * * *,每天凌晨4点0分0秒触发',
|
||||||
rules: [{ required: true, message: '此项必填' }]
|
rules: [{ required: true, message: '此项必填' }]
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,177 +20,204 @@
|
|||||||
<div class="layout-left">
|
<div class="layout-left">
|
||||||
<div class="pipeline-container">
|
<div class="pipeline-container">
|
||||||
<div class="pipeline">
|
<div class="pipeline">
|
||||||
<div class="stages">
|
<v-draggable v-model="pipeline.stages" class="stages" item-key="id" handle=".stage-move-handle" :disabled="!userStore.isPlus">
|
||||||
<div class="stage first-stage">
|
<template #header>
|
||||||
<div class="title">
|
<div class="stage first-stage">
|
||||||
<pi-editable model-value="触发源" :disabled="true" />
|
<div class="title stage-move-handle">
|
||||||
</div>
|
<pi-editable model-value="触发源" :disabled="true" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(trigger, index) of pipeline.triggers" :key="trigger.id" class="task-container">
|
<div class="tasks">
|
||||||
<div class="line">
|
<div class="task-container first-task">
|
||||||
<div class="flow-line"></div>
|
<div class="line line-right">
|
||||||
</div>
|
<div class="flow-line"></div>
|
||||||
<div class="task">
|
</div>
|
||||||
<a-button shape="round" @click="triggerEdit(trigger, index)">
|
<div class="task">
|
||||||
<fs-icon icon="ion:time"></fs-icon>
|
<a-button shape="round" type="primary" @click="run()">
|
||||||
{{ trigger.title }}
|
<fs-icon icon="ion:play"></fs-icon>
|
||||||
</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>
|
|
||||||
并行任务
|
|
||||||
</a-button>
|
</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>
|
|
||||||
|
|
||||||
<div v-if="editMode" class="stage last-stage">
|
<div v-if="editMode" class="task-container is-add">
|
||||||
<div class="title">
|
<div class="line line-right">
|
||||||
<pi-editable model-value="新阶段" :disabled="true" />
|
<div class="flow-line"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tasks">
|
<div class="task">
|
||||||
<div class="task-container first-task">
|
<a-button shape="round" type="dashed" @click="triggerAdd">
|
||||||
<div class="line">
|
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||||
<div class="flow-line"></div>
|
触发源(定时)
|
||||||
<fs-icon class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd()"></fs-icon>
|
</a-button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="task-container">
|
</div>
|
||||||
<div class="line">
|
</template>
|
||||||
<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>
|
|
||||||
|
|
||||||
添加通知
|
<template #item="{ element: stage, index }">
|
||||||
</a-button>
|
<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>
|
</div>
|
||||||
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
|
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!userStore.isPlus">
|
||||||
<div class="line">
|
<template #item="{ element: task, index: taskIndex }">
|
||||||
<div class="flow-line"></div>
|
<div
|
||||||
</div>
|
class="task-container"
|
||||||
<div class="task">
|
:class="{
|
||||||
<a-button shape="round" @click="notificationEdit(item, ii as number)">
|
'first-task': taskIndex === 0
|
||||||
<fs-icon icon="ion:notifications"></fs-icon>
|
}"
|
||||||
【通知】 {{ item.type }}
|
>
|
||||||
</a-button>
|
<div class="line line-left">
|
||||||
</div>
|
<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 class="tasks">
|
||||||
</div>
|
<div class="task-container first-task">
|
||||||
<div v-else class="stage last-stage">
|
<div class="line line-left">
|
||||||
<div class="title">
|
<div class="flow-line"></div>
|
||||||
<pi-editable model-value="结束" :disabled="true" />
|
<fs-icon class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd()"></fs-icon>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="pipeline.notifications?.length > 0" class="tasks">
|
<div class="task">
|
||||||
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
|
<a-button shape="round" type="dashed" @click="stageAdd()">
|
||||||
<div class="line">
|
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||||
<div class="flow-line"></div>
|
添加任务
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task">
|
<div class="task-container">
|
||||||
<a-button shape="round" @click="notificationEdit(item, index)">
|
<div class="line line-left">
|
||||||
<fs-icon icon="ion:notifications"></fs-icon>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="tasks">
|
<div v-else class="stage last-stage">
|
||||||
<div class="task-container first-task">
|
<div class="title">
|
||||||
<div class="line">
|
<pi-editable model-value="结束" :disabled="true" />
|
||||||
<div class="flow-line"></div>
|
</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>
|
||||||
<div class="task">
|
</div>
|
||||||
<a-button shape="round" type="dashed">
|
<div v-else class="tasks">
|
||||||
<fs-icon icon="ion:notifications"></fs-icon>
|
<div class="task-container first-task">
|
||||||
通知未设置
|
<div class="line line-left">
|
||||||
</a-button>
|
<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>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</v-draggable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,18 +256,19 @@ import PiTriggerForm from "./component/trigger-form/index.vue";
|
|||||||
import PiNotificationForm from "./component/notification-form/index.vue";
|
import PiNotificationForm from "./component/notification-form/index.vue";
|
||||||
import PiTaskView from "./component/task-view/index.vue";
|
import PiTaskView from "./component/task-view/index.vue";
|
||||||
import PiStatusShow from "./component/status-show.vue";
|
import PiStatusShow from "./component/status-show.vue";
|
||||||
|
import VDraggable from "vuedraggable";
|
||||||
import _ from "lodash-es";
|
import _ from "lodash-es";
|
||||||
import { message, Modal, notification } from "ant-design-vue";
|
import { message, Modal, notification } from "ant-design-vue";
|
||||||
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type";
|
import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type";
|
||||||
import type { Runnable } from "@certd/pipeline";
|
import type { Runnable } from "@certd/pipeline";
|
||||||
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
|
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
|
||||||
import { FsIcon } from "@fast-crud/fast-crud";
|
import { FsIcon } from "@fast-crud/fast-crud";
|
||||||
|
import { useUserStore } from "/@/store/modules/user";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "PipelineEdit",
|
name: "PipelineEdit",
|
||||||
// eslint-disable-next-line vue/no-unused-components
|
// 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: {
|
props: {
|
||||||
pipelineId: {
|
pipelineId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -271,6 +299,8 @@ export default defineComponent({
|
|||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const loadCurrentHistoryDetail = async () => {
|
const loadCurrentHistoryDetail = async () => {
|
||||||
console.log("load history logs");
|
console.log("load history logs");
|
||||||
const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id });
|
const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id });
|
||||||
@@ -626,6 +656,7 @@ export default defineComponent({
|
|||||||
currentHistory,
|
currentHistory,
|
||||||
histories,
|
histories,
|
||||||
goBack,
|
goBack,
|
||||||
|
userStore,
|
||||||
...useTaskRet,
|
...useTaskRet,
|
||||||
...useStageRet,
|
...useStageRet,
|
||||||
...useTrigger(),
|
...useTrigger(),
|
||||||
@@ -700,39 +731,39 @@ export default defineComponent({
|
|||||||
.title {
|
.title {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
display: flex;
|
||||||
&.first-stage {
|
.stage-move-handle {
|
||||||
.line {
|
cursor: move;
|
||||||
width: 50% !important;
|
margin-left: 4px;
|
||||||
.flow-line {
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.last-stage {
|
//.sortable-ghost {
|
||||||
.line {
|
// .line {
|
||||||
width: 50% !important;
|
// visibility: hidden;
|
||||||
left: 0;
|
// }
|
||||||
right: auto;
|
//}
|
||||||
.flow-line {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
.add-stage-btn {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -25px;
|
top: -25px;
|
||||||
right: 0;
|
width: 25px;
|
||||||
width: 100%;
|
|
||||||
|
&.line-left {
|
||||||
|
left: 25px;
|
||||||
|
.flow-line {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.line-right {
|
||||||
|
right: 25px;
|
||||||
|
.flow-line {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.flow-line {
|
.flow-line {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: 28px;
|
|
||||||
margin-right: 28px;
|
|
||||||
border: 1px solid #c7c7c7;
|
border: 1px solid #c7c7c7;
|
||||||
border-top: 0;
|
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 {
|
.tasks {
|
||||||
.task-container {
|
.task-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -760,18 +837,6 @@ export default defineComponent({
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
&.first-task {
|
|
||||||
.line {
|
|
||||||
.flow-line {
|
|
||||||
margin: 0;
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
.add-stage-btn {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
.task {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -780,14 +845,29 @@ export default defineComponent({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
.copy {
|
.task-title {
|
||||||
|
&.in-edit {
|
||||||
|
margin-right: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 60px;
|
right: 60px;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
|
//font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
}
|
}
|
||||||
|
&.copy {
|
||||||
|
right: 80px;
|
||||||
|
}
|
||||||
|
&.drag {
|
||||||
|
right: 60px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Pipeline } from "@certd/pipeline";
|
import type { Pipeline } from "@certd/pipeline";
|
||||||
import { FormItemProps } from "@fast-crud/fast-crud";
|
import { DynamicType, FormItemProps } from "@fast-crud/fast-crud";
|
||||||
export type PipelineDetail = {
|
export type PipelineDetail = {
|
||||||
pipeline: Pipeline;
|
pipeline: Pipeline;
|
||||||
};
|
};
|
||||||
@@ -24,7 +24,7 @@ export type PluginDefine = {
|
|||||||
title: string;
|
title: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
input: {
|
input: {
|
||||||
[key: string]: FormItemProps;
|
[key: string]: DynamicType<FormItemProps>;
|
||||||
};
|
};
|
||||||
output: {
|
output: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
koa:
|
koa:
|
||||||
port: 7001
|
port: 7001
|
||||||
|
# key: ./data/ssl/cert.key
|
||||||
|
# cert: ./data/ssl/cert.crt
|
||||||
#plus:
|
#plus:
|
||||||
# server:
|
# server:
|
||||||
# baseUrl: 'http://127.0.0.1:11007'
|
# baseUrl: 'http://127.0.0.1:11007'
|
||||||
|
|||||||
@@ -3,6 +3,29 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||||
|
|
||||||
### Performance Improvements
|
### Performance Improvements
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@certd/ui-server",
|
"name": "@certd/ui-server",
|
||||||
"version": "1.24.1",
|
"version": "1.24.3",
|
||||||
"description": "fast-server base midway",
|
"description": "fast-server base midway",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alicloud/cs20151215": "^3.0.3",
|
"@alicloud/cs20151215": "^3.0.3",
|
||||||
"@alicloud/pop-core": "^1.7.10",
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
"@certd/acme-client": "^1.24.1",
|
"@certd/acme-client": "^1.24.3",
|
||||||
"@certd/lib-huawei": "^1.24.1",
|
"@certd/lib-huawei": "^1.24.3",
|
||||||
"@certd/lib-k8s": "^1.24.1",
|
"@certd/lib-k8s": "^1.24.3",
|
||||||
"@certd/midway-flyway-js": "^1.22.6",
|
"@certd/midway-flyway-js": "^1.22.6",
|
||||||
"@certd/pipeline": "^1.24.1",
|
"@certd/pipeline": "^1.24.3",
|
||||||
"@certd/plugin-cert": "^1.24.1",
|
"@certd/plugin-cert": "^1.24.3",
|
||||||
"@certd/plugin-plus": "^1.24.1",
|
"@certd/plugin-plus": "^1.24.3",
|
||||||
"@koa/cors": "^5.0.0",
|
"@koa/cors": "^5.0.0",
|
||||||
"@midwayjs/bootstrap": "^3.16.2",
|
"@midwayjs/bootstrap": "^3.16.2",
|
||||||
"@midwayjs/cache": "^3.14.0",
|
"@midwayjs/cache": "^3.14.0",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@midwayjs/static-file": "^3.16.4",
|
"@midwayjs/static-file": "^3.16.4",
|
||||||
"@midwayjs/typeorm": "^3.16.4",
|
"@midwayjs/typeorm": "^3.16.4",
|
||||||
"@midwayjs/validate": "^3.16.4",
|
"@midwayjs/validate": "^3.16.4",
|
||||||
|
"ali-oss": "^6.21.0",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"basic-ftp": "^5.0.5",
|
"basic-ftp": "^5.0.5",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^4.9.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"glob": "^10.4.5",
|
"glob": "^10.4.5",
|
||||||
"https-proxy-agent": "^7.0.4",
|
"https-proxy-agent": "^7.0.5",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
@@ -61,14 +62,17 @@
|
|||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"ssh2": "^1.15.0",
|
"ssh2": "^1.15.0",
|
||||||
|
"strip-ansi": "^7.1.0",
|
||||||
"svg-captcha": "^1.4.0",
|
"svg-captcha": "^1.4.0",
|
||||||
"tencentcloud-sdk-nodejs": "^4.0.44",
|
"tencentcloud-sdk-nodejs": "^4.0.44",
|
||||||
"typeorm": "^0.3.20"
|
"typeorm": "^0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@midwayjs/mock": "^3.16.4",
|
"@midwayjs/mock": "^3.16.4",
|
||||||
|
"@types/ali-oss": "^6.16.11",
|
||||||
"@types/cache-manager": "^3.4.3",
|
"@types/cache-manager": "^3.4.3",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/koa": "2.13.4",
|
"@types/koa": "2.13.4",
|
||||||
|
|||||||
@@ -49,13 +49,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async add(bean: PipelineEntity) {
|
async add(bean: PipelineEntity) {
|
||||||
if (!isPlus()) {
|
await this.save(bean);
|
||||||
const count = await this.repository.count();
|
|
||||||
if (count >= freeCount) {
|
|
||||||
throw new NeedVIPException('免费版最多只能创建10个pipeline');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await super.add(bean);
|
|
||||||
return bean;
|
return bean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,19 +99,37 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async save(bean: 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()) {
|
if (!isPlus()) {
|
||||||
const count = await this.repository.count();
|
let count = await this.repository.count();
|
||||||
|
if (isUpdate) {
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
if (count >= freeCount) {
|
if (count >= freeCount) {
|
||||||
throw new NeedVIPException('免费版最多只能创建10个pipeline');
|
throw new NeedVIPException('免费版最多只能创建10个pipeline');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!isUpdate) {
|
||||||
|
//如果是添加,先保存一下,获取到id,更新pipeline.id
|
||||||
|
await this.addOrUpdate(bean);
|
||||||
|
}
|
||||||
await this.clearTriggers(bean.id);
|
await this.clearTriggers(bean.id);
|
||||||
if (bean.content) {
|
if (bean.content) {
|
||||||
const pipeline = JSON.parse(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.addOrUpdate(bean);
|
||||||
await this.registerTriggerById(bean.id);
|
await this.registerTriggerById(bean.id);
|
||||||
|
return bean;
|
||||||
}
|
}
|
||||||
|
|
||||||
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
|
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class CronTask {
|
|||||||
this.job = req.job;
|
this.job = req.job;
|
||||||
this.name = req.name;
|
this.name = req.name;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
this.logger.info(`[cron] CronTask created [${this.name}], cron:${this.cron}`);
|
||||||
this.genNextTime();
|
this.genNextTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ export * from './plugin-host/index.js';
|
|||||||
export * from './plugin-huawei/index.js';
|
export * from './plugin-huawei/index.js';
|
||||||
export * from './plugin-demo/index.js';
|
export * from './plugin-demo/index.js';
|
||||||
export * from './plugin-other/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 { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
|
||||||
import { Autowire, ILogger } from '@certd/pipeline';
|
import { Autowire, ILogger } from '@certd/pipeline';
|
||||||
import { AliyunAccess } from '../access/index.js';
|
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
|
||||||
|
|
||||||
@IsDnsProvider({
|
@IsDnsProvider({
|
||||||
name: 'aliyun',
|
name: 'aliyun',
|
||||||
@@ -16,13 +16,14 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
|||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
async onInstance() {
|
async onInstance() {
|
||||||
const access: any = this.access;
|
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,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
endpoint: 'https://alidns.aliyuncs.com',
|
endpoint: 'https://alidns.aliyuncs.com',
|
||||||
apiVersion: '2015-01-09',
|
apiVersion: '2015-01-09',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// async getDomainList() {
|
// async getDomainList() {
|
||||||
@@ -100,6 +101,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
|||||||
// Line: 'oversea' // 海外
|
// Line: 'oversea' // 海外
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const requestOption = {
|
const requestOption = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from './dns-provider/index.js';
|
export * from './dns-provider/index.js';
|
||||||
export * from './plugin/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 { 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 { appendTimeSuffix } from '../../utils/index.js';
|
||||||
import { CertInfo } from '@certd/plugin-cert';
|
import { CertInfo } from '@certd/plugin-cert';
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
|||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
namespace!: string;
|
namespace: string = 'default';
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: 'ingress名称',
|
title: 'ingress名称',
|
||||||
value: '',
|
value: '',
|
||||||
@@ -109,7 +109,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
|||||||
this.K8sClient = sdk.K8sClient;
|
this.K8sClient = sdk.K8sClient;
|
||||||
}
|
}
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
console.log('开始部署证书到阿里云cdn');
|
this.logger.info('开始部署证书到阿里云cdn');
|
||||||
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
|
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
|
||||||
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
|
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
|
||||||
const client = await this.getClient(access, regionId);
|
const client = await this.getClient(access, regionId);
|
||||||
@@ -144,7 +144,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const ingressList = await k8sClient.getIngressList({ namespace });
|
const ingressList = await k8sClient.getIngressList({ namespace });
|
||||||
console.log('ingressList:', ingressList);
|
this.logger.info('ingressList:', ingressList);
|
||||||
if (!ingressList || !ingressList.items) {
|
if (!ingressList || !ingressList.items) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -200,14 +200,15 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(aliyunProvider: any, regionId: string) {
|
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,
|
accessKeyId: aliyunProvider.accessKeyId,
|
||||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||||
endpoint: `https://cs.${regionId}.aliyuncs.com`,
|
endpoint: `https://cs.${regionId}.aliyuncs.com`,
|
||||||
apiVersion: '2015-12-15',
|
apiVersion: '2015-12-15',
|
||||||
});
|
})
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKubeConfig(client: any, clusterId: string, isPrivateIpAddress = false) {
|
async getKubeConfig(client: any, clusterId: string, isPrivateIpAddress = false) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { AliyunAccess } from '../../access/index.js';
|
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: 'DeployCertToAliyunCDN',
|
name: 'DeployCertToAliyunCDN',
|
||||||
title: '部署证书至阿里云CDN',
|
title: '部署证书至阿里云CDN',
|
||||||
@@ -50,23 +50,23 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
console.log('开始部署证书到阿里云cdn');
|
this.logger.info('开始部署证书到阿里云cdn');
|
||||||
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
|
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
|
||||||
const client = await this.getClient(access);
|
const client = await this.getClient(access);
|
||||||
const params = await this.buildParams();
|
const params = await this.buildParams();
|
||||||
await this.doRequest(client, params);
|
await this.doRequest(client, params);
|
||||||
console.log('部署完成');
|
this.logger.info('部署完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
const Core = await import('@alicloud/pop-core');
|
const client = new AliyunClient({logger:this.logger})
|
||||||
|
await client.init({
|
||||||
return new Core.default({
|
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
endpoint: 'https://cdn.aliyuncs.com',
|
endpoint: 'https://cdn.aliyuncs.com',
|
||||||
apiVersion: '2018-05-10',
|
apiVersion: '2018-05-10',
|
||||||
});
|
})
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildParams() {
|
async buildParams() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { AliyunAccess } from '../../access/index.js';
|
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: 'DeployCertToAliyunDCDN',
|
name: 'DeployCertToAliyunDCDN',
|
||||||
title: '部署证书至阿里云DCDN',
|
title: '部署证书至阿里云DCDN',
|
||||||
@@ -59,13 +59,14 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
const sdk = await import('@alicloud/pop-core');
|
const client = new AliyunClient({logger:this.logger})
|
||||||
return new sdk.default({
|
await client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
endpoint: 'https://dcdn.aliyuncs.com',
|
endpoint: 'https://dcdn.aliyuncs.com',
|
||||||
apiVersion: '2018-01-15',
|
apiVersion: '2018-01-15',
|
||||||
});
|
})
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildParams() {
|
async buildParams() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
||||||
import { appendTimeSuffix, checkRet } from '../../utils/index.js';
|
import { appendTimeSuffix, checkRet } from '../../utils/index.js';
|
||||||
import { AliyunAccess } from '../../access/index.js';
|
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: 'uploadCertToAliyun',
|
name: 'uploadCertToAliyun',
|
||||||
@@ -86,13 +86,14 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(aliyunProvider: AliyunAccess) {
|
async getClient(aliyunProvider: AliyunAccess) {
|
||||||
const Core = await import('@alicloud/pop-core');
|
const client = new AliyunClient({logger:this.logger})
|
||||||
return new Core.default({
|
await client.init({
|
||||||
accessKeyId: aliyunProvider.accessKeyId,
|
accessKeyId: aliyunProvider.accessKeyId,
|
||||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||||
endpoint: 'https://cas.aliyuncs.com',
|
endpoint: 'https://cas.aliyuncs.com',
|
||||||
apiVersion: '2018-07-13',
|
apiVersion: '2018-07-13',
|
||||||
});
|
})
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//注册插件
|
//注册插件
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class DemoAccess implements IAccess {
|
|||||||
},
|
},
|
||||||
//是否必填
|
//是否必填
|
||||||
required: true,
|
required: true,
|
||||||
|
//改属性是否需要加密
|
||||||
encrypt: 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 path from 'path';
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { ILogger } from '@certd/pipeline';
|
import { ILogger } from '@certd/pipeline';
|
||||||
import iconv from 'iconv-lite';
|
|
||||||
import { SshAccess } from '../access/index.js';
|
import { SshAccess } from '../access/index.js';
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
export class AsyncSsh2Client {
|
export class AsyncSsh2Client {
|
||||||
conn: ssh2.Client;
|
conn: ssh2.Client;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
@@ -18,7 +18,7 @@ export class AsyncSsh2Client {
|
|||||||
this.encoding = connConf.encoding;
|
this.encoding = connConf.encoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
convert(buffer: Buffer) {
|
convert(iconv: any, buffer: Buffer) {
|
||||||
if (this.encoding) {
|
if (this.encoding) {
|
||||||
return iconv.decode(buffer, this.encoding);
|
return iconv.decode(buffer, this.encoding);
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,8 @@ export class AsyncSsh2Client {
|
|||||||
this.logger.info('script 为空,取消执行');
|
this.logger.info('script 为空,取消执行');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let iconv: any = await import('iconv-lite');
|
||||||
|
iconv = iconv.default;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.logger.info(`执行命令:[${this.connConf.host}][exec]: ` + script);
|
this.logger.info(`执行命令:[${this.connConf.host}][exec]: ` + script);
|
||||||
this.conn.exec(script, (err: Error, stream: any) => {
|
this.conn.exec(script, (err: Error, stream: any) => {
|
||||||
@@ -97,7 +99,7 @@ export class AsyncSsh2Client {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('data', (ret: Buffer) => {
|
.on('data', (ret: Buffer) => {
|
||||||
const out = this.convert(ret);
|
const out = this.convert(iconv, ret);
|
||||||
data += out;
|
data += out;
|
||||||
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
|
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
|
||||||
})
|
})
|
||||||
@@ -106,7 +108,7 @@ export class AsyncSsh2Client {
|
|||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
})
|
})
|
||||||
.stderr.on('data', (ret: Buffer) => {
|
.stderr.on('data', (ret: Buffer) => {
|
||||||
const err = this.convert(ret);
|
const err = this.convert(iconv, ret);
|
||||||
data += err;
|
data += err;
|
||||||
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
|
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
|
||||||
});
|
});
|
||||||
@@ -123,22 +125,33 @@ export class AsyncSsh2Client {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const output: string[] = [];
|
const output: string[] = [];
|
||||||
|
function ansiHandle(data: string) {
|
||||||
|
data = data.replace(/\[[0-9]+;1H/g, '\n');
|
||||||
|
data = stripAnsi(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
stream
|
stream
|
||||||
.on('close', () => {
|
.on('close', () => {
|
||||||
this.logger.info('Stream :: close');
|
this.logger.info('Stream :: close');
|
||||||
resolve(output);
|
resolve(output);
|
||||||
})
|
})
|
||||||
.on('data', (ret: Buffer) => {
|
.on('data', (ret: Buffer) => {
|
||||||
const data = this.convert(ret);
|
const data = ansiHandle(ret.toString());
|
||||||
this.logger.info('' + data);
|
this.logger.info(data);
|
||||||
output.push(data);
|
output.push(data);
|
||||||
})
|
})
|
||||||
|
.on('error', (err: any) => {
|
||||||
|
reject(err);
|
||||||
|
this.logger.error(err);
|
||||||
|
})
|
||||||
.stderr.on('data', (ret: Buffer) => {
|
.stderr.on('data', (ret: Buffer) => {
|
||||||
const data = this.convert(ret);
|
const data = ansiHandle(ret.toString());
|
||||||
output.push(data);
|
output.push(data);
|
||||||
this.logger.info(`[${this.connConf.host}][error]: ` + 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('请注意:windows下,文件目录分隔应该写成\\而不是/');
|
||||||
this.logger.info('--------------------------');
|
this.logger.info('--------------------------');
|
||||||
}
|
}
|
||||||
const spec = await conn.exec('echo %COMSPEC%');
|
const isCmd = await this.isCmd(conn);
|
||||||
if (spec.toString().trim() === '%COMSPEC%') {
|
if (!isCmd) {
|
||||||
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
|
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
|
||||||
} else {
|
} else {
|
||||||
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
|
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await conn.exec(mkdirCmd);
|
await conn.shell(mkdirCmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
await conn.fastPut({ sftp, ...transport });
|
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> }) {
|
async exec(options: { connectConf: SshAccess; script: string | Array<string> }) {
|
||||||
let { script } = options;
|
let { script } = options;
|
||||||
const { connectConf } = options;
|
const { connectConf } = options;
|
||||||
if (_.isArray(script)) {
|
|
||||||
script = script as Array<string>;
|
this.logger.info('命令:', script);
|
||||||
script = script.join('\n');
|
|
||||||
}
|
|
||||||
this.logger.info('执行命令:', script);
|
|
||||||
return await this._call({
|
return await this._call({
|
||||||
connectConf,
|
connectConf,
|
||||||
callable: async (conn: AsyncSsh2Client) => {
|
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({
|
return await this._call({
|
||||||
connectConf,
|
connectConf,
|
||||||
callable: async (conn: AsyncSsh2Client) => {
|
callable: async (conn: AsyncSsh2Client) => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import path from 'path';
|
|||||||
export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: '证书保存路径',
|
title: '证书保存路径',
|
||||||
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.pem',
|
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.pem',
|
||||||
component: {
|
component: {
|
||||||
placeholder: './tmp/cert.pem',
|
placeholder: './tmp/cert.pem',
|
||||||
},
|
},
|
||||||
@@ -25,12 +25,32 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
|||||||
crtPath!: string;
|
crtPath!: string;
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: '私钥保存路径',
|
title: '私钥保存路径',
|
||||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.key',
|
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.key',
|
||||||
component: {
|
component: {
|
||||||
placeholder: './tmp/cert.key',
|
placeholder: './tmp/cert.key',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
keyPath!: string;
|
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({
|
@TaskInput({
|
||||||
title: '域名证书',
|
title: '域名证书',
|
||||||
helper: '请选择前置任务输出的域名证书',
|
helper: '请选择前置任务输出的域名证书',
|
||||||
@@ -44,14 +64,28 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
@TaskOutput({
|
@TaskOutput({
|
||||||
title: '证书保存路径',
|
title: '证书保存路径',
|
||||||
|
type: 'HostCrtPath',
|
||||||
})
|
})
|
||||||
hostCrtPath!: string;
|
hostCrtPath!: string;
|
||||||
|
|
||||||
@TaskOutput({
|
@TaskOutput({
|
||||||
title: '私钥保存路径',
|
title: '私钥保存路径',
|
||||||
|
type: 'HostKeyPath',
|
||||||
})
|
})
|
||||||
hostKeyPath!: string;
|
hostKeyPath!: string;
|
||||||
|
|
||||||
|
@TaskOutput({
|
||||||
|
title: 'PFX保存路径',
|
||||||
|
type: 'HostPfxPath',
|
||||||
|
})
|
||||||
|
hostPfxPath!: string;
|
||||||
|
|
||||||
|
@TaskOutput({
|
||||||
|
title: 'DER保存路径',
|
||||||
|
type: 'HostDerPath',
|
||||||
|
})
|
||||||
|
hostDerPath!: string;
|
||||||
|
|
||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
copyFile(srcFile: string, destFile: string) {
|
copyFile(srcFile: string, destFile: string) {
|
||||||
@@ -63,37 +97,38 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
|||||||
fs.copyFileSync(srcFile, destFile);
|
fs.copyFileSync(srcFile, destFile);
|
||||||
}
|
}
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
let { crtPath, keyPath } = this;
|
let { crtPath, keyPath, pfxPath, derPath } = this;
|
||||||
const certReader = new CertReader(this.cert);
|
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);
|
const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath }) => {
|
||||||
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
|
this.logger.info('复制到目标路径');
|
||||||
// crtPath = path.resolve(crtPath);
|
if (crtPath) {
|
||||||
// keyPath = path.resolve(keyPath);
|
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
|
||||||
this.copyFile(saveCrtPath, crtPath);
|
this.copyFile(tmpCrtPath, crtPath);
|
||||||
this.copyFile(saveKeyPath, keyPath);
|
this.hostCrtPath = crtPath;
|
||||||
this.logger.info('证书复制成功:crtPath=', crtPath, ',keyPath=', keyPath);
|
}
|
||||||
|
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('请注意,如果使用的是相对路径,那么文件就在你的数据库同级目录下,默认是/data/certd/下面');
|
||||||
this.logger.info('请注意,如果使用的是绝对路径,文件在容器内的目录下,你需要给容器做目录映射才能复制到宿主机');
|
this.logger.info('请注意,如果使用的是绝对路径,文件在容器内的目录下,你需要给容器做目录映射才能复制到宿主机');
|
||||||
} catch (e) {
|
};
|
||||||
this.logger.error(`复制失败:${e.message}`);
|
|
||||||
throw e;
|
await certReader.readCertFile({ logger: this.logger, handle });
|
||||||
} finally {
|
|
||||||
//删除临时文件
|
|
||||||
this.logger.info('删除临时文件');
|
|
||||||
fs.unlinkSync(saveCrtPath);
|
|
||||||
fs.unlinkSync(saveKeyPath);
|
|
||||||
}
|
|
||||||
this.logger.info('执行完成');
|
this.logger.info('执行完成');
|
||||||
//输出
|
|
||||||
this.hostCrtPath = crtPath;
|
|
||||||
this.hostKeyPath = keyPath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
|
|||||||
vModel: 'value',
|
vModel: 'value',
|
||||||
rows: 6,
|
rows: 6,
|
||||||
},
|
},
|
||||||
|
helper: '注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行',
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
script!: string;
|
script!: string;
|
||||||
@@ -40,11 +41,13 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
|
|||||||
const { script, accessId } = this;
|
const { script, accessId } = this;
|
||||||
const connectConf = await this.accessService.getById(accessId);
|
const connectConf = await this.accessService.getById(accessId);
|
||||||
const sshClient = new SshClient(this.logger);
|
const sshClient = new SshClient(this.logger);
|
||||||
const ret = await sshClient.exec({
|
|
||||||
|
const scripts = script.split('\n');
|
||||||
|
await sshClient.exec({
|
||||||
connectConf,
|
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 { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
||||||
import { SshClient } from '../../lib/ssh.js';
|
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 * as fs from 'fs';
|
||||||
import { SshAccess } from '../../access/index.js';
|
import { SshAccess } from '../../access/index.js';
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ import { SshAccess } from '../../access/index.js';
|
|||||||
})
|
})
|
||||||
export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: '证书保存路径',
|
title: 'PEM证书保存路径',
|
||||||
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号',
|
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.pem',
|
||||||
component: {
|
component: {
|
||||||
placeholder: '/root/deploy/nginx/cert.pem',
|
placeholder: '/root/deploy/nginx/cert.pem',
|
||||||
},
|
},
|
||||||
@@ -26,12 +26,31 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
crtPath!: string;
|
crtPath!: string;
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: '私钥保存路径',
|
title: '私钥保存路径',
|
||||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号',
|
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.key',
|
||||||
component: {
|
component: {
|
||||||
placeholder: '/root/deploy/nginx/cert.key',
|
placeholder: '/root/deploy/nginx/cert.key',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
keyPath!: string;
|
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({
|
@TaskInput({
|
||||||
title: '域名证书',
|
title: '域名证书',
|
||||||
helper: '请选择前置任务输出的域名证书',
|
helper: '请选择前置任务输出的域名证书',
|
||||||
@@ -87,9 +106,23 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
})
|
})
|
||||||
hostKeyPath!: string;
|
hostKeyPath!: string;
|
||||||
|
|
||||||
|
@TaskOutput({
|
||||||
|
title: 'PFX保存路径',
|
||||||
|
})
|
||||||
|
hostPfxPath!: string;
|
||||||
|
|
||||||
|
@TaskOutput({
|
||||||
|
title: 'DER保存路径',
|
||||||
|
})
|
||||||
|
hostDerPath!: string;
|
||||||
|
|
||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
copyFile(srcFile: string, destFile: string) {
|
copyFile(srcFile: string, destFile: string) {
|
||||||
|
if (!srcFile || !destFile) {
|
||||||
|
this.logger.warn(`srcFile:${srcFile} 或 destFile:${destFile} 为空,不复制`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const dir = destFile.substring(0, destFile.lastIndexOf('/'));
|
const dir = destFile.substring(0, destFile.lastIndexOf('/'));
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
@@ -99,16 +132,17 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
const { crtPath, keyPath, cert, accessId } = this;
|
const { crtPath, keyPath, cert, accessId } = this;
|
||||||
const certReader = new CertReader(cert);
|
const certReader = new CertReader(cert);
|
||||||
this.logger.info('将证书写入本地缓存文件');
|
|
||||||
const saveCrtPath = certReader.saveToFile('crt');
|
const handle = async (opts: CertReaderHandleContext) => {
|
||||||
const saveKeyPath = certReader.saveToFile('key');
|
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath } = opts;
|
||||||
this.logger.info('本地文件写入成功');
|
|
||||||
try {
|
|
||||||
if (this.copyToThisHost) {
|
if (this.copyToThisHost) {
|
||||||
this.logger.info('复制到目标路径');
|
this.logger.info('复制到目标路径');
|
||||||
this.copyFile(saveCrtPath, crtPath);
|
this.copyFile(tmpCrtPath, crtPath);
|
||||||
this.copyFile(saveKeyPath, keyPath);
|
this.copyFile(tmpKeyPath, keyPath);
|
||||||
this.logger.info('证书复制成功:crtPath=', crtPath, ',keyPath=', 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 {
|
} else {
|
||||||
if (!accessId) {
|
if (!accessId) {
|
||||||
throw new Error('主机登录授权配置不能为空');
|
throw new Error('主机登录授权配置不能为空');
|
||||||
@@ -116,35 +150,49 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
this.logger.info('准备上传文件到服务器');
|
this.logger.info('准备上传文件到服务器');
|
||||||
const connectConf: SshAccess = await this.accessService.getById(accessId);
|
const connectConf: SshAccess = await this.accessService.getById(accessId);
|
||||||
const sshClient = new SshClient(this.logger);
|
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({
|
await sshClient.uploadFiles({
|
||||||
connectConf,
|
connectConf,
|
||||||
transports: [
|
transports,
|
||||||
{
|
|
||||||
localPath: saveCrtPath,
|
|
||||||
remotePath: crtPath,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
localPath: saveKeyPath,
|
|
||||||
remotePath: keyPath,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
mkdirs: this.mkdirs,
|
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}`);
|
await certReader.readCertFile({
|
||||||
throw e;
|
logger: this.logger,
|
||||||
} finally {
|
handle,
|
||||||
//删除临时文件
|
});
|
||||||
this.logger.info('删除临时文件');
|
|
||||||
fs.unlinkSync(saveCrtPath);
|
|
||||||
fs.unlinkSync(saveKeyPath);
|
|
||||||
}
|
|
||||||
this.logger.info('执行完成');
|
|
||||||
//输出
|
|
||||||
this.hostCrtPath = crtPath;
|
|
||||||
this.hostKeyPath = keyPath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
|
|||||||
lang: 'cn',
|
lang: 'cn',
|
||||||
error_on_empty: 'no',
|
error_on_empty: 'no',
|
||||||
},
|
},
|
||||||
timeout: 5000,
|
timeout: 10000,
|
||||||
};
|
};
|
||||||
_.merge(config, options);
|
_.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
|
cd certd
|
||||||
|
|
||||||
|
# 切换到最新的版本tag,v2分支可能不稳定
|
||||||
|
checkout tags/vx.x.x # x.x.x为最新的版本号
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install -g pnpm@8.15.7
|
npm install -g pnpm@8.15.7
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# 初始化构建
|
# 初始化构建
|
||||||
lerna run build
|
npm run init
|
||||||
```
|
```
|
||||||
|
|
||||||
启动 server:
|
启动 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