Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe2d2c328 | ||
|
|
20f5865bb9 | ||
|
|
2b224c712f | ||
|
|
c446e24f1a | ||
|
|
2623f45a3b | ||
|
|
52e7208e8f | ||
|
|
d1498a7160 | ||
|
|
5c270b6b9d | ||
|
|
18718f6a25 | ||
|
|
653f409d91 | ||
|
|
0f0af2f309 | ||
|
|
7908ab79da | ||
|
|
ae3daa9bcf | ||
|
|
01df4d0f1d | ||
|
|
25ff6906c6 | ||
|
|
695548eade | ||
|
|
6221a4e464 | ||
|
|
115b819c66 | ||
|
|
bceb8cce0d | ||
|
|
8d2cf2095c | ||
|
|
1b1a1a5bc2 | ||
|
|
935ebe022a | ||
|
|
ff356571c8 | ||
|
|
76fb2141e4 | ||
|
|
b220500f40 | ||
|
|
1cbf70fb6a | ||
|
|
52ec48656d | ||
|
|
fddf3a0f68 | ||
|
|
98520a1213 | ||
|
|
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 |
4
.github/workflows/build-image.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
# cache: 'npm'
|
||||
# working-directory: ./packages/ui/certd-client
|
||||
- run: |
|
||||
npm install -g pnpm
|
||||
npm install -g pnpm@8.15.7
|
||||
pnpm install
|
||||
npm run build
|
||||
working-directory: ./packages/ui/certd-client
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
context: ./packages/ui/
|
||||
tags: |
|
||||
|
||||
13
.gitignore
vendored
@@ -19,24 +19,13 @@ gen
|
||||
/*.log
|
||||
|
||||
/packages/ui/*/.idea
|
||||
|
||||
/packages/ui/*/node_modules
|
||||
|
||||
/packages/*/node_modules
|
||||
/packages/ui/certd-server/tmp/
|
||||
/packages/ui/certd-ui/dist/
|
||||
/other
|
||||
/dev-sidecar-test
|
||||
/packages/core/certd/yarn.lock
|
||||
/packages/test
|
||||
/test/own
|
||||
/pnpm-lock.yaml
|
||||
|
||||
docker/image/workspace
|
||||
/packages/core/lego
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
test/**/*.js
|
||||
/packages/ui/certd-server/data/db.sqlite
|
||||
/packages/ui/certd-server/data/keys.yaml
|
||||
/packages/pro/
|
||||
/packages/pro/
|
||||
|
||||
40
CHANGELOG.md
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复腾讯云cdn证书部署后会自动关闭hsts,http2.0等配置的bug ([7908ab7](https://github.com/certd/certd/commit/7908ab79da624c94fa05849925b15e480e3317c4))
|
||||
* 修复腾讯云tke证书部署报错的bug ([653f409](https://github.com/certd/certd/commit/653f409d91a441850d6381f89a8dd390831f0d5e))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 插件选择支持搜索 ([d1498a7](https://github.com/certd/certd/commit/d1498a71601b74d38343b1d070eadd03705dd9d5))
|
||||
* 前置任务步骤增加错误提示 ([ae3daa9](https://github.com/certd/certd/commit/ae3daa9bcf4fc363825aad9b77f5d3879aeeff70))
|
||||
* 群晖部署教程 ([0f0af2f](https://github.com/certd/certd/commit/0f0af2f309390f388e7a272cea3a1dd30c01977d))
|
||||
* 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e))
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde))
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577))
|
||||
* 修复windows下无法执行第二条命令的bug ([71ac8aa](https://github.com/certd/certd/commit/71ac8aae4aa694e1a23761e9761c9fba30b43a21))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 阶段、任务、步骤全面支持拖动排序 ([bd73a16](https://github.com/certd/certd/commit/bd73a163cd0497f062bd424ddc6bc9bbc95f81ea))
|
||||
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
|
||||
* 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327))
|
||||
* 西部数据支持用户级的apikey ([1c17b41](https://github.com/certd/certd/commit/1c17b41e160944b073e1849e6f9467c3659a4bfc))
|
||||
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
|
||||
* 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda))
|
||||
* 支持阿里云oss ([87a2673](https://github.com/certd/certd/commit/87a2673e8c33dff6eda1b836d92ecc121564ed78))
|
||||
* 支持西部数码DNS ([c59cab1](https://github.com/certd/certd/commit/c59cab1aaeb19f86df8e3e0d8127cbd0a9ef77f3))
|
||||
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
|
||||
* client 请求超时时间延长为10s ([ff46771](https://github.com/certd/certd/commit/ff46771d8dd43e71c1ca70e3ba783945750342cc))
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
10
README.md
@@ -18,7 +18,8 @@ https://afdian.com/a/greper
|
||||
专业版特权
|
||||
1. 证书流水线条数无限制(免费版限制10条)
|
||||
2. 免配置发邮件功能
|
||||
3. 更多功能增加中...
|
||||
3. FTP上传、cdnfly、宝塔等部署插件
|
||||
4. 更多功能增加中...
|
||||
************************
|
||||
|
||||
## 一、特性
|
||||
@@ -156,17 +157,18 @@ docker compose up -d
|
||||
* [腾讯云](./doc/tencent/tencent.md)
|
||||
* [windows主机](./doc/host/host.md)
|
||||
* [google证书](./doc/google/google.md)
|
||||
* [群晖部署certd及证书更新教程](./doc/synology/index.md)
|
||||
|
||||
|
||||
## 八、问题处理
|
||||
### 7.1 忘记管理员密码
|
||||
解决方法如下:
|
||||
1. 修改docker-compose.yaml文件,将环境变量`certd_system_resetAdminPassword`改为`true`
|
||||
1. 修改docker-compose.yaml文件,将环境变量`certd_system_resetAdminPasswd`改为`true`
|
||||
```yaml
|
||||
services:
|
||||
certd:
|
||||
environment: # 环境变量
|
||||
- certd_system_resetAdminPassword=false
|
||||
- certd_system_resetAdminPasswd=false
|
||||
```
|
||||
2. 重启容器
|
||||
```shell
|
||||
@@ -174,7 +176,7 @@ docker compose up -d
|
||||
docker logs -f --tail 500 certd
|
||||
# 观察日志,当日志中输出“重置1号管理员用户的密码完成”,即可操作下一步
|
||||
```
|
||||
3. 修改docker-compose.yaml,将`certd_system_resetAdminPassword`改回`false`
|
||||
3. 修改docker-compose.yaml,将`certd_system_resetAdminPasswd`改回`false`
|
||||
4. 再次重启容器
|
||||
```shell
|
||||
docker compose up -d
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
7
|
||||
|
||||
BIN
doc/synology/images/1.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
doc/synology/images/2.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
doc/synology/images/3.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
doc/synology/images/4.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
doc/synology/images/5.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
doc/synology/images/6.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
doc/synology/images/deploy.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
41
doc/synology/index.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 群晖部署和证书更新
|
||||
|
||||
|
||||
## 一、群晖系统上部署Certd教程
|
||||
|
||||
### 1. 打开Container Manager
|
||||
|
||||

|
||||
|
||||
### 2. 新增项目
|
||||
|
||||

|
||||
|
||||
### 3. 配置Certd项目
|
||||
|
||||

|
||||
|
||||
### 4. 外网访问设置
|
||||
|
||||

|
||||
|
||||
### 5. 确认项目信息
|
||||
|
||||

|
||||
|
||||
点击完成安装,等待certd启动完成即可
|
||||
|
||||
### 6. 门户配置向导【可选】
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 二、更新群晖证书
|
||||
|
||||
## 1. 前提条件
|
||||
* 已经部署了certd
|
||||
* 群晖上已经设置好了证书(证书建议设置好描述,插件需要根据描述查找证书)
|
||||
|
||||
## 2. 在certd上配置自动更新群晖证书插件
|
||||

|
||||
@@ -1,14 +0,0 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
ftp:
|
||||
# 镜像 # ↓↓↓↓↓ --- 1、 镜像版本号,建议改成固定版本号【可选】
|
||||
image: gists/pure-ftpd
|
||||
container_name: ftp # 容器名
|
||||
restart: unless-stopped # 自动重启
|
||||
volumes:
|
||||
- /data/ftp2/:/home/ftpuser
|
||||
ports: # 端口映射
|
||||
- "21:21"
|
||||
- "30000-30009:30000-30009"
|
||||
environment: # 环境变量
|
||||
- TZ=Asia/Shanghai
|
||||
@@ -1,31 +1,38 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
certd:
|
||||
# 镜像 # ↓↓↓↓↓ --- 1、 镜像版本号,建议改成固定版本号【可选】
|
||||
# 镜像 # ↓↓↓↓↓ --- 镜像版本号,建议改成固定版本号【可选】
|
||||
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
|
||||
container_name: certd # 容器名
|
||||
restart: unless-stopped # 自动重启
|
||||
volumes:
|
||||
# ↓↓↓↓↓ ------------------------------------------------------- 2、 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下【可选】
|
||||
# ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下【可选】
|
||||
- /data/certd:/app/data
|
||||
ports: # 端口映射
|
||||
# ↓↓↓↓ ----------------------------------------------------------3、如果端口有冲突,可以修改第一个7001为其他不冲突的端口号【可选】
|
||||
# ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突,可以修改第一个7001为其他不冲突的端口号【可选】
|
||||
- "7001:7001"
|
||||
dns:
|
||||
# 如果出现getaddrinfo ENOTFOUND等错误,可以尝试修改或注释dns配置
|
||||
- 223.5.5.5
|
||||
- 223.6.6.6
|
||||
# ↓↓↓↓ ----------------------------------------------------------如果你服务器部署在国外,可以用8.8.8.8替换上面的dns【可选】
|
||||
# ↓↓↓↓ ---------------------------------------------------------- 如果你服务器部署在国外,可以用8.8.8.8替换上面的dns【可选】
|
||||
# - 8.8.8.8
|
||||
# - 8.8.4.4
|
||||
environment: # 环境变量
|
||||
- TZ=Asia/Shanghai
|
||||
#- HTTPS_PROXY=http://xxxxxx:xx
|
||||
#- HTTP_PROXY=http://xxxxxx:xx
|
||||
# ↑↑↑↑↑ ------------------------------------- 这里可以设置http代理【可选】
|
||||
- certd_system_resetAdminPasswd=false
|
||||
# ↑↑↑↑↑---------------------------4、如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false【可选】
|
||||
# ↑↑↑↑↑--------------------------- 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false【可选】
|
||||
- certd_cron_immediateTriggerOnce=false
|
||||
# ↑↑↑↑↑---------------------------5、如果设置为true,启动后所有配置了cron的流水线任务都将被立即触发一次【可选】
|
||||
# ↑↑↑↑↑--------------------------- 如果设置为true,启动后所有配置了cron的流水线任务都将被立即触发一次【可选】
|
||||
- VITE_APP_ICP_NO=
|
||||
# ↑↑↑↑↑ -----------------------------------------6、这里可以设置备案号【可选】
|
||||
# ↑↑↑↑↑ ----------------------------------------- 这里可以设置备案号【可选】
|
||||
#- certd_koa_key=./data/ssl/cert.key
|
||||
#- certd_koa_cert=./data/ssl/cert.crt
|
||||
# ↑↑↑↑↑ ----------------------------------------- 配置证书和key,则表示https方式启动,访问网址要使用 https://your.domain:7001【可选】
|
||||
|
||||
# 设置环境变量即可自定义certd配置
|
||||
# 服务端配置项见: packages/ui/certd-server/src/config/config.default.ts
|
||||
# 服务端配置规则: certd_ + 配置项, 点号用_代替
|
||||
|
||||
20
init.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
current_pwd=$(pwd)
|
||||
|
||||
echo "开始设置git配置"
|
||||
|
||||
read -p "请输入username:" username
|
||||
git config user.name $username
|
||||
|
||||
read -p "请输入email:" email
|
||||
git config user.email $email
|
||||
|
||||
git config credential.helper "store --file=$current_pwd/.git/credential.store"
|
||||
echo "已设置记住git账号密码"
|
||||
|
||||
git config core.autocrlf input
|
||||
echo "已设置auto crlf = input"
|
||||
|
||||
git config core.filemode false
|
||||
echo "已设置忽略文件模式变化"
|
||||
|
||||
echo "git配置完成"
|
||||
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.24.1"
|
||||
"version": "1.24.4"
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/publishlab/node-acme-client/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.24.3](https://github.com/publishlab/node-acme-client/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.24.2](https://github.com/publishlab/node-acme-client/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/publishlab/node-acme-client/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
|
||||
|
||||
## [1.24.1](https://github.com/publishlab/node-acme-client/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"description": "Simple and unopinionated ACME client",
|
||||
"private": false,
|
||||
"author": "nmorsman",
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.4",
|
||||
"main": "src/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/publishlab/node-acme-client",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
@@ -59,5 +59,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
||||
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ exports.directory = {
|
||||
*/
|
||||
|
||||
exports.crypto = require('./crypto');
|
||||
exports.forge = require('./crypto/forge');
|
||||
// exports.forge = require('./crypto/forge');
|
||||
|
||||
/**
|
||||
* Axios
|
||||
|
||||
@@ -111,7 +111,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
||||
log(`DNS query finished successfully, found ${recordValues.length} TXT records`);
|
||||
|
||||
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
||||
throw new Error(`Authorization not found in DNS TXT record: ${recordName}`);
|
||||
throw new Error(`Authorization not found in DNS TXT record: ${recordName},need:${keyAuthorization},found:${recordValues}`);
|
||||
}
|
||||
|
||||
log(`Key authorization match for ${challenge.type}/${recordName}, ACME challenge verified`);
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 前置任务步骤增加错误提示 ([ae3daa9](https://github.com/certd/certd/commit/ae3daa9bcf4fc363825aad9b77f5d3879aeeff70))
|
||||
* 群晖部署教程 ([0f0af2f](https://github.com/certd/certd/commit/0f0af2f309390f388e7a272cea3a1dd30c01977d))
|
||||
* 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e))
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde))
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda))
|
||||
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1 +1 @@
|
||||
23:56
|
||||
17:29
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": false,
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.4",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -57,5 +57,5 @@
|
||||
"vite": "^4.3.8",
|
||||
"vue-tsc": "^1.6.5"
|
||||
},
|
||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
||||
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
import { IContext } from "../core/index.js";
|
||||
|
||||
export type HttpClient = AxiosInstance;
|
||||
export type UserContext = IContext;
|
||||
export type PipelineContext = IContext;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RegistryItem } from "../registry/index.js";
|
||||
import { Decorator } from "../decorator/index.js";
|
||||
import { IEmailService } from "../service/index.js";
|
||||
import { FileStore } from "./file-store.js";
|
||||
import { hashUtils } from "../utils/index.js";
|
||||
// import { TimeoutPromise } from "../utils/util.promise.js";
|
||||
|
||||
export type ExecutorOptions = {
|
||||
@@ -69,6 +70,7 @@ export class Executor {
|
||||
}
|
||||
|
||||
async run(runtimeId: any = 0, triggerType: string) {
|
||||
let intervalFlushLogId: any = undefined;
|
||||
try {
|
||||
await this.init();
|
||||
const trigger = { type: triggerType };
|
||||
@@ -76,8 +78,14 @@ export class Executor {
|
||||
this.runtime = new RunHistory(runtimeId, trigger, this.pipeline);
|
||||
this.logger.info(`pipeline.${this.pipeline.id} start`);
|
||||
await this.notification("start");
|
||||
|
||||
this.runtime.start(this.pipeline);
|
||||
intervalFlushLogId = setInterval(async () => {
|
||||
await this.onChanged(this.runtime);
|
||||
}, 5000);
|
||||
|
||||
await this.runWithHistory(this.pipeline, "pipeline", async () => {
|
||||
await this.runStages(this.pipeline);
|
||||
return await this.runStages(this.pipeline);
|
||||
});
|
||||
if (this.lastRuntime && this.lastRuntime.pipeline.status?.status === ResultType.error) {
|
||||
await this.notification("turnToSuccess");
|
||||
@@ -85,55 +93,35 @@ export class Executor {
|
||||
await this.notification("success");
|
||||
} catch (e: any) {
|
||||
await this.notification("error", e);
|
||||
this.logger.error("pipeline 执行失败", e.stack);
|
||||
this.logger.error("pipeline 执行失败", e);
|
||||
} finally {
|
||||
clearInterval(intervalFlushLogId);
|
||||
await this.onChanged(this.runtime);
|
||||
await this.pipelineContext.setObj("lastRuntime", this.runtime);
|
||||
this.logger.info(`pipeline.${this.pipeline.id} end`);
|
||||
}
|
||||
}
|
||||
|
||||
async runWithHistory(runnable: Runnable, runnableType: string, run: () => Promise<void>) {
|
||||
async runWithHistory(runnable: Runnable, runnableType: string, run: () => Promise<ResultType | void>) {
|
||||
runnable.runnableType = runnableType;
|
||||
this.runtime.start(runnable);
|
||||
// const timeout = runnable.timeout ?? 20 * 60 * 1000;
|
||||
await this.onChanged(this.runtime);
|
||||
|
||||
if (runnable.strategy?.runStrategy === RunStrategy.SkipWhenSucceed) {
|
||||
//如果是成功后跳过策略
|
||||
const lastNode = this.lastStatusMap.get(runnable.id);
|
||||
const lastResult = lastNode?.status?.status;
|
||||
const lastInput = JSON.stringify(lastNode?.status?.input);
|
||||
let inputChanged = false;
|
||||
if (runnableType === "step") {
|
||||
const step = runnable as Step;
|
||||
const input = JSON.stringify(step.input);
|
||||
if (input != null && lastInput !== input) {
|
||||
//参数有变化
|
||||
inputChanged = true;
|
||||
}
|
||||
}
|
||||
if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
|
||||
runnable.status!.output = lastNode?.status?.output;
|
||||
runnable.status!.files = lastNode?.status?.files;
|
||||
this.runtime.skip(runnable);
|
||||
await this.onChanged(this.runtime);
|
||||
return ResultType.skip;
|
||||
}
|
||||
}
|
||||
const intervalFlushLogId = setInterval(async () => {
|
||||
await this.onChanged(this.runtime);
|
||||
}, 5000);
|
||||
|
||||
// const timeout = runnable.timeout ?? 20 * 60 * 1000;
|
||||
try {
|
||||
if (this.abort.signal.aborted) {
|
||||
this.runtime.cancel(runnable);
|
||||
return ResultType.canceled;
|
||||
}
|
||||
await run();
|
||||
const resultType = await run();
|
||||
if (this.abort.signal.aborted) {
|
||||
this.runtime.cancel(runnable);
|
||||
return ResultType.canceled;
|
||||
}
|
||||
if (resultType == ResultType.skip) {
|
||||
this.runtime.skip(runnable);
|
||||
return resultType;
|
||||
}
|
||||
this.runtime.success(runnable);
|
||||
return ResultType.success;
|
||||
} catch (e: any) {
|
||||
@@ -141,7 +129,6 @@ export class Executor {
|
||||
throw e;
|
||||
} finally {
|
||||
this.runtime.finally(runnable);
|
||||
clearInterval(intervalFlushLogId);
|
||||
await this.onChanged(this.runtime);
|
||||
}
|
||||
}
|
||||
@@ -150,7 +137,7 @@ export class Executor {
|
||||
const resList: ResultType[] = [];
|
||||
for (const stage of pipeline.stages) {
|
||||
const res: ResultType = await this.runWithHistory(stage, "stage", async () => {
|
||||
await this.runStage(stage);
|
||||
return await this.runStage(stage);
|
||||
});
|
||||
resList.push(res);
|
||||
}
|
||||
@@ -163,6 +150,7 @@ export class Executor {
|
||||
const runner = async () => {
|
||||
return this.runWithHistory(task, "task", async () => {
|
||||
await this.runTask(task);
|
||||
return ResultType.success;
|
||||
});
|
||||
};
|
||||
runnerList.push(runner);
|
||||
@@ -205,7 +193,7 @@ export class Executor {
|
||||
for (const step of task.steps) {
|
||||
step.runnableType = "step";
|
||||
const res: ResultType = await this.runWithHistory(step, "step", async () => {
|
||||
await this.runStep(step);
|
||||
return await this.runStep(step);
|
||||
});
|
||||
resList.push(res);
|
||||
}
|
||||
@@ -224,19 +212,45 @@ export class Executor {
|
||||
// @ts-ignore
|
||||
const define: PluginDefine = plugin.define;
|
||||
//从outputContext读取输入参数
|
||||
Decorator.inject(define.input, instance, step.input, (item, key) => {
|
||||
const input = _.cloneDeep(step.input);
|
||||
Decorator.inject(define.input, instance, input, (item, key) => {
|
||||
if (item.component?.name === "pi-output-selector") {
|
||||
const contextKey = step.input[key];
|
||||
const contextKey = input[key];
|
||||
if (contextKey != null) {
|
||||
if (typeof contextKey !== "string") {
|
||||
throw new Error(`步骤${step.title}的${item.title}属性必须为String类型,请重新配置该属性`);
|
||||
}
|
||||
// "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert",
|
||||
const arr = contextKey.split(".");
|
||||
const id = arr[1];
|
||||
const outputKey = arr[2];
|
||||
step.input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
|
||||
input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
|
||||
if (input[key] == null) {
|
||||
this.logger.warn(`${item.title}的配置未找到对应的输出值,请确认对应的前置任务是否存在或者是否执行正确`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const newInputHash = hashUtils.md5(JSON.stringify(input));
|
||||
step.status!.inputHash = newInputHash;
|
||||
//判断是否需要跳过
|
||||
const lastNode = this.lastStatusMap.get(step.id);
|
||||
const lastResult = lastNode?.status?.status;
|
||||
let inputChanged = true;
|
||||
const lastInputHash = lastNode?.status?.inputHash;
|
||||
if (lastInputHash && newInputHash && lastInputHash === newInputHash) {
|
||||
//参数有变化
|
||||
inputChanged = false;
|
||||
}
|
||||
if (step.strategy?.runStrategy === RunStrategy.SkipWhenSucceed) {
|
||||
if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
|
||||
step.status!.output = lastNode?.status?.output;
|
||||
step.status!.files = lastNode?.status?.files;
|
||||
return ResultType.skip;
|
||||
}
|
||||
}
|
||||
|
||||
const http = createAxiosService({ logger: currentLogger });
|
||||
const taskCtx: TaskInstanceContext = {
|
||||
pipeline: this.pipeline,
|
||||
@@ -244,6 +258,7 @@ export class Executor {
|
||||
lastStatus,
|
||||
http,
|
||||
logger: currentLogger,
|
||||
inputChanged,
|
||||
accessService: this.options.accessService,
|
||||
emailService: this.options.emailService,
|
||||
pipelineContext: this.pipelineContext,
|
||||
@@ -271,7 +286,6 @@ export class Executor {
|
||||
// this.runtime.context[stepOutputKey] = instance[key];
|
||||
});
|
||||
step.status!.files = instance.getFiles();
|
||||
|
||||
//更新pipeline vars
|
||||
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
||||
// 判断 pipelineVars 有值时更新
|
||||
@@ -288,17 +302,17 @@ export class Executor {
|
||||
let subject = "";
|
||||
let content = "";
|
||||
if (when === "start") {
|
||||
subject = `【CertD】开始执行,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||
content = subject;
|
||||
subject = `【CertD】开始执行,【${this.pipeline.id}】${this.pipeline.title}`;
|
||||
content = `buildId:${this.runtime.id}`;
|
||||
} else if (when === "success") {
|
||||
subject = `【CertD】执行成功,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||
content = subject;
|
||||
subject = `【CertD】执行成功,【${this.pipeline.id}】${this.pipeline.title}`;
|
||||
content = `buildId:${this.runtime.id}`;
|
||||
} else if (when === "turnToSuccess") {
|
||||
subject = `【CertD】执行成功(错误转成功),${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||
content = subject;
|
||||
subject = `【CertD】执行成功(错误转成功),【${this.pipeline.id}】${this.pipeline.title}`;
|
||||
content = `buildId:${this.runtime.id}`;
|
||||
} else if (when === "error") {
|
||||
subject = `【CertD】执行失败,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||
content = `<pre>${error.message}</pre>`;
|
||||
subject = `【CertD】执行失败,【${this.pipeline.id}】${this.pipeline.title}`;
|
||||
content = `buildId:${this.runtime.id}\nerror:${error.message}`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { logger } from "../utils/index.js";
|
||||
import { setLogger } from "@certd/plus";
|
||||
import { setLogger, isPlus } from "@certd/plus";
|
||||
setLogger(logger);
|
||||
export * from "@certd/plus";
|
||||
|
||||
export function checkPlus() {
|
||||
if (!isPlus()) {
|
||||
throw new Error("此为专业版功能,请升级到专业版");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,8 @@ export class RunHistory {
|
||||
this._loggers[runnable.id] = buildLogger((text) => {
|
||||
this.logs[runnable.id].push(text);
|
||||
});
|
||||
const input = (runnable as Step).input;
|
||||
const status: HistoryResult = {
|
||||
output: {},
|
||||
input: _.cloneDeep(input),
|
||||
status: ResultType.start,
|
||||
startTime: now,
|
||||
result: ResultType.start,
|
||||
@@ -109,8 +107,8 @@ export class RunHistory {
|
||||
|
||||
logError(runnable: Runnable, e: Error) {
|
||||
// @ts-ignore
|
||||
const errorInfo = runnable.runnableType == "step" ? e.stack : e.message;
|
||||
this._loggers[runnable.id].error(`[${runnable.runnableType}] [${runnable.title}]<id:${runnable.id}> :${errorInfo}`);
|
||||
const errorInfo = runnable.runnableType === "step" ? e : e.message;
|
||||
this._loggers[runnable.id].error(`[${runnable.runnableType}] [${runnable.title}]<id:${runnable.id}> :`, errorInfo);
|
||||
}
|
||||
|
||||
finally(runnable: Runnable) {
|
||||
|
||||
@@ -118,7 +118,8 @@ export type HistoryResultGroup = {
|
||||
};
|
||||
};
|
||||
export type HistoryResult = {
|
||||
input: any;
|
||||
// input: any;
|
||||
inputHash?: string;
|
||||
output: any;
|
||||
files?: FileItem[];
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Logger } from "log4js";
|
||||
import { IAccessService } from "../access/index.js";
|
||||
import { IEmailService } from "../service/index.js";
|
||||
import { IContext } from "../core/index.js";
|
||||
import { AxiosInstance } from "axios";
|
||||
import { ILogger, logger } from "../utils/index.js";
|
||||
import { HttpClient } from "../utils/util.request";
|
||||
|
||||
export enum ContextScope {
|
||||
global,
|
||||
@@ -17,6 +17,7 @@ export enum ContextScope {
|
||||
export type TaskOutputDefine = {
|
||||
title: string;
|
||||
value?: any;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type TaskInputDefine = FormItemProps;
|
||||
@@ -59,11 +60,12 @@ export type TaskInstanceContext = {
|
||||
pipeline: Pipeline;
|
||||
step: Step;
|
||||
logger: Logger;
|
||||
inputChanged: boolean;
|
||||
accessService: IAccessService;
|
||||
emailService: IEmailService;
|
||||
pipelineContext: IContext;
|
||||
userContext: IContext;
|
||||
http: AxiosInstance;
|
||||
http: HttpClient;
|
||||
fileStore: FileStore;
|
||||
lastStatus?: Runnable;
|
||||
signal: AbortSignal;
|
||||
|
||||
@@ -21,5 +21,6 @@ export const pluginGroups = {
|
||||
huawei: new PluginGroup("huawei", "华为云", 3),
|
||||
tencent: new PluginGroup("tencent", "腾讯云", 4),
|
||||
host: new PluginGroup("host", "主机", 5),
|
||||
cdn: new PluginGroup("cdn", "CDN", 6),
|
||||
other: new PluginGroup("other", "其他", 7),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import sleep from "./util.sleep.js";
|
||||
import { request } from "./util.request.js";
|
||||
import { http } from "./util.request.js";
|
||||
export * from "./util.request.js";
|
||||
export * from "./util.log.js";
|
||||
export * from "./util.file.js";
|
||||
export * from "./util.sp.js";
|
||||
export * as promises from "./util.promise.js";
|
||||
export * from "./util.hash.js";
|
||||
export const utils = {
|
||||
sleep,
|
||||
http: request,
|
||||
http,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -1,29 +1,39 @@
|
||||
import axios from "axios";
|
||||
import axios, { AxiosRequestConfig } from "axios";
|
||||
import { logger } from "./util.log.js";
|
||||
import { Logger } from "log4js";
|
||||
|
||||
export class HttpError extends Error {
|
||||
request?: { url: string; method: string; data?: any };
|
||||
response?: { data: any };
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
code?: string;
|
||||
request?: { url: string; method: string; params?: any; data?: any };
|
||||
response?: { data: any };
|
||||
cause?: any;
|
||||
constructor(error: any) {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
super(error.message);
|
||||
this.name = error.name;
|
||||
this.stack = error.stack;
|
||||
this.status = error?.response?.status;
|
||||
this.statusText = error?.response?.statusText;
|
||||
this.code = error.code;
|
||||
this.cause = error.cause;
|
||||
|
||||
this.status = error.response?.status;
|
||||
this.statusText = error.response?.statusText;
|
||||
this.request = {
|
||||
url: error?.response?.config?.url,
|
||||
method: error?.response?.config?.method,
|
||||
data: error?.response?.config?.data,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
params: error.config?.params,
|
||||
data: error.config?.data,
|
||||
};
|
||||
this.response = {
|
||||
data: error?.response?.data,
|
||||
data: error.response?.data,
|
||||
};
|
||||
|
||||
delete error.response;
|
||||
delete error.config;
|
||||
delete error.request;
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -35,7 +45,7 @@ export function createAxiosService({ logger }: { logger: Logger }) {
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
(config: any) => {
|
||||
logger.info(`http request:${config.url},method:${config.method}`);
|
||||
logger.info(`http request:${config.url},method:${config.method},params:${JSON.stringify(config.params)}`);
|
||||
return config;
|
||||
},
|
||||
(error: Error) => {
|
||||
@@ -67,9 +77,13 @@ export function createAxiosService({ logger }: { logger: Logger }) {
|
||||
// default: break
|
||||
// }
|
||||
logger.error(
|
||||
`请求出错:status:${error?.response?.status},statusText:${error?.response?.statusText},url:${error?.config?.url},method:${error?.config?.method}。`
|
||||
`请求出错:status:${error.response?.status},statusText:${error.response?.statusText},url:${error.config?.url},method:${error.config?.method}。`
|
||||
);
|
||||
logger.error("返回数据:", JSON.stringify(error?.response?.data));
|
||||
logger.error("返回数据:", JSON.stringify(error.response?.data));
|
||||
|
||||
if (error instanceof AggregateError) {
|
||||
logger.error(error);
|
||||
}
|
||||
const err = new HttpError(error);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
@@ -77,4 +91,8 @@ export function createAxiosService({ logger }: { logger: Logger }) {
|
||||
return service;
|
||||
}
|
||||
|
||||
export const request = createAxiosService({ logger });
|
||||
export const http = createAxiosService({ logger }) as HttpClient;
|
||||
export type HttpClientResponse<R> = any;
|
||||
export type HttpClient = {
|
||||
request<D = any, R = any>(config: AxiosRequestConfig<D>): Promise<HttpClientResponse<R>>;
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export type SpawnOption = {
|
||||
cmd: string | string[];
|
||||
onStdout?: (data: string) => void;
|
||||
onStderr?: (data: string) => void;
|
||||
env: any;
|
||||
env?: any;
|
||||
logger?: ILogger;
|
||||
options?: any;
|
||||
};
|
||||
@@ -66,6 +66,8 @@ async function spawn(opts: SpawnOption): Promise<string> {
|
||||
cmd = item;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd = opts.cmd;
|
||||
}
|
||||
log.info(`执行命令: ${cmd}`);
|
||||
let stdout = "";
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-huawei",
|
||||
"private": false,
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.3",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
"types": "./dist/d/index.d.ts",
|
||||
@@ -16,5 +16,5 @@
|
||||
"axios": "^1.7.2",
|
||||
"rollup": "^3.7.4"
|
||||
},
|
||||
"gitHead": "47fe3d5826661f678d081ab53e67c847a3239d88"
|
||||
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-k8s",
|
||||
"private": false,
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.4",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@kubernetes/client-node": "0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/pipeline": "^1.24.1",
|
||||
"@certd/pipeline": "^1.24.4",
|
||||
"@rollup/plugin-commonjs": "^23.0.4",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
@@ -37,5 +37,5 @@
|
||||
"tslib": "^2.5.2",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
||||
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/midway-flyway-js",
|
||||
"version": "1.22.6",
|
||||
"version": "1.24.4",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@rollup/plugin-terser": "^0.4.3",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/node": "16",
|
||||
"@types/node": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"better-sqlite3": "^11.1.2",
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e))
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
|
||||
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
|
||||
* 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda))
|
||||
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-cert",
|
||||
"private": false,
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.4",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -13,8 +13,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.24.1",
|
||||
"@certd/pipeline": "^1.24.1",
|
||||
"@certd/acme-client": "^1.24.4",
|
||||
"@certd/pipeline": "^1.24.4",
|
||||
"jszip": "^3.10.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"psl": "^1.9.0"
|
||||
@@ -53,5 +53,5 @@
|
||||
"vite": "^3.1.0",
|
||||
"vue-tsc": "^0.38.9"
|
||||
},
|
||||
"gitHead": "e5da46cfc31b2e30a4903bcb2251b1851265ef41"
|
||||
"gitHead": "c49ccbde93dbad7062ac39d4f18eca7d561f573f"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export type CertInfo = {
|
||||
crt: string;
|
||||
key: string;
|
||||
csr: string;
|
||||
pfx?: string;
|
||||
der?: string;
|
||||
};
|
||||
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
|
||||
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
|
||||
@@ -81,6 +83,7 @@ export class AcmeService {
|
||||
if (conf.key == null) {
|
||||
conf.key = await this.createNewKey();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
this.logger.info(`创建新的Accountkey:${email}`);
|
||||
}
|
||||
let directoryUrl = "";
|
||||
if (isTest) {
|
||||
@@ -124,7 +127,7 @@ export class AcmeService {
|
||||
}
|
||||
|
||||
async createNewKey() {
|
||||
const key = await acme.forge.createPrivateKey();
|
||||
const key = await acme.crypto.createPrivateKey(2048);
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
@@ -236,7 +239,10 @@ export class AcmeService {
|
||||
const privateKeyType = options.privateKeyType || "rsa_2048";
|
||||
const privateKeyArr = privateKeyType.split("_");
|
||||
const type = privateKeyArr[0];
|
||||
const size = parseInt(privateKeyArr[1]);
|
||||
let size = 2048;
|
||||
if (privateKeyArr.length > 1) {
|
||||
size = parseInt(privateKeyArr[1]);
|
||||
}
|
||||
if (type == "ec") {
|
||||
const name: any = "P-" + size;
|
||||
privateKey = await acme.crypto.createPrivateEcdsaKey(name);
|
||||
@@ -305,13 +311,13 @@ export class AcmeService {
|
||||
|
||||
private async testDirectory(directoryUrl: string) {
|
||||
try {
|
||||
await utils.http({
|
||||
await utils.http.request({
|
||||
url: directoryUrl,
|
||||
method: "GET",
|
||||
timeout: 5000,
|
||||
timeout: 10000,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(`${directoryUrl},测试访问失败`, e);
|
||||
this.logger.error(`${directoryUrl},测试访问失败`, e.stack);
|
||||
return false;
|
||||
}
|
||||
this.logger.info(`${directoryUrl},测试访问成功`);
|
||||
|
||||
@@ -3,6 +3,8 @@ import dayjs from "dayjs";
|
||||
import type { CertInfo } from "./acme.js";
|
||||
import { CertReader } from "./cert-reader.js";
|
||||
import JSZip from "jszip";
|
||||
import { CertConverter } from "./convert.js";
|
||||
import fs from "fs";
|
||||
|
||||
export { CertReader };
|
||||
export type { CertInfo };
|
||||
@@ -15,6 +17,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
tokenSeparators: [",", " ", ",", "、", "|"],
|
||||
},
|
||||
required: true,
|
||||
col: {
|
||||
@@ -25,7 +28,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
|
||||
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
||||
"3、多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com)\n" +
|
||||
"4、输入一个回车之后,再输入下一个",
|
||||
"4、输入一个空格之后,再输入下一个",
|
||||
})
|
||||
domains!: string[];
|
||||
|
||||
@@ -41,6 +44,18 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "PFX证书密码",
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
},
|
||||
required: false,
|
||||
order: 100,
|
||||
helper: "PFX格式证书是否需要加密",
|
||||
})
|
||||
pfxPassword!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "更新天数",
|
||||
value: 20,
|
||||
@@ -77,13 +92,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
successNotify = true;
|
||||
|
||||
@TaskInput({
|
||||
title: "配置说明",
|
||||
order: 9999,
|
||||
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
|
||||
})
|
||||
intro!: string;
|
||||
|
||||
// @TaskInput({
|
||||
// title: "CsrInfo",
|
||||
// helper: "暂时没有用",
|
||||
@@ -135,22 +143,44 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
|
||||
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
|
||||
|
||||
if (cert.pfx == null || cert.der == null) {
|
||||
try {
|
||||
const converter = new CertConverter({ logger: this.logger });
|
||||
const res = await converter.convert({
|
||||
cert,
|
||||
pfxPassword: this.pfxPassword,
|
||||
});
|
||||
const pfxBuffer = fs.readFileSync(res.pfxPath);
|
||||
cert.pfx = pfxBuffer.toString("base64");
|
||||
|
||||
const derBuffer = fs.readFileSync(res.derPath);
|
||||
cert.der = derBuffer.toString("base64");
|
||||
|
||||
this.logger.info("转换证书格式成功");
|
||||
isNew = true;
|
||||
} catch (e) {
|
||||
this.logger.error("转换证书格式失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
const applyTime = dayjs(certReader.detail.notBefore).format("YYYYMMDD_HHmmss");
|
||||
await this.zipCert(cert, applyTime);
|
||||
const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore);
|
||||
await this.zipCert(cert, zipFileName);
|
||||
} else {
|
||||
this.extendsFiles();
|
||||
}
|
||||
// thi
|
||||
// s.logger.info(JSON.stringify(certReader.detail));
|
||||
}
|
||||
|
||||
async zipCert(cert: CertInfo, applyTime: string) {
|
||||
async zipCert(cert: CertInfo, filename: string) {
|
||||
const zip = new JSZip();
|
||||
zip.file("cert.crt", cert.crt);
|
||||
zip.file("cert.key", cert.key);
|
||||
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
|
||||
const filename = `cert_${domain_name}_${applyTime}.zip`;
|
||||
if (cert.pfx) {
|
||||
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
|
||||
}
|
||||
if (cert.der) {
|
||||
zip.file("cert.der", Buffer.from(cert.der, "base64"));
|
||||
}
|
||||
const content = await zip.generateAsync({ type: "nodebuffer" });
|
||||
this.saveFile(filename, content);
|
||||
this.logger.info(`已保存文件:${filename}`);
|
||||
@@ -161,14 +191,14 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
*/
|
||||
async condition() {
|
||||
if (this.forceUpdate) {
|
||||
this.logger.info("强制更新证书选项已勾选,准备申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
let inputChanged = false;
|
||||
const oldInput = JSON.stringify(this.lastStatus?.input?.domains);
|
||||
const thisInput = JSON.stringify(this.domains);
|
||||
if (oldInput !== thisInput) {
|
||||
inputChanged = true;
|
||||
const inputChanged = this.ctx.inputChanged;
|
||||
if (inputChanged) {
|
||||
this.logger.info("输入参数变更,准备申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
let oldCert: CertReader | undefined = undefined;
|
||||
@@ -182,11 +212,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inputChanged) {
|
||||
this.logger.info("输入参数变更,申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
const ret = this.isWillExpire(oldCert.expires, this.renewDays);
|
||||
if (!ret.isWillExpire) {
|
||||
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}天`);
|
||||
|
||||
@@ -3,7 +3,14 @@ import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { crypto } from "@certd/acme-client";
|
||||
export class CertReader implements CertInfo {
|
||||
import { ILogger } from "@certd/pipeline";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export type CertReaderHandleContext = { reader: CertReader; tmpCrtPath: string; tmpKeyPath: string; tmpPfxPath?: string; tmpDerPath?: string };
|
||||
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
|
||||
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
|
||||
export class CertReader {
|
||||
cert: CertInfo;
|
||||
crt: string;
|
||||
key: string;
|
||||
csr: string;
|
||||
@@ -11,30 +18,31 @@ export class CertReader implements CertInfo {
|
||||
detail: any;
|
||||
expires: number;
|
||||
constructor(certInfo: CertInfo) {
|
||||
this.cert = certInfo;
|
||||
this.crt = certInfo.crt;
|
||||
this.key = certInfo.key;
|
||||
this.csr = certInfo.csr;
|
||||
|
||||
const { detail, expires } = this.getCrtDetail(this.crt);
|
||||
const { detail, expires } = this.getCrtDetail(this.cert.crt);
|
||||
this.detail = detail;
|
||||
this.expires = expires.getTime();
|
||||
}
|
||||
|
||||
toCertInfo(): CertInfo {
|
||||
return {
|
||||
crt: this.crt,
|
||||
key: this.key,
|
||||
csr: this.csr,
|
||||
};
|
||||
return this.cert;
|
||||
}
|
||||
|
||||
getCrtDetail(crt: string) {
|
||||
getCrtDetail(crt: string = this.cert.crt) {
|
||||
const detail = crypto.readCertificateInfo(crt.toString());
|
||||
const expires = detail.notAfter;
|
||||
return { detail, expires };
|
||||
}
|
||||
|
||||
saveToFile(type: "crt" | "key", filepath?: string) {
|
||||
saveToFile(type: "crt" | "key" | "pfx" | "der", filepath?: string) {
|
||||
if (!this.cert[type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filepath == null) {
|
||||
//写入临时目录
|
||||
filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.${type}`);
|
||||
@@ -44,8 +52,52 @@ export class CertReader implements CertInfo {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filepath, this[type]);
|
||||
if (type === "crt" || type === "key") {
|
||||
fs.writeFileSync(filepath, this.cert[type]);
|
||||
} else {
|
||||
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
|
||||
async readCertFile(opts: HandleOpts) {
|
||||
const logger = opts.logger;
|
||||
logger.info("将证书写入本地缓存文件");
|
||||
const tmpCrtPath = this.saveToFile("crt");
|
||||
const tmpKeyPath = this.saveToFile("key");
|
||||
const tmpPfxPath = this.saveToFile("pfx");
|
||||
const tmpDerPath = this.saveToFile("der");
|
||||
logger.info("本地文件写入成功");
|
||||
try {
|
||||
return await opts.handle({
|
||||
reader: this,
|
||||
tmpCrtPath: tmpCrtPath,
|
||||
tmpKeyPath: tmpKeyPath,
|
||||
tmpPfxPath: tmpPfxPath,
|
||||
tmpDerPath: tmpDerPath,
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
//删除临时文件
|
||||
logger.info("删除临时文件");
|
||||
function removeFile(filepath?: string) {
|
||||
if (filepath) {
|
||||
fs.unlinkSync(filepath);
|
||||
}
|
||||
}
|
||||
removeFile(tmpCrtPath);
|
||||
removeFile(tmpKeyPath);
|
||||
removeFile(tmpPfxPath);
|
||||
removeFile(tmpDerPath);
|
||||
}
|
||||
}
|
||||
|
||||
buildCertFileName(suffix: string, applyTime: number, prefix = "cert") {
|
||||
const detail = this.getCrtDetail();
|
||||
let domain = detail.detail.domains.commonName;
|
||||
domain = domain.replace(".", "_").replace("*", "_");
|
||||
const timeStr = dayjs(applyTime).format("YYYYMMDDHHmmss");
|
||||
return `${prefix}_${domain}_${timeStr}.${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ILogger, sp } from "@certd/pipeline";
|
||||
import type { CertInfo } from "../cert-plugin/acme.js";
|
||||
import { CertReader, CertReaderHandleContext } from "../cert-plugin/cert-reader.js";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import fs from "fs";
|
||||
|
||||
export { CertReader };
|
||||
export type { CertInfo };
|
||||
|
||||
export class CertConverter {
|
||||
logger: ILogger;
|
||||
|
||||
constructor(opts: { logger: ILogger }) {
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
async convert(opts: { cert: CertInfo; pfxPassword: string }): Promise<{
|
||||
pfxPath: string;
|
||||
derPath: string;
|
||||
}> {
|
||||
const certReader = new CertReader(opts.cert);
|
||||
let pfxPath: string;
|
||||
let derPath: string;
|
||||
const handle = async (opts: CertReaderHandleContext) => {
|
||||
// 调用openssl 转pfx
|
||||
pfxPath = await this.convertPfx(opts);
|
||||
|
||||
// 转der
|
||||
derPath = await this.convertDer(opts);
|
||||
};
|
||||
|
||||
await certReader.readCertFile({ logger: this.logger, handle });
|
||||
|
||||
return {
|
||||
pfxPath,
|
||||
derPath,
|
||||
};
|
||||
}
|
||||
|
||||
async exec(cmd: string) {
|
||||
await sp.spawn({
|
||||
cmd: cmd,
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
private async convertPfx(opts: CertReaderHandleContext, pfxPassword?: string) {
|
||||
const { tmpCrtPath, tmpKeyPath } = opts;
|
||||
|
||||
const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", "cert.pfx");
|
||||
|
||||
const dir = path.dirname(pfxPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
let passwordArg = "-passout pass:";
|
||||
if (pfxPassword) {
|
||||
passwordArg = `-password pass:${pfxPassword}`;
|
||||
}
|
||||
await this.exec(`openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`);
|
||||
return pfxPath;
|
||||
// const fileBuffer = fs.readFileSync(pfxPath);
|
||||
// this.pfxCert = fileBuffer.toString("base64");
|
||||
//
|
||||
// const applyTime = new Date().getTime();
|
||||
// const filename = reader.buildCertFileName("pfx", applyTime);
|
||||
// this.saveFile(filename, fileBuffer);
|
||||
}
|
||||
|
||||
private async convertDer(opts: CertReaderHandleContext) {
|
||||
const { tmpCrtPath } = opts;
|
||||
const derPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.der`);
|
||||
|
||||
const dir = path.dirname(derPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`);
|
||||
|
||||
return derPath;
|
||||
|
||||
// const fileBuffer = fs.readFileSync(derPath);
|
||||
// this.derCert = fileBuffer.toString("base64");
|
||||
//
|
||||
// const applyTime = new Date().getTime();
|
||||
// const filename = reader.buildCertFileName("der", applyTime);
|
||||
// this.saveFile(filename, fileBuffer);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../.
|
||||
import { CertReader } from "./cert-reader.js";
|
||||
import { CertApplyBasePlugin } from "./base.js";
|
||||
|
||||
export { CertReader };
|
||||
export type { CertInfo };
|
||||
export * from "./cert-reader.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "CertApply",
|
||||
@@ -37,11 +37,31 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
{ value: "zerossl", label: "ZeroSSL" },
|
||||
],
|
||||
},
|
||||
helper: "如果letsencrypt.org或dv.acme-v02.api.pki.goog无法访问,请尝试开启代理选项\n如果使用ZeroSSL、google证书,需要提供EAB授权",
|
||||
helper: "Let's Encrypt最简单,如果使用ZeroSSL、google证书,需要提供EAB授权",
|
||||
required: true,
|
||||
})
|
||||
sslProvider!: SSLProvider;
|
||||
|
||||
@TaskInput({
|
||||
title: "EAB授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "eab",
|
||||
},
|
||||
maybeNeed: true,
|
||||
required: true,
|
||||
helper:
|
||||
"需要提供EAB授权\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials' \n Google:请查看[google获取eab帮助文档](https://github.com/certd/certd/blob/v2/doc/google/google.md)",
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.sslProvider === 'zerossl' || form.sslProvider === 'google'
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
eabAccessId!: number;
|
||||
|
||||
@TaskInput({
|
||||
title: "加密算法",
|
||||
value: "rsa_2048",
|
||||
@@ -62,18 +82,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
})
|
||||
privateKeyType!: PrivateKeyType;
|
||||
|
||||
@TaskInput({
|
||||
title: "EAB授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
type: "eab",
|
||||
},
|
||||
maybeNeed: true,
|
||||
helper:
|
||||
"如果使用ZeroSSL或者google证书,需要提供EAB授权\nZeroSSL:请前往 https://app.zerossl.com/developer 生成 'EAB Credentials' \n Google:请前往https://github.com/certd/certd/blob/v2/doc/google/google.md",
|
||||
})
|
||||
eabAccessId!: number;
|
||||
|
||||
@TaskInput({
|
||||
title: "DNS提供商",
|
||||
component: {
|
||||
@@ -81,7 +89,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
},
|
||||
required: true,
|
||||
helper:
|
||||
"请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您的dns解析提供商,您可以将域名解析服务器设置成上面的任意一个提供商",
|
||||
"请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商,您需要将域名解析服务器设置成上面的任意一个提供商",
|
||||
})
|
||||
dnsProviderType!: string;
|
||||
|
||||
@@ -92,13 +100,14 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
},
|
||||
required: true,
|
||||
helper: "请选择dns解析提供商授权",
|
||||
reference: [
|
||||
{
|
||||
src: "form.dnsProviderType",
|
||||
dest: "component.type",
|
||||
type: "computed",
|
||||
},
|
||||
],
|
||||
mergeScript: `return {
|
||||
component:{
|
||||
type: ctx.compute(({form})=>{
|
||||
return form.dnsProviderType
|
||||
})
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
dnsProviderAccess!: string;
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
FROM node:20-alpine AS builder
|
||||
FROM node:18-alpine AS builder
|
||||
EXPOSE 7001
|
||||
WORKDIR /workspace/
|
||||
COPY . /workspace/
|
||||
RUN npm install -g pnpm
|
||||
# armv7 目前只能用node18, pnpm9不支持node18,所以pnpm只能用8.15.7版本
|
||||
# https://github.com/nodejs/docker-node/issues/1946
|
||||
RUN npm install -g pnpm@8.15.7
|
||||
|
||||
#RUN cd /workspace/certd-client && pnpm install && npm run build
|
||||
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
|
||||
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
|
||||
RUN cd /workspace/certd-server && yarn install && npm run build-on-docker
|
||||
|
||||
|
||||
FROM node:20-alpine
|
||||
FROM node:18-alpine
|
||||
RUN apk add --no-cache openssl
|
||||
WORKDIR /app/
|
||||
COPY --from=builder /workspace/certd-server/ /app/
|
||||
RUN chmod +x /app/tools/linux/*
|
||||
|
||||
@@ -3,6 +3,29 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 插件选择支持搜索 ([d1498a7](https://github.com/certd/certd/commit/d1498a71601b74d38343b1d070eadd03705dd9d5))
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/ui-client
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 阶段、任务、步骤全面支持拖动排序 ([bd73a16](https://github.com/certd/certd/commit/bd73a163cd0497f062bd424ddc6bc9bbc95f81ea))
|
||||
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
|
||||
* 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327))
|
||||
* client 请求超时时间延长为10s ([ff46771](https://github.com/certd/certd/commit/ff46771d8dd43e71c1ca70e3ba783945750342cc))
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-client",
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
@@ -55,16 +55,16 @@
|
||||
"vue-cropperjs": "^5.0.0",
|
||||
"vue-i18n": "^9.10.2",
|
||||
"vue-router": "^4.3.0",
|
||||
"vuedraggable": "^2.24.3"
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/pipeline": "^1.24.1",
|
||||
"@certd/pipeline": "^1.24.4",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/chai": "^4.3.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^20.11.28",
|
||||
"@types/node": "^18",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
|
||||
@@ -112,7 +112,7 @@ function createRequestFunction(service: any) {
|
||||
headers: {
|
||||
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
||||
},
|
||||
timeout: 5000,
|
||||
timeout: 10000,
|
||||
baseURL: env.API,
|
||||
data: {}
|
||||
};
|
||||
|
||||
@@ -70,6 +70,7 @@ const onError = (error: any) => {
|
||||
}
|
||||
.vcron-select-input {
|
||||
min-height: 22px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.vcron-select-container {
|
||||
display: flex;
|
||||
|
||||
@@ -3,7 +3,7 @@ import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
|
||||
import PiDnsProviderSelector from "./dns-provider-selector/index.vue";
|
||||
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
|
||||
import PiEditable from "./editable.vue";
|
||||
import VipButton from "./vip-button/index.vue";
|
||||
import vip from "./vip-button/install.js";
|
||||
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
|
||||
import CronEditor from "./cron-editor/index.vue";
|
||||
import { CronLight } from "@vue-js-cron/light";
|
||||
@@ -15,12 +15,14 @@ export default {
|
||||
app.component("PiEditable", PiEditable);
|
||||
app.component("PiOutputSelector", PiOutputSelector);
|
||||
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
|
||||
app.component("VipButton", VipButton);
|
||||
|
||||
app.component("CronLight", CronLight);
|
||||
app.component("CronEditor", CronEditor);
|
||||
|
||||
app.component("CheckCircleOutlined", CheckCircleOutlined);
|
||||
app.component("InfoCircleOutlined", InfoCircleOutlined);
|
||||
app.component("UndoOutlined", UndoOutlined);
|
||||
|
||||
app.use(vip);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { message, notification } from "ant-design-vue";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
export default {
|
||||
mounted(el: any, binding: any, vnode: any) {
|
||||
const { value } = binding;
|
||||
const userStore = useUserStore();
|
||||
el.className = el.className + " need-plus";
|
||||
if (!userStore.isPlus) {
|
||||
function checkPlus() {
|
||||
// 事件处理代码
|
||||
notification.warn({
|
||||
message: "此为专业版功能,请升级到专业版"
|
||||
});
|
||||
}
|
||||
el.addEventListener("click", function (event: any) {
|
||||
checkPlus();
|
||||
});
|
||||
el.addEventListener("move", function (event: any) {
|
||||
checkPlus();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -129,8 +129,10 @@ function openUpgrade() {
|
||||
<div>
|
||||
<h3 class="block-header">专业版特权</h3>
|
||||
<ul>
|
||||
<li>证书流水线数量无限制</li>
|
||||
<li>可加VIP群,需求优先实现</li>
|
||||
<li>证书流水线数量无限制(免费版限制10条)</li>
|
||||
<li>免配置发邮件功能</li>
|
||||
<li>FTP上传、cdnfly、宝塔、易盾等部署插件</li>
|
||||
<li>更多特权敬请期待</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import VipButton from "./index.vue";
|
||||
import plus from "./directive.js";
|
||||
export default function (app: any) {
|
||||
app.component("VipButton", VipButton);
|
||||
app.directive("plus", plus);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as UserApi from "/src/api/modules/api.user";
|
||||
import { RegisterReq } from "/src/api/modules/api.user";
|
||||
// @ts-ignore
|
||||
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
|
||||
import { Modal, notification } from "ant-design-vue";
|
||||
import { message, Modal, notification } from "ant-design-vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import { mitter } from "/src/utils/util.mitt";
|
||||
@@ -67,7 +67,14 @@ export const useUserStore = defineStore({
|
||||
LocalStorage.remove(TOKEN_KEY);
|
||||
LocalStorage.remove(USER_INFO_KEY);
|
||||
},
|
||||
|
||||
checkPlus() {
|
||||
if (!this.isPlus) {
|
||||
notification.warn({
|
||||
message: "此为专业版功能,请先升级到专业版"
|
||||
});
|
||||
throw new Error("此为专业版功能,请升级到专业版");
|
||||
}
|
||||
},
|
||||
async register(user: RegisterReq) {
|
||||
await UserApi.register(user);
|
||||
notification.success({
|
||||
|
||||
@@ -22,15 +22,14 @@ h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.fs-desc{
|
||||
.fs-desc {
|
||||
font-size: 12px;
|
||||
color:#888888;
|
||||
color: #888888;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ant-btn-link {
|
||||
height: 24px;
|
||||
}
|
||||
@@ -45,69 +44,99 @@ h1, h2, h3, h4, h5, h6 {
|
||||
vertical-align: 0 !important;
|
||||
}
|
||||
|
||||
.pointer{
|
||||
cursor: pointer;
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flex-center{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-o{
|
||||
|
||||
.flex-o {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex{
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-1{
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mb-2{
|
||||
margin-bottom:2px;
|
||||
}
|
||||
.ml-5{
|
||||
margin-left:5px;
|
||||
}
|
||||
.ml-20{
|
||||
margin-left:20px;
|
||||
}
|
||||
.ml-15{
|
||||
margin-left:15px;
|
||||
.flex-col{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mr-5{
|
||||
.scroll-y{
|
||||
overflow-y: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.ml-20 {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ml-15 {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.mr-20{
|
||||
margin-right: 20px;
|
||||
}
|
||||
.mr-15{
|
||||
margin-right: 15px;
|
||||
}
|
||||
.mt-5{
|
||||
margin-top:5px;
|
||||
}
|
||||
.mt-10{
|
||||
margin-top:10px;
|
||||
}
|
||||
.mb-10{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.m-10{
|
||||
margin:10px;
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.p-5{
|
||||
padding:5px;
|
||||
.mr-20 {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.p-10{
|
||||
padding:10px;
|
||||
|
||||
.mr-15 {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.mt-5 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.m-10 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
@@ -116,18 +145,42 @@ h1, h2, h3, h4, h5, h6 {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.w-100{
|
||||
width: 100%;
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.block-header{
|
||||
margin:3px;
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
margin: 3px;
|
||||
padding-top: 15px;
|
||||
padding-bottom:3px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
|
||||
|
||||
.color-blue{
|
||||
color: #1890ff;
|
||||
}
|
||||
.color-blue {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
|
||||
.icon-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fs-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.need-plus {
|
||||
color: #c5913f !important;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import _ from "lodash-es";
|
||||
import { compute } from "@fast-crud/fast-crud";
|
||||
|
||||
export function useReference(form: any) {
|
||||
if (!form.reference) {
|
||||
return;
|
||||
}
|
||||
for (const reference of form.reference) {
|
||||
_.set(
|
||||
form,
|
||||
reference.dest,
|
||||
compute<any>((scope) => {
|
||||
return _.get(scope, reference.src);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
40
packages/ui/certd-client/src/use/use-refrence.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import _ from "lodash-es";
|
||||
import { compute } from "@fast-crud/fast-crud";
|
||||
|
||||
export function useReference(formItem: any) {
|
||||
if (formItem.reference) {
|
||||
for (const reference of formItem.reference) {
|
||||
_.set(
|
||||
formItem,
|
||||
reference.dest,
|
||||
compute<any>((scope) => {
|
||||
return _.get(scope, reference.src);
|
||||
})
|
||||
);
|
||||
}
|
||||
delete formItem.reference;
|
||||
}
|
||||
|
||||
if (formItem.mergeScript) {
|
||||
const ctx = {
|
||||
compute
|
||||
};
|
||||
const script = formItem.mergeScript;
|
||||
const func = new Function("ctx", script);
|
||||
const merged = func(ctx);
|
||||
_.merge(formItem, merged);
|
||||
|
||||
delete formItem.mergeScript;
|
||||
}
|
||||
//helper
|
||||
if (formItem.helper && typeof formItem.helper === "string") {
|
||||
//正则表达式替换 [name](url) 成 <a href="url" >
|
||||
let helper = formItem.helper.replace(/\[(.*)\]\((.*)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
helper = helper.replace(/\n/g, "<br/>");
|
||||
formItem.helper = {
|
||||
render: () => {
|
||||
return <div innerHTML={helper}></div>;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud";
|
||||
import { ColumnCompositionProps, dict, compute } from "@fast-crud/fast-crud";
|
||||
// @ts-ignore
|
||||
import * as api from "./api";
|
||||
// @ts-ignore
|
||||
@@ -32,11 +32,25 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
|
||||
...value,
|
||||
key
|
||||
};
|
||||
const column = _.merge({ title: key }, defaultPluginConfig, field);
|
||||
let column = _.merge({ title: key }, defaultPluginConfig, field);
|
||||
|
||||
//eval
|
||||
if (column.mergeScript) {
|
||||
const ctx = {
|
||||
compute
|
||||
};
|
||||
const script = column.mergeScript;
|
||||
delete column.mergeScript;
|
||||
const func = new Function("ctx", script);
|
||||
const merged = func(ctx);
|
||||
column = _.merge(column, merged);
|
||||
}
|
||||
|
||||
//设置默认值
|
||||
if (column.value != null && _.get(form, key) == null) {
|
||||
//设置默认值
|
||||
_.set(form, key, column.value);
|
||||
}
|
||||
//字段配置赋值
|
||||
columnsRef.value[key] = column;
|
||||
console.log("form", columnsRef.value);
|
||||
});
|
||||
@@ -55,7 +69,12 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
|
||||
},
|
||||
form: {
|
||||
component: {
|
||||
disabled: false
|
||||
disabled: false,
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: any) => {
|
||||
input = input?.toLowerCase();
|
||||
return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0;
|
||||
}
|
||||
},
|
||||
rules: [{ required: true, message: "请选择类型" }],
|
||||
valueChange: {
|
||||
|
||||
@@ -22,12 +22,17 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
||||
form: {
|
||||
...inputDefine,
|
||||
show: compute((ctx) => {
|
||||
console.log(formWrapperRef);
|
||||
const form = formWrapperRef.value.getFormData();
|
||||
if (!form) {
|
||||
return false;
|
||||
}
|
||||
return form?.certApplyPlugin === plugin.name;
|
||||
|
||||
let inputDefineShow = true;
|
||||
if (inputDefine.show != null) {
|
||||
const computeShow = inputDefine.show as any;
|
||||
inputDefineShow = computeShow.computeFn({ form });
|
||||
}
|
||||
return form?.certApplyPlugin === plugin.name && inputDefineShow;
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -60,8 +65,8 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
||||
render: () => {
|
||||
return (
|
||||
<ul>
|
||||
<li>Lego-ACME:基于Lego实现,支持海量DNS提供商</li>
|
||||
<li>JS-ACME:如果你的域名DNS属于阿里云、腾讯云、Cloudflare可以选择用它来申请</li>
|
||||
<li>JS-ACME:如果你的域名DNS属于阿里云、腾讯云、Cloudflare、西部数码可以选择用它来申请</li>
|
||||
<li>Lego-ACME:基于Lego实现,支持海量DNS提供商,熟悉LEGO的用户可以使用</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +83,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
||||
vModel: "modelValue",
|
||||
placeholder: "0 0 4 * * *"
|
||||
},
|
||||
helper: "请输入cron表达式, 例如:0 0 4 * * *,每天凌晨4点触发",
|
||||
helper: "点击上面的按钮,选择每天几点几分定时执行, 例如:0 0 4 * * *,每天凌晨4点0分0秒触发",
|
||||
order: 100
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,11 +10,52 @@ import { env } from "/@/utils/util.env";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
import dayjs from "dayjs";
|
||||
import { useSettingStore } from "/@/store/modules/settings";
|
||||
import _ from "lodash-es";
|
||||
|
||||
export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const lastResRef = ref();
|
||||
|
||||
function setRunnableIds(pipeline: any) {
|
||||
const idMap: any = {};
|
||||
function createId(oldId: any) {
|
||||
if (oldId == null) {
|
||||
return nanoid();
|
||||
}
|
||||
const newId = nanoid();
|
||||
idMap[oldId] = newId;
|
||||
return newId;
|
||||
}
|
||||
if (pipeline.stages) {
|
||||
for (const stage of pipeline.stages) {
|
||||
stage.id = createId(stage.id);
|
||||
if (stage.tasks) {
|
||||
for (const task of stage.tasks) {
|
||||
task.id = createId(task.id);
|
||||
if (task.steps) {
|
||||
for (const step of task.steps) {
|
||||
step.id = createId(step.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const trigger of pipeline.triggers) {
|
||||
trigger.id = nanoid();
|
||||
}
|
||||
for (const notification of pipeline.notifications) {
|
||||
notification.id = nanoid();
|
||||
}
|
||||
|
||||
let content = JSON.stringify(pipeline);
|
||||
for (const key in idMap) {
|
||||
content = content.replaceAll(key, idMap[key]);
|
||||
}
|
||||
return JSON.parse(content);
|
||||
}
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
@@ -34,9 +75,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||
title: form.title
|
||||
});
|
||||
} else {
|
||||
const content = JSON.parse(form.content);
|
||||
content.title = form.title;
|
||||
form.content = JSON.stringify(content);
|
||||
//复制的流水线
|
||||
delete form.status;
|
||||
delete form.lastHistoryTime;
|
||||
delete form.lastVars;
|
||||
delete form.createTime;
|
||||
delete form.id;
|
||||
let pipeline = JSON.parse(form.content);
|
||||
pipeline.title = form.title;
|
||||
pipeline = setRunnableIds(pipeline);
|
||||
form.content = JSON.stringify(pipeline);
|
||||
}
|
||||
|
||||
const res = await api.AddObj(form);
|
||||
@@ -48,12 +96,11 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||
// 添加certd pipeline
|
||||
const triggers = [];
|
||||
if (form.triggerCron) {
|
||||
triggers.push({ id: nanoid(), title: "定时触发", type: "timer", props: { cron: form.triggerCron } });
|
||||
triggers.push({ title: "定时触发", type: "timer", props: { cron: form.triggerCron } });
|
||||
}
|
||||
const notifications = [];
|
||||
if (form.emailNotify) {
|
||||
notifications.push({
|
||||
id: nanoid(),
|
||||
type: "email",
|
||||
when: ["error", "turnToSuccess"],
|
||||
options: {
|
||||
@@ -61,19 +108,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||
}
|
||||
});
|
||||
}
|
||||
const pipeline = {
|
||||
let pipeline = {
|
||||
title: form.domains[0] + "证书自动化",
|
||||
stages: [
|
||||
{
|
||||
id: nanoid(),
|
||||
title: "证书申请阶段",
|
||||
tasks: [
|
||||
{
|
||||
id: nanoid(),
|
||||
title: "证书申请任务",
|
||||
steps: [
|
||||
{
|
||||
id: nanoid(),
|
||||
title: "申请证书",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
@@ -92,8 +136,10 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||
triggers,
|
||||
notifications
|
||||
};
|
||||
pipeline = setRunnableIds(pipeline);
|
||||
|
||||
const id = await api.Save({
|
||||
title: pipeline.title,
|
||||
content: JSON.stringify(pipeline),
|
||||
keepHistoryCount: 30
|
||||
});
|
||||
@@ -145,15 +191,18 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
||||
},
|
||||
copy: {
|
||||
click: async (context) => {
|
||||
userStore.checkPlus();
|
||||
const { ui } = useUi();
|
||||
// @ts-ignore
|
||||
const row = context[ui.tableColumn.row];
|
||||
let row = context[ui.tableColumn.row];
|
||||
row = _.cloneDeep(row);
|
||||
row.title = row.title + "_copy";
|
||||
await crudExpose.openCopy({
|
||||
row: row,
|
||||
index: context.index
|
||||
});
|
||||
}
|
||||
},
|
||||
class: "need-plus"
|
||||
},
|
||||
config: {
|
||||
order: 1,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="700px" @after-open-change="stepDrawerOnAfterVisibleChange">
|
||||
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="700px">
|
||||
<template #title>
|
||||
编辑步骤
|
||||
<a-button v-if="editMode" @click="stepDelete()">
|
||||
@@ -8,43 +8,61 @@
|
||||
</template>
|
||||
<template v-if="currentStep">
|
||||
<pi-container v-if="currentStep._isAdd" class="pi-step-form">
|
||||
<a-tabs tab-position="left">
|
||||
<a-tab-pane v-for="group of pluginGroups.groups" :key="group.key" :tab="group.title">
|
||||
<a-row :gutter="10">
|
||||
<a-col v-for="item of group.plugins" :key="item.key" class="step-plugin" :span="12">
|
||||
<a-card
|
||||
hoverable
|
||||
:class="{ current: item.name === currentStep.type }"
|
||||
@click="stepTypeSelected(item)"
|
||||
@dblclick="
|
||||
stepTypeSelected(item);
|
||||
stepTypeSave();
|
||||
"
|
||||
>
|
||||
<a-card-meta>
|
||||
<template #title>
|
||||
<a-avatar :src="item.icon || '/images/plugin.png'" />
|
||||
<span class="title">{{ item.title }}</span>
|
||||
<vip-button v-if="item.needPlus" mode="icon" />
|
||||
</template>
|
||||
<template #description>
|
||||
<span :title="item.desc">{{ item.desc }}</span>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div style="padding: 20px; margin-left: 100px">
|
||||
<a-button v-if="editMode" type="primary" @click="stepTypeSave"> 确定 </a-button>
|
||||
<template #header>
|
||||
<a-row :gutter="10" class="mb-10">
|
||||
<a-col :span="24" style="padding-left: 20px">
|
||||
<a-input-search v-model:value="pluginSearch.keyword" placeholder="搜索插件" :allow-clear="true" :show-search="true"></a-input-search>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<div class="flex-col h-100 w-100 overflow-hidden">
|
||||
<a-tabs v-model:active-key="pluginGroupActive" tab-position="left" class="flex-1 overflow-hidden">
|
||||
<a-tab-pane v-for="group of computedPluginGroups" :key="group.key" :tab="group.title" class="scroll-y">
|
||||
<a-row v-if="!group.plugins || group.plugins.length === 0" :gutter="10">
|
||||
<a-col class="flex-o">
|
||||
<div class="flex-o m-10">没有找到插件</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-else :gutter="10">
|
||||
<a-col v-for="item of group.plugins" :key="item.key" class="step-plugin" :span="12">
|
||||
<a-card
|
||||
hoverable
|
||||
:class="{ current: item.name === currentStep.type }"
|
||||
@click="stepTypeSelected(item)"
|
||||
@dblclick="
|
||||
stepTypeSelected(item);
|
||||
stepTypeSave();
|
||||
"
|
||||
>
|
||||
<a-card-meta>
|
||||
<template #title>
|
||||
<a-avatar :src="item.icon || '/images/plugin.png'" />
|
||||
<span class="title">{{ item.title }}</span>
|
||||
<vip-button v-if="item.needPlus" mode="icon" />
|
||||
</template>
|
||||
<template #description>
|
||||
<span :title="item.desc">{{ item.desc }}</span>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="padding: 20px; margin-left: 100px">
|
||||
<a-button v-if="editMode" type="primary" @click="stepTypeSave"> 确定 </a-button>
|
||||
</div>
|
||||
</template>
|
||||
</pi-container>
|
||||
<pi-container v-else class="pi-step-form">
|
||||
<a-form ref="stepFormRef" class="step-form" :model="currentStep" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||
<template #header>
|
||||
<div class="mb-10">
|
||||
<a-alert type="info" :message="currentPlugin.title" :description="currentPlugin.desc"> </a-alert>
|
||||
</div>
|
||||
</template>
|
||||
<a-form ref="stepFormRef" class="step-form" :model="currentStep" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||
<fs-form-item
|
||||
v-model="currentStep.title"
|
||||
:item="{
|
||||
@@ -59,16 +77,16 @@
|
||||
:get-context-fn="blankFn"
|
||||
/>
|
||||
<template v-for="(item, key) in currentPlugin.input" :key="key">
|
||||
<fs-form-item v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
|
||||
<fs-form-item v-if="item.show !== false" v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
|
||||
</template>
|
||||
|
||||
<fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-form-item v-if="editMode" :wrapper-col="{ span: 14, offset: 4 }">
|
||||
<div v-if="editMode" class="bottom-button">
|
||||
<a-button type="primary" @click="stepSave"> 确定 </a-button>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</pi-container>
|
||||
</template>
|
||||
@@ -77,15 +95,18 @@
|
||||
|
||||
<script lang="tsx">
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { computed, inject, Ref, ref } from "vue";
|
||||
import { computed, inject, Ref, ref, watch } from "vue";
|
||||
import _ from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
import { CopyOutlined } from "@ant-design/icons-vue";
|
||||
import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
import { compute, useCompute } from "@fast-crud/fast-crud";
|
||||
import { useReference } from "/@/use/use-refrence";
|
||||
|
||||
export default {
|
||||
name: "PiStepForm",
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: { CopyOutlined },
|
||||
props: {
|
||||
editMode: {
|
||||
@@ -106,7 +127,6 @@ export default {
|
||||
const mode: Ref = ref("add");
|
||||
const callback: Ref = ref();
|
||||
const currentStep: Ref = ref({ title: undefined, input: {} });
|
||||
const currentPlugin: Ref = ref({});
|
||||
const stepFormRef: Ref = ref(null);
|
||||
const stepDrawerVisible: Ref = ref(false);
|
||||
const rules: Ref = ref({
|
||||
@@ -150,15 +170,10 @@ export default {
|
||||
stepDrawerVisible.value = false;
|
||||
};
|
||||
|
||||
const stepDrawerOnAfterVisibleChange = (val: any) => {
|
||||
console.log("stepDrawerOnAfterVisibleChange", val);
|
||||
};
|
||||
|
||||
const stepOpen = (step: any, emit: any) => {
|
||||
callback.value = emit;
|
||||
currentStep.value = _.merge({ input: {}, strategy: {} }, step);
|
||||
|
||||
console.log("currentStepOpen", currentStep.value);
|
||||
if (step.type) {
|
||||
changeCurrentPlugin(currentStep.value);
|
||||
}
|
||||
@@ -189,33 +204,41 @@ export default {
|
||||
stepOpen(step, emit);
|
||||
};
|
||||
|
||||
const currentPluginDefine = ref();
|
||||
|
||||
function getContext() {
|
||||
return {
|
||||
form: currentStep.value.input
|
||||
};
|
||||
}
|
||||
const { doComputed } = useCompute();
|
||||
const currentPlugin = doComputed(() => {
|
||||
return currentPluginDefine.value;
|
||||
}, getContext);
|
||||
const changeCurrentPlugin = (step: any) => {
|
||||
const stepType = step.type;
|
||||
const pluginDefine = pluginGroups.get(stepType);
|
||||
if (pluginDefine) {
|
||||
step.type = stepType;
|
||||
step._isAdd = false;
|
||||
currentPlugin.value = _.cloneDeep(pluginDefine);
|
||||
for (let key in currentPlugin.value.input) {
|
||||
const input = currentPlugin.value.input[key];
|
||||
if (input?.reference) {
|
||||
for (const reference of input.reference) {
|
||||
_.set(
|
||||
input,
|
||||
reference.dest,
|
||||
computed<any>(() => {
|
||||
const scope = {
|
||||
form: currentStep.value.input
|
||||
};
|
||||
return _.get(scope, reference.src);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
//设置初始值
|
||||
if ((input.default != null || input.value != null) && currentStep.value.input[key] == null) {
|
||||
currentStep.value.input[key] = input.default ?? input.value;
|
||||
}
|
||||
step.type = stepType;
|
||||
step._isAdd = false;
|
||||
|
||||
let pluginDefine = pluginGroups.get(stepType);
|
||||
if (pluginDefine == null) {
|
||||
console.log("插件未找到", stepType);
|
||||
return;
|
||||
}
|
||||
pluginDefine = _.cloneDeep(pluginDefine);
|
||||
const columns = pluginDefine.input;
|
||||
for (let key in columns) {
|
||||
const column = columns[key];
|
||||
useReference(column);
|
||||
}
|
||||
|
||||
currentPluginDefine.value = pluginDefine;
|
||||
|
||||
for (let key in pluginDefine.input) {
|
||||
const column = pluginDefine.input[key];
|
||||
//设置初始值
|
||||
if ((column.default != null || column.value != null) && currentStep.value.input[key] == null) {
|
||||
currentStep.value.input[key] = column.default ?? column.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +281,45 @@ export default {
|
||||
const blankFn = () => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const pluginSearch = ref({
|
||||
keyword: "",
|
||||
result: []
|
||||
});
|
||||
const pluginGroupActive = ref("all");
|
||||
const computedPluginGroups: any = computed(() => {
|
||||
const groups = pluginGroups.groups;
|
||||
if (pluginSearch.value.keyword) {
|
||||
const keyword = pluginSearch.value.keyword.toLowerCase();
|
||||
const list = groups.all.plugins.filter((plugin) => {
|
||||
return (
|
||||
plugin.title?.toLowerCase().includes(keyword) || plugin.desc?.toLowerCase().includes(keyword) || plugin.name?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
return {
|
||||
search: { key: "search", title: "搜索结果", plugins: list }
|
||||
};
|
||||
} else {
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => {
|
||||
return pluginSearch.value.keyword;
|
||||
},
|
||||
(val: any) => {
|
||||
if (val) {
|
||||
pluginGroupActive.value = "search";
|
||||
} else {
|
||||
pluginGroupActive.value = "all";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
pluginGroupActive,
|
||||
computedPluginGroups,
|
||||
pluginSearch,
|
||||
stepTypeSelected,
|
||||
stepTypeSave,
|
||||
pluginGroups,
|
||||
@@ -269,7 +330,6 @@ export default {
|
||||
stepView,
|
||||
stepDrawerShow,
|
||||
stepDrawerVisible,
|
||||
stepDrawerOnAfterVisibleChange,
|
||||
currentStep,
|
||||
currentPlugin,
|
||||
stepSave,
|
||||
@@ -317,8 +377,23 @@ export default {
|
||||
|
||||
<style lang="less">
|
||||
.pi-step-form {
|
||||
.bottom-button {
|
||||
padding: 20px;
|
||||
padding-bottom: 5px;
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 10px;
|
||||
padding: 0px;
|
||||
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
.ant-tabs-tabpane {
|
||||
padding-right: 10px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.ant-card {
|
||||
margin-bottom: 10px;
|
||||
|
||||
|
||||
@@ -43,25 +43,22 @@
|
||||
<a-button type="primary" @click="stepAdd(currentTask)">添加步骤</a-button>
|
||||
</template>
|
||||
</a-descriptions>
|
||||
<a-list class="step-list" item-layout="horizontal" :data-source="currentTask.steps">
|
||||
<template #renderItem="{ item, index }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a key="edit" @click="stepEdit(currentTask, item, index)">编辑</a>
|
||||
<a key="edit" @click="stepCopy(currentTask, item, index)">复制</a>
|
||||
<v-draggable v-model="currentTask.steps" class="step-list" handle=".handle" item-key="id" :disabled="!userStore.isPlus">
|
||||
<template #item="{ element, index }">
|
||||
<div class="step-row">
|
||||
<div class="text">
|
||||
<fs-icon icon="ion:flash"></fs-icon>
|
||||
<h4 class="title">{{ element.title }}</h4>
|
||||
</div>
|
||||
<div class="action">
|
||||
<a key="edit" @click="stepEdit(currentTask, element, index)">编辑</a>
|
||||
<a key="edit" @click="stepCopy(currentTask, element, index)">复制</a>
|
||||
<a key="remove" @click="stepDelete(currentTask, index)">删除</a>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
<template #avatar>
|
||||
<fs-icon icon="ion:flash"></fs-icon>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<fs-icon v-plus class="icon-button handle" title="拖动排序" icon="ion:move-outline"></fs-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list>
|
||||
</v-draggable>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
@@ -85,10 +82,11 @@ import { nanoid } from "nanoid";
|
||||
import PiStepForm from "../step-form/index.vue";
|
||||
import { Modal } from "ant-design-vue";
|
||||
import { CopyOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
import VDraggable from "vuedraggable";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
export default {
|
||||
name: "PiTaskForm",
|
||||
components: { CopyOutlined, PiStepForm },
|
||||
components: { CopyOutlined, PiStepForm, VDraggable },
|
||||
props: {
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
@@ -97,6 +95,7 @@ export default {
|
||||
},
|
||||
emits: ["update"],
|
||||
setup(props: any, ctx: any) {
|
||||
const userStore = useUserStore();
|
||||
function useStep() {
|
||||
const stepFormRef: Ref<any> = ref(null);
|
||||
const currentStepIndex = ref(0);
|
||||
@@ -251,6 +250,7 @@ export default {
|
||||
};
|
||||
}
|
||||
return {
|
||||
userStore,
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 16 },
|
||||
...useTaskForm(),
|
||||
@@ -268,5 +268,42 @@ export default {
|
||||
.ant-list .ant-list-item .ant-list-item-meta .ant-list-item-meta-title {
|
||||
margin: 0;
|
||||
}
|
||||
.ant-list .ant-list-item .ant-list-item-action {
|
||||
display: flex;
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.step-list {
|
||||
padding: 10px;
|
||||
.icon-button {
|
||||
font-size: 18px;
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step-row {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.text {
|
||||
display: flex;
|
||||
> * {
|
||||
margin: 0px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> * {
|
||||
margin-right: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
name: 'cron-editor',
|
||||
vModel: 'modelValue'
|
||||
},
|
||||
helper: 'cron表达式,例如: 0 0 3 * * * ,表示每天凌晨3点触发',
|
||||
helper: '点击上面的按钮,选择每天几点几分定时执行, 例如:0 0 4 * * *,每天凌晨4点0分0秒触发',
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
}"
|
||||
/>
|
||||
|
||||
@@ -20,177 +20,204 @@
|
||||
<div class="layout-left">
|
||||
<div class="pipeline-container">
|
||||
<div class="pipeline">
|
||||
<div class="stages">
|
||||
<div class="stage first-stage">
|
||||
<div class="title">
|
||||
<pi-editable model-value="触发源" :disabled="true" />
|
||||
</div>
|
||||
<div class="tasks">
|
||||
<div class="task-container first-task">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="primary" @click="run">
|
||||
<fs-icon icon="ion:play"></fs-icon>
|
||||
手动触发
|
||||
</a-button>
|
||||
</div>
|
||||
<v-draggable v-model="pipeline.stages" class="stages" item-key="id" handle=".stage-move-handle" :disabled="!userStore.isPlus">
|
||||
<template #header>
|
||||
<div class="stage first-stage">
|
||||
<div class="title stage-move-handle">
|
||||
<pi-editable model-value="触发源" :disabled="true" />
|
||||
</div>
|
||||
<div v-for="(trigger, index) of pipeline.triggers" :key="trigger.id" class="task-container">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="triggerEdit(trigger, index)">
|
||||
<fs-icon icon="ion:time"></fs-icon>
|
||||
{{ trigger.title }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="editMode" class="task-container is-add">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed" @click="triggerAdd">
|
||||
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||
触发源(定时)
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
|
||||
<div class="title">
|
||||
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
|
||||
</div>
|
||||
<div class="tasks">
|
||||
<div
|
||||
v-for="(task, taskIndex) of stage.tasks"
|
||||
:key="task.id"
|
||||
class="task-container"
|
||||
:class="{
|
||||
'first-task': taskIndex === 0
|
||||
}"
|
||||
>
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
<fs-icon v-if="editMode" class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd(index)"></fs-icon>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
|
||||
<a-popover title="步骤">
|
||||
<!-- :open="true"-->
|
||||
<template #content>
|
||||
<div v-for="(item, index) of task.steps" class="flex-o w-100">
|
||||
<span class="ellipsis flex-1">{{ index + 1 }}. {{ item.title }} </span>
|
||||
<pi-status-show :status="item.status?.result"></pi-status-show>
|
||||
<fs-icon class="pointer color-blue" title="重新运行此步骤" icon="SyncOutlined" @click="run(item.id)"></fs-icon>
|
||||
</div>
|
||||
</template>
|
||||
<span class="flex-o w-100">
|
||||
<span class="ellipsis flex-1" :class="{ 'mr-15': editMode }">{{ task.title }}</span>
|
||||
<pi-status-show :status="task.status?.result"></pi-status-show>
|
||||
</span>
|
||||
</a-popover>
|
||||
</a-button>
|
||||
<fs-icon v-if="editMode" class="copy" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="editMode" class="task-container is-add">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-tooltip>
|
||||
<a-button type="dashed" shape="round" @click="taskAdd(stage, index)">
|
||||
<fs-icon class="font-20" icon="ion:add-circle-outline"></fs-icon>
|
||||
并行任务
|
||||
<div class="tasks">
|
||||
<div class="task-container first-task">
|
||||
<div class="line line-right">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="primary" @click="run()">
|
||||
<fs-icon icon="ion:play"></fs-icon>
|
||||
手动触发
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(trigger, index) of pipeline.triggers" :key="trigger.id" class="task-container">
|
||||
<div class="line line-right">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="triggerEdit(trigger, index)">
|
||||
<fs-icon icon="ion:time"></fs-icon>
|
||||
{{ trigger.title }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="editMode" class="stage last-stage">
|
||||
<div class="title">
|
||||
<pi-editable model-value="新阶段" :disabled="true" />
|
||||
</div>
|
||||
<div class="tasks">
|
||||
<div class="task-container first-task">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
<fs-icon class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd()"></fs-icon>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed" @click="stageAdd()">
|
||||
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||
添加任务
|
||||
</a-button>
|
||||
<div v-if="editMode" class="task-container is-add">
|
||||
<div class="line line-right">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed" @click="triggerAdd">
|
||||
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||
触发源(定时)
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-container">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed" @click="notificationAdd()">
|
||||
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
添加通知
|
||||
</a-button>
|
||||
<template #item="{ element: stage, index }">
|
||||
<div :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
|
||||
<div class="title">
|
||||
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
|
||||
<div v-plus class="icon-box stage-move-handle">
|
||||
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="notificationEdit(item, ii as number)">
|
||||
<fs-icon icon="ion:notifications"></fs-icon>
|
||||
【通知】 {{ item.type }}
|
||||
</a-button>
|
||||
</div>
|
||||
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!userStore.isPlus">
|
||||
<template #item="{ element: task, index: taskIndex }">
|
||||
<div
|
||||
class="task-container"
|
||||
:class="{
|
||||
'first-task': taskIndex === 0
|
||||
}"
|
||||
>
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
<fs-icon v-if="editMode" class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd(index)"></fs-icon>
|
||||
</div>
|
||||
<div class="line line-right">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
|
||||
<a-popover title="步骤" :trigger="editMode ? 'none' : 'hover'">
|
||||
<!-- :open="true"-->
|
||||
<template #content>
|
||||
<div v-for="(item, index) of task.steps" class="flex-o w-100">
|
||||
<span class="ellipsis flex-1">{{ index + 1 }}. {{ item.title }} </span>
|
||||
<pi-status-show v-if="!editMode" :status="item.status?.result"></pi-status-show>
|
||||
<fs-icon
|
||||
v-if="!editMode"
|
||||
class="pointer color-blue ml-2"
|
||||
title="重新运行此步骤"
|
||||
icon="SyncOutlined"
|
||||
@click="run(item.id)"
|
||||
></fs-icon>
|
||||
</div>
|
||||
</template>
|
||||
<span class="flex-o w-100">
|
||||
<span class="ellipsis flex-1 task-title" :class="{ 'in-edit': editMode }">{{ task.title }}</span>
|
||||
<pi-status-show :status="task.status?.result"></pi-status-show>
|
||||
</span>
|
||||
</a-popover>
|
||||
</a-button>
|
||||
<div class="icon-box action copy">
|
||||
<fs-icon v-if="editMode" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
|
||||
</div>
|
||||
<div v-plus class="icon-box task-move-handle action drag">
|
||||
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div v-if="editMode" class="task-container is-add">
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="line line-right">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-tooltip>
|
||||
<a-button type="dashed" shape="round" @click="taskAdd(stage, index)">
|
||||
<fs-icon class="font-20" icon="ion:add-circle-outline"></fs-icon>
|
||||
并行任务
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-draggable>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div v-if="editMode" class="stage last-stage">
|
||||
<div class="title">
|
||||
<pi-editable model-value="新阶段" :disabled="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="stage last-stage">
|
||||
<div class="title">
|
||||
<pi-editable model-value="结束" :disabled="true" />
|
||||
</div>
|
||||
<div v-if="pipeline.notifications?.length > 0" class="tasks">
|
||||
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
<div class="tasks">
|
||||
<div class="task-container first-task">
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
<fs-icon class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd()"></fs-icon>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed" @click="stageAdd()">
|
||||
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||
添加任务
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="notificationEdit(item, index)">
|
||||
<fs-icon icon="ion:notifications"></fs-icon>
|
||||
<div class="task-container">
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed" @click="notificationAdd()">
|
||||
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||
|
||||
【通知】 {{ item.type }}
|
||||
</a-button>
|
||||
添加通知
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="notificationEdit(item, ii as number)">
|
||||
<fs-icon icon="ion:notifications"></fs-icon>
|
||||
【通知】 {{ item.type }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tasks">
|
||||
<div class="task-container first-task">
|
||||
<div class="line">
|
||||
<div class="flow-line"></div>
|
||||
<div v-else class="stage last-stage">
|
||||
<div class="title">
|
||||
<pi-editable model-value="结束" :disabled="true" />
|
||||
</div>
|
||||
<div v-if="pipeline.notifications?.length > 0" class="tasks">
|
||||
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" @click="notificationEdit(item, index)">
|
||||
<fs-icon icon="ion:notifications"></fs-icon>
|
||||
|
||||
【通知】 {{ item.type }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed">
|
||||
<fs-icon icon="ion:notifications"></fs-icon>
|
||||
通知未设置
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-else class="tasks">
|
||||
<div class="task-container first-task">
|
||||
<div class="line line-left">
|
||||
<div class="flow-line"></div>
|
||||
</div>
|
||||
<div class="task">
|
||||
<a-button shape="round" type="dashed">
|
||||
<fs-icon icon="ion:notifications"></fs-icon>
|
||||
通知未设置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-draggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,18 +256,19 @@ import PiTriggerForm from "./component/trigger-form/index.vue";
|
||||
import PiNotificationForm from "./component/notification-form/index.vue";
|
||||
import PiTaskView from "./component/task-view/index.vue";
|
||||
import PiStatusShow from "./component/status-show.vue";
|
||||
import VDraggable from "vuedraggable";
|
||||
import _ from "lodash-es";
|
||||
import { message, Modal, notification } from "ant-design-vue";
|
||||
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
|
||||
import { nanoid } from "nanoid";
|
||||
import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type";
|
||||
import type { Runnable } from "@certd/pipeline";
|
||||
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
|
||||
import { FsIcon } from "@fast-crud/fast-crud";
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
export default defineComponent({
|
||||
name: "PipelineEdit",
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
|
||||
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable },
|
||||
props: {
|
||||
pipelineId: {
|
||||
type: [Number, String],
|
||||
@@ -271,6 +299,8 @@ export default defineComponent({
|
||||
router.back();
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const loadCurrentHistoryDetail = async () => {
|
||||
console.log("load history logs");
|
||||
const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id });
|
||||
@@ -626,6 +656,7 @@ export default defineComponent({
|
||||
currentHistory,
|
||||
histories,
|
||||
goBack,
|
||||
userStore,
|
||||
...useTaskRet,
|
||||
...useStageRet,
|
||||
...useTrigger(),
|
||||
@@ -700,39 +731,39 @@ export default defineComponent({
|
||||
.title {
|
||||
padding: 20px;
|
||||
color: gray;
|
||||
}
|
||||
&.first-stage {
|
||||
.line {
|
||||
width: 50% !important;
|
||||
.flow-line {
|
||||
border-left: 0;
|
||||
}
|
||||
display: flex;
|
||||
.stage-move-handle {
|
||||
cursor: move;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
&.last-stage {
|
||||
.line {
|
||||
width: 50% !important;
|
||||
left: 0;
|
||||
right: auto;
|
||||
.flow-line {
|
||||
border-right: 0;
|
||||
}
|
||||
.add-stage-btn {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//.sortable-ghost {
|
||||
// .line {
|
||||
// visibility: hidden;
|
||||
// }
|
||||
//}
|
||||
.line {
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
width: 25px;
|
||||
|
||||
&.line-left {
|
||||
left: 25px;
|
||||
.flow-line {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.line-right {
|
||||
right: 25px;
|
||||
.flow-line {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
height: 100%;
|
||||
margin-left: 28px;
|
||||
margin-right: 28px;
|
||||
border: 1px solid #c7c7c7;
|
||||
border-top: 0;
|
||||
}
|
||||
@@ -751,6 +782,52 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.task-container:first-child {
|
||||
.line {
|
||||
width: 50px;
|
||||
|
||||
&.line-left {
|
||||
left: 0;
|
||||
.flow-line {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.line-right {
|
||||
right: 0;
|
||||
.flow-line {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-stage-btn {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.first-stage {
|
||||
.line {
|
||||
.flow-line {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.last-stage {
|
||||
.line {
|
||||
width: 50% !important;
|
||||
right: auto;
|
||||
.flow-line {
|
||||
border-right: 0;
|
||||
}
|
||||
.add-stage-btn {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tasks {
|
||||
.task-container {
|
||||
width: 100%;
|
||||
@@ -760,18 +837,6 @@ export default defineComponent({
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
&.first-task {
|
||||
.line {
|
||||
.flow-line {
|
||||
margin: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
.add-stage-btn {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -780,14 +845,29 @@ export default defineComponent({
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
|
||||
.copy {
|
||||
.task-title {
|
||||
&.in-edit {
|
||||
margin-right: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
top: 18px;
|
||||
//font-size: 18px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
&.copy {
|
||||
right: 80px;
|
||||
}
|
||||
&.drag {
|
||||
right: 60px;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Pipeline } from "@certd/pipeline";
|
||||
import { FormItemProps } from "@fast-crud/fast-crud";
|
||||
import { DynamicType, FormItemProps } from "@fast-crud/fast-crud";
|
||||
export type PipelineDetail = {
|
||||
pipeline: Pipeline;
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export type PluginDefine = {
|
||||
title: string;
|
||||
desc?: string;
|
||||
input: {
|
||||
[key: string]: FormItemProps;
|
||||
[key: string]: DynamicType<FormItemProps>;
|
||||
};
|
||||
output: {
|
||||
[key: string]: any;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
koa:
|
||||
port: 7001
|
||||
|
||||
# key: ./data/ssl/cert.key
|
||||
# cert: ./data/ssl/cert.crt
|
||||
#plus:
|
||||
# server:
|
||||
# baseUrl: 'http://127.0.0.1:11007'
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复腾讯云cdn证书部署后会自动关闭hsts,http2.0等配置的bug ([7908ab7](https://github.com/certd/certd/commit/7908ab79da624c94fa05849925b15e480e3317c4))
|
||||
* 修复腾讯云tke证书部署报错的bug ([653f409](https://github.com/certd/certd/commit/653f409d91a441850d6381f89a8dd390831f0d5e))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e))
|
||||
|
||||
## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde))
|
||||
|
||||
## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577))
|
||||
* 修复windows下无法执行第二条命令的bug ([71ac8aa](https://github.com/certd/certd/commit/71ac8aae4aa694e1a23761e9761c9fba30b43a21))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac))
|
||||
* 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327))
|
||||
* 西部数据支持用户级的apikey ([1c17b41](https://github.com/certd/certd/commit/1c17b41e160944b073e1849e6f9467c3659a4bfc))
|
||||
* 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5))
|
||||
* 支持阿里云oss ([87a2673](https://github.com/certd/certd/commit/87a2673e8c33dff6eda1b836d92ecc121564ed78))
|
||||
* 支持西部数码DNS ([c59cab1](https://github.com/certd/certd/commit/c59cab1aaeb19f86df8e3e0d8127cbd0a9ef77f3))
|
||||
* 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723))
|
||||
|
||||
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-server",
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.4",
|
||||
"description": "fast-server base midway",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -21,13 +21,13 @@
|
||||
"dependencies": {
|
||||
"@alicloud/cs20151215": "^3.0.3",
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@certd/acme-client": "^1.24.1",
|
||||
"@certd/lib-huawei": "^1.24.1",
|
||||
"@certd/lib-k8s": "^1.24.1",
|
||||
"@certd/midway-flyway-js": "^1.22.6",
|
||||
"@certd/pipeline": "^1.24.1",
|
||||
"@certd/plugin-cert": "^1.24.1",
|
||||
"@certd/plugin-plus": "^1.24.1",
|
||||
"@certd/acme-client": "^1.24.4",
|
||||
"@certd/lib-huawei": "^1.24.3",
|
||||
"@certd/lib-k8s": "^1.24.4",
|
||||
"@certd/midway-flyway-js": "^1.24.4",
|
||||
"@certd/pipeline": "^1.24.4",
|
||||
"@certd/plugin-cert": "^1.24.4",
|
||||
"@certd/plugin-plus": "^1.24.4",
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@midwayjs/bootstrap": "^3.16.2",
|
||||
"@midwayjs/cache": "^3.14.0",
|
||||
@@ -39,6 +39,7 @@
|
||||
"@midwayjs/static-file": "^3.16.4",
|
||||
"@midwayjs/typeorm": "^3.16.4",
|
||||
"@midwayjs/validate": "^3.16.4",
|
||||
"ali-oss": "^6.21.0",
|
||||
"axios": "^1.7.2",
|
||||
"basic-ftp": "^5.0.5",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -46,8 +47,9 @@
|
||||
"cache-manager": "^3.6.3",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^10.4.5",
|
||||
"https-proxy-agent": "^7.0.4",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
@@ -61,20 +63,24 @@
|
||||
"nanoid": "^4.0.0",
|
||||
"nodemailer": "^6.9.3",
|
||||
"pg": "^8.12.0",
|
||||
"querystring": "^0.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ssh2": "^1.15.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"syno": "^2.2.0",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.44",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midwayjs/mock": "^3.16.4",
|
||||
"@types/ali-oss": "^6.16.11",
|
||||
"@types/cache-manager": "^3.4.3",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "16",
|
||||
"@types/node": "^18",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"@types/ssh2": "^1.15.0",
|
||||
"c8": "^8.0.1",
|
||||
@@ -86,7 +92,7 @@
|
||||
"typescript": "~5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -49,13 +49,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
}
|
||||
|
||||
async add(bean: PipelineEntity) {
|
||||
if (!isPlus()) {
|
||||
const count = await this.repository.count();
|
||||
if (count >= freeCount) {
|
||||
throw new NeedVIPException('免费版最多只能创建10个pipeline');
|
||||
}
|
||||
}
|
||||
await super.add(bean);
|
||||
await this.save(bean);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@@ -105,19 +99,37 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
}
|
||||
|
||||
async save(bean: PipelineEntity) {
|
||||
let old = null;
|
||||
if (bean.id > 0) {
|
||||
//修改
|
||||
old = await this.info(bean.id);
|
||||
}
|
||||
const isUpdate = bean.id > 0 && old != null;
|
||||
if (!isPlus()) {
|
||||
const count = await this.repository.count();
|
||||
let count = await this.repository.count();
|
||||
if (isUpdate) {
|
||||
count -= 1;
|
||||
}
|
||||
if (count >= freeCount) {
|
||||
throw new NeedVIPException('免费版最多只能创建10个pipeline');
|
||||
}
|
||||
}
|
||||
if (!isUpdate) {
|
||||
//如果是添加,先保存一下,获取到id,更新pipeline.id
|
||||
await this.addOrUpdate(bean);
|
||||
}
|
||||
await this.clearTriggers(bean.id);
|
||||
if (bean.content) {
|
||||
const pipeline = JSON.parse(bean.content);
|
||||
bean.title = pipeline.title;
|
||||
if (pipeline.title) {
|
||||
bean.title = pipeline.title;
|
||||
}
|
||||
pipeline.id = bean.id;
|
||||
bean.content = JSON.stringify(pipeline);
|
||||
}
|
||||
await this.addOrUpdate(bean);
|
||||
await this.registerTriggerById(bean.id);
|
||||
return bean;
|
||||
}
|
||||
|
||||
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export class CronTask {
|
||||
this.job = req.job;
|
||||
this.name = req.name;
|
||||
this.logger = logger;
|
||||
this.logger.info(`[cron] CronTask created [${this.name}], cron:${this.cron}`);
|
||||
this.genNextTime();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,3 +6,5 @@ export * from './plugin-host/index.js';
|
||||
export * from './plugin-huawei/index.js';
|
||||
export * from './plugin-demo/index.js';
|
||||
export * from './plugin-other/index.js';
|
||||
export * from './plugin-west/index.js';
|
||||
export * from './plugin-doge/index.js';
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { IsAccess, AccessInput } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'aliyun',
|
||||
title: '阿里云授权',
|
||||
desc: '',
|
||||
})
|
||||
export class AliyunAccess {
|
||||
@AccessInput({
|
||||
title: 'accessKeyId',
|
||||
component: {
|
||||
placeholder: 'accessKeyId',
|
||||
},
|
||||
helper: '登录阿里云控制台->AccessKey管理页面获取。',
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = '';
|
||||
@AccessInput({
|
||||
title: 'accessKeySecret',
|
||||
component: {
|
||||
placeholder: 'accessKeySecret',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
helper: '注意:证书申请需要dns解析权限;其他阿里云插件,需要对应的权限,比如证书上传需要证书管理权限;嫌麻烦就用主账号的全量权限的accessKey',
|
||||
})
|
||||
accessKeySecret = '';
|
||||
}
|
||||
|
||||
new AliyunAccess();
|
||||
@@ -1 +0,0 @@
|
||||
export * from './aliyun-access.js';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
|
||||
import { Autowire, ILogger } from '@certd/pipeline';
|
||||
import { AliyunAccess } from '../access/index.js';
|
||||
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
|
||||
|
||||
@IsDnsProvider({
|
||||
name: 'aliyun',
|
||||
@@ -16,13 +16,14 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||
logger!: ILogger;
|
||||
async onInstance() {
|
||||
const access: any = this.access;
|
||||
const Core = await import('@alicloud/pop-core');
|
||||
this.client = new Core.default({
|
||||
|
||||
this.client = new AliyunClient({logger:this.logger})
|
||||
await this.client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: 'https://alidns.aliyuncs.com',
|
||||
apiVersion: '2015-01-09',
|
||||
});
|
||||
})
|
||||
}
|
||||
//
|
||||
// async getDomainList() {
|
||||
@@ -100,6 +101,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||
// Line: 'oversea' // 海外
|
||||
};
|
||||
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './dns-provider/index.js';
|
||||
export * from './plugin/index.js';
|
||||
export * from './access/index.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
|
||||
import { AliyunAccess } from '../../access/index.js';
|
||||
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
|
||||
import { appendTimeSuffix } from '../../utils/index.js';
|
||||
import { CertInfo } from '@certd/plugin-cert';
|
||||
|
||||
@@ -51,7 +51,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
namespace!: string;
|
||||
namespace: string = 'default';
|
||||
@TaskInput({
|
||||
title: 'ingress名称',
|
||||
value: '',
|
||||
@@ -109,7 +109,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
||||
this.K8sClient = sdk.K8sClient;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
console.log('开始部署证书到阿里云cdn');
|
||||
this.logger.info('开始部署证书到阿里云cdn');
|
||||
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
|
||||
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
|
||||
const client = await this.getClient(access, regionId);
|
||||
@@ -144,7 +144,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
||||
},
|
||||
};
|
||||
const ingressList = await k8sClient.getIngressList({ namespace });
|
||||
console.log('ingressList:', ingressList);
|
||||
this.logger.info('ingressList:', ingressList);
|
||||
if (!ingressList || !ingressList.items) {
|
||||
return;
|
||||
}
|
||||
@@ -200,14 +200,15 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(aliyunProvider: any, regionId: string) {
|
||||
const Core = await import('@alicloud/pop-core');
|
||||
|
||||
return new Core.default({
|
||||
const client = new AliyunClient({logger:this.logger})
|
||||
await client.init({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: `https://cs.${regionId}.aliyuncs.com`,
|
||||
apiVersion: '2015-12-15',
|
||||
});
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
async getKubeConfig(client: any, clusterId: string, isPrivateIpAddress = false) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||
import dayjs from 'dayjs';
|
||||
import { AliyunAccess } from '../../access/index.js';
|
||||
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunCDN',
|
||||
title: '部署证书至阿里云CDN',
|
||||
@@ -50,23 +50,23 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
console.log('开始部署证书到阿里云cdn');
|
||||
this.logger.info('开始部署证书到阿里云cdn');
|
||||
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
|
||||
const client = await this.getClient(access);
|
||||
const params = await this.buildParams();
|
||||
await this.doRequest(client, params);
|
||||
console.log('部署完成');
|
||||
this.logger.info('部署完成');
|
||||
}
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
const Core = await import('@alicloud/pop-core');
|
||||
|
||||
return new Core.default({
|
||||
const client = new AliyunClient({logger:this.logger})
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: 'https://cdn.aliyuncs.com',
|
||||
apiVersion: '2018-05-10',
|
||||
});
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
async buildParams() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||
import dayjs from 'dayjs';
|
||||
import { AliyunAccess } from '../../access/index.js';
|
||||
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunDCDN',
|
||||
title: '部署证书至阿里云DCDN',
|
||||
@@ -59,13 +59,14 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
const sdk = await import('@alicloud/pop-core');
|
||||
return new sdk.default({
|
||||
const client = new AliyunClient({logger:this.logger})
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: 'https://dcdn.aliyuncs.com',
|
||||
apiVersion: '2018-01-15',
|
||||
});
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
async buildParams() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
||||
import { appendTimeSuffix, checkRet } from '../../utils/index.js';
|
||||
import { AliyunAccess } from '../../access/index.js';
|
||||
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'uploadCertToAliyun',
|
||||
@@ -86,13 +86,14 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(aliyunProvider: AliyunAccess) {
|
||||
const Core = await import('@alicloud/pop-core');
|
||||
return new Core.default({
|
||||
const client = new AliyunClient({logger:this.logger})
|
||||
await client.init({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: 'https://cas.aliyuncs.com',
|
||||
apiVersion: '2018-07-13',
|
||||
});
|
||||
})
|
||||
return client
|
||||
}
|
||||
}
|
||||
//注册插件
|
||||
|
||||
@@ -34,6 +34,7 @@ export class DemoAccess implements IAccess {
|
||||
},
|
||||
//是否必填
|
||||
required: true,
|
||||
//改属性是否需要加密
|
||||
encrypt: true,
|
||||
})
|
||||
//属性名称
|
||||
|
||||
39
packages/ui/certd-server/src/plugins/plugin-doge/access.ts
Normal file
@@ -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,44 @@
|
||||
import crypto from 'crypto';
|
||||
import querystring from 'querystring';
|
||||
import { DogeCloudAccess } from '../access.js';
|
||||
import { HttpClient } from '@certd/pipeline';
|
||||
|
||||
export class DogeClient {
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
http: HttpClient;
|
||||
constructor(access: DogeCloudAccess, http: HttpClient) {
|
||||
this.accessKey = access.accessKey;
|
||||
this.secretKey = access.secretKey;
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
async request(apiPath: string, data: any = {}, jsonMode = false, ignoreResNullCode = 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 == null && ignoreResNullCode) {
|
||||
//ignore
|
||||
} else if (res.code !== 200) {
|
||||
throw new Error('API Error: ' + res.msg);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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;
|
||||
|
||||
@TaskInput({
|
||||
title: '忽略部署接口报错',
|
||||
helper: '当该域名部署后报错,但是实际上已经部署成功时,可以勾选',
|
||||
value: false,
|
||||
component: {
|
||||
name: 'a-switch',
|
||||
type: 'checked',
|
||||
},
|
||||
})
|
||||
ignoreDeployNullCode = false;
|
||||
|
||||
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,
|
||||
},
|
||||
this.ignoreDeployNullCode
|
||||
);
|
||||
}
|
||||
}
|
||||
new DogeCloudDeployToCDNPlugin();
|
||||
@@ -0,0 +1 @@
|
||||
export * from './deploy-to-cdn/index.js';
|
||||
@@ -3,8 +3,8 @@ import ssh2, { ConnectConfig } from 'ssh2';
|
||||
import path from 'path';
|
||||
import * as _ from 'lodash-es';
|
||||
import { ILogger } from '@certd/pipeline';
|
||||
import iconv from 'iconv-lite';
|
||||
import { SshAccess } from '../access/index.js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
export class AsyncSsh2Client {
|
||||
conn: ssh2.Client;
|
||||
logger: ILogger;
|
||||
@@ -18,7 +18,7 @@ export class AsyncSsh2Client {
|
||||
this.encoding = connConf.encoding;
|
||||
}
|
||||
|
||||
convert(buffer: Buffer) {
|
||||
convert(iconv: any, buffer: Buffer) {
|
||||
if (this.encoding) {
|
||||
return iconv.decode(buffer, this.encoding);
|
||||
}
|
||||
@@ -79,6 +79,8 @@ export class AsyncSsh2Client {
|
||||
this.logger.info('script 为空,取消执行');
|
||||
return;
|
||||
}
|
||||
let iconv: any = await import('iconv-lite');
|
||||
iconv = iconv.default;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.logger.info(`执行命令:[${this.connConf.host}][exec]: ` + script);
|
||||
this.conn.exec(script, (err: Error, stream: any) => {
|
||||
@@ -97,7 +99,7 @@ export class AsyncSsh2Client {
|
||||
}
|
||||
})
|
||||
.on('data', (ret: Buffer) => {
|
||||
const out = this.convert(ret);
|
||||
const out = this.convert(iconv, ret);
|
||||
data += out;
|
||||
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
|
||||
})
|
||||
@@ -106,7 +108,7 @@ export class AsyncSsh2Client {
|
||||
this.logger.error(err);
|
||||
})
|
||||
.stderr.on('data', (ret: Buffer) => {
|
||||
const err = this.convert(ret);
|
||||
const err = this.convert(iconv, ret);
|
||||
data += err;
|
||||
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
|
||||
});
|
||||
@@ -123,22 +125,33 @@ export class AsyncSsh2Client {
|
||||
return;
|
||||
}
|
||||
const output: string[] = [];
|
||||
function ansiHandle(data: string) {
|
||||
data = data.replace(/\[[0-9]+;1H/g, '\n');
|
||||
data = stripAnsi(data);
|
||||
return data;
|
||||
}
|
||||
stream
|
||||
.on('close', () => {
|
||||
this.logger.info('Stream :: close');
|
||||
resolve(output);
|
||||
})
|
||||
.on('data', (ret: Buffer) => {
|
||||
const data = this.convert(ret);
|
||||
this.logger.info('' + data);
|
||||
const data = ansiHandle(ret.toString());
|
||||
this.logger.info(data);
|
||||
output.push(data);
|
||||
})
|
||||
.on('error', (err: any) => {
|
||||
reject(err);
|
||||
this.logger.error(err);
|
||||
})
|
||||
.stderr.on('data', (ret: Buffer) => {
|
||||
const data = this.convert(ret);
|
||||
const data = ansiHandle(ret.toString());
|
||||
output.push(data);
|
||||
this.logger.info(`[${this.connConf.host}][error]: ` + data);
|
||||
});
|
||||
stream.end(script + '\nexit\n');
|
||||
//保证windows下正常退出
|
||||
const exit = '\r\nexit\r\n';
|
||||
stream.end(script + exit);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -182,14 +195,14 @@ export class SshClient {
|
||||
this.logger.info('请注意:windows下,文件目录分隔应该写成\\而不是/');
|
||||
this.logger.info('--------------------------');
|
||||
}
|
||||
const spec = await conn.exec('echo %COMSPEC%');
|
||||
if (spec.toString().trim() === '%COMSPEC%') {
|
||||
const isCmd = await this.isCmd(conn);
|
||||
if (!isCmd) {
|
||||
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
|
||||
} else {
|
||||
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
|
||||
}
|
||||
}
|
||||
await conn.exec(mkdirCmd);
|
||||
await conn.shell(mkdirCmd);
|
||||
}
|
||||
|
||||
await conn.fastPut({ sftp, ...transport });
|
||||
@@ -199,24 +212,69 @@ export class SshClient {
|
||||
});
|
||||
}
|
||||
|
||||
async isCmd(conn: AsyncSsh2Client) {
|
||||
const spec = await conn.exec('echo %COMSPEC%');
|
||||
if (spec.toString().trim() === '%COMSPEC%') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
* Start-Service sshd
|
||||
*
|
||||
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
|
||||
* @param options
|
||||
*/
|
||||
async exec(options: { connectConf: SshAccess; script: string | Array<string> }) {
|
||||
let { script } = options;
|
||||
const { connectConf } = options;
|
||||
if (_.isArray(script)) {
|
||||
script = script as Array<string>;
|
||||
script = script.join('\n');
|
||||
}
|
||||
this.logger.info('执行命令:', script);
|
||||
|
||||
this.logger.info('命令:', script);
|
||||
return await this._call({
|
||||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
return await conn.exec(script as string);
|
||||
let isWinCmd = false;
|
||||
if (connectConf.windows) {
|
||||
isWinCmd = await this.isCmd(conn);
|
||||
}
|
||||
if (isWinCmd) {
|
||||
//组合成&&的形式
|
||||
if (typeof script === 'string') {
|
||||
script = script.split('\n');
|
||||
}
|
||||
script = script as Array<string>;
|
||||
script = script.join('&& ');
|
||||
} else {
|
||||
if (_.isArray(script)) {
|
||||
script = script as Array<string>;
|
||||
script = script.join('\n');
|
||||
}
|
||||
}
|
||||
await conn.exec(script);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async shell(options: { connectConf: SshAccess; script: string }): Promise<string[]> {
|
||||
const { connectConf, script } = options;
|
||||
//废弃
|
||||
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string[]> {
|
||||
let { script } = options;
|
||||
const { connectConf } = options;
|
||||
if (_.isArray(script)) {
|
||||
script = script as Array<string>;
|
||||
if (connectConf.windows) {
|
||||
script = script.join('\r\n');
|
||||
} else {
|
||||
script = script.join('\n');
|
||||
}
|
||||
} else {
|
||||
if (connectConf.windows) {
|
||||
script = script.replaceAll('\n', '\r\n');
|
||||
}
|
||||
}
|
||||
return await this._call({
|
||||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import path from 'path';
|
||||
export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.pem',
|
||||
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.pem',
|
||||
component: {
|
||||
placeholder: './tmp/cert.pem',
|
||||
},
|
||||
@@ -25,12 +25,32 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
||||
crtPath!: string;
|
||||
@TaskInput({
|
||||
title: '私钥保存路径',
|
||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.key',
|
||||
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.key',
|
||||
component: {
|
||||
placeholder: './tmp/cert.key',
|
||||
},
|
||||
})
|
||||
keyPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'PFX证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.pfx',
|
||||
component: {
|
||||
placeholder: './tmp/cert.pfx',
|
||||
},
|
||||
})
|
||||
pfxPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'DER证书保存路径',
|
||||
helper:
|
||||
'需要有写入权限,路径要包含文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.der\n.der和.cer是相同的东西,改个后缀名即可',
|
||||
component: {
|
||||
placeholder: './tmp/cert.der 或 ./tmp/cert.cer',
|
||||
},
|
||||
})
|
||||
derPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
@@ -44,14 +64,28 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
||||
|
||||
@TaskOutput({
|
||||
title: '证书保存路径',
|
||||
type: 'HostCrtPath',
|
||||
})
|
||||
hostCrtPath!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: '私钥保存路径',
|
||||
type: 'HostKeyPath',
|
||||
})
|
||||
hostKeyPath!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: 'PFX保存路径',
|
||||
type: 'HostPfxPath',
|
||||
})
|
||||
hostPfxPath!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: 'DER保存路径',
|
||||
type: 'HostDerPath',
|
||||
})
|
||||
hostDerPath!: string;
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
copyFile(srcFile: string, destFile: string) {
|
||||
@@ -63,37 +97,38 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
let { crtPath, keyPath } = this;
|
||||
let { crtPath, keyPath, pfxPath, derPath } = this;
|
||||
const certReader = new CertReader(this.cert);
|
||||
this.logger.info('将证书写入本地缓存文件');
|
||||
const saveCrtPath = certReader.saveToFile('crt');
|
||||
const saveKeyPath = certReader.saveToFile('key');
|
||||
this.logger.info('本地文件写入成功');
|
||||
try {
|
||||
this.logger.info('复制到目标路径');
|
||||
|
||||
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
|
||||
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
|
||||
// crtPath = path.resolve(crtPath);
|
||||
// keyPath = path.resolve(keyPath);
|
||||
this.copyFile(saveCrtPath, crtPath);
|
||||
this.copyFile(saveKeyPath, keyPath);
|
||||
this.logger.info('证书复制成功:crtPath=', crtPath, ',keyPath=', keyPath);
|
||||
const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath }) => {
|
||||
this.logger.info('复制到目标路径');
|
||||
if (crtPath) {
|
||||
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
|
||||
this.copyFile(tmpCrtPath, crtPath);
|
||||
this.hostCrtPath = crtPath;
|
||||
}
|
||||
if (keyPath) {
|
||||
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
|
||||
this.copyFile(tmpKeyPath, keyPath);
|
||||
this.hostKeyPath = keyPath;
|
||||
}
|
||||
if (pfxPath) {
|
||||
pfxPath = pfxPath.startsWith('/') ? pfxPath : path.join(Constants.dataDir, pfxPath);
|
||||
this.copyFile(tmpPfxPath, pfxPath);
|
||||
this.hostPfxPath = pfxPath;
|
||||
}
|
||||
if (derPath) {
|
||||
derPath = derPath.startsWith('/') ? derPath : path.join(Constants.dataDir, derPath);
|
||||
this.copyFile(tmpDerPath, derPath);
|
||||
this.hostDerPath = derPath;
|
||||
}
|
||||
this.logger.info('请注意,如果使用的是相对路径,那么文件就在你的数据库同级目录下,默认是/data/certd/下面');
|
||||
this.logger.info('请注意,如果使用的是绝对路径,文件在容器内的目录下,你需要给容器做目录映射才能复制到宿主机');
|
||||
} catch (e) {
|
||||
this.logger.error(`复制失败:${e.message}`);
|
||||
throw e;
|
||||
} finally {
|
||||
//删除临时文件
|
||||
this.logger.info('删除临时文件');
|
||||
fs.unlinkSync(saveCrtPath);
|
||||
fs.unlinkSync(saveKeyPath);
|
||||
}
|
||||
};
|
||||
|
||||
await certReader.readCertFile({ logger: this.logger, handle });
|
||||
|
||||
this.logger.info('执行完成');
|
||||
//输出
|
||||
this.hostCrtPath = crtPath;
|
||||
this.hostKeyPath = keyPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
|
||||
vModel: 'value',
|
||||
rows: 6,
|
||||
},
|
||||
helper: '注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行',
|
||||
required: true,
|
||||
})
|
||||
script!: string;
|
||||
@@ -40,11 +41,13 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
|
||||
const { script, accessId } = this;
|
||||
const connectConf = await this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
const ret = await sshClient.exec({
|
||||
|
||||
const scripts = script.split('\n');
|
||||
await sshClient.exec({
|
||||
connectConf,
|
||||
script,
|
||||
script: scripts,
|
||||
});
|
||||
this.logger.info('exec res:', ret);
|
||||
// this.logger.info('exec res:', ret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
||||
import { SshClient } from '../../lib/ssh.js';
|
||||
import { CertInfo, CertReader } from '@certd/plugin-cert';
|
||||
import { CertInfo, CertReader, CertReaderHandleContext } from '@certd/plugin-cert';
|
||||
import * as fs from 'fs';
|
||||
import { SshAccess } from '../../access/index.js';
|
||||
|
||||
@@ -17,8 +17,8 @@ import { SshAccess } from '../../access/index.js';
|
||||
})
|
||||
export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号',
|
||||
title: 'PEM证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.pem',
|
||||
component: {
|
||||
placeholder: '/root/deploy/nginx/cert.pem',
|
||||
},
|
||||
@@ -26,12 +26,31 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
crtPath!: string;
|
||||
@TaskInput({
|
||||
title: '私钥保存路径',
|
||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号',
|
||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.key',
|
||||
component: {
|
||||
placeholder: '/root/deploy/nginx/cert.key',
|
||||
},
|
||||
})
|
||||
keyPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'PFX证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.pfx',
|
||||
component: {
|
||||
placeholder: '/root/deploy/nginx/cert.pfx',
|
||||
},
|
||||
})
|
||||
pfxPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'DER证书保存路径',
|
||||
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号,例如:/tmp/cert.der',
|
||||
component: {
|
||||
placeholder: '/root/deploy/nginx/cert.der',
|
||||
},
|
||||
})
|
||||
derPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
@@ -87,9 +106,23 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
hostKeyPath!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: 'PFX保存路径',
|
||||
})
|
||||
hostPfxPath!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: 'DER保存路径',
|
||||
})
|
||||
hostDerPath!: string;
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
copyFile(srcFile: string, destFile: string) {
|
||||
if (!srcFile || !destFile) {
|
||||
this.logger.warn(`srcFile:${srcFile} 或 destFile:${destFile} 为空,不复制`);
|
||||
return;
|
||||
}
|
||||
const dir = destFile.substring(0, destFile.lastIndexOf('/'));
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
@@ -99,16 +132,17 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
async execute(): Promise<void> {
|
||||
const { crtPath, keyPath, cert, accessId } = this;
|
||||
const certReader = new CertReader(cert);
|
||||
this.logger.info('将证书写入本地缓存文件');
|
||||
const saveCrtPath = certReader.saveToFile('crt');
|
||||
const saveKeyPath = certReader.saveToFile('key');
|
||||
this.logger.info('本地文件写入成功');
|
||||
try {
|
||||
|
||||
const handle = async (opts: CertReaderHandleContext) => {
|
||||
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath } = opts;
|
||||
if (this.copyToThisHost) {
|
||||
this.logger.info('复制到目标路径');
|
||||
this.copyFile(saveCrtPath, crtPath);
|
||||
this.copyFile(saveKeyPath, keyPath);
|
||||
this.logger.info('证书复制成功:crtPath=', crtPath, ',keyPath=', keyPath);
|
||||
this.copyFile(tmpCrtPath, crtPath);
|
||||
this.copyFile(tmpKeyPath, keyPath);
|
||||
this.copyFile(tmpPfxPath, this.pfxPath);
|
||||
this.copyFile(tmpDerPath, this.derPath);
|
||||
|
||||
this.logger.info(`证书复制成功:crtPath=${crtPath},keyPath=${keyPath},pfxPath=${this.pfxPath},derPath=${this.derPath}`);
|
||||
} else {
|
||||
if (!accessId) {
|
||||
throw new Error('主机登录授权配置不能为空');
|
||||
@@ -116,35 +150,49 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
this.logger.info('准备上传文件到服务器');
|
||||
const connectConf: SshAccess = await this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
const transports: any = [];
|
||||
if (crtPath) {
|
||||
transports.push({
|
||||
localPath: tmpCrtPath,
|
||||
remotePath: crtPath,
|
||||
});
|
||||
}
|
||||
if (keyPath) {
|
||||
transports.push({
|
||||
localPath: tmpKeyPath,
|
||||
remotePath: keyPath,
|
||||
});
|
||||
}
|
||||
if (this.pfxPath) {
|
||||
transports.push({
|
||||
localPath: tmpPfxPath,
|
||||
remotePath: this.pfxPath,
|
||||
});
|
||||
}
|
||||
if (this.derPath) {
|
||||
transports.push({
|
||||
localPath: tmpDerPath,
|
||||
remotePath: this.derPath,
|
||||
});
|
||||
}
|
||||
await sshClient.uploadFiles({
|
||||
connectConf,
|
||||
transports: [
|
||||
{
|
||||
localPath: saveCrtPath,
|
||||
remotePath: crtPath,
|
||||
},
|
||||
{
|
||||
localPath: saveKeyPath,
|
||||
remotePath: keyPath,
|
||||
},
|
||||
],
|
||||
transports,
|
||||
mkdirs: this.mkdirs,
|
||||
});
|
||||
this.logger.info('证书上传成功:crtPath=', crtPath, ',keyPath=', keyPath);
|
||||
this.logger.info(`证书上传成功:crtPath=${crtPath},keyPath=${keyPath},pfxPath=${this.pfxPath},derPath=${this.derPath}`);
|
||||
|
||||
//输出
|
||||
this.hostCrtPath = crtPath;
|
||||
this.hostKeyPath = keyPath;
|
||||
this.hostPfxPath = this.pfxPath;
|
||||
this.hostDerPath = this.derPath;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`上传失败:${e.message}`);
|
||||
throw e;
|
||||
} finally {
|
||||
//删除临时文件
|
||||
this.logger.info('删除临时文件');
|
||||
fs.unlinkSync(saveCrtPath);
|
||||
fs.unlinkSync(saveKeyPath);
|
||||
}
|
||||
this.logger.info('执行完成');
|
||||
//输出
|
||||
this.hostCrtPath = crtPath;
|
||||
this.hostKeyPath = keyPath;
|
||||
};
|
||||
await certReader.readCertFile({
|
||||
logger: this.logger,
|
||||
handle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
|
||||
lang: 'cn',
|
||||
error_on_empty: 'no',
|
||||
},
|
||||
timeout: 5000,
|
||||
timeout: 10000,
|
||||
};
|
||||
_.merge(config, options);
|
||||
|
||||
|
||||
@@ -93,20 +93,20 @@ export class DeployToCdnPlugin extends AbstractTaskPlugin {
|
||||
|
||||
buildParams() {
|
||||
return {
|
||||
Https: {
|
||||
Switch: 'on',
|
||||
CertInfo: {
|
||||
Domain: this.domainName,
|
||||
Route: 'Https.CertInfo',
|
||||
Value: JSON.stringify({
|
||||
update: {
|
||||
Certificate: this.cert.crt,
|
||||
PrivateKey: this.cert.key,
|
||||
},
|
||||
},
|
||||
Domain: this.domainName,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async doRequest(params: any) {
|
||||
const client = await this.getClient();
|
||||
const ret = await client.UpdateDomainConfig(params);
|
||||
const ret = await client.ModifyDomainConfig(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info('设置腾讯云CDN证书成功:', ret.RequestId);
|
||||
return ret.RequestId;
|
||||
|
||||
@@ -6,7 +6,7 @@ import dayjs from 'dayjs';
|
||||
name: 'DeployCertToTencentCLB',
|
||||
title: '部署到腾讯云CLB',
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: '暂时只支持单向认证证书,暂时只支持通用负载均衡',
|
||||
desc: '暂时只支持单向认证证书,暂时只支持通用负载均衡,必须开启sni',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -93,14 +93,13 @@ export class DeployToClbPlugin extends AbstractTaskPlugin {
|
||||
accessId!: string;
|
||||
|
||||
client: any;
|
||||
ClbClient: any;
|
||||
async onInstance() {
|
||||
this.client = await this.getClient();
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/clb/v20180317/index.js');
|
||||
this.ClbClient = sdk.v20180317.Client;
|
||||
const ClbClient = sdk.v20180317.Client;
|
||||
|
||||
const accessProvider = (await this.accessService.getById(this.accessId)) as TencentAccess;
|
||||
|
||||
@@ -118,7 +117,7 @@ export class DeployToClbPlugin extends AbstractTaskPlugin {
|
||||
},
|
||||
};
|
||||
|
||||
return new this.ClbClient(clientConfig);
|
||||
return new ClbClient(clientConfig);
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
|
||||
@@ -89,19 +89,16 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
cert!: any;
|
||||
|
||||
TkeClient: any;
|
||||
K8sClient: any;
|
||||
|
||||
async onInstance() {
|
||||
// const TkeClient = this.tencentcloud.tke.v20180525.Client;
|
||||
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/tke/v20220501/index.js');
|
||||
this.TkeClient = sdk.v20220501.Client;
|
||||
const k8sSdk = await import('@certd/lib-k8s');
|
||||
this.K8sClient = k8sSdk.K8sClient;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const accessProvider = await this.accessService.getById(this.accessId);
|
||||
const tkeClient = this.getTkeClient(accessProvider, this.region);
|
||||
const tkeClient = await this.getTkeClient(accessProvider, this.region);
|
||||
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, this.clusterId);
|
||||
|
||||
this.logger.info('kubeconfig已成功获取');
|
||||
@@ -127,7 +124,9 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
|
||||
await this.restartIngress({ k8sClient });
|
||||
}
|
||||
|
||||
getTkeClient(accessProvider: any, region = 'ap-guangzhou') {
|
||||
async getTkeClient(accessProvider: any, region = 'ap-guangzhou') {
|
||||
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/tke/v20180525/index.js');
|
||||
const TkeClient = sdk.v20180525.Client;
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
@@ -141,7 +140,7 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
|
||||
},
|
||||
};
|
||||
|
||||
return new this.TkeClient(clientConfig);
|
||||
return new TkeClient(clientConfig);
|
||||
}
|
||||
|
||||
async getTkeKubeConfig(client: any, clusterId: string) {
|
||||
|
||||
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();
|
||||