Compare commits

..

103 Commits

Author SHA1 Message Date
xiaojunnuo
7145aa60ca v0.2.2 2021-12-04 18:43:58 +08:00
xiaojunnuo
b1181c0f9a refactor: 1 2021-12-04 18:39:05 +08:00
xiaojunnuo
bace2a7c25 refactor: 1 2021-12-04 18:34:52 +08:00
xiaojunnuo
a70b4373de v0.2.1 2021-12-04 17:11:07 +08:00
xiaojunnuo
b7c12e6d91 perf: 支持阿里云 ack ingress 2021-12-04 16:57:12 +08:00
xiaojunnuo
6a88dd476e refactor: 0.2.0 2021-12-02 17:07:10 +08:00
xiaojunnuo
5fbd774266 v0.2.0 2021-12-02 17:01:24 +08:00
xiaojunnuo
bdec010d2e refactor: 1 2021-12-02 17:00:24 +08:00
xiaojunnuo
05a00b7b78 feat: 私钥升级为PKCS8 2021-12-02 16:47:46 +08:00
xiaojunnuo
eaf23c3034 v0.1.21 2021-11-04 18:02:14 +08:00
xiaojunnuo
276a8b35e5 feat: 支持腾讯云nginx-ingress 2021-11-04 18:00:30 +08:00
xiaojunnuo
466d659f6e fix: 修改ssh privateKey参数名 2021-06-09 17:50:18 +08:00
xiaojunnuo
84e26381b5 v0.1.20 2021-06-02 09:15:30 +08:00
xiaojunnuo
469b5a5f69 fix: fix 任务成功后不需要重新运行 2021-06-02 09:14:10 +08:00
xiaojunnuo
ad77ebd2f9 refactor: 1 2021-03-17 18:06:06 +08:00
xiaojunnuo
b75543c3bc v0.1.19 2021-03-16 19:14:31 +08:00
xiaojunnuo
0677275742 refactor: 1 2021-03-16 18:40:16 +08:00
xiaojunnuo
0c3724e0ad refactor: 1 2021-03-16 18:27:24 +08:00
xiaojunnuo
803083d23c refactor: 1 2021-03-16 18:25:11 +08:00
xiaojunnuo
f4f8067a12 refactor: new client 2021-03-15 19:04:46 +08:00
xiaojunnuo
caa9f084d6 v0.1.18 2021-03-15 11:54:09 +08:00
xiaojunnuo
81407b65d1 refactor: dir with md5 2021-02-26 15:27:34 +08:00
xiaojunnuo
8a24293fd7 refactor: dir with md5 2021-02-26 15:25:04 +08:00
xiaojunnuo
8f1886a585 refactor: dir with suffix 2021-02-26 15:23:53 +08:00
xiaojunnuo
0a64e5fa67 refactor: md5 dir 2021-02-26 15:22:21 +08:00
xiaojunnuo
7a70603971 refactor: doc 2021-02-09 21:23:58 +08:00
xiaojunnuo
0d5e00e744 refactor: doc 2021-02-09 21:22:07 +08:00
xiaojunnuo
91ba1433af refactor: doc 2021-02-09 21:20:41 +08:00
xiaojunnuo
12e56d14f2 refactor: icons 2021-02-09 21:13:19 +08:00
xiaojunnuo
7326119f52 refactor: export 2021-02-09 21:07:19 +08:00
xiaojunnuo
136983cf14 refactor: export 2021-02-09 19:01:15 +08:00
xiaojunnuo
105a1b80ae refactor: export 2021-02-09 18:40:39 +08:00
xiaojunnuo
b8000ca533 refactor: export 2021-02-09 18:40:29 +08:00
xiaojunnuo
c3e374e6e5 refactor: export 2021-02-09 18:05:01 +08:00
xiaojunnuo
a9b6e87249 refactor: 按需加载 2021-02-09 17:19:00 +08:00
xiaojunnuo
61de5422bf refactor: logo 2021-02-09 16:30:59 +08:00
xiaojunnuo
f96697f619 refactor: logo 2021-02-09 15:42:10 +08:00
xiaojunnuo
b4560d6370 refactor: docker 2021-02-09 11:03:11 +08:00
xiaojunnuo
a7bcde8d82 refactor: host 2021-02-09 10:57:08 +08:00
xiaojunnuo
34bb4d54c2 refactor: deploy image 2021-02-08 23:01:07 +08:00
xiaojunnuo
e0116a1a03 refactor: deploy image 2021-02-08 22:37:54 +08:00
xiaojunnuo
12fec7939d refactor: deploy 2021-02-08 21:16:56 +08:00
xiaojunnuo
ff8e02cceb v0.1.17 2021-02-08 18:19:45 +08:00
xiaojunnuo
8122bed97f refactor: host 2021-02-08 18:18:23 +08:00
xiaojunnuo
991c3dbb76 v0.1.16 2021-02-08 15:46:27 +08:00
xiaojunnuo
399c23623d refactor: host 2021-02-08 15:46:03 +08:00
xiaojunnuo
2232f21b48 refactor: transfer 2021-02-08 15:12:35 +08:00
xiaojunnuo
e41c084381 refactor: transfer 2021-02-08 15:00:04 +08:00
xiaojunnuo
520b27e0dc refactor: ui 2021-02-08 14:31:12 +08:00
xiaojunnuo
ace7e0247a v0.1.15 2021-02-08 14:18:14 +08:00
xiaojunnuo
9ae414b1c6 refactor: 重构 2021-02-08 14:00:28 +08:00
xiaojunnuo
cb8c8186f1 refactor: 重构 2021-02-08 13:40:28 +08:00
xiaojunnuo
82f86d9556 refactor: move 2021-02-08 00:21:36 +08:00
xiaojunnuo
cfb1034450 refactor: host 2021-02-07 23:17:44 +08:00
xiaojunnuo
2a07442a85 refactor: ui 2021-02-07 18:32:38 +08:00
xiaojunnuo
68c1eff81d refactor: fix 2021-02-07 13:53:30 +08:00
xiaojunnuo
baec15dfc6 refactor: fix 2021-02-07 13:47:09 +08:00
xiaojunnuo
6eb9817296 refactor: fix 2021-02-07 10:54:55 +08:00
xiaojunnuo
b9d5d33aaa refactor: delete task 2021-02-05 18:20:33 +08:00
xiaojunnuo
560519894c refactor: fix bug 2021-02-05 18:13:24 +08:00
xiaojunnuo
9f434b0968 v0.1.14 2021-02-05 17:19:35 +08:00
xiaojunnuo
07066dde87 refactor: exitCode 2021-02-05 17:19:20 +08:00
xiaojunnuo
074c8f7cd0 v0.1.13 2021-02-05 17:08:11 +08:00
xiaojunnuo
e9df2355f4 refactor: fix bug 2021-02-05 17:07:49 +08:00
xiaojunnuo
45547d6f94 refactor: 0.1.12 2021-02-05 14:45:53 +08:00
xiaojunnuo
4a421d5b14 v0.1.12 2021-02-05 14:32:14 +08:00
xiaojunnuo
7b9825eb40 refactor: 重构优化 2021-02-05 14:31:52 +08:00
xiaojunnuo
5cde165f0b refactor: 重构优化 2021-02-05 14:30:31 +08:00
xiaojunnuo
305824ff1a refactor: ui 2021-02-04 22:07:01 +08:00
xiaojunnuo
86ddb72227 refactor: ui 2021-02-04 21:24:07 +08:00
xiaojunnuo
cca33478e4 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	ui/certd-ui/src/api/util.input.handler.js
2021-02-04 20:32:34 +08:00
xiaojunnuo
a8f41d3c48 refactor: form input 2021-02-04 20:31:04 +08:00
xiaojunnuo
a25a15ca6e refactor: 重构优化 2021-02-04 18:44:16 +08:00
xiaojunnuo
a39dac4dbd refactor: rename ui 2021-02-04 11:17:54 +08:00
xiaojunnuo
eab0c3be60 refactor: form input 2021-01-31 02:09:54 +08:00
xiaojunnuo
b4ee3d0dfc refactor: input render 2021-01-30 00:06:50 +08:00
xiaojunnuo
2f03e18c59 refactor: ui 2021-01-28 01:09:17 +08:00
xiaojunnuo
232cd7215e refactor: ui 2021-01-28 01:07:56 +08:00
xiaojunnuo
86b1e9959b refactor: ui 2021-01-28 01:00:06 +08:00
xiaojunnuo
fd130f86fd refactor: define 2021-01-26 00:58:00 +08:00
xiaojunnuo
2669f509e1 refactor: ui 2021-01-24 00:36:53 +08:00
xiaojunnuo
d3619ad60f refactor: ui prepare 2021-01-21 23:59:06 +08:00
xiaojunnuo
c26417d769 refactor: release 2021-01-18 22:31:45 +08:00
xiaojunnuo
2942d39dfe v0.1.11 2021-01-18 22:30:04 +08:00
xiaojunnuo
df65b0509e Merge remote-tracking branch 'origin/master' 2021-01-18 22:29:36 +08:00
xiaojunnuo
fbde35483b refactor: 去掉可选链 2021-01-18 22:28:41 +08:00
xiaojunnuo
7370f8b83b docs: cron 2021-01-15 17:03:35 +08:00
xiaojunnuo
8b0ca1da2e perf: 小优化 2021-01-14 23:04:47 +08:00
xiaojunnuo
466f2b1a02 refactor: md 2021-01-12 09:44:41 +08:00
xiaojunnuo
f1d6cce88c refactor: md 2021-01-12 09:31:04 +08:00
xiaojunnuo
ad7ababb4c docs: md 2021-01-11 23:15:35 +08:00
xiaojunnuo
72fa623674 docs: md 2021-01-11 23:08:24 +08:00
xiaojunnuo
e5d117c134 docs: md 2021-01-11 23:04:23 +08:00
xiaojunnuo
f8944a1331 docs: license 2021-01-11 22:56:55 +08:00
xiaojunnuo
e850855154 docs: md 2021-01-11 22:52:56 +08:00
xiaojunnuo
06eacee90c docs: md 2021-01-11 22:49:24 +08:00
xiaojunnuo
b1e100982e refactor: md 2021-01-11 19:00:12 +08:00
xiaojunnuo
8f30158b00 refactor: 1 2021-01-08 17:17:54 +08:00
xiaojunnuo
576f7db978 refactor: 1 2021-01-08 17:14:15 +08:00
xiaojunnuo
137e043dfe refactor: 1 2021-01-08 16:59:53 +08:00
xiaojunnuo
15467fc233 v0.1.10 2021-01-08 15:57:57 +08:00
xiaojunnuo
eec0fcdcf1 refactor: 1 2021-01-08 15:57:38 +08:00
xiaojunnuo
a41dee015e fix: 修复plugins为null的问题 2021-01-08 15:56:50 +08:00
183 changed files with 4919 additions and 27770 deletions

24
.gitignore vendored
View File

@@ -1,11 +1,27 @@
# IntelliJ project files
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/
.idea
*.iml
out
gen
node_modules/
/test/*.private.*
/other/node-acme-client/.idea/
/*.log
/other/certd-run
/other/node-acme-client
/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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Greper
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

139
README.md
View File

@@ -0,0 +1,139 @@
# CertD
CertD 是一个帮助你全自动申请和部署SSL证书的工具。
后缀D取自linux守护进程的命名风格意为证书守护进程。
## 特性
本项目不仅支持证书申请过程自动化,还可以自动化部署证书,让你的证书永不过期。
* 全自动申请证书
* 全自动部署证书(目前支持服务器上传部署、阿里云、腾讯云等)
* 可与CI/DI工具结合使用
## 免费证书申请说明
* 本项目ssl证书提供商为letencrypt
* 申请过程遵循acme协议
* 需要验证域名所有权一般有两种方式目前本项目仅支持dns-01
* http-01 在网站根目录下放置一份txt文件
* dns-01 需要给域名添加txt解析记录泛域名只能用这种方式
* 证书续期:
* 实际上acme并没有续期概念。
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书。
* 免费证书过期时间90天以后可能还会缩短所以自动化部署必不可少
## 快速开始
本案例演示如何配置自动申请证书并部署到阿里云CDN然后快要到期前自动更新证书并重新部署
1. 环境准备
安装[nodejs](https://nodejs.org/zh-cn/)
2. 生成node项目
通过ui生成 https://certd.docmirror.cn/
开始生成证书,先填写域名,支持将多个域名打到一个证书上
![](./doc/step1.png)
配置证书详细信息
![](./doc/step2.png)
配置证书部署流程
![](./doc/step3.png)
配置好之后点击导出按钮导出一个node项目包
4. 运行
将导出的压缩包解压,然后执行如下命令,即可开始申请证书并部署
```
npm install
npm run certd
```
5. 执行效果
生成的证书默认会存储在 `${home}/.certd/${email}/certs/${domain}/current`目录下
```
[2021-01-08T16:15:04.681] [INFO] certd - 任务完成
[2021-01-08T16:15:04.681] [INFO] certd - ---------------------------任务结果总览--------------------------
[2021-01-08T16:15:04.682] [INFO] certd - 【更新证书】--------------------------------------- [success]
证书申请成功
[2021-01-08T16:15:04.682] [INFO] certd - 【流程1-部署到阿里云CDN】---------------------------- [success] 执行成功
[2021-01-08T16:15:04.682] [INFO] certd - └【上传到阿里云】-------------------------------- [success] 执行成功
[2021-01-08T16:15:04.682] [INFO] certd - └【部署证书到CDN】------------------------------- [success] 执行成功
```
6. 证书续期
实际上没有证书续期的概念,只有重新生成一份新的证书,然后重新部署证书
所以每天定时运行即可当证书过期日前20天时会重新申请新的证书然后执行部署任务。
7. 其他说明
证书的部署任务执行后会记录执行结果,已经成功过的不会重复执行
所以当你临时需要将证书部署到其他地方时,直接追加部署任务,然后再次运行即可
## CI/DI集成与自动续期重新部署
集成前将以上导出的node项目提交到内网git仓库或者私有git仓库由于包含敏感信息不要提交到公开git仓库
### jenkins任务
1. 创建任务
选择构建自由风格的任务
2. 配置git
配置cert-run的git地址
3. 构建触发器
配置 `H 3 * * *` 每天凌晨3点-4点执行一次
4. 构建环境
勾选 `Provide Node & npm bin/ folder to PATH`提供nodejs运行环境
如果没有此选项需要jenkins安装`nodejs`插件
5. 构建
执行shell
```
npm install --production #执行过一次之后,就可以注释掉,加快执行速度
npm run post
```
6. 构建后操作
邮件通知
配置你的邮箱地址,可以在执行失败时收到邮件通知。
## API
先列个提纲,待完善
参数示例参考https://gitee.com/certd/certd/blob/master/test/options.js
### 授权提供者
用于dns验证接口调用
#### aliyun
#### dnspod
### deploy插件
部署任务插件
#### 阿里云
##### 上传到阿里云
type = uploadCertToAliyun
##### 部署到阿里云DNS
type = deployCertToAliyunCDN
##### 部署到阿里云CLB
type = deployCertToAliyunCLB
#### 腾讯云
##### 上传到腾讯云
type = uploadCertToTencent
##### 部署到腾讯云DNS
type = deployCertToTencentDNS
##### 部署到腾讯云CLB
type = deployCertToTencentCLB
##### 部署到腾讯云TKE-ingress
type = deployCertToTencentTKEIngress
### 更多部署插件
等你来提需求

BIN
doc/step1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doc/step2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
doc/step3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
doc/tasks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,6 +1,6 @@
{
"packages": [
"packages/*"
"packages/*/*"
],
"version": "0.1.9"
"version": "0.2.2"
}

View File

@@ -6,10 +6,11 @@
"lerna": "^3.18.4"
},
"scripts": {
"submodule": "git submodule update --init --recursive"
"start": "lerna bootstrap --hoist",
"i-all": "lerna link && lerna exec npm install "
},
"license": "MIT",
"dependencies": {
"lodash": "^4.17.20"
"lodash-es": "^4.17.20"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
{
"name": "@certd/api",
"version": "0.1.7",
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"log4js": "^6.3.0",
"qs": "^6.9.4"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
}
}

View File

@@ -1,4 +0,0 @@
export { AbstractDnsProvider } from './dns-provider/index.js'
export { Store } from './store/store.js'
export { util } from './utils/index.js'
export { AbstractPlugin } from './plugin/index.js'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2020
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}

7
packages/core/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -0,0 +1,26 @@
{
"name": "@certd/api",
"version": "0.2.1",
"description": "",
"main": "src/index.js",
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"log4js": "^6.3.0",
"qs": "^6.9.4"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -0,0 +1,2 @@
import { Registry } from '../registry/registry.js'
export const accessProviderRegistry = new Registry()

View File

@@ -1,8 +1,10 @@
import _ from 'lodash-es'
import logger from '../utils/util.log.js'
import commonUtil from '../utils/util.common.js'
export class AbstractDnsProvider {
constructor () {
constructor ({ accessProviders }) {
this.logger = logger
this.accessProviders = commonUtil.arrayToMap(accessProviders)
}
async createRecord ({ fullRecord, type, value }) {
@@ -31,4 +33,15 @@ export class AbstractDnsProvider {
}
return domain
}
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
let access = accessProvider
if (typeof accessProvider === 'string' && accessProviders) {
access = accessProviders[accessProvider]
}
if (access == null) {
throw new Error(`accessProvider ${accessProvider}不存在`)
}
return access
}
}

View File

@@ -0,0 +1,4 @@
import { Registry } from '../registry/registry.js'
export { AbstractDnsProvider } from './abstract-dns-provider.js'
export const dnsProviderRegistry = new Registry()

View File

@@ -0,0 +1,6 @@
export * from './dns-provider/index.js'
export * from './plugin/index.js'
export * from './access-provider/index.js'
export { Store } from './store/store.js'
export { util } from './utils/index.js'
// module.createRequireFromPath()

View File

@@ -2,10 +2,15 @@ import fs from 'fs'
import logger from '../utils/util.log.js'
import dayjs from 'dayjs'
import Sleep from '../utils/util.sleep.js'
import commonUtil from '../utils/util.common.js'
export class AbstractPlugin {
constructor ({ accessProviders }) {
constructor (options) {
if (options == null) {
throw new Error('插件安装失败:参数不允许为空')
}
const { accessProviders } = options
this.logger = logger
this.accessProviders = accessProviders
this.accessProviders = commonUtil.arrayToMap(accessProviders)
}
appendTimeSuffix (name) {
@@ -52,7 +57,7 @@ export class AbstractPlugin {
}
/**
* 回退如有必要
* 回退用于单元测试
* @param options
*/
async rollback (options) {
@@ -60,10 +65,14 @@ export class AbstractPlugin {
}
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
let access = accessProvider
if (typeof accessProvider === 'string' && accessProviders) {
accessProvider = accessProviders[accessProvider]
access = accessProviders[accessProvider]
}
return accessProvider
if (access == null) {
throw new Error(`accessProvider ${accessProvider}不存在`)
}
return access
}
async sleep (time) {

View File

@@ -0,0 +1,3 @@
import { Registry } from '../registry/registry.js'
export { AbstractPlugin } from './abstract-plugin.js'
export const pluginRegistry = new Registry()

View File

@@ -0,0 +1,46 @@
export class Registry {
constructor () {
this.collection = {}
}
install (target) {
if (target == null) {
return
}
if (this.collection == null) {
this.collection = {}
}
let defineName = target.define ? target.define().name : null
if (defineName == null) {
defineName = target.name
}
this.register(defineName, target)
}
register (key, value) {
if (!key || value == null) {
return
}
this.collection[key] = value
}
get (name) {
if (!name) {
throw new Error('插件名称不能为空')
}
if (!this.collection) {
this.collection = {}
}
const plugin = this.collection[name]
if (!plugin) {
throw new Error(`插件${name}还未注册`)
}
return plugin
}
getCollection () {
return this.collection
}
}

View File

@@ -2,6 +2,7 @@ import logger from './util.log.js'
import path from './util.path.js'
import { request } from './util.request.js'
import sleep from './util.sleep.js'
import common from './util.common.js'
export const util = {
logger, path, request, sleep
logger, path, request, sleep, common
}

View File

@@ -0,0 +1,33 @@
import _ from 'lodash-es'
export default {
arrayToMap (array) {
if (!array) {
return {}
}
if (!_.isArray(array)) {
return array
}
const map = {}
for (const item of array) {
if (item.key) {
map[item.key] = item
}
}
return map
},
mapToArray (map) {
if (!map) {
return []
}
if (_.isArray(map)) {
return map
}
const array = []
for (const key in map) {
const item = map[key]
item.key = key
array.push(item)
}
return array
}
}

7
packages/core/certd/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -1,18 +1,17 @@
{
"name": "@certd/certd",
"version": "0.1.7",
"description": "",
"main": "./src/index.js",
"version": "0.2.1",
"description": "a ssl cert keeper",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \\\"Error: no test specified\\\" && exit 1"
},
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"@certd/acme-client": "^0.1.6",
"@certd/api": "^0.1.7",
"@certd/providers": "^0.1.7",
"@certd/acme-client": "^0.2.0",
"@certd/api": "^0.2.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"node-forge": "^0.10.0"
@@ -25,5 +24,6 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
}
},
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -1,17 +1,16 @@
import { util, Store } from '@certd/api'
import { util, Store, dnsProviderRegistry } from '@certd/api'
import { AcmeService } from './acme.js'
import { FileStore } from './store/file-store.js'
import { CertStore } from './store/cert-store.js'
import { DnsProviderFactory } from '@certd/providers'
import dayjs from 'dayjs'
import forge from 'node-forge'
const logger = util.logger
export class Certd {
constructor (options) {
this.options = options
this.email = options.cert.email
this.domains = options.cert.domains
this.domain = this.getMainDomain(options.cert.domains)
if (!(options.store instanceof Store)) {
this.store = new FileStore(options.store || {})
@@ -19,33 +18,11 @@ export class Certd {
this.certStore = new CertStore({
store: this.store,
email: options.cert.email,
domain: this.domain
domains: this.domains
})
this.acme = new AcmeService(this.store)
}
getMainDomain (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
if (domains.length > 0) {
return domains[0]
}
}
//
// buildDomainFileName (domains) {
// const domain = this.getMainDomain(domains)
// return domain.replace(/\*/g, '_')
// }
//
// buildCertDir (email, domains) {
// const domainFileName = this.buildDomainFileName(domains)
// return path.join(email, '/certs/', domainFileName)
// }
async certApply () {
let oldCert
try {
@@ -95,16 +72,14 @@ export class Certd {
}
createDnsProvider (options) {
const accessProviders = options.accessProviders
const providerOptions = accessProviders[options.cert.dnsProvider]
return DnsProviderFactory.createByType(providerOptions.providerType, providerOptions)
return this.createProviderByType(options.cert.dnsProvider, options.accessProviders)
}
async writeCert (cert) {
const newPath = await this.certStore.writeCert(cert)
return {
realPath: this.certStore.store.getActualKey(newPath),
currentPath: this.certStore.store.getActualKey(this.certStore.currentRootPath)
currentPath: this.certStore.store.getActualKey(this.certStore.currentMarkPath)
}
}
@@ -114,7 +89,7 @@ export class Certd {
return null
}
const { detail, expires } = this.getCrtDetail(cert.crt)
const domain = this.getMainDomain(this.options.cert.domains)
const domain = this.certStore.getMainDomain(this.options.cert.domains)
return {
...cert, detail, expires, domain, domains: this.domains, email: this.email
}
@@ -144,4 +119,13 @@ export class Certd {
leftDays
}
}
createProviderByType (props, accessProviders) {
const { type } = props
const Provider = dnsProviderRegistry.get(type)
if (Provider == null) {
throw new Error('暂不支持此dnsProvider,请先注册该provider' + type)
}
return new Provider({ accessProviders, props })
}
}

View File

@@ -1,29 +1,50 @@
import dayjs from 'dayjs'
import crypto from 'crypto'
// eslint-disable-next-line no-unused-vars
function md5 (content) {
return crypto.createHash('md5').update(content).digest('hex')
}
export class CertStore {
constructor ({ store, email, domain }) {
constructor ({ store, email, domains }) {
this.store = store
this.email = email
this.domain = domain
this.domains = domains
this.domain = this.getMainDomain(this.domains)
this.safetyDomain = this.getSafetyDomain(this.domain)
this.domainDir = this.safetyDomain + '-' + md5(this.getDomainStr(this.domains))
// this.domainDir = this.safetyDomain
this.certsRootPath = this.store.buildKey(this.email, 'certs')
this.currentRootPath = this.store.buildKey(this.certsRootPath, this.safetyDomain, 'current')
this.currentMarkPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current.json')
}
// getAccountConfig () {
// return this.store.get(this.accountConfigKey)
// }
//
// setAccountConfig (email, account) {
// return this.store.set(this.accountConfigKey, account)
// }
getMainDomain (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
if (domains.length > 0) {
return domains[0]
}
}
getDomainStr (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
return domains.join(',')
}
buildNewCertRootPath (dir) {
if (dir == null) {
dir = dayjs().format('YYYY.MM.DD.HHmmss')
}
return this.store.buildKey(this.certsRootPath, this.safetyDomain, dir)
return this.store.buildKey(this.certsRootPath, this.domainDir, dir)
}
formatCert (pem) {
@@ -43,15 +64,19 @@ export class CertStore {
await this.store.set(priKey, this.formatCert(cert.key.toString()))
await this.store.set(csrKey, cert.csr.toString())
await this.store.link(newDir, this.currentRootPath)
await this.store.set(this.currentMarkPath, JSON.stringify({ latest: newDir }))
return newDir
}
async readCert (dir) {
if (dir == null) {
dir = this.currentRootPath
dir = await this.getCurrentDir()
}
if (dir == null) {
return
}
const crtKey = this.buildKey(dir, this.safetyDomain + '.crt')
const priKey = this.buildKey(dir, this.safetyDomain + '.key')
const csrKey = this.buildKey(dir, this.safetyDomain + '.csr')
@@ -80,13 +105,23 @@ export class CertStore {
return domain.replace(/\*/g, '_')
}
getCurrentFile (file) {
const key = this.buildKey(this.currentRootPath, file)
async getCurrentDir () {
const current = await this.store.get(this.currentMarkPath)
if (current == null) {
return null
}
return JSON.parse(current).latest
}
async getCurrentFile (file) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.get(key)
}
setCurrentFile (file, value) {
const key = this.buildKey(this.currentRootPath, file)
async setCurrentFile (file, value) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.set(key, value)
}
}

View File

@@ -1,6 +1,6 @@
import chai from 'chai'
import { Certd } from '../src/index.js'
import { createOptions } from '../../../test/options.js'
import { createOptions } from '../../../../test/options.js'
const { expect } = chai
const fakeCrt = `-----BEGIN CERTIFICATE-----
MIIFSTCCBDGgAwIBAgITAPoZZk/LhVIyXoic2NnJyxubezANBgkqhkiG9w0BAQsF
@@ -66,9 +66,9 @@ describe('Certd', function () {
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.club']
const certd = new Certd(options)
const currentRootPath = certd.certStore.currentRootPath
const currentRootPath = certd.certStore.currentMarkPath
console.log('rootDir', currentRootPath)
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club\\current/)
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club-\w*\\current.json/)
})
it('#writeAndReadCert', async function () {
const options = createOptions()
@@ -83,6 +83,6 @@ describe('Certd', function () {
expect(cert.key).to.be.ok
expect(cert.detail).to.be.ok
expect(cert.expires).to.be.ok
console.log('expires:', cert.expires)
console.log('cert:', JSON.stringify(cert))
})
})

7
packages/core/executor/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -1,22 +1,24 @@
{
"name": "@certd/executor",
"version": "0.1.9",
"version": "0.2.2",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "echo \\\"Error: no test specified\\\" && exit 1",
"build": "webpack --config webpack.config.cjs ",
"rollup": "rollup --config rollup.config.js"
},
"type": "module",
"dependencies": {
"@certd/api": "^0.1.7",
"@certd/certd": "^0.1.7",
"@certd/plugins": "^0.1.9",
"@certd/api": "^0.2.1",
"@certd/certd": "^0.2.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20"
},
"devDependencies": {
"@certd/plugin-aliyun": "^0.2.2",
"@certd/plugin-host": "^0.2.1",
"@certd/plugin-tencent": "^0.2.2",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.1",
@@ -32,5 +34,6 @@
},
"author": "Greper",
"license": "MIT",
"sideEffects": false
"sideEffects": false,
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -1,6 +1,5 @@
import { Certd } from '@certd/certd'
import DefaultPlugins from '@certd/plugins'
import { util } from '@certd/api'
import { pluginRegistry, util } from '@certd/api'
import _ from 'lodash-es'
import dayjs from 'dayjs'
import { Trace } from './trace.js'
@@ -11,46 +10,41 @@ function createDefaultOptions () {
args: {
forceCert: false,
forceDeploy: true,
forceRedeploy: false
forceRedeploy: false,
doNotThrowError: false // 部署流程执行有错误时,不抛异常,此时整个任务执行完毕后,可以返回结果,你可以在返回结果中处理
}
}
}
export class Executor {
constructor () {
this.usePlugins(DefaultPlugins)
this.trace = new Trace()
}
use (plugin) {
if (plugin == null) {
return
}
this.plugins[plugin.name] = plugin
if (plugin.define) {
const define = plugin.define()
this.plugins[define.name] = plugin
}
}
usePlugins (plugins) {
if (plugins) {
for (const plugin of plugins) {
this.use(plugin)
}
}
}
async run (options) {
logger.info('------------------- Cert-D ---------------------')
try {
this.transfer(options)
options = _.merge(createDefaultOptions(), options)
return await this.doRun(options)
} catch (e) {
logger.error('任务执行出错', e)
logger.error('任务执行出错', e)
throw e
}
}
transfer (options) {
const providers = options.accessProviders
if (_.isArray(providers)) {
const map = {}
for (const provider of providers) {
if (provider.key) {
map[provider.key] = provider
}
}
options.accessProviders = map
}
}
async doRun (options) {
// 申请证书
logger.info('任务开始')
@@ -64,8 +58,11 @@ export class Executor {
logger.info('----------------------')
if (!cert.isNew) {
// 如果没有更新
if (!options.args?.forceDeploy && !options.args?.forceRedeploy) {
// 且不需要强制运行deploy
if (options.args.forceRedeploy) {
// 强制重新部署,清空保存的状态
await certd.certStore.setCurrentFile('context.json', '{}')
} else if (!options.args.forceDeploy) {
// 且不需要强制deploy
logger.info('证书无更新,无需重新部署')
logger.info('任务完成')
return { cert }
@@ -81,26 +78,26 @@ export class Executor {
context.certIsNew = !!cert.isNew
const trace = new Trace(context)
const resultTrace = trace.getInstance({ type: 'result' })
// 运行部署任务
try {
await this.runDeploys({ options, cert, context, trace })
} finally {
await certd.certStore.setCurrentFile('context.json', JSON.stringify(context))
}
logger.info('任务完成')
trace.print()
const result = trace.get({ })
const returnData = {
const result = resultTrace.get({ })
if (result) {
if (result.status === 'error' && options.args.doNotThrowError === false) {
throw new Error(result.remark)
}
}
return {
cert,
context,
result
}
if (result.status === 'error') {
const err = new Error(result.remark)
err.data = returnData
}
return returnData
}
async runCertd (certd) {
@@ -119,10 +116,13 @@ export class Executor {
for (const deploy of options.deploy) {
const deployName = deploy.deployName
logger.info(`------------【${deployName}】-----------`)
const deployTrace = trace.getInstance({ type: 'deploy', deployName })
if (deploy.disabled === true) {
logger.info('此流程已被禁用,跳过')
logger.info('')
trace.set({ deployName, value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
deployTrace.set({ value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
deployTrace.set({ tasks: null })
continue
}
try {
@@ -131,13 +131,15 @@ export class Executor {
context[deployName] = {}
}
const taskContext = context[deployName]
// 开始执行任务列表
await this.runTask({ options, cert, task, context: taskContext, deploy, trace })
}
trace.set({ deployName, value: { status: 'success', remark: '执行成功' } })
deployTrace.set({ value: { status: 'success', remark: '执行成功' } })
trace.set({ type: 'result', value: { status: 'success', remark: '执行成功' } })
} catch (e) {
trace.set({ deployName, value: { status: 'error', remark: '执行失败:' + e.message } })
trace.set({ value: { status: 'error', remark: deployName + '执行失败:' + e.message } })
deployTrace.set({ value: { status: 'error', remark: '执行失败:' + e.message } })
trace.set({ type: 'result', value: { status: 'error', remark: deployName + '执行失败:' + e.message } })
logger.error('流程执行失败', e)
}
@@ -147,7 +149,7 @@ export class Executor {
async runTask ({ options, task, cert, context, deploy, trace }) {
const taskType = task.type
const Plugin = this.plugins[taskType]
const Plugin = pluginRegistry.get(taskType)
const deployName = deploy.deployName
const taskName = task.taskName
if (Plugin == null) {
@@ -158,18 +160,20 @@ export class Executor {
if (Plugin instanceof Function) {
instance = new Plugin({ accessProviders: options.accessProviders })
}
const traceStatus = trace.get({ deployName: deploy.deployName, taskName: taskName })
if (traceStatus?.status === 'success' && !options?.args?.forceRedeploy) {
const taskTrace = trace.getInstance({ type: 'deploy', deployName, taskName })
const traceStatus = taskTrace.get({})
if (traceStatus && traceStatus.status === 'success' && !options.args.forceRedeploy) {
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
trace.set({ deployName, taskName, value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
taskTrace.set({ value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
return
}
logger.info(`----【${taskName}】开始执行`)
try {
// 执行任务
await instance.execute({ cert, props: task.props, context })
trace.set({ deployName, taskName, value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
taskTrace.set({ value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
} catch (e) {
trace.set({ deployName, taskName, value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
taskTrace.set({ value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
throw e
}
logger.info(`----任务【${taskName}】执行完成`)

View File

@@ -6,19 +6,30 @@ export class Trace {
this.context = context
}
set ({ deployName, taskName, prop, value }) {
const key = this.buildTraceKey({ deployName, taskName, prop })
getInstance ({ type, deployName, taskName }) {
return {
get: ({ prop }) => {
return this.get({ type, deployName, taskName, prop })
},
set: ({ prop, value }) => {
this.set({ type, deployName, taskName, prop, value })
}
}
}
set ({ type, deployName, taskName, prop, value }) {
const key = this.buildTraceKey({ type, deployName, taskName, prop })
const oldValue = _.get(this.context, key) || {}
_.merge(oldValue, value)
_.set(this.context, key, oldValue)
}
get ({ deployName, taskName, prop }) {
return _.get(this.context, this.buildTraceKey({ deployName, taskName, prop }))
get ({ type, deployName, taskName, prop }) {
return _.get(this.context, this.buildTraceKey({ type, deployName, taskName, prop }))
}
buildTraceKey ({ deployName, taskName, prop }) {
let key = '__trace__'
buildTraceKey ({ type = 'default', deployName, taskName, prop }) {
let key = '__trace__.' + type
if (deployName) {
key += '.'
key += deployName.replace(/\./g, '_')
@@ -46,7 +57,7 @@ export class Trace {
} else {
this.printTraceLine({ current: 'skip', remark: '还未到过期时间,跳过' }, '更新证书')
}
const trace = this.get({ })
const trace = this.get({ type: 'deploy' })
// logger.info('trace', trace)
for (const deployName in trace) {
if (trace[deployName] == null) {
@@ -64,11 +75,14 @@ export class Trace {
}
}
}
const result = this.get({ type: 'result' })
if (result) {
this.printTraceLine(result, 'result', '')
}
const mainContext = {}
_.merge(mainContext, context)
delete mainContext.__trace__
logger.info('context:', JSON.stringify(mainContext))
logger.info('context', JSON.stringify(mainContext))
}
printTraceLine (traceStatus, name, prefix = '') {

View File

@@ -1,11 +1,19 @@
import pkg from 'chai'
import { Executor } from '../src/index.js'
import { createOptions } from '../../../test/options.js'
import { createOptions } from '../../../../test/options.js'
import PluginAliyun from '@certd/plugin-aliyun'
import PluginTencent from '@certd/plugin-tencent'
import PluginHost from '@certd/plugin-host'
const { expect } = pkg
// 安装默认插件和授权提供者
PluginAliyun.install()
PluginTencent.install()
PluginHost.install()
describe('AutoDeploy', function () {
it('#run', async function () {
this.timeout(20000)
this.timeout(120000)
const options = createOptions()
const executor = new Executor()
const ret = await executor.run(options)
@@ -13,15 +21,18 @@ describe('AutoDeploy', function () {
expect(ret.cert).ok
})
it('#forceCert', async function () {
this.timeout(20000)
this.timeout(120000)
const executor = new Executor()
const options = createOptions()
const ret = await executor.run(options, { forceCert: true, forceDeploy: false })
options.args.forceCert = true
options.args.forceDeploy = true
const ret = await executor.run(options)
expect(ret).ok
expect(ret.cert).ok
})
it('#forceDeploy', async function () {
this.timeout(20000)
this.timeout(120000)
const executor = new Executor()
const options = createOptions()
const ret = await executor.run(options, { forceCert: false, forceDeploy: true, forceRedeploy: true })

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
{
"name": "@certd/plugins",
"version": "0.1.9",
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/api": "^0.1.7",
"@certd/certd": "^0.1.7",
"dayjs": "^1.9.7",
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.20",
"ssh2": "^0.8.9",
"tencentcloud-sdk-nodejs": "^4.0.44"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"author": "Greper",
"license": "MIT"
}

View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -0,0 +1,29 @@
{
"name": "@certd/plugin-aliyun",
"version": "0.2.2",
"description": "",
"main": "src/index.js",
"type": "module",
"dependencies": {
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0",
"@alicloud/pop-core": "^1.7.10",
"@certd/api": "^0.2.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20"
},
"devDependencies": {
"@certd/certd": "^0.2.1",
"@certd/plugin-common": "^0.2.1",
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"author": "Greper",
"license": "MIT",
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -0,0 +1,30 @@
export class AliyunAccessProvider {
static define () {
return {
name: 'aliyun',
label: '阿里云',
desc: '',
input: {
accessKeyId: {
type: String,
component: {
placeholder: 'accessKeyId',
rules: [{ required: true, message: '必填项' }]
},
required: true
},
accessKeySecret: {
type: String,
component: {
placeholder: 'accessKeySecret',
rules: [{ required: true, message: '必填项' }]
}
}
},
output: {
}
}
}
}

View File

@@ -2,20 +2,41 @@ import { AbstractDnsProvider } from '@certd/api'
import Core from '@alicloud/pop-core'
import _ from 'lodash-es'
export class AliyunDnsProvider extends AbstractDnsProvider {
constructor (dnsProviderConfig) {
super()
static define () {
return {
name: 'aliyun',
label: '阿里云',
desc: '',
input: {
accessProvider: {
label: '授权',
type: [String, Object],
desc: '需要aliyun类型的授权',
component: {
name: 'access-provider-selector',
filter: 'aliyun'
},
required: true
}
},
output: {
}
}
}
constructor (args) {
super(args)
const { props } = args
const accessProvider = this.getAccessProvider(props.accessProvider)
this.client = new Core({
accessKeyId: dnsProviderConfig.accessKeyId,
accessKeySecret: dnsProviderConfig.accessKeySecret,
accessKeyId: accessProvider.accessKeyId,
accessKeySecret: accessProvider.accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
})
}
static name () {
return 'aliyun'
}
async getDomainList () {
const params = {
RegionId: 'cn-hangzhou'
@@ -85,7 +106,9 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
this.logger.info('添加域名解析成功:', value, value, ret.RecordId)
return ret.RecordId
} catch (e) {
// e.code === 'DomainRecordDuplicate'
if (e.code === 'DomainRecordDuplicate') {
return
}
this.logger.info('添加域名解析出错', e)
throw e
}

View File

@@ -0,0 +1,25 @@
import _ from 'lodash-es'
import { AliyunDnsProvider } from './dns-providers/aliyun.js'
import { AliyunAccessProvider } from './access-providers/aliyun.js'
import { UploadCertToAliyun } from './plugins/upload-to-aliyun/index.js'
import { DeployCertToAliyunCDN } from './plugins/deploy-to-cdn/index.js'
import { DeployCertToAliyunAckIngress } from './plugins/deploy-to-ack-ingress/index.js'
import { pluginRegistry, accessProviderRegistry, dnsProviderRegistry } from '@certd/api'
export const Plugins = {
UploadCertToAliyun,
DeployCertToAliyunCDN,
DeployCertToAliyunAckIngress
}
export default {
install () {
_.forEach(Plugins, item => {
pluginRegistry.install(item)
})
accessProviderRegistry.install(AliyunAccessProvider)
dnsProviderRegistry.install(AliyunDnsProvider)
}
}

View File

@@ -0,0 +1,200 @@
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
import Core from '@alicloud/pop-core'
import { K8sClient } from '@certd/plugin-common'
const ROAClient = Core.ROAClient
const define = {
name: 'deployCertToAliyunAckIngress',
label: '部署到阿里云AckIngress',
input: {
clusterId: {
label: '集群id',
component: {
placeholder: '集群id'
},
required: true
},
secretName: {
label: '保密字典Id',
component: {
placeholder: '保密字典Id'
},
required: true
},
regionId: {
label: '大区',
value: 'cn-shanghai',
component: {
placeholder: '集群所属大区'
},
required: true
},
namespace: {
label: '命名空间',
value: 'default',
component: {
placeholder: '命名空间'
},
required: true
},
ingressName: {
label: 'ingress名称',
value: '',
component: {
placeholder: 'ingress名称'
},
required: true,
helper: '可以传入一个数组'
},
ingressClass: {
label: 'ingress类型',
value: 'nginx',
component: {
placeholder: '暂时只支持nginx类型'
},
required: true
},
isPrivateIpAddress: {
label: '是否私网ip',
value: false,
component: {
placeholder: '集群连接端点是否是私网ip'
},
helper: '如果您当前certd运行在同一个私网下可以选择是。',
required: true
},
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'access授权',
component: {
name: 'access-provider-selector',
filter: 'aliyun'
},
required: true
}
},
output: {
}
}
export class DeployCertToAliyunAckIngress extends AbstractAliyunPlugin {
static define () {
return define
}
async execute ({ cert, props, context }) {
const accessProvider = this.getAccessProvider(props.accessProvider)
const client = this.getClient(accessProvider, props.regionId)
const kubeConfigStr = await this.getKubeConfig(client, props.clusterId, props.isPrivateIpAddress)
this.logger.info('kubeconfig已成功获取')
const k8sClient = new K8sClient(kubeConfigStr)
const ingressType = props.ingressClass || 'qcloud'
if (ingressType === 'qcloud') {
throw new Error('暂未实现')
// await this.patchQcloudCertSecret({ k8sClient, props, context })
} else {
await this.patchNginxCertSecret({ cert, k8sClient, props, context })
}
await this.sleep(3000) // 停留2秒等待secret部署完成
// await this.restartIngress({ k8sClient, props })
return true
}
async restartIngress ({ k8sClient, props }) {
const { namespace } = props
const body = {
metadata: {
labels: {
certd: this.appendTimeSuffix('certd')
}
}
}
const ingressList = await k8sClient.getIngressList({ namespace })
console.log('ingressList:', ingressList)
if (!ingressList || !ingressList.body || !ingressList.body.items) {
return
}
const ingressNames = ingressList.body.items.filter(item => {
if (!item.spec.tls) {
return false
}
for (const tls of item.spec.tls) {
if (tls.secretName === props.secretName) {
return true
}
}
return false
}).map(item => {
return item.metadata.name
})
for (const ingress of ingressNames) {
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
this.logger.info(`ingress已重启:${ingress}`)
}
}
async patchNginxCertSecret ({ cert, k8sClient, props, context }) {
const crt = cert.crt
const key = cert.key
const crtBase64 = Buffer.from(crt).toString('base64')
const keyBase64 = Buffer.from(key).toString('base64')
const { namespace, secretName } = props
const body = {
data: {
'tls.crt': crtBase64,
'tls.key': keyBase64
},
metadata: {
labels: {
certd: this.appendTimeSuffix('certd')
}
}
}
let secretNames = secretName
if (typeof secretName === 'string') {
secretNames = [secretName]
}
for (const secret of secretNames) {
await k8sClient.patchSecret({ namespace, secretName: secret, body })
this.logger.info(`CertSecret已更新:${secret}`)
}
}
getClient (aliyunProvider, regionId) {
return new ROAClient({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: `https://cs.${regionId}.aliyuncs.com`,
apiVersion: '2015-12-15'
})
}
async getKubeConfig (client, clusterId, isPrivateIpAddress = false) {
const httpMethod = 'GET'
const uriPath = `/k8s/${clusterId}/user_config`
const queries = {
PrivateIpAddress: isPrivateIpAddress
}
const body = '{}'
const headers = {
'Content-Type': 'application/json'
}
const requestOption = {}
try {
const res = await client.request(httpMethod, uriPath, queries, body, headers, requestOption)
return res.config
} catch (e) {
console.error('请求出错:', e)
throw e
}
}
}

View File

@@ -0,0 +1,107 @@
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
import Core from '@alicloud/pop-core'
import dayjs from 'dayjs'
const define = {
name: 'deployCertToAliyunCDN',
label: '部署到阿里云CDN',
input: {
domainName: {
label: 'cdn加速域名',
component: {
placeholder: 'cdn加速域名'
},
required: true
},
certName: {
label: '证书名称',
component: {
placeholder: '上传后将以此名称作为前缀'
}
},
from: {
default: 'upload',
label: '证书来源',
required: true,
component: {
required: true,
placeholder: '证书来源',
name: 'a-select',
options: [
{ value: 'upload', label: '直接上传' },
{ value: 'cas', label: '从证书库', title: '需要uploadCertToAliyun作为前置任务' }
]
},
desc: '如果选择‘从证书库’类型,则需要以《上传证书到阿里云》作为前置任务'
},
// serverCertificateStatus: {
// label: '启用https',
// options: [
// { value: 'on', label: '开启HTTPS并更新证书' },
// { value: 'auto', label: '若HTTPS开启则更新未开启不更新' }
// ],
// required:true
// },
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'access授权',
component: {
name: 'access-provider-selector',
filter: 'aliyun'
},
required: true
}
},
output: {
}
}
export class DeployCertToAliyunCDN extends AbstractAliyunPlugin {
static define () {
return define
}
async execute ({ cert, props, context }) {
const accessProvider = this.getAccessProvider(props.accessProvider)
const client = this.getClient(accessProvider)
const params = this.buildParams(props, context, cert)
await this.doRequest(client, params)
}
getClient (aliyunProvider) {
return new Core({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: 'https://cdn.aliyuncs.com',
apiVersion: '2018-05-10'
})
}
buildParams (args, context, cert) {
const { certName, from, domainName } = args
const CertName = certName + '-' + dayjs().format('YYYYMMDDHHmmss')
const params = {
RegionId: 'cn-hangzhou',
DomainName: domainName,
ServerCertificateStatus: 'on',
CertName: CertName,
CertType: from,
ServerCertificate: cert.crt,
PrivateKey: cert.key
}
return params
}
async doRequest (client, params) {
const requestOption = {
method: 'POST'
}
const ret = await client.request('SetDomainServerCertificate', params, requestOption)
this.checkRet(ret)
this.logger.info('设置cdn证书成功:', ret.RequestId)
}
}

View File

@@ -1,38 +1,41 @@
import Core from '@alicloud/pop-core'
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
export class UploadCertToAliyun extends AbstractAliyunPlugin {
/**
* 插件定义
* 名称
* 入参
* 出参
*/
static define () {
return {
name: 'uploadCertToAliyun',
label: '上传证书到阿里云',
input: {
name: {
label: '证书名称'
},
regionId: {
label: '大区',
value: 'cn-hangzhou'
},
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=aliyun]'
}
const define = {
name: 'uploadCertToAliyun',
label: '上传证书到阿里云',
input: {
name: {
label: '证书名称',
desc: '证书上传后将以此参数作为名称前缀'
},
regionId: {
label: '大区',
default: 'cn-hangzhou',
required: true
},
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'access授权',
component: {
name: 'access-provider-selector',
filter: 'aliyun'
},
output: {
aliyunCertId: {
type: String,
desc: '上传成功后的阿里云CertId'
}
}
required: true
}
},
output: {
aliyunCertId: {
type: String,
desc: '上传成功后的阿里云CertId'
}
}
}
export class UploadCertToAliyun extends AbstractAliyunPlugin {
static define () {
return define
}
getClient (aliyunProvider) {

View File

@@ -1,6 +1,10 @@
import pkg from 'chai'
import { createOptions } from '../../../../test/options.js'
import { Certd } from '../../src/index.js'
import { createOptions } from '../../../../../test/options.js'
import { Certd } from '@certd/certd'
import PluginAliyun from '../../src/index.js'
// 安装默认插件和授权提供者
PluginAliyun.install()
const { expect } = pkg
describe('AliyunDnsProvider', function () {
it('#申请证书-aliyun', async function () {

View File

@@ -1,28 +1,33 @@
import pkg from 'chai'
import AliyunDnsProvider from '../../src/dns-provider/impl/aliyun.js'
import { createOptions } from '../../../../test/options.js'
import { Certd } from '../../src/index.js'
import { AliyunDnsProvider } from '../../src/dns-providers/aliyun.js'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
export function getPluginOptions () {
const options = createOptions()
return { accessProviders: options.accessProviders, props: options.cert.dnsProvider }
}
describe('AliyunDnsProvider', function () {
it('#getDomainList', async function () {
const options = createOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
const options = getPluginOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options)
const domainList = await aliyunDnsProvider.getDomainList()
console.log('domainList', domainList)
expect(domainList.length).gt(0)
})
it('#getRecords', async function () {
const options = createOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
const options = getPluginOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options)
const recordList = await aliyunDnsProvider.getRecords('docmirror.cn', '*')
console.log('recordList', recordList)
expect(recordList.length).gt(0)
})
it('#createAndRemoveRecord', async function () {
const options = createOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
const options = getPluginOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options)
const record = await aliyunDnsProvider.createRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa' })
console.log('recordId', record)
expect(record != null).ok

View File

@@ -0,0 +1,42 @@
import _ from 'lodash-es'
import optionsPrivate from '../../../test/options.private.mjs'
const defaultOptions = {
version: '1.0.0',
args: {
directory: 'test',
dry: false
},
accessProviders: {
aliyun: {
providerType: 'aliyun',
accessKeyId: '',
accessKeySecret: ''
},
myLinux: {
providerType: 'SSH',
username: 'xxx',
password: 'xxx',
host: '1111.com',
port: 22,
publicKey: ''
}
},
cert: {
domains: ['*.docmirror.club', 'docmirror.club'],
email: 'xiaojunnuo@qq.com',
dnsProvider: { type: 'aliyun', accessProvider: 'aliyun' },
certProvider: 'letsencrypt',
csrInfo: {
country: 'CN',
state: 'GuangDong',
locality: 'ShengZhen',
organization: 'CertD Org.',
organizationUnit: 'IT Department',
emailAddress: 'xiaojunnuo@qq.com'
}
}
}
_.merge(defaultOptions, optionsPrivate)
export default defaultOptions

View File

@@ -0,0 +1,67 @@
import pkg from 'chai'
import { DeployCertToAliyunAckIngress } from '../../src/plugins/deploy-to-ack-ingress/index.js'
import { Certd } from '@certd/certd'
import { createOptions } from '../../../../../test/options.js'
import { K8sClient } from '@certd/plugin-common'
const { expect } = pkg
async function getOptions () {
const options = createOptions()
options.args.test = false
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: {
accessProvider: 'aliyun-yonsz-prod',
regionId: 'cn-shanghai',
clusterId: 'c9e107ca518314f70973636965037fc00',
secretName: 'default-ingress-secret1638601684896',
namespace: 'default',
ingressClass: 'nginx'
},
context
}
return { options, deployOpts }
}
describe('DeployCertToAliyunAckIngressNginx', function () {
it('#getAliyunSecrets', async function () {
this.timeout(50000)
const { options, deployOpts } = await getOptions()
const plugin = new DeployCertToAliyunAckIngress(options)
const ackClient = plugin.getClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.regionId)
const kubeConfig = await plugin.getKubeConfig(ackClient, deployOpts.props.clusterId, false)
const k8sClient = new K8sClient(kubeConfig)
const secrets = await k8sClient.getSecret({ namespace: 'default' })
console.log('secrets:', secrets)
})
it('#getAliyunIngreses', async function () {
this.timeout(50000)
const { options, deployOpts } = await getOptions()
const plugin = new DeployCertToAliyunAckIngress(options)
const ackClient = plugin.getClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.regionId)
const kubeConfig = await plugin.getKubeConfig(ackClient, deployOpts.props.clusterId, false)
const k8sClient = new K8sClient(kubeConfig)
const list = await k8sClient.getIngressList({ namespace: 'default' })
console.log('list:', list)
})
it('#execute', async function () {
this.timeout(5000)
const { options, deployOpts } = await getOptions()
const plugin = new DeployCertToAliyunAckIngress(options)
const ret = await plugin.doExecute(deployOpts)
console.log('success', ret)
})
})

View File

@@ -1,20 +1,20 @@
import pkg from 'chai'
import { DeployCertToAliyunCDN } from '../../src/aliyun/deploy-to-cdn/index.js'
import { DeployCertToAliyunCDN } from '../../src/plugins/deploy-to-cdn/index.js'
import { Certd } from '@certd/certd'
import createOptions from '../../../../test/options.js'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('DeployToAliyunCDN', function () {
it('#execute', async function () {
this.timeout(5000)
const options = createOptions()
const plugin = new DeployCertToAliyunCDN()
const plugin = new DeployCertToAliyunCDN(options)
options.cert.domains = ['*.docmirror.cn', 'docmirror.cn']
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const ret = await plugin.doExecute({
accessProviders: options.accessProviders,
cert,
props: { domainName: 'certd-cdn-upload.docmirror.cn', certName: 'certd部署测试', certType: 'cas', accessProvider: 'aliyun' }
props: { domainName: 'certd-cdn-upload.docmirror.cn', certName: 'certd部署测试', from: 'cas', accessProvider: 'aliyun' }
})
console.log('context:', context, ret)
})

View File

@@ -1,7 +1,7 @@
import pkg from 'chai'
import { UploadCertToAliyun } from '../../src/aliyun/upload-to-aliyun/index.js'
import { UploadCertToAliyun } from '../../src/plugins/upload-to-aliyun/index.js'
import { Certd } from '@certd/certd'
import { createOptions } from '../../../../test/options.js'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('PluginUploadToAliyun', function () {
it('#execute', async function () {
@@ -9,12 +9,11 @@ describe('PluginUploadToAliyun', function () {
const options = createOptions()
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['_.docmirror.cn']
const plugin = new UploadCertToAliyun()
const plugin = new UploadCertToAliyun(options)
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: { accessProvider: 'aliyun' },
context
@@ -22,7 +21,7 @@ describe('PluginUploadToAliyun', function () {
await plugin.doExecute(deployOpts)
console.log('context:', context)
// await plugin.sleep(1000)
// await plugin.sleep(1000)
// await plugin.rollback(deployOpts)
})
})

View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -0,0 +1,23 @@
{
"name": "@certd/plugin-common",
"version": "0.2.1",
"description": "",
"main": "src/index.js",
"type": "module",
"dependencies": {
"kubernetes-client": "^9.0.0"
},
"devDependencies": {
"@certd/certd": "^0.2.1",
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"author": "Greper",
"license": "MIT",
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -0,0 +1 @@
export { K8sClient } from './lib/k8s.client.js'

View File

@@ -45,8 +45,8 @@ export class K8sClient {
* @param opts = {namespace:default}
* @returns secretsList
*/
async getSecret (opts) {
const namespace = opts?.namespace || 'default'
async getSecret (opts = {}) {
const namespace = opts.namespace || 'default'
const secrets = await this.client.api.v1.namespaces(namespace).secrets.get()
return secrets
}
@@ -57,7 +57,7 @@ export class K8sClient {
* @returns {Promise<*>}
*/
async createSecret (opts) {
const namespace = opts?.namespace || 'default'
const namespace = opts.namespace || 'default'
const created = await this.client.api.v1.namespaces(namespace).secrets.post({
body: opts.body
})
@@ -66,8 +66,8 @@ export class K8sClient {
}
async updateSecret (opts) {
const namespace = opts?.namespace || 'default'
const secretName = opts?.secretName
const namespace = opts.namespace || 'default'
const secretName = opts.secretName
if (secretName == null) {
throw new Error('secretName 不能为空')
}
@@ -77,8 +77,8 @@ export class K8sClient {
}
async patchSecret (opts) {
const namespace = opts?.namespace || 'default'
const secretName = opts?.secretName
const namespace = opts.namespace || 'default'
const secretName = opts.secretName
if (secretName == null) {
throw new Error('secretName 不能为空')
}
@@ -87,9 +87,14 @@ export class K8sClient {
})
}
async getIngressList (opts) {
const namespace = opts.namespace || 'default'
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get()
}
async getIngress (opts) {
const namespace = opts?.namespace || 'default'
const ingressName = opts?.ingressName
const namespace = opts.namespace || 'default'
const ingressName = opts.ingressName
if (!ingressName) {
throw new Error('ingressName 不能为空')
}
@@ -97,8 +102,8 @@ export class K8sClient {
}
async patchIngress (opts) {
const namespace = opts?.namespace || 'default'
const ingressName = opts?.ingressName
const namespace = opts.namespace || 'default'
const ingressName = opts.ingressName
if (!ingressName) {
throw new Error('ingressName 不能为空')
}

View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -0,0 +1,26 @@
{
"name": "@certd/plugin-host",
"version": "0.2.1",
"description": "",
"main": "src/index.js",
"type": "module",
"dependencies": {
"@certd/api": "^0.2.1",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"ssh2": "^0.8.9"
},
"devDependencies": {
"@certd/certd": "^0.2.1",
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"author": "Greper",
"license": "MIT",
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -0,0 +1,26 @@
export class SSHAccessProvider {
static define () {
return {
name: 'ssh',
label: '主机',
desc: '',
input: {
host: { required: true },
port: {
label: '端口',
type: Number,
default: '22',
required: true
},
username: {
default: 'root',
required: true
},
password: { desc: '登录密码' },
privateKey: {
desc: '密钥,密码或此项必填一项'
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
import _ from 'lodash-es'
import { SSHAccessProvider } from './access-providers/ssh.js'
import { UploadCertToHost } from './plugins/upload-to-host/index.js'
import { HostShellExecute } from './plugins/host-shell-execute/index.js'
import { pluginRegistry, accessProviderRegistry } from '@certd/api'
export const DefaultPlugins = {
UploadCertToHost,
HostShellExecute
}
export default {
install () {
_.forEach(DefaultPlugins, item => {
pluginRegistry.install(item)
})
accessProviderRegistry.install(SSHAccessProvider)
}
}

View File

@@ -12,14 +12,21 @@ export class HostShellExecute extends AbstractHostPlugin {
name: 'hostShellExecute',
label: '执行远程主机脚本命令',
input: {
script: {
label: 'shell脚本命令'
},
accessProvider: {
label: '主机登录配置',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含用户名密码的对象',
options: 'accessProviders[type=ssh]'
desc: '登录',
component: {
name: 'access-provider-selector',
filter: 'ssh'
},
required: true
},
script: {
label: 'shell脚本命令',
component: {
name: 'a-textarea'
}
}
},
output: {
@@ -32,7 +39,7 @@ export class HostShellExecute extends AbstractHostPlugin {
const { script, accessProvider } = props
const connectConf = this.getAccessProvider(accessProvider)
const sshClient = new SshClient()
const ret = await sshClient.shell({
const ret = await sshClient.exec({
connectConf,
script
})

View File

@@ -1,6 +1,8 @@
import ssh2 from 'ssh2'
import logger from '../utils/util.log.js'
import path from 'path'
import { util } from '@certd/api'
import _ from 'lodash-es'
const logger = util.logger
export class SshClient {
/**
*
@@ -13,7 +15,7 @@ export class SshClient {
}
* @param transports
*/
uploadFiles ({ connectConf, transports }) {
uploadFiles ({ connectConf, transports, sudo = false }) {
const conn = new ssh2.Client()
return new Promise((resolve, reject) => {
@@ -27,7 +29,8 @@ export class SshClient {
try {
for (const transport of transports) {
logger.info('上传文件:', JSON.stringify(transport))
await this.exec({ conn, cmd: 'mkdir ' + path.dirname(transport.remotePath) })
sudo = sudo ? 'sudo' : ''
await this.exec({ connectConf, script: `${sudo} mkdir -p ${path.dirname(transport.remotePath)} ` })
await this.fastPut({ sftp, ...transport })
}
resolve()
@@ -41,6 +44,43 @@ export class SshClient {
})
}
exec ({ connectConf, script }) {
if (_.isArray(script)) {
script = script.join('\n')
}
console.log('执行命令:', script)
return new Promise((resolve, reject) => {
this.connect({
connectConf,
onReady: (conn) => {
conn.exec(script, (err, stream) => {
if (err) {
reject(err)
return
}
let data = null
stream.on('close', (code, signal) => {
console.log(`[${connectConf.host}][close]:code:${code}`)
data = data ? data.toString() : null
if (code === 0) {
resolve(data)
} else {
reject(new Error(data))
}
conn.end()
}).on('data', (ret) => {
console.log(`[${connectConf.host}][info]: ` + ret)
data = ret
}).stderr.on('data', (err) => {
console.log(`[${connectConf.host}][error]: ` + err)
data = err
})
})
}
})
})
}
shell ({ connectConf, script }) {
return new Promise((resolve, reject) => {
this.connect({
@@ -87,24 +127,4 @@ export class SshClient {
})
})
}
exec ({ conn, cmd }) {
return new Promise((resolve, reject) => {
conn.exec(cmd, (err, stream) => {
if (err) {
logger.error('执行命令出错', err)
reject(err)
// return conn.end()
}
stream.on('close', (code, signal) => {
// logger.info('Stream :: close :: code: ' + code + ', signal: ' + signal)
// conn.end()
resolve()
}).on('data', (data) => {
logger.info('data', data.toString())
})
})
})
}
}

View File

@@ -13,16 +13,23 @@ export class UploadCertToHost extends AbstractHostPlugin {
label: '上传证书到主机',
input: {
crtPath: {
label: '证书路径'
label: '证书保存路径'
},
keyPath: {
label: '私钥路径'
label: '私钥保存路径'
},
accessProvider: {
label: '主机登录配置',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含用户名密码的对象',
options: 'accessProviders[type=ssh]'
desc: 'access授权',
component: {
name: 'access-provider-selector',
filter: 'ssh'
},
required: true
},
sudo: {
label: '是否sudo'
}
},
output: {

View File

@@ -0,0 +1,52 @@
import pkg from 'chai'
import { HostShellExecute } from '../../src/plugins/host-shell-execute/index.js'
import { Certd } from '@certd/certd'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('HostShellExecute', function () {
it('#execute', async function () {
this.timeout(10000)
const options = createOptions()
options.args = { test: false }
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']
const plugin = new HostShellExecute(options)
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const uploadOpts = {
cert,
props: { script: ['ls ', 'ls '], accessProvider: 'aliyun-ssh' },
context
}
const ret = await plugin.doExecute(uploadOpts)
expect(ret).ok
console.log('-----' + JSON.stringify(ret))
})
it('#execute-hk-restart-docker', async function () {
this.timeout(10000)
const options = createOptions()
const plugin = new HostShellExecute(options)
const uploadOpts = {
props: { script: ['cd /home/ubuntu/deloy/nginx-proxy\nsudo docker-compose build\nsudo docker-compose up -d\n'], accessProvider: 'aliyun-ssh-hk' },
context: {}
}
const ret = await plugin.doExecute(uploadOpts)
expect(ret).ok
console.log('-----' + JSON.stringify(ret))
})
it('#execute-publicKey-login', async function () {
this.timeout(10000)
const options = createOptions()
const plugin = new HostShellExecute(options)
const shellOpts = {
props: { script: ['ls'], accessProvider: 'tencent-ssh-base01' },
context: {}
}
const ret = await plugin.doExecute(shellOpts)
expect(ret).ok
console.log('-----' + JSON.stringify(ret))
})
})

View File

@@ -0,0 +1,48 @@
import pkg from 'chai'
import { UploadCertToHost } from '../../src/plugins/upload-to-host/index.js'
import { Certd } from '@certd/certd'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('PluginUploadToHost', function () {
it('#execute', async function () {
this.timeout(10000)
const options = createOptions()
options.args = { test: false }
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']
const plugin = new UploadCertToHost(options)
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const uploadOpts = {
cert,
props: { crtPath: '/root/certd/test/test.crt', keyPath: '/root/certd/test/test.key', accessProvider: 'aliyun-ssh' },
context
}
await plugin.doExecute(uploadOpts)
console.log('context:', context)
await plugin.doRollback(uploadOpts)
})
it('#execute-to-ubantu', async function () {
this.timeout(10000)
const options = createOptions()
options.args = { test: false }
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']
const plugin = new UploadCertToHost(options)
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const uploadOpts = {
cert,
props: { crtPath: '/home/ubuntu/deloy/nginx-proxy/ssl/test.crt', keyPath: '/home/ubuntu/deloy/nginx-proxy/ssl/test.key', accessProvider: 'aliyun-ssh-hk' },
context
}
await plugin.doExecute(uploadOpts)
console.log('context:', context)
await plugin.doRollback(uploadOpts)
})
})

View File

@@ -0,0 +1,14 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}

View File

@@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@@ -0,0 +1,28 @@
{
"name": "@certd/plugin-tencent",
"version": "0.2.2",
"description": "",
"main": "src/index.js",
"type": "module",
"dependencies": {
"@certd/api": "^0.2.1",
"@certd/plugin-common": "^0.2.1",
"dayjs": "^1.9.7",
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.20",
"tencentcloud-sdk-nodejs": "^4.0.44"
},
"devDependencies": {
"@certd/certd": "^0.2.1",
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
},
"author": "Greper",
"license": "MIT",
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

@@ -0,0 +1,26 @@
export class DnspodAccessProvider {
static define () {
return {
name: 'dnspod',
label: 'dnspod',
desc: '腾讯云的域名解析接口已迁移到dnspod',
input: {
id: {
type: String,
component: {
placeholder: 'dnspod接口账户id',
rules: [{ required: true, message: '该项必填' }]
}
},
token: {
type: String,
label: 'token',
component: {
placeholder: '开放接口token',
rules: [{ required: true, message: '该项必填' }]
}
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
export class TencentAccessProvider {
static define () {
return {
name: 'tencent',
label: '腾讯云',
input: {
secretId: {
type: String,
label: 'secretId',
component: {
placeholder: 'secretId',
rules: [{ required: true, message: '该项必填' }]
}
},
secretKey: {
type: String,
label: 'secretKey',
component: {
placeholder: 'secretKey',
rules: [{ required: true, message: '该项必填' }]
}
}
}
}
}
}

View File

@@ -2,19 +2,34 @@ import { AbstractDnsProvider, util } from '@certd/api'
import _ from 'lodash-es'
const request = util.request
export class DnspodDnsProvider extends AbstractDnsProvider {
static name () {
return 'dnspod'
}
constructor (dnsProviderConfig) {
super()
if (!dnsProviderConfig.id || !dnsProviderConfig.token) {
throw new Error('请正确配置dnspod的 id 和 token')
static define () {
return {
name: 'dnspod',
label: 'dnspod(腾讯云)',
desc: '腾讯云的域名解析接口已迁移到dnspod',
input: {
accessProvider: {
label: '授权',
type: [String, Object],
desc: '需要dnspod类型的授权',
component: {
name: 'access-provider-selector',
filter: 'dnspod'
},
required: true
}
}
}
this.loginToken = dnsProviderConfig.id + ',' + dnsProviderConfig.token
}
async doRequest (options) {
constructor (args) {
super(args)
const { props } = args
const accessProvider = this.getAccessProvider(props.accessProvider)
this.loginToken = accessProvider.id + ',' + accessProvider.token
}
async doRequest (options, successCodes = []) {
const config = {
method: 'post',
formData: {
@@ -28,8 +43,11 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
_.merge(config, options)
const ret = await request(config)
if (ret?.status?.code !== '1') {
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
if (!ret || !ret.status) {
const code = ret.status.code
if (code !== '1' || !successCodes.includes(code)) {
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
}
}
return ret
}
@@ -58,7 +76,7 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
value: value,
mx: 1
}
})
}, ['104'])// 104错误码为记录已存在无需再次添加
this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
return ret.record
}

View File

@@ -0,0 +1,34 @@
import _ from 'lodash-es'
import { TencentAccessProvider } from './access-providers/tencent.js'
import { DnspodAccessProvider } from './access-providers/dnspod.js'
import { DnspodDnsProvider } from './dns-providers/dnspod.js'
import { UploadCertToTencent } from './plugins/upload-to-tencent/index.js'
import { DeployCertToTencentCDN } from './plugins/deploy-to-cdn/index.js'
import { DeployCertToTencentCLB } from './plugins/deploy-to-clb/index.js'
import { DeployCertToTencentTKEIngress } from './plugins/deploy-to-tke-ingress/index.js'
import { pluginRegistry, accessProviderRegistry, dnsProviderRegistry } from '@certd/api'
export const DefaultPlugins = {
UploadCertToTencent,
DeployCertToTencentTKEIngress,
DeployCertToTencentCDN,
DeployCertToTencentCLB
}
export default {
install () {
_.forEach(DefaultPlugins, item => {
pluginRegistry.install(item)
})
accessProviderRegistry.install(TencentAccessProvider)
accessProviderRegistry.install(DnspodAccessProvider)
dnsProviderRegistry.install(DnspodDnsProvider)
}
}

View File

@@ -1,4 +1,4 @@
import { AbstractTencentPlugin } from '../../tencent/abstract-tencent.js'
import { AbstractTencentPlugin } from '../abstract-tencent.js'
import dayjs from 'dayjs'
import tencentcloud from 'tencentcloud-sdk-nodejs'
@@ -19,35 +19,35 @@ export class DeployCertToTencentCDN extends AbstractTencentPlugin {
required: true
},
certName: {
label: '证书名称'
label: '证书名称',
desc: '证书上传后将以此参数作为名称前缀'
},
certType: {
value: 'upload',
default: 'upload',
label: '证书来源',
options: [
{ value: 'upload', label: '直接上传' },
{ value: 'cloud', label: '从证书库', desc: '需要uploadCertToTencent作为前置任务' }
],
desc: '如果选择‘从证书库’类型,则需要以《上传证书到腾讯云》作为前置任务',
required: true
},
// serverCertificateStatus: {
// label: '启用https',
// options: [
// { value: 'on', label: '开启HTTPS并更新证书' },
// { value: 'auto', label: '若HTTPS开启则更新未开启不更新' }
// ],
// required:true
// },
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=aliyun]',
desc: 'access 授权',
component: {
name: 'access-provider-selector',
filter: 'tencent'
},
required: true
}
},
output: {
tencentCertId: {
type: String,
desc: '证书来源选择上传时将返回此id'
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { AbstractTencentPlugin } from '../../tencent/abstract-tencent.js'
import { AbstractTencentPlugin } from '../abstract-tencent.js'
import tencentcloud from 'tencentcloud-sdk-nodejs'
export class DeployCertToTencentCLB extends AbstractTencentPlugin {
/**
@@ -15,16 +15,19 @@ export class DeployCertToTencentCLB extends AbstractTencentPlugin {
input: {
region: {
label: '大区',
value: 'ap-guangzhou'
default: 'ap-guangzhou',
required: true
},
domain: {
label: '域名',
type: [String, Array],
required: true,
desc: '要更新的支持https的负载均衡的域名'
},
loadBalancerId: {
label: '负载均衡ID',
desc: '如果没有配置则根据域名匹配负载均衡下的监听器根据域名匹配时暂时只支持前100个'
desc: '如果没有配置则根据域名匹配负载均衡下的监听器根据域名匹配时暂时只支持前100个',
required: true
},
listenerId: {
label: '监听器ID',
@@ -37,8 +40,11 @@ export class DeployCertToTencentCLB extends AbstractTencentPlugin {
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=tencent]',
desc: 'access授权',
component: {
name: 'access-provider-selector',
filter: 'tencent'
},
required: true
}
},

View File

@@ -1,6 +1,6 @@
import { AbstractTencentPlugin } from '../../tencent/abstract-tencent.js'
import { AbstractTencentPlugin } from '../abstract-tencent.js'
import tencentcloud from 'tencentcloud-sdk-nodejs'
import { K8sClient } from '../../utils/util.k8s.client.js'
import { K8sClient } from '@certd/plugin-common'
export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
/**
* 插件定义
@@ -16,27 +16,36 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
input: {
region: {
label: '大区',
value: 'ap-guangzhou'
default: 'ap-guangzhou',
required: true
},
clusterId: {
label: '集群ID',
required: true,
desc: '例如cls-6lbj1vee'
desc: '例如cls-6lbj1vee',
request: true
},
namespace: {
label: '集群namespace',
value: 'default'
label: '集群namespace',
default: 'default',
required: true
},
secreteName: {
type: [String, Array],
label: '证书的secret名称',
desc: '支持多个(传入数组)'
desc: '支持多个(传入数组)',
required: true
},
ingressName: {
type: [String, Array],
label: 'ingress名称',
desc: '支持多个(传入数组)'
},
ingressClass: {
type: String,
label: 'ingress类型',
desc: '可选 qcloud / nginx'
},
clusterIp: {
type: String,
label: '集群内网ip',
@@ -44,20 +53,19 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
},
clusterDomain: {
type: String,
label: '集群域名,可不填,默认为:[clusterId].ccs.tencent-cloud.com'
label: '集群域名',
desc: '可不填,默认为:[clusterId].ccs.tencent-cloud.com'
},
/**
* AccessProvider的key,或者一个包含access的具体的对象
*/
accessProvider: {
label: 'Access提供者',
label: 'Access授权',
type: [String, Object],
desc: '请选择access提供者',
desc: 'access授权',
component: {
name: 'accessProviderSelect',
props: {
filterType: 'tencent'
}
name: 'access-provider-selector',
filter: 'tencent'
},
required: true
}
@@ -83,7 +91,13 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
// 修改内网解析ip地址
k8sClient.setLookup({ [clusterDomain]: { ip: props.clusterIp } })
}
await this.patchCertSecret({ k8sClient, props, context })
const ingressType = props.ingressClass || 'qcloud'
if (ingressType === 'qcloud') {
await this.patchQcloudCertSecret({ k8sClient, props, context })
} else {
await this.patchNginxCertSecret({ cert, k8sClient, props, context })
}
await this.sleep(2000) // 停留2秒等待secret部署完成
await this.restartIngress({ k8sClient, props })
return true
@@ -118,7 +132,7 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
return ret.Kubeconfig
}
async patchCertSecret ({ k8sClient, props, context }) {
async patchQcloudCertSecret ({ k8sClient, props, context }) {
const { tencentCertId } = context
if (tencentCertId == null) {
throw new Error('请先将【上传证书到腾讯云】作为前置任务')
@@ -148,6 +162,35 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
}
}
async patchNginxCertSecret ({ cert, k8sClient, props, context }) {
const crt = cert.crt
const key = cert.key
const crtBase64 = Buffer.from(crt).toString('base64')
const keyBase64 = Buffer.from(key).toString('base64')
const { namespace, secretName } = props
const body = {
data: {
'tls.crt': crtBase64,
'tls.key': keyBase64
},
metadata: {
labels: {
certd: this.appendTimeSuffix('certd')
}
}
}
let secretNames = secretName
if (typeof secretName === 'string') {
secretNames = [secretName]
}
for (const secret of secretNames) {
await k8sClient.patchSecret({ namespace, secretName: secret, body })
this.logger.info(`CertSecret已更新:${secret}`)
}
}
async restartIngress ({ k8sClient, props }) {
const { namespace, ingressName } = props

View File

@@ -1,4 +1,3 @@
import dayjs from 'dayjs'
import tencentcloud from 'tencentcloud-sdk-nodejs'
import { AbstractTencentPlugin } from '../abstract-tencent.js'
@@ -18,10 +17,14 @@ export class UploadCertToTencent extends AbstractTencentPlugin {
label: '证书名称'
},
accessProvider: {
label: 'Access提供者',
label: 'Access授权',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=tencent]'
desc: 'access授权',
component: {
name: 'access-provider-selector',
filter: 'tencent'
},
required: true
}
},
output: {

View File

@@ -1,13 +1,20 @@
import pkg from 'chai'
import { Certd } from '../../src/index.js'
import { createOptions } from '../../../../test/options.js'
import PluginTencent from '../../src/index.js'
import { createOptions } from '../../../../../test/options.js'
import { Certd } from '@certd/certd'
const { expect } = pkg
// 安装默认插件和授权提供者
PluginTencent.install()
describe('DnspodDnsProvider', function () {
it('#申请证书', async function () {
this.timeout(300000)
const options = createOptions()
options.cert.domains = ['*.certd.xyz', '*.test.certd.xyz', '*.base.certd.xyz', 'certd.xyz']
options.cert.dnsProvider = 'dnspod'
options.cert.dnsProvider = {
type: 'dnspod',
accessProvider: 'dnspod'
}
options.args = { forceCert: true }
const certd = new Certd(options)
const cert = await certd.certApply()

View File

@@ -1,20 +1,31 @@
import pkg from 'chai'
import DnspodDnsProvider from '../../src/dns-provider/impl/dnspod.js'
import { Certd } from '../../src/index.js'
import { createOptions } from '../../../../test/options.js'
import { DnspodDnsProvider } from '../../src/dns-providers/dnspod.js'
import { createOptions, getDnsProviderOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('DnspodDnsProvider', function () {
it('#getDomainList', async function () {
const options = createOptions()
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
let options = createOptions()
options.cert.dnsProvider = {
type: 'dnspod',
accessProvider: 'dnspod'
}
options = getDnsProviderOptions(options)
const dnsProvider = new DnspodDnsProvider(options)
const domainList = await dnsProvider.getDomainList()
console.log('domainList', domainList)
expect(domainList.length).gt(0)
})
it('#createRecord&removeRecord', async function () {
const options = createOptions()
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
let options = createOptions()
options.cert.dnsProvider = {
type: 'dnspod',
accessProvider: 'dnspod'
}
options = getDnsProviderOptions(options)
const dnsProvider = new DnspodDnsProvider(options)
const record = await dnsProvider.createRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa' })
console.log('recordId', record.id)
expect(record.id != null).ok

View File

@@ -1,35 +1,33 @@
import pkg from 'chai'
import { DeployCertToTencentCDN } from '../../src/tencent/deploy-to-cdn/index.js'
import { DeployCertToTencentCDN } from '../../src/plugins/deploy-to-cdn/index.js'
import { Certd } from '@certd/certd'
import { UploadCertToTencent } from '../../src/tencent/upload-to-tencent/index.js'
import { createOptions } from '../../../../test/options.js'
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('DeployToTencentCDN', function () {
it('#execute-from-store', async function () {
const options = createOptions()
options.args.test = false
const certd = new Certd(options)
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
const cert = await certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
const context = {}
const uploadPlugin = new UploadCertToTencent()
const uploadPlugin = new UploadCertToTencent(options)
const uploadOptions = {
accessProviders: options.accessProviders,
cert,
props: { name: 'certd部署测试', accessProvider: 'tencent' },
context
}
await uploadPlugin.doExecute(uploadOptions)
const deployPlugin = new DeployCertToTencentCDN()
const deployPlugin = new DeployCertToTencentCDN(options)
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: { domainName: 'tentcent-certd.docmirror.cn', certName: 'certd部署测试', accessProvider: 'tencent' },
context
}
const ret = await deployPlugin.doExecute(deployOpts)
expect(ret).ok
await deployPlugin.doExecute(deployOpts)
console.log('context:', context)
expect(context.tencentCertId).ok
await uploadPlugin.doRollback(uploadOptions)
})
@@ -38,17 +36,17 @@ describe('DeployToTencentCDN', function () {
options.args.test = false
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']
const plugin = new DeployCertToTencentCDN()
const plugin = new DeployCertToTencentCDN(options)
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: { domainName: 'tentcent-certd.docmirror.cn', accessProvider: 'tencent' },
context
}
const ret = await plugin.doExecute(deployOpts)
console.log('context:', context, ret)
expect(context).be.empty
})
})

View File

@@ -1,23 +1,22 @@
import pkg from 'chai'
import { DeployCertToTencentCLB } from '../../src/tencent/deploy-to-clb/index.js'
import { DeployCertToTencentCLB } from '../../src/plugins/deploy-to-clb/index.js'
import { Certd } from '@certd/certd'
// eslint-disable-next-line no-unused-vars
import { createOptions } from '../../../../test/options.js'
import { UploadCertToTencent } from '../../src/tencent/upload-to-tencent/index.js'
import { createOptions } from '../../../../../test/options.js'
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
const { expect } = pkg
describe('DeployToTencentCLB', function () {
it('#execute-getClbList', async function () {
const options = createOptions()
options.args.test = false
options.cert.dnsProvider = 'tencent-yonsz'
const deployPlugin = new DeployCertToTencentCLB()
const deployPlugin = new DeployCertToTencentCLB(options)
const props = {
region: 'ap-guangzhou',
domain: 'certd-test-no-sni.base.yonsz.net',
accessProvider: 'tencent-yonsz'
}
const accessProviders = options.accessProviders
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider, accessProviders)
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
const { region } = props
const client = deployPlugin.getClient(accessProvider, region)
@@ -34,13 +33,14 @@ describe('DeployToTencentCLB', function () {
region: 'ap-guangzhou',
domain: 'certd-test-no-sni.base.yonsz.net',
accessProvider: 'tencent-yonsz',
loadBalancerId: 'lb-59yhe5xo'
loadBalancerId: 'lb-59yhe5xo',
listenerId: 'lbl-1vfwx8dq'
}
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
const { region } = props
const client = deployPlugin.getClient(accessProvider, region)
const ret = await deployPlugin.getListenerList(client, props.loadBalancerId, props)
const ret = await deployPlugin.getListenerList(client, props.loadBalancerId, [props.listenerId])
expect(ret.length > 0).ok
console.log('clb count:', ret.length, ret)
})
@@ -54,10 +54,9 @@ describe('DeployToTencentCLB', function () {
options.cert.domains = ['*.docmirror.cn']
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const deployPlugin = new DeployCertToTencentCLB()
const deployPlugin = new DeployCertToTencentCLB(options)
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: {
region: 'ap-guangzhou',
@@ -72,7 +71,7 @@ describe('DeployToTencentCLB', function () {
console.log('ret:', ret)
// 删除测试证书
const uploadPlugin = new UploadCertToTencent()
const uploadPlugin = new UploadCertToTencent(options)
await uploadPlugin.doRollback(deployOpts)
})
@@ -83,10 +82,9 @@ describe('DeployToTencentCLB', function () {
options.cert.dnsProvider = 'tencent-yonsz'
const certd = new Certd(options)
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
const deployPlugin = new DeployCertToTencentCLB()
const deployPlugin = new DeployCertToTencentCLB(options)
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: {
region: 'ap-guangzhou',
@@ -98,10 +96,10 @@ describe('DeployToTencentCLB', function () {
context
}
const ret = await deployPlugin.doExecute(deployOpts)
expect(ret).ok
console.log('ret:', ret)
expect(ret).ok
// 删除测试证书
const uploadPlugin = new UploadCertToTencent()
const uploadPlugin = new UploadCertToTencent(options)
await uploadPlugin.doRollback(deployOpts)
})
})

View File

@@ -0,0 +1,59 @@
import pkg from 'chai'
import { DeployCertToTencentTKEIngress } from '../../src/plugins/deploy-to-tke-ingress/index.js'
import { Certd } from '@certd/certd'
import { createOptions } from '../../../../../test/options.js'
import { K8sClient } from '../../src/utils/util.k8s.client.js'
const { expect } = pkg
async function getOptions () {
const options = createOptions()
options.args.test = false
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: {
accessProvider: 'tencent-yonsz',
region: 'ap-guangzhou',
clusterId: 'cls-6lbj1vee'
},
context
}
return { options, deployOpts }
}
describe('DeployCertToTencentTKEIngressNginx', function () {
it('#getTKESecrets', async function () {
this.timeout(50000)
const { options, deployOpts } = await getOptions()
const plugin = new DeployCertToTencentTKEIngress(options)
const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props.clusterId)
const k8sClient = new K8sClient(kubeConfig)
k8sClient.setLookup({
'cls-6lbj1vee.ccs.tencent-cloud.com': { ip: '13.123.123.123' }
})
const secrets = await k8sClient.getSecret({ namespace: 'stress' })
console.log('secrets:', secrets)
})
it('#execute', async function () {
this.timeout(5000)
const { options, deployOpts } = await getOptions()
deployOpts.props.ingressName = 'stress-ingress-nginx'
deployOpts.props.ingressClass = 'nginx'
deployOpts.props.secretName = 'stress-all'
deployOpts.props.namespace = 'stress'
const plugin = new DeployCertToTencentTKEIngress(options)
const ret = await plugin.doExecute(deployOpts)
console.log('sucess', ret)
})
})

Some files were not shown because too many files have changed in this diff Show More